@softize/opus 23.0.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 (95) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/bin/cli.mjs +2 -0
  3. package/bin/lib/check.mjs +241 -42
  4. package/bin/lib/copy.mjs +16 -34
  5. package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +6 -0
  6. package/docs/adr/0039-filter-presentation-is-independent-from-filter-type.md +3 -3
  7. package/docs/adr/0040-tabs-use-a-single-navigation-presentation.md +5 -6
  8. package/docs/adr/0042-toggle-group-shares-the-grouped-control-grammar.md +79 -0
  9. package/docs/adr/0043-neutral-surfaces-and-interactions-use-opaque-role-tokens.md +73 -0
  10. package/docs/adr/0044-neutral-appearance-derives-from-bounded-axes-per-mode.md +133 -0
  11. package/docs/adr/0045-surface-replaces-content-and-card.md +230 -0
  12. package/docs/adr/0046-structural-body-owns-content-rhythm.md +103 -0
  13. package/docs/adr/0047-pane-is-only-a-split-layout-unit.md +71 -0
  14. package/docs/adr/0048-modal-intro-separates-content-from-chrome.md +34 -0
  15. package/package.json +5 -1
  16. package/registry/skills/build-opus-ui/SKILL.md +11 -11
  17. package/registry/skills/build-opus-ui/references/evaluations.md +10 -9
  18. package/registry/skills/build-opus-ui/references/ui-patterns.md +17 -16
  19. package/registry/templates/app/src/App.tsx +24 -12
  20. package/src/ui/appearance.ts +149 -0
  21. package/src/ui/components/patterns/action-form-card.tsx +46 -33
  22. package/src/ui/components/patterns/action-list-dialog.tsx +2 -4
  23. package/src/ui/components/patterns/confirm.tsx +3 -1
  24. package/src/ui/components/patterns/fill-region.ts +8 -7
  25. package/src/ui/components/patterns/form-dialog.tsx +1 -3
  26. package/src/ui/components/patterns/form.tsx +8 -7
  27. package/src/ui/components/patterns/list.tsx +15 -19
  28. package/src/ui/components/patterns/page.tsx +15 -11
  29. package/src/ui/components/patterns/pane.tsx +9 -240
  30. package/src/ui/components/patterns/presentation.tsx +14 -14
  31. package/src/ui/components/patterns/sidebar.tsx +7 -17
  32. package/src/ui/components/patterns/split.tsx +111 -59
  33. package/src/ui/components/patterns/{surface-header.tsx → structural-header.tsx} +50 -19
  34. package/src/ui/components/patterns/structural-media.tsx +26 -0
  35. package/src/ui/components/patterns/surface-anatomy.ts +71 -61
  36. package/src/ui/components/patterns/surface-assistant.tsx +2 -4
  37. package/src/ui/components/patterns/surface.tsx +620 -0
  38. package/src/ui/components/patterns/trigger.tsx +11 -9
  39. package/src/ui/components/primitives/ask.tsx +155 -83
  40. package/src/ui/components/primitives/button.tsx +11 -9
  41. package/src/ui/components/primitives/checkbox.tsx +8 -8
  42. package/src/ui/components/primitives/command.tsx +1 -1
  43. package/src/ui/components/primitives/composer.tsx +47 -45
  44. package/src/ui/components/primitives/detail.tsx +9 -2
  45. package/src/ui/components/primitives/dialog.tsx +60 -26
  46. package/src/ui/components/primitives/drawer.tsx +58 -12
  47. package/src/ui/components/primitives/icon-picker.tsx +90 -73
  48. package/src/ui/components/primitives/input-group.tsx +35 -28
  49. package/src/ui/components/primitives/input-otp.tsx +16 -16
  50. package/src/ui/components/primitives/input.tsx +20 -10
  51. package/src/ui/components/primitives/item.tsx +9 -12
  52. package/src/ui/components/primitives/menu.tsx +44 -65
  53. package/src/ui/components/primitives/metric-card.tsx +75 -59
  54. package/src/ui/components/primitives/radio-group.tsx +9 -9
  55. package/src/ui/components/primitives/select.tsx +344 -209
  56. package/src/ui/components/primitives/switch.tsx +10 -10
  57. package/src/ui/components/primitives/table.tsx +29 -24
  58. package/src/ui/components/primitives/tabs.tsx +4 -2
  59. package/src/ui/components/primitives/textarea.tsx +6 -6
  60. package/src/ui/components/primitives/toggle-group-context.ts +24 -0
  61. package/src/ui/components/primitives/toggle-group.tsx +116 -74
  62. package/src/ui/components/primitives/toggle.tsx +82 -16
  63. package/src/ui/docs/DocBrowser.tsx +77 -69
  64. package/src/ui/docs/content/action-form-card.md +9 -9
  65. package/src/ui/docs/content/action-list.md +76 -70
  66. package/src/ui/docs/content/action-view.md +33 -18
  67. package/src/ui/docs/content/button.md +1 -1
  68. package/src/ui/docs/content/communication.md +1 -1
  69. package/src/ui/docs/content/customization.md +64 -11
  70. package/src/ui/docs/content/detail.md +8 -6
  71. package/src/ui/docs/content/dialog.md +100 -58
  72. package/src/ui/docs/content/drawer.md +63 -19
  73. package/src/ui/docs/content/field.md +14 -15
  74. package/src/ui/docs/content/icon-tile.md +14 -13
  75. package/src/ui/docs/content/page.md +34 -26
  76. package/src/ui/docs/content/pane.md +41 -290
  77. package/src/ui/docs/content/presentation.md +3 -3
  78. package/src/ui/docs/content/separator.md +6 -6
  79. package/src/ui/docs/content/sidebar.md +68 -266
  80. package/src/ui/docs/content/skeleton.md +20 -18
  81. package/src/ui/docs/content/split.md +14 -14
  82. package/src/ui/docs/content/surface.md +513 -0
  83. package/src/ui/docs/content/tabs.md +20 -20
  84. package/src/ui/docs/content/toggle.md +117 -65
  85. package/src/ui/docs/content/tokens.md +124 -40
  86. package/src/ui/docs/registry.tsx +4 -6
  87. package/src/ui/index.ts +30 -27
  88. package/src/ui/meta.ts +15 -27
  89. package/src/ui/react.tsx +40 -43
  90. package/src/ui/theme.css +311 -64
  91. package/src/ui/components/patterns/content-header.tsx +0 -528
  92. package/src/ui/components/patterns/surface-media.tsx +0 -69
  93. package/src/ui/components/primitives/card.tsx +0 -191
  94. package/src/ui/docs/content/card.md +0 -292
  95. package/src/ui/docs/content/content.md +0 -275
@@ -1,8 +1,8 @@
1
- import * as React from "react"
2
- import { Switch as SwitchPrimitive } from "radix-ui"
1
+ import * as React from "react";
2
+ import { Switch as SwitchPrimitive } from "radix-ui";
3
3
 
4
- import { cn } from '../../lib/cn.ts'
5
- import type { ControlSize } from './control.ts'
4
+ import { cn } from "../../lib/cn.ts";
5
+ import type { ControlSize } from "./control.ts";
6
6
 
7
7
  /** `size` usa os nomes da escala de control.ts; a chave é um trilho (não uma linha), então a
8
8
  * medida é a do glifo daquele tamanho: `sm` acompanha o svg de um controle sm (0.875rem). */
@@ -11,15 +11,15 @@ function Switch({
11
11
  size = "default",
12
12
  ...props
13
13
  }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
14
- size?: Extract<ControlSize, "sm" | "default">
14
+ size?: Extract<ControlSize, "sm" | "default">;
15
15
  }) {
16
16
  return (
17
17
  <SwitchPrimitive.Root
18
18
  data-slot="switch"
19
19
  data-size={size}
20
20
  className={cn(
21
- "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
22
- className
21
+ "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-interaction-selected",
22
+ className,
23
23
  )}
24
24
  {...props}
25
25
  >
@@ -27,11 +27,11 @@ function Switch({
27
27
  data-slot="switch-thumb"
28
28
  // px: o percurso do thumb desconta a borda (um pixel de cada lado) no calc do translate.
29
29
  className={cn(
30
- "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
30
+ "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
31
31
  )}
32
32
  />
33
33
  </SwitchPrimitive.Root>
34
- )
34
+ );
35
35
  }
36
36
 
37
- export { Switch }
37
+ export { Switch };
@@ -1,18 +1,23 @@
1
- import * as React from "react"
1
+ import * as React from "react";
2
2
 
3
- import { cn } from '../../lib/cn.ts'
3
+ import { cn } from "../../lib/cn.ts";
4
4
 
5
- type TableVariant = 'plain' | 'framed'
6
- const TableVariantContext = React.createContext<TableVariant>('plain')
5
+ type TableVariant = "plain" | "framed";
6
+ const TableVariantContext = React.createContext<TableVariant>("plain");
7
7
 
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
- variant?: TableVariant
10
+ variant?: TableVariant;
11
11
  /** Classes do contêiner responsável pela rolagem e pela moldura da tabela. */
12
- containerClassName?: string
12
+ containerClassName?: string;
13
13
  }
14
14
 
15
- function Table({ className, containerClassName, variant = 'plain', ...props }: TableProps) {
15
+ function Table({
16
+ className,
17
+ containerClassName,
18
+ variant = "plain",
19
+ ...props
20
+ }: TableProps) {
16
21
  return (
17
22
  <TableVariantContext.Provider value={variant}>
18
23
  <div
@@ -20,7 +25,7 @@ function Table({ className, containerClassName, variant = 'plain', ...props }: T
20
25
  data-variant={variant}
21
26
  className={cn(
22
27
  "relative w-full overflow-x-auto",
23
- variant === 'framed' && 'rounded-lg border border-border',
28
+ variant === "framed" && "rounded-lg border border-border",
24
29
  containerClassName,
25
30
  )}
26
31
  >
@@ -31,22 +36,22 @@ function Table({ className, containerClassName, variant = 'plain', ...props }: T
31
36
  />
32
37
  </div>
33
38
  </TableVariantContext.Provider>
34
- )
39
+ );
35
40
  }
36
41
 
37
42
  function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
38
- const variant = React.useContext(TableVariantContext)
43
+ const variant = React.useContext(TableVariantContext);
39
44
  return (
40
45
  <thead
41
46
  data-slot="table-header"
42
47
  className={cn(
43
48
  "[&_tr]:border-b [&_tr:hover]:bg-transparent",
44
- variant === 'framed' && 'bg-muted/30 dark:bg-input/30',
49
+ variant === "framed" && "bg-control",
45
50
  className,
46
51
  )}
47
52
  {...props}
48
53
  />
49
- )
54
+ );
50
55
  }
51
56
 
52
57
  function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
@@ -56,7 +61,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
56
61
  className={cn("[&_tr:last-child]:border-0", className)}
57
62
  {...props}
58
63
  />
59
- )
64
+ );
60
65
  }
61
66
 
62
67
  function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
@@ -65,11 +70,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
65
70
  data-slot="table-footer"
66
71
  className={cn(
67
72
  "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
68
- className
73
+ className,
69
74
  )}
70
75
  {...props}
71
76
  />
72
- )
77
+ );
73
78
  }
74
79
 
75
80
  function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
@@ -77,12 +82,12 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
77
82
  <tr
78
83
  data-slot="table-row"
79
84
  className={cn(
80
- "border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
81
- className
85
+ "border-b transition-colors hover:bg-interaction-hover has-aria-expanded:bg-interaction-selected data-[state=selected]:bg-interaction-selected",
86
+ className,
82
87
  )}
83
88
  {...props}
84
89
  />
85
- )
90
+ );
86
91
  }
87
92
 
88
93
  function TableHead({ className, ...props }: React.ComponentProps<"th">) {
@@ -91,11 +96,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
91
96
  data-slot="table-head"
92
97
  className={cn(
93
98
  "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]",
94
- className
99
+ className,
95
100
  )}
96
101
  {...props}
97
102
  />
98
- )
103
+ );
99
104
  }
100
105
 
101
106
  function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -104,11 +109,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
104
109
  data-slot="table-cell"
105
110
  className={cn(
106
111
  "px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[0.125rem]",
107
- className
112
+ className,
108
113
  )}
109
114
  {...props}
110
115
  />
111
- )
116
+ );
112
117
  }
113
118
 
114
119
  function TableCaption({
@@ -121,7 +126,7 @@ function TableCaption({
121
126
  className={cn("mt-4 text-sm text-muted-foreground", className)}
122
127
  {...props}
123
128
  />
124
- )
129
+ );
125
130
  }
126
131
 
127
132
  export {
@@ -133,4 +138,4 @@ export {
133
138
  TableRow,
134
139
  TableCell,
135
140
  TableCaption,
136
- }
141
+ };
@@ -68,6 +68,7 @@ function TabsList({
68
68
  const size = React.useContext(TabsSizeContext);
69
69
  const orientation = React.useContext(TabsOrientationContext);
70
70
  const distribution = React.useContext(TabsDistributionContext);
71
+ const framed = React.useContext(TabsFramedContext);
71
72
  const hasHorizontalLine = orientation === "horizontal";
72
73
  return (
73
74
  <TabsPrimitive.List
@@ -78,6 +79,7 @@ function TabsList({
78
79
  "group/tabs-list inline-flex w-fit items-center justify-center gap-1 rounded-none bg-transparent p-0 text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
79
80
  controlHeight[size],
80
81
  distribution === "equal" && orientation === "horizontal" && "w-full",
82
+ framed && orientation === "horizontal" && "px-1 pt-1",
81
83
  className,
82
84
  )}
83
85
  style={{
@@ -152,7 +154,7 @@ function TabsListItem({
152
154
  hasHorizontalLine &&
153
155
  cn(
154
156
  "group/tab-list-item rounded-none px-0 py-0 hover:bg-transparent active:brightness-100 data-[state=active]:bg-transparent data-[state=active]:hover:bg-transparent",
155
- framed ? "pb-2" : "pb-1.5",
157
+ framed ? "pb-1" : "pb-1.5",
156
158
  ),
157
159
  className,
158
160
  )}
@@ -172,7 +174,7 @@ function TabsListItem({
172
174
  <span
173
175
  data-slot="tabs-list-item-content"
174
176
  className={cn(
175
- "inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-accent hover:text-accent-foreground active:brightness-90 group-data-[state=active]/tab-list-item:text-foreground",
177
+ "inline-flex items-center justify-center gap-1.5 rounded-md px-3 transition-colors hover:bg-interaction-hover hover:text-accent-foreground active:bg-interaction-pressed group-data-[state=active]/tab-list-item:text-foreground",
176
178
  distribution === "equal" && "w-full",
177
179
  controlHeight[size],
178
180
  )}
@@ -1,18 +1,18 @@
1
- import * as React from "react"
1
+ import * as React from "react";
2
2
 
3
- import { cn } from '../../lib/cn.ts'
3
+ import { cn } from "../../lib/cn.ts";
4
4
 
5
5
  function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
6
  return (
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-muted/30 px-3 py-2 text-sm transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:bg-input/30 dark:aria-invalid:ring-context-danger/40",
11
- className
10
+ "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-control px-3 py-2 text-sm transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[0.1875rem] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-context-danger aria-invalid:ring-context-danger/20 dark:aria-invalid:ring-context-danger/40",
11
+ className,
12
12
  )}
13
13
  {...props}
14
14
  />
15
- )
15
+ );
16
16
  }
17
17
 
18
- export { Textarea }
18
+ export { Textarea };
@@ -0,0 +1,24 @@
1
+ import * as React from "react";
2
+
3
+ import type { ControlShape } from "./control.ts";
4
+
5
+ type ToggleGroupMode = "connected" | "spaced" | "divided" | "segmented";
6
+ type ToggleGroupDistribution = "content" | "equal";
7
+
8
+ type ToggleGroupContextValue = {
9
+ mode: ToggleGroupMode;
10
+ orientation: "horizontal" | "vertical";
11
+ variant?: "default" | "outline" | null;
12
+ size?: "default" | "sm" | "lg" | null;
13
+ shape?: ControlShape | null;
14
+ };
15
+
16
+ const ToggleGroupContext = React.createContext<ToggleGroupContextValue | null>(
17
+ null,
18
+ );
19
+
20
+ export {
21
+ ToggleGroupContext,
22
+ type ToggleGroupDistribution,
23
+ type ToggleGroupMode,
24
+ };
@@ -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 text-muted-foreground hover:bg-muted/50 hover:text-foreground data-[state=on]:bg-muted/50 data-[state=on]:text-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 };