@stackloop/ui 4.1.2 → 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
@@ -225,9 +225,18 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
225
225
  **ButtonGroup**:
226
226
  - **Description:** Segmented button group with a rounded outer container and separate buttons divided by borders.
227
227
  - **Props:**
228
- - **`options`**: `{ value: string; label: string; icon?: ReactNode; disabled?: boolean }[]` — required.
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.
229
237
  - **`value`**: `string` — optional selected value.
230
238
  - **`onChange`**: `(value: string) => void` — optional change callback.
239
+ - **`onOptionClick`**: `(option, index) => void` — optional group-level callback for any option click.
231
240
  - **`size`**: `'sm' | 'md' | 'lg'` — default: `'md'`.
232
241
  - **`disabled`**: `boolean` — default: `false`.
233
242
  - **`className`**: `string` — optional.
@@ -248,6 +257,32 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
248
257
  value={selectedRange}
249
258
  onChange={setSelectedRange}
250
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
+ />
251
286
  ```
252
287
 
253
288
  **Input**:
@@ -652,20 +687,72 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
652
687
  />
653
688
  ```
654
689
 
655
- **Dropdown**:
656
- - **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.
657
- - **Props:**
658
- - **`options`**: `{ value: string; label: string; icon?: ReactNode }[]` required.
659
- - **`value`**: `string` optional.
660
- - **`onChange`**: `(value: string) => void` — required.
661
- - **`placeholder`**: `string` default: `'Select an option'`.
662
- - **`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.
663
712
  - **Usage:**
664
713
 
665
714
  ```jsx
666
- import { Dropdown } from '@stackloop/ui'
667
-
668
- <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>
669
756
  ```
670
757
 
671
758
  **Tooltip**:
@@ -695,7 +782,7 @@ Call `setupRippleEffects()` only once per app (for example in `main.tsx`) to avo
695
782
  ```
696
783
 
697
784
  **Select**:
698
- - **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.).
699
786
  - **Props:**
700
787
  - **`options`**: `{ value: string; label: string; icon?: ReactNode; disabled?: boolean }[]` — required. Array of selectable options with optional icons and disabled state.
701
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 {};