@dust-tt/sparkle 0.4.11 → 0.4.12-rc-2

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.
@@ -4,6 +4,30 @@ import * as React from "react";
4
4
  import { cn } from "@sparkle/lib/utils";
5
5
 
6
6
  import type { ButtonProps, ButtonSizeType, ButtonVariantType } from "./Button";
7
+ import { Button } from "./Button";
8
+ import type { DropdownMenuItemProps } from "./Dropdown";
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuTrigger,
14
+ } from "./Dropdown";
15
+
16
+ type ButtonGroupButtonItem = {
17
+ type: "button";
18
+ props: ButtonProps;
19
+ };
20
+
21
+ type ButtonGroupDropdownItem = {
22
+ type: "dropdown";
23
+ triggerProps: Omit<ButtonProps, "onClick">;
24
+ dropdownProps: {
25
+ items: DropdownMenuItemProps[];
26
+ align?: "start" | "center" | "end";
27
+ };
28
+ };
29
+
30
+ export type ButtonGroupItem = ButtonGroupButtonItem | ButtonGroupDropdownItem;
7
31
 
8
32
  type DisallowedButtonGroupVariant =
9
33
  | "ghost"
@@ -58,13 +82,16 @@ const buttonGroupVariants = cva("s-inline-flex", {
58
82
  export interface ButtonGroupProps
59
83
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
60
84
  VariantProps<typeof buttonGroupVariants> {
61
- children: React.ReactElement<ButtonProps> | React.ReactElement<ButtonProps>[];
85
+ /**
86
+ * Array of button or dropdown items to render in the group.
87
+ */
88
+ items: ButtonGroupItem[];
62
89
  /**
63
90
  * Variant to apply to all buttons in the group.
64
91
  */
65
92
  variant?: ButtonGroupVariantType;
66
93
  /**
67
- * Size to apply to all buttons in the group. Mini buttons must opt-in per child.
94
+ * Size to apply to all buttons in the group. Mini buttons must opt-in per item.
68
95
  */
69
96
  size?: Exclude<ButtonSizeType, "mini">;
70
97
  /**
@@ -86,26 +113,19 @@ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
86
113
  size,
87
114
  disabled,
88
115
  removeGaps = true,
89
- children,
116
+ items,
90
117
  ...props
91
118
  },
92
119
  ref
93
120
  ) => {
94
- const childrenArray = React.Children.toArray(
95
- children
96
- ) as React.ReactElement<ButtonProps>[];
97
-
98
- const clonedChildren = childrenArray.map((child, index) => {
99
- if (!React.isValidElement<ButtonProps>(child)) {
100
- return child;
101
- }
102
-
103
- const totalChildren = childrenArray.length;
121
+ const totalItems = items?.length ?? 0;
122
+
123
+ const renderedItems = items.map((item, index) => {
104
124
  const isFirst = index === 0;
105
- const isLast = index === totalChildren - 1;
125
+ const isLast = index === totalItems - 1;
106
126
 
107
127
  const borderRadiusClasses = (() => {
108
- if (!removeGaps || totalChildren === 1) {
128
+ if (!removeGaps || totalItems === 1) {
109
129
  return "";
110
130
  }
111
131
 
@@ -140,22 +160,61 @@ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
140
160
  return isLast ? "" : "s-border-b-0";
141
161
  })();
142
162
 
143
- const nextVariant = sanitizeVariant(variant ?? child.props.variant);
144
- const nextSize = (size ?? child.props.size) as ButtonProps["size"];
145
-
146
- const overrides = {
147
- variant: nextVariant,
148
- size: nextSize,
149
- disabled: disabled ?? child.props.disabled,
150
- isRounded: false,
151
- className: cn(
152
- child.props.className,
153
- borderRadiusClasses,
154
- borderClasses
155
- ),
156
- } as Partial<ButtonProps>;
157
-
158
- return React.cloneElement(child, overrides);
163
+ if (item.type === "button") {
164
+ const nextVariant = sanitizeVariant(variant ?? item.props.variant);
165
+ const rawSize = size ?? item.props.size;
166
+ const nextSize: Exclude<ButtonSizeType, "mini"> | undefined =
167
+ rawSize === "mini"
168
+ ? undefined
169
+ : (rawSize as Exclude<ButtonSizeType, "mini"> | undefined);
170
+
171
+ return (
172
+ <Button
173
+ key={index}
174
+ {...item.props}
175
+ variant={nextVariant}
176
+ size={nextSize}
177
+ disabled={disabled ?? item.props.disabled}
178
+ isRounded={false}
179
+ className={cn(
180
+ item.props.className,
181
+ borderRadiusClasses,
182
+ borderClasses
183
+ )}
184
+ />
185
+ );
186
+ }
187
+
188
+ const nextVariant = sanitizeVariant(variant ?? item.triggerProps.variant);
189
+ const rawSize = size ?? item.triggerProps.size;
190
+ const nextSize: Exclude<ButtonSizeType, "mini"> | undefined =
191
+ rawSize === "mini"
192
+ ? undefined
193
+ : (rawSize as Exclude<ButtonSizeType, "mini"> | undefined);
194
+
195
+ return (
196
+ <DropdownMenu key={index}>
197
+ <DropdownMenuTrigger asChild>
198
+ <Button
199
+ {...item.triggerProps}
200
+ variant={nextVariant}
201
+ size={nextSize}
202
+ disabled={disabled ?? item.triggerProps.disabled}
203
+ isRounded={false}
204
+ className={cn(
205
+ item.triggerProps.className,
206
+ borderRadiusClasses,
207
+ borderClasses
208
+ )}
209
+ />
210
+ </DropdownMenuTrigger>
211
+ <DropdownMenuContent align={item.dropdownProps.align ?? "center"}>
212
+ {item.dropdownProps.items.map((dropdownItem, dropdownIndex) => (
213
+ <DropdownMenuItem key={dropdownIndex} {...dropdownItem} />
214
+ ))}
215
+ </DropdownMenuContent>
216
+ </DropdownMenu>
217
+ );
159
218
  });
160
219
 
161
220
  return (
@@ -169,7 +228,7 @@ const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
169
228
  role="group"
170
229
  {...props}
171
230
  >
172
- {clonedChildren}
231
+ {renderedItems}
173
232
  </div>
174
233
  );
175
234
  }
@@ -17,7 +17,7 @@ export type {
17
17
  RegularButtonProps,
18
18
  } from "./Button";
19
19
  export { Button } from "./Button";
20
- export type { ButtonGroupProps } from "./ButtonGroup";
20
+ export type { ButtonGroupItem, ButtonGroupProps } from "./ButtonGroup";
21
21
  export { ButtonGroup } from "./ButtonGroup";
22
22
  export { ButtonsSwitch, ButtonsSwitchList } from "./ButtonsSwitch";
23
23
  export type { CardProps } from "./Card";
@@ -6,23 +6,27 @@ import {
6
6
  BUTTON_VARIANTS,
7
7
  type ButtonVariantType,
8
8
  } from "@sparkle/components/Button";
9
- import type { ButtonGroupVariantType } from "@sparkle/components/ButtonGroup";
9
+ import type {
10
+ ButtonGroupItem,
11
+ ButtonGroupVariantType,
12
+ } from "@sparkle/components/ButtonGroup";
10
13
 
11
14
  import {
12
- Button,
15
+ ArrowPathIcon,
13
16
  ButtonGroup,
17
+ ChevronDownIcon,
18
+ ClipboardIcon,
14
19
  PlusIcon,
15
20
  RobotIcon,
16
21
  Separator,
22
+ TrashIcon,
17
23
  } from "../index_with_tw_base";
18
24
 
19
- const DEFAULT_CHILDREN = (
20
- <>
21
- <Button label="First" />
22
- <Button label="Second" />
23
- <Button label="Third" />
24
- </>
25
- );
25
+ const DEFAULT_ITEMS: ButtonGroupItem[] = [
26
+ { type: "button", props: { label: "First" } },
27
+ { type: "button", props: { label: "Second" } },
28
+ { type: "button", props: { label: "Third" } },
29
+ ];
26
30
 
27
31
  const DISALLOWED_GROUP_VARIANTS: ButtonVariantType[] = [
28
32
  "ghost",
@@ -41,12 +45,12 @@ const meta = {
41
45
  tags: ["autodocs"],
42
46
  argTypes: {
43
47
  variant: {
44
- description: "Variant applied to every child button",
48
+ description: "Variant applied to every button",
45
49
  control: { type: "select" },
46
50
  options: BUTTON_GROUP_VARIANTS,
47
51
  },
48
52
  size: {
49
- description: "Size applied to every child button",
53
+ description: "Size applied to every button",
50
54
  control: { type: "select" },
51
55
  options: BUTTON_SIZES.filter((size) => size !== "mini"),
52
56
  },
@@ -63,12 +67,12 @@ const meta = {
63
67
  description: "Remove gaps and merge button borders",
64
68
  control: "boolean",
65
69
  },
66
- children: {
70
+ items: {
67
71
  table: { disable: true },
68
72
  },
69
73
  },
70
74
  args: {
71
- children: DEFAULT_CHILDREN,
75
+ items: DEFAULT_ITEMS,
72
76
  variant: "outline",
73
77
  size: "sm",
74
78
  orientation: "horizontal",
@@ -85,55 +89,58 @@ type Story = StoryObj<typeof meta>;
85
89
  export const Playground: Story = {};
86
90
 
87
91
  export const WithIcons: Story = {
88
- render: () => (
89
- <ButtonGroup variant="outline" size="sm">
90
- <Button icon={PlusIcon} label="Add" />
91
- <Button icon={RobotIcon} label="Agent" />
92
- <Button label="More" />
93
- </ButtonGroup>
94
- ),
92
+ args: {
93
+ items: [
94
+ { type: "button", props: { icon: PlusIcon, label: "Add" } },
95
+ { type: "button", props: { icon: RobotIcon, label: "Agent" } },
96
+ { type: "button", props: { label: "More" } },
97
+ ],
98
+ },
95
99
  };
96
100
 
97
101
  export const WithCounters: Story = {
98
- render: () => (
99
- <ButtonGroup variant="outline" size="sm">
100
- <Button label="Inbox" isCounter counterValue="5" />
101
- <Button label="Sent" isCounter counterValue="12" />
102
- <Button label="Drafts" isCounter counterValue="3" />
103
- </ButtonGroup>
104
- ),
102
+ args: {
103
+ items: [
104
+ {
105
+ type: "button",
106
+ props: { label: "Inbox", isCounter: true, counterValue: "5" },
107
+ },
108
+ {
109
+ type: "button",
110
+ props: { label: "Sent", isCounter: true, counterValue: "12" },
111
+ },
112
+ {
113
+ type: "button",
114
+ props: { label: "Drafts", isCounter: true, counterValue: "3" },
115
+ },
116
+ ],
117
+ },
105
118
  };
106
119
 
107
120
  export const Vertical: Story = {
108
- render: () => (
109
- <ButtonGroup variant="outline" size="sm" orientation="vertical">
110
- <Button label="First" />
111
- <Button label="Second" />
112
- <Button label="Third" />
113
- </ButtonGroup>
114
- ),
121
+ args: {
122
+ orientation: "vertical",
123
+ },
115
124
  };
116
125
 
117
126
  export const Disabled: Story = {
118
- render: () => (
119
- <ButtonGroup variant="outline" size="sm" disabled>
120
- <Button label="First" />
121
- <Button label="Second" />
122
- <Button label="Third" />
123
- </ButtonGroup>
124
- ),
127
+ args: {
128
+ disabled: true,
129
+ },
125
130
  };
126
131
 
127
132
  export const WithGaps: Story = {
128
- render: () => (
129
- <ButtonGroup variant="outline" size="sm" removeGaps={false}>
130
- <Button label="First" />
131
- <Button label="Second" />
132
- <Button label="Third" />
133
- </ButtonGroup>
134
- ),
133
+ args: {
134
+ removeGaps: false,
135
+ },
135
136
  };
136
137
 
138
+ const VARIANT_ITEMS: ButtonGroupItem[] = [
139
+ { type: "button", props: { label: "One" } },
140
+ { type: "button", props: { label: "Two" } },
141
+ { type: "button", props: { label: "Three" } },
142
+ ];
143
+
137
144
  const ButtonGroupByVariant = ({
138
145
  variant,
139
146
  }: {
@@ -143,21 +150,9 @@ const ButtonGroupByVariant = ({
143
150
  <Separator />
144
151
  <h3 className="s-text-primary dark:s-text-primary-50">{variant}</h3>
145
152
  <div className="s-flex s-items-center s-gap-4">
146
- <ButtonGroup variant={variant} size="xs">
147
- <Button label="One" />
148
- <Button label="Two" />
149
- <Button label="Three" />
150
- </ButtonGroup>
151
- <ButtonGroup variant={variant} size="sm">
152
- <Button label="One" />
153
- <Button label="Two" />
154
- <Button label="Three" />
155
- </ButtonGroup>
156
- <ButtonGroup variant={variant} size="md">
157
- <Button label="One" />
158
- <Button label="Two" />
159
- <Button label="Three" />
160
- </ButtonGroup>
153
+ <ButtonGroup variant={variant} size="xs" items={VARIANT_ITEMS} />
154
+ <ButtonGroup variant={variant} size="sm" items={VARIANT_ITEMS} />
155
+ <ButtonGroup variant={variant} size="md" items={VARIANT_ITEMS} />
161
156
  </div>
162
157
  </>
163
158
  );
@@ -172,3 +167,107 @@ export const Gallery: Story = {
172
167
  </div>
173
168
  ),
174
169
  };
170
+
171
+ export const WithDropdownMenu: Story = {
172
+ render: () => (
173
+ <div className="s-flex s-flex-col s-gap-4">
174
+ <div>
175
+ <h3 className="s-mb-2 s-text-sm s-font-medium">
176
+ Split button with dropdown
177
+ </h3>
178
+ <ButtonGroup
179
+ variant="outline"
180
+ items={[
181
+ {
182
+ type: "button",
183
+ props: {
184
+ icon: ClipboardIcon,
185
+ tooltip: "Copy to clipboard",
186
+ variant: "ghost-secondary",
187
+ size: "xs",
188
+ },
189
+ },
190
+ {
191
+ type: "dropdown",
192
+ triggerProps: {
193
+ variant: "ghost-secondary",
194
+ size: "xs",
195
+ icon: ChevronDownIcon,
196
+ },
197
+ dropdownProps: {
198
+ items: [
199
+ { label: "Retry", icon: ArrowPathIcon },
200
+ { label: "Delete", icon: TrashIcon, variant: "warning" },
201
+ ],
202
+ },
203
+ },
204
+ ]}
205
+ />
206
+ </div>
207
+
208
+ <div>
209
+ <h3 className="s-mb-2 s-text-sm s-font-medium">Multiple variations</h3>
210
+ <div className="s-flex s-flex-wrap s-gap-4">
211
+ <ButtonGroup
212
+ variant="outline"
213
+ items={[
214
+ { type: "button", props: { label: "Copy", size: "sm" } },
215
+ {
216
+ type: "dropdown",
217
+ triggerProps: { size: "sm", icon: ChevronDownIcon },
218
+ dropdownProps: {
219
+ items: [
220
+ { label: "Option 1" },
221
+ { label: "Option 2" },
222
+ { label: "Option 3" },
223
+ ],
224
+ },
225
+ },
226
+ ]}
227
+ />
228
+
229
+ <ButtonGroup
230
+ variant="primary"
231
+ items={[
232
+ { type: "button", props: { label: "Save", size: "sm" } },
233
+ {
234
+ type: "dropdown",
235
+ triggerProps: { size: "sm", icon: ChevronDownIcon },
236
+ dropdownProps: {
237
+ items: [
238
+ { label: "Save and close" },
239
+ { label: "Save as draft" },
240
+ ],
241
+ },
242
+ },
243
+ ]}
244
+ />
245
+
246
+ <ButtonGroup
247
+ variant="outline"
248
+ items={[
249
+ {
250
+ type: "button",
251
+ props: { icon: PlusIcon, label: "Add", size: "sm" },
252
+ },
253
+ {
254
+ type: "button",
255
+ props: { icon: RobotIcon, label: "Agent", size: "sm" },
256
+ },
257
+ {
258
+ type: "dropdown",
259
+ triggerProps: { size: "sm", icon: ChevronDownIcon },
260
+ dropdownProps: {
261
+ items: [
262
+ { label: "More options", icon: PlusIcon },
263
+ { label: "Settings" },
264
+ ],
265
+ },
266
+ },
267
+ ]}
268
+ />
269
+ </div>
270
+ </div>
271
+ </div>
272
+ ),
273
+ };