@stackloop/ui 4.1.1 → 4.2.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/README.md CHANGED
@@ -73,7 +73,7 @@ import '@stackloop/ui/theme.css'
73
73
  Import components from the package root:
74
74
 
75
75
  ```tsx
76
- import { Button, Modal, Input } from '@stackloop/ui'
76
+ import { Button, ButtonGroup, Modal, Input } from '@stackloop/ui'
77
77
  ```
78
78
 
79
79
  All components are **client-side** components with `'use client'` directive, making them compatible with Next.js App Router.
@@ -160,7 +160,7 @@ import { Button, Modal } from '@stackloop/ui'
160
160
 
161
161
  Components with the `animate` prop:
162
162
 
163
- - `Accordion`, `AudioRecorder`, `Badge`, `BottomSheet`, `Button`, `Card`, `CameraCapture`, `Checkbox`, `CountrySelect`, `DatePicker`, `Drawer`, `Dropdown`, `DualSlider`, `FileUploader`, `FloatingActionButton`, `Input`, `Modal`, `MultiSelect`, `Pagination`, `PhoneInput`, `RadioPills`, `Select`, `Slider`, `Spinner`, `StepProgress`, `Table`, `Textarea`, `ThumbnailGrid`, `Toggle`, `ToastProvider`
163
+ - `Accordion`, `AudioRecorder`, `Badge`, `BottomSheet`, `Button`, `ButtonGroup`, `Card`, `CameraCapture`, `Checkbox`, `CountrySelect`, `DatePicker`, `Drawer`, `Dropdown`, `DualSlider`, `FileUploader`, `FloatingActionButton`, `Input`, `Modal`, `MultiSelect`, `Pagination`, `PhoneInput`, `RadioPills`, `Select`, `Slider`, `Spinner`, `StepProgress`, `Table`, `Textarea`, `ThumbnailGrid`, `Toggle`, `ToastProvider`
164
164
 
165
165
  ## Ripple Behavior
166
166
 
@@ -222,6 +222,69 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
222
222
  <Button variant="outline" size="lg" onClick={() => {}}>Save</Button>
223
223
  ```
224
224
 
225
+ **ButtonGroup**:
226
+ - **Description:** Segmented button group with a rounded outer container and separate buttons divided by borders.
227
+ - **Props:**
228
+ - **`options`**: `ButtonGroupOption[]` — required.
229
+ - `value`: `string` — required unique option value.
230
+ - `label`: `string` — required fallback text label.
231
+ - `icon`: `ReactNode` — optional leading icon.
232
+ - `disabled`: `boolean` — optional per-option disabled state.
233
+ - `className`: `string` — optional per-option class override.
234
+ - `ariaLabel`: `string` — optional aria-label for accessibility.
235
+ - `render`: `(option, state) => ReactNode` — optional custom content renderer. If omitted, the default icon + label layout is used.
236
+ - `onClick`: `(option, event) => void` — optional per-option click callback.
237
+ - **`value`**: `string` — optional selected value.
238
+ - **`onChange`**: `(value: string) => void` — optional change callback.
239
+ - **`onOptionClick`**: `(option, index) => void` — optional group-level callback for any option click.
240
+ - **`size`**: `'sm' | 'md' | 'lg'` — default: `'md'`.
241
+ - **`disabled`**: `boolean` — default: `false`.
242
+ - **`className`**: `string` — optional.
243
+ - **`animate`**: `boolean` — default: `true`.
244
+ - **Usage:**
245
+
246
+ ```jsx
247
+ import { ButtonGroup } from '@stackloop/ui'
248
+
249
+ const options = [
250
+ { label: 'Day', value: 'day' },
251
+ { label: 'Week', value: 'week' },
252
+ { label: 'Month', value: 'month' }
253
+ ]
254
+
255
+ <ButtonGroup
256
+ options={options}
257
+ value={selectedRange}
258
+ onChange={setSelectedRange}
259
+ />
260
+
261
+ // Custom render + per-option and group-level click hooks
262
+ const customOptions = [
263
+ {
264
+ label: 'Overview',
265
+ value: 'overview',
266
+ render: (option, state) => (
267
+ <span className="flex flex-col items-start leading-tight">
268
+ <span className="text-[10px] uppercase tracking-[0.2em] opacity-70">View</span>
269
+ <span className={state.selected ? 'font-semibold' : 'font-medium'}>{option.label}</span>
270
+ </span>
271
+ ),
272
+ onClick: (option) => console.log('Option clicked:', option.value)
273
+ },
274
+ { label: 'Details', value: 'details' },
275
+ { label: 'Activity', value: 'activity' }
276
+ ]
277
+
278
+ <ButtonGroup
279
+ options={customOptions}
280
+ value={selectedView}
281
+ onChange={setSelectedView}
282
+ onOptionClick={(option, index) => {
283
+ console.log('Group handler:', option.value, index)
284
+ }}
285
+ />
286
+ ```
287
+
225
288
  **Input**:
226
289
  - **Description:** Unified input API with smart type routing. Supports native text/password/email/etc plus `phone`, `country`, and `date` while keeping a consistent `value` + `onChange` pattern.
227
290
  - **Props:**
@@ -624,20 +687,72 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
624
687
  />
625
688
  ```
626
689
 
627
- **Dropdown**:
628
- - **Description:** General-purpose select component with optional search, clear, and icons. Use this for general UI selections outside of forms. For form-specific needs with validation, use the **Select** component instead.
629
- - **Props:**
630
- - **`options`**: `{ value: string; label: string; icon?: ReactNode }[]` required.
631
- - **`value`**: `string` optional.
632
- - **`onChange`**: `(value: string) => void` — required.
633
- - **`placeholder`**: `string` default: `'Select an option'`.
634
- - **`label`**, **`error`**, **`searchable`** (default `false`), **`clearable`** (default `true`), **`disabled`**, **`className`**.
690
+ **DropdownMenu**:
691
+ - **Description:** Headless-style dropdown menu primitive with custom trigger support, outside-click close behavior, and nested submenus.
692
+ - **Placement behavior:**
693
+ - Root menu opens **down or up** based on viewport space (or can be forced).
694
+ - Nested submenus open **right or left** based on side space (or can be forced).
695
+ - **Use case:**
696
+ - Nested navigation or action menus where each menu item can itself open another submenu.
697
+ - Context menus launched from custom triggers like icon buttons, cards, or toolbar actions.
698
+ - **Core Components:**
699
+ - **`DropdownMenu`**: Root wrapper with controlled/uncontrolled open state.
700
+ - **`DropdownMenuTrigger`**: Trigger button or custom trigger with `asChild`.
701
+ - **`DropdownMenuContent`**: Root menu panel.
702
+ - **`DropdownMenuItem`**: Clickable item (button or custom child with `asChild`).
703
+ - **`DropdownMenuSub`**: Nested menu wrapper.
704
+ - **`DropdownMenuSubTrigger`**: Trigger for nested menu.
705
+ - **`DropdownMenuSubContent`**: Nested panel.
706
+ - **Root Props (`DropdownMenu`):**
707
+ - **`open`**: `boolean` — optional controlled state.
708
+ - **`defaultOpen`**: `boolean` — default: `false`.
709
+ - **`onOpenChange`**: `(open: boolean) => void` — optional callback.
710
+ - **`placement`**: `'auto' | 'top' | 'bottom'` — default: `'auto'`.
711
+ - **`className`**: `string` — optional.
635
712
  - **Usage:**
636
713
 
637
714
  ```jsx
638
- import { Dropdown } from '@stackloop/ui'
639
-
640
- <Dropdown options={[{value:'a',label:'A'}]} value={val} onChange={setVal} searchable />
715
+ import {
716
+ DropdownMenu,
717
+ DropdownMenuTrigger,
718
+ DropdownMenuContent,
719
+ DropdownMenuItem,
720
+ DropdownMenuSub,
721
+ DropdownMenuSubTrigger,
722
+ DropdownMenuSubContent
723
+ } from '@stackloop/ui'
724
+
725
+ <DropdownMenu>
726
+ <DropdownMenuTrigger>Open menu</DropdownMenuTrigger>
727
+ <DropdownMenuContent>
728
+ <DropdownMenuItem asChild>
729
+ <a href="/dashboard">Dashboard</a>
730
+ </DropdownMenuItem>
731
+
732
+ <DropdownMenuSub>
733
+ <DropdownMenuSubTrigger>Products</DropdownMenuSubTrigger>
734
+ <DropdownMenuSubContent>
735
+ <DropdownMenuItem asChild>
736
+ <a href="/products/new">New Arrivals</a>
737
+ </DropdownMenuItem>
738
+
739
+ <DropdownMenuSub>
740
+ <DropdownMenuSubTrigger>Categories</DropdownMenuSubTrigger>
741
+ <DropdownMenuSubContent>
742
+ <DropdownMenuItem asChild>
743
+ <a href="/products/categories/tools">Tools</a>
744
+ </DropdownMenuItem>
745
+ <DropdownMenuItem asChild>
746
+ <a href="/products/categories/home">Home</a>
747
+ </DropdownMenuItem>
748
+ </DropdownMenuSubContent>
749
+ </DropdownMenuSub>
750
+ </DropdownMenuSubContent>
751
+ </DropdownMenuSub>
752
+
753
+ <DropdownMenuItem onClick={() => console.log('Signed out')}>Sign out</DropdownMenuItem>
754
+ </DropdownMenuContent>
755
+ </DropdownMenu>
641
756
  ```
642
757
 
643
758
  **Tooltip**:
@@ -667,7 +782,7 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
667
782
  ```
668
783
 
669
784
  **Select**:
670
- - **Description:** **Form-specific** select component with label, error, hint, and validation support. Specifically designed for use in forms with proper semantics, accessibility, and validation integration. Use this component when building forms. For general UI selections (navigation, filters, etc.), use the **Dropdown** component instead. Based on Dropdown but includes form-specific features like `required` prop, hint text, and better integration with form libraries (React Hook Form, Formik, etc.).
785
+ - **Description:** **Form-specific** select component with label, error, hint, and validation support. Specifically designed for use in forms with proper semantics, accessibility, and validation integration. Use this component when building forms. It includes form-focused features like `required`, hint text, and better integration with form libraries (React Hook Form, Formik, etc.).
671
786
  - **Props:**
672
787
  - **`options`**: `{ value: string; label: string; icon?: ReactNode; disabled?: boolean }[]` — required. Array of selectable options with optional icons and disabled state.
673
788
  - **`value`**: `string` — optional. Currently selected value.
@@ -4,11 +4,20 @@ export interface ButtonGroupOption {
4
4
  label: string;
5
5
  icon?: React.ReactNode;
6
6
  disabled?: boolean;
7
+ className?: string;
8
+ ariaLabel?: string;
9
+ render?: (option: ButtonGroupOption, state: {
10
+ selected: boolean;
11
+ disabled: boolean;
12
+ index: number;
13
+ }) => React.ReactNode;
14
+ onClick?: (option: ButtonGroupOption, event: React.MouseEvent<HTMLButtonElement>) => void;
7
15
  }
8
16
  export interface ButtonGroupProps {
9
17
  options: ButtonGroupOption[];
10
18
  value?: string;
11
19
  onChange?: (value: string) => void;
20
+ onOptionClick?: (option: ButtonGroupOption, index: number) => void;
12
21
  size?: 'sm' | 'md' | 'lg';
13
22
  disabled?: boolean;
14
23
  className?: string;
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { type HTMLMotionProps } from 'framer-motion';
3
+ type RootVerticalPlacement = 'auto' | 'top' | 'bottom';
4
+ type SubmenuHorizontalPlacement = 'auto' | 'left' | 'right';
5
+ export interface DropdownMenuProps {
6
+ children: React.ReactNode;
7
+ open?: boolean;
8
+ defaultOpen?: boolean;
9
+ onOpenChange?: (open: boolean) => void;
10
+ placement?: RootVerticalPlacement;
11
+ className?: string;
12
+ }
13
+ export declare const DropdownMenu: React.FC<DropdownMenuProps>;
14
+ export interface DropdownMenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
15
+ asChild?: boolean;
16
+ children: React.ReactNode;
17
+ }
18
+ export declare const DropdownMenuTrigger: React.ForwardRefExoticComponent<DropdownMenuTriggerProps & React.RefAttributes<HTMLElement>>;
19
+ export interface DropdownMenuContentProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
20
+ children: React.ReactNode;
21
+ align?: 'start' | 'end';
22
+ sideOffset?: number;
23
+ animate?: boolean;
24
+ }
25
+ export declare const DropdownMenuContent: React.FC<DropdownMenuContentProps>;
26
+ export interface DropdownMenuItemProps extends React.HTMLAttributes<HTMLElement> {
27
+ children: React.ReactNode;
28
+ asChild?: boolean;
29
+ disabled?: boolean;
30
+ closeOnSelect?: boolean;
31
+ }
32
+ export declare const DropdownMenuItem: React.ForwardRefExoticComponent<DropdownMenuItemProps & React.RefAttributes<HTMLElement>>;
33
+ export interface DropdownMenuSubProps {
34
+ children: React.ReactNode;
35
+ defaultOpen?: boolean;
36
+ placement?: SubmenuHorizontalPlacement;
37
+ }
38
+ export declare const DropdownMenuSub: React.FC<DropdownMenuSubProps>;
39
+ export interface DropdownMenuSubTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
40
+ asChild?: boolean;
41
+ children: React.ReactNode;
42
+ }
43
+ export declare const DropdownMenuSubTrigger: React.ForwardRefExoticComponent<DropdownMenuSubTriggerProps & React.RefAttributes<HTMLElement>>;
44
+ export interface DropdownMenuSubContentProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
45
+ children: React.ReactNode;
46
+ sideOffset?: number;
47
+ animate?: boolean;
48
+ }
49
+ export declare const DropdownMenuSubContent: React.FC<DropdownMenuSubContentProps>;
50
+ export {};