@ideasui/button 0.0.2-beta.1 → 0.0.2-canary.1786301183157

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
@@ -56,19 +56,20 @@ function ActionMenu() {
56
56
 
57
57
  ### Button Props
58
58
 
59
- | Prop | Type | Default | Description |
60
- | :----------- | :------------------------------------------------- | :---------- | :--------------------------------------------- |
61
- | `variant` | `'solid' \| 'outline' \| 'ghost'` | `'solid'` | Visual style variant. |
62
- | `color` | `SemanticColor` | `'default'` | Semantic color intent (primary, success, etc). |
63
- | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | The size of the button. |
64
- | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | The corner radius. |
65
- | `loading` | `boolean` | `false` | Shows a spinner and disables interaction. |
66
- | `disabled` | `boolean` | `false` | Disables the button. |
67
- | `fullWidth` | `boolean` | `false` | Expands to fill the container width. |
68
- | `startIcon` | `ReactNode` | — | Icon displayed before the label. |
69
- | `endIcon` | `ReactNode` | — | Icon displayed after the label. |
70
- | `shortcut` | `string` | — | Keyboard shortcut hint. |
71
- | `classNames` | `ButtonClassNames` | — | Custom classes for internal slots. |
59
+ | Prop | Type | Default | Description |
60
+ | :----------- | :------------------------------------------------- | :---------- | :-------------------------------------------------------------------------------------------------------------- |
61
+ | `variant` | `'solid' \| 'outline' \| 'ghost'` | `'solid'` | Visual style variant. |
62
+ | `color` | `SemanticColor` | `'default'` | Semantic color intent (primary, success, etc). |
63
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | The size of the button. |
64
+ | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | The corner radius. |
65
+ | `loading` | `boolean` | `false` | Shows a spinner and disables interaction. |
66
+ | `disabled` | `boolean` | `false` | Disables the button. |
67
+ | `fullWidth` | `boolean` | `false` | Expands to fill the container width. |
68
+ | `isIconOnly` | `boolean` | `false` | Formats the button for icon-only display (square, equal padding). Enforces accessibility props at compile-time. |
69
+ | `startIcon` | `ReactNode` | — | Icon displayed before the label. |
70
+ | `endIcon` | `ReactNode` | — | Icon displayed after the label. |
71
+ | `shortcut` | `string` | — | Keyboard shortcut hint. |
72
+ | `classNames` | `ButtonClassNames` | — | Custom classes for internal slots. |
72
73
 
73
74
  ### Granular Styling (`classNames`)
74
75
 
@@ -93,6 +94,7 @@ The `classNames` prop allows you to override styles for specific internal elemen
93
94
  - Proper `aria-live` regions for loading states.
94
95
  - High-contrast focus rings for keyboard navigation.
95
96
  - Semantic HTML tags based on the `as` prop.
97
+ - **Icon-Only Trigger Safety**: When `isIconOnly` is `true`, TypeScript compile-time checks enforce that either `aria-label` or `aria-labelledby` is supplied to ensure the button has a screen-readable name (WCAG 2.1 AA).
96
98
 
97
99
  ## License
98
100
 
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import {Button}from'react-aria-components';import {cn,mergePropsWithContext,getAccessibleName}from'@ideasui/utils';import {createContext,forwardRef,useContext}from'react';import {button}from'@ideasui/theme/recipes';import {jsx,jsxs,Fragment}from'react/jsx-runtime';var C=createContext(null);function E(){return useContext(C)}var v=forwardRef(({children:o,className:c,size:t,color:s,variant:u,isDisabled:a,isAttached:n=true,isVertical:l=false,radius:d,fullWidth:i,disableAnimation:p=true,showDivider:m,...B},h)=>{let e={size:t,color:s,variant:u,isDisabled:a,isAttached:n,isVertical:l,radius:d,fullWidth:i,disableAnimation:p,showDivider:m},I=cn(i?"flex":"inline-flex",l?"flex-col":"flex-row",n?"isolate":l?"gap-y-2":"gap-x-2",i&&"w-full",c);return jsx(C.Provider,{value:e,children:jsx("div",{ref:h,className:I,"data-attached":n,"data-full-width":i,"data-slot":"button-group","data-vertical":l,role:"group",...B,children:o})})});v.displayName="IdeasUI.Button.Group";var T=createContext(null);function P(){let o=useContext(T);if(!o)throw new Error("Button sub-components must be rendered within a Button component");return o}var f=forwardRef(({children:o,className:c,placement:t="start",...s},u)=>{let{styles:a,classNames:n}=P(),{icon:l}=a,d=t==="start"?n?.startIcon:n?.endIcon;return jsx("span",{ref:u,"aria-hidden":"true",className:cn(l(),t==="end"?"order-last":"order-first",d,c),"data-slot":"button-icon",...s,children:o})});f.displayName="IdeasUI.Button.Icon";var g=forwardRef(({className:o,label:c="Loading",...t},s)=>{let{styles:u,classNames:a}=P(),{loader:n,icon:l}=u;return jsxs("span",{ref:s,className:cn(n(),l(),"inline-flex shrink-0 items-center justify-center",a?.spinner,o),"data-slot":"button-spinner",role:"status",...t,children:[jsx("svg",{className:"size-full animate-spin",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2.5",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"})}),jsx("span",{className:"sr-only",children:c})]})});g.displayName="IdeasUI.Button.Spinner";var S=forwardRef(({children:o,className:c,...t},s)=>{let{styles:u,classNames:a}=P(),{shortcut:n}=u;return jsx("kbd",{ref:s,className:cn(n(),a?.shortcut,c),"data-slot":"button-shortcut",...t,children:o})});S.displayName="IdeasUI.Button.Shortcut";var L=forwardRef(({children:o,className:c,...t},s)=>{let{styles:u,classNames:a}=P(),{label:n}=u;return jsx("span",{ref:s,className:cn(n(),a?.label,c),"data-slot":"button-label",...t,children:o})});L.displayName="IdeasUI.Button.Label";var j=({isLoading:o,loadingIndicator:c,loadingPosition:t="start",isIconOnly:s,startIcon:u,endIcon:a,shortcut:n,children:l,renderProps:d})=>{let i=typeof l=="function"?l(d):l,p=c||jsx(g,{});if(s||o&&t==="center")return o?p:i;let m=o&&t==="start",B=o&&t==="end";return jsxs(Fragment,{children:[m?jsx(f,{placement:"start",children:p}):null,u&&!m?jsx(f,{placement:"start",children:u}):null,typeof i=="string"||typeof i=="number"?jsx(L,{children:i}):i,a&&!B?jsx(f,{placement:"end",children:a}):null,B?jsx(f,{placement:"end",children:p}):null,n?jsx(S,{children:n}):null]})},D=forwardRef((o,c)=>{let{isLoading:t,loadingIndicator:s,loadingPosition:u,isIconOnly:a,startIcon:n,endIcon:l,shortcut:d,children:i,className:p,classNames:m,...B}=o,h=E(),e=mergePropsWithContext(o,h),I=e.isDisabled||t,w=button({variant:e.variant,size:e.size,color:e.color,radius:e.radius,fullWidth:e.fullWidth,isIconOnly:a,isDisabled:I,isLoading:t,disableAnimation:e.disableAnimation,isAttached:e.isAttached,isVertical:e.isVertical,elevation:e.elevation,showDivider:e.showDivider??e.isAttached}),R=getAccessibleName(e,typeof i=="function"?null:i);return jsx(Button,{ref:c,"aria-busy":t,"aria-label":t&&!R?typeof s=="string"?s:"Loading":R,className:N=>w.base({className:cn(typeof p=="function"?p(N):p,m?.base)}),"data-attached":e.isAttached,"data-slot":"button","data-vertical":e.isVertical,isDisabled:I,isPending:t,...B,children:N=>jsx(T.Provider,{value:{styles:w,classNames:m},children:jsx(j,{endIcon:l,isIconOnly:a,isLoading:t,loadingIndicator:s,loadingPosition:u,renderProps:N,shortcut:d,startIcon:n,children:i})})})});D.displayName="IdeasUI.Button";var x=D;x.Icon=f;x.Shortcut=S;x.Spinner=g;x.Label=L;x.Group=v;
2
- export{x as Button,v as ButtonGroup,C as ButtonGroupContext,E as useButtonGroupContext};
1
+ import {Button}from'react-aria-components';import {cn,mergePropsWithContext,getAccessibleName}from'@ideasui/utils';import {createContext,forwardRef,useMemo,useContext}from'react';import {button}from'@ideasui/theme/recipes';import {jsx,jsxs,Fragment}from'react/jsx-runtime';var g=createContext(null);function G(){return useContext(g)}var S=forwardRef(({children:o,className:c,size:t,color:r,variant:a,isDisabled:l,isAttached:n=true,isVertical:u=false,radius:d="md",fullWidth:i,disableAnimation:p=false,divider:m="full",isIconOnly:B,...P},e)=>{let C=useMemo(()=>({size:t,color:r,variant:a,isDisabled:l,isAttached:n,isVertical:u,radius:d,fullWidth:i,disableAnimation:p,divider:m,isIconOnly:B}),[t,r,a,l,n,u,d,i,p,m,B]),b="isolate";n||(b=u?"gap-y-2":"gap-x-2");let N=cn(i?"flex":"inline-flex",u?"flex-col":"flex-row",b,i&&"w-full",c);return jsx(g.Provider,{value:C,children:jsx("div",{ref:e,className:N,"data-attached":n,"data-full-width":i,"data-slot":"button-group","data-vertical":u,...P,children:o})})});S.displayName="IdeasUI.Button.Group";var M=createContext(null);function h(){let o=useContext(M);if(!o)throw new Error("Button sub-components must be rendered within a Button component");return o}var f=forwardRef(({children:o,className:c,placement:t="start",...r},a)=>{let{styles:l,classNames:n}=h(),{icon:u}=l,d=t==="start"?n?.startIcon:n?.endIcon;return jsx("span",{ref:a,"aria-hidden":"true",className:cn(u(),t==="end"?"order-last":"order-first",d,c),"data-slot":"button-icon",...r,children:o})});f.displayName="IdeasUI.Button.Icon";var L=forwardRef(({className:o,label:c="Loading",...t},r)=>{let{styles:a,classNames:l}=h(),{loader:n,icon:u}=a;return jsxs("span",{ref:r,className:cn(n(),u(),"inline-flex shrink-0 items-center justify-center",l?.spinner,o),"data-slot":"button-spinner",...t,children:[jsx("svg",{className:"size-full animate-spin",fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"2.5",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:jsx("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"})}),jsx("span",{className:"sr-only",children:c})]})});L.displayName="IdeasUI.Button.Spinner";var w=forwardRef(({children:o,className:c,...t},r)=>{let{styles:a,classNames:l}=h(),{shortcut:n}=a;return jsx("kbd",{ref:r,className:cn(n(),l?.shortcut,c),"data-slot":"button-shortcut",...t,children:o})});w.displayName="IdeasUI.Button.Shortcut";var R=forwardRef(({children:o,className:c,...t},r)=>{let{styles:a,classNames:l}=h(),{label:n}=a;return jsx("span",{ref:r,className:cn(n(),l?.label,c),"data-slot":"button-label",...t,children:o})});R.displayName="IdeasUI.Button.Label";var F=({isLoading:o,loadingIndicator:c,loadingPosition:t="start",isIconOnly:r,startIcon:a,endIcon:l,shortcut:n,children:u,renderProps:d})=>{let i=typeof u=="function"?u(d):u,p=c||jsx(L,{});if(r||o&&t==="center")return o?p:i;let m=o&&t==="start",B=o&&t==="end";return jsxs(Fragment,{children:[m?jsx(f,{placement:"start",children:p}):null,a&&!m?jsx(f,{placement:"start",children:a}):null,typeof i=="string"||typeof i=="number"?jsx(R,{children:i}):i,l&&!B?jsx(f,{placement:"end",children:l}):null,B?jsx(f,{placement:"end",children:p}):null,n?jsx(w,{children:n}):null]})},H=forwardRef((o,c)=>{let{isLoading:t,loadingIndicator:r,loadingPosition:a,isIconOnly:l,startIcon:n,endIcon:u,shortcut:d,children:i,className:p,classNames:m,...B}=o,P=G(),e=mergePropsWithContext(o,P),C=e.isDisabled||t,b=button({variant:e.variant,size:e.size,color:e.color,radius:e.radius,fullWidth:e.fullWidth,isIconOnly:e.isIconOnly,isDisabled:C,isLoading:t,disableAnimation:e.disableAnimation,isAttached:e.isAttached,isVertical:e.isVertical,elevation:e.elevation,divider:e.divider||(e.isAttached?"full":"none")}),N={styles:b,classNames:m},E=getAccessibleName(e,typeof i=="function"?null:i);return jsx(Button,{ref:c,"aria-busy":t,"aria-label":t&&!E?typeof r=="string"?r:"Loading":E,className:v=>b.base({className:cn(typeof p=="function"?p(v):p,m?.base)}),"data-attached":e.isAttached,"data-slot":"button","data-vertical":e.isVertical,isDisabled:C,isPending:t,...B,children:v=>jsx(M.Provider,{value:N,children:jsx(F,{endIcon:u,isIconOnly:e.isIconOnly,isLoading:t,loadingIndicator:r,loadingPosition:a,renderProps:v,shortcut:d,startIcon:n,children:i})})})});H.displayName="IdeasUI.Button";var I=H;I.Icon=f;I.Shortcut=w;I.Spinner=L;I.Label=R;I.Group=S;
2
+ export{I as Button,S as ButtonGroup,g as ButtonGroupContext,G as useButtonGroupContext};
package/package.json CHANGED
@@ -1,22 +1,26 @@
1
1
  {
2
2
  "name": "@ideasui/button",
3
- "version": "0.0.2-beta.1",
4
- "description": "A high-performance, accessible Button and ButtonGroup component library for React, built with Tailwind CSS v4 and React Aria.",
5
- "author": "IdeasUI<support@ideasui.com>",
3
+ "version": "0.0.2-canary.1786301183157",
4
+ "description": "Accessible, high-performance Button and ButtonGroup components for React. Built with Tailwind CSS v4, React Aria, and OKLCH color palettes.",
5
+ "author": "IdeasUI<ideas2logic@gmail.com>",
6
6
  "homepage": "https://ideasui.com",
7
7
  "license": "MIT",
8
8
  "keywords": [
9
- "ideasui",
10
9
  "react",
11
10
  "button",
12
11
  "button-group",
13
- "button-component",
14
- "accessible-components",
15
- "ui-components",
12
+ "react-aria",
13
+ "tailwindcss",
14
+ "tailwind-v4",
15
+ "accessible-ui",
16
+ "wcag",
17
+ "ideasui",
18
+ "components",
16
19
  "design-system",
17
- "tailwindcss"
20
+ "headless-ui"
18
21
  ],
19
22
  "main": "dist/index.mjs",
23
+ "types": "dist/index.d.mts",
20
24
  "exports": {
21
25
  ".": {
22
26
  "types": "./dist/index.d.mts",
@@ -46,9 +50,8 @@
46
50
  },
47
51
  "dependencies": {
48
52
  "react-aria-components": "^1.16.0",
49
- "@ideasui/theme": "0.0.5-beta.1",
50
- "@ideasui/utils": "0.0.2-beta.1"
53
+ "@ideasui/theme": "0.0.5-canary.1786301183157",
54
+ "@ideasui/utils": "0.0.2-canary.1786301183157"
51
55
  },
52
- "module": "dist/index.mjs",
53
- "types": "dist/index.d.mts"
56
+ "module": "dist/index.mjs"
54
57
  }
@@ -1,20 +1,21 @@
1
1
  'use client';
2
2
 
3
- import type { ButtonProps } from './button.types';
3
+ import type { ButtonProps as ButtonProperties } from './button.types';
4
4
 
5
5
  import { createContext, useContext } from 'react';
6
6
 
7
7
  export interface ButtonGroupContextType {
8
- size?: ButtonProps['size'];
9
- color?: ButtonProps['color'];
10
- variant?: ButtonProps['variant'];
8
+ size?: ButtonProperties['size'];
9
+ color?: ButtonProperties['color'];
10
+ variant?: ButtonProperties['variant'];
11
11
  isDisabled?: boolean;
12
12
  isAttached?: boolean;
13
13
  isVertical?: boolean;
14
- radius?: ButtonProps['radius'];
14
+ radius?: ButtonProperties['radius'];
15
15
  fullWidth?: boolean;
16
16
  disableAnimation?: boolean;
17
- showDivider?: boolean;
17
+ divider?: ButtonProperties['divider'];
18
+ isIconOnly?: boolean;
18
19
  }
19
20
 
20
21
  export const ButtonGroupContext = createContext<ButtonGroupContextType | null>(null);
@@ -3,7 +3,7 @@
3
3
  import type { HTMLAttributes, ReactNode, JSX } from 'react';
4
4
  import type { ButtonGroupContextType } from './button-group-context';
5
5
 
6
- import { forwardRef } from 'react';
6
+ import { forwardRef, useMemo } from 'react';
7
7
  import { cn } from '@ideasui/utils';
8
8
 
9
9
  import { ButtonGroupContext } from './button-group-context';
@@ -12,7 +12,7 @@ import { ButtonGroupContext } from './button-group-context';
12
12
  * ButtonGroup Component
13
13
  * ---------------------------------------------------------------------------------------------*/
14
14
 
15
- export interface ButtonGroupProps
15
+ export interface ButtonGroupProperties
16
16
  extends Omit<HTMLAttributes<HTMLDivElement>, 'color'>, ButtonGroupContextType {
17
17
  /**
18
18
  * Whether the buttons in the group should be vertical.
@@ -25,17 +25,17 @@ export interface ButtonGroupProps
25
25
  */
26
26
  isAttached?: boolean;
27
27
  /**
28
- * Whether to show a divider between the buttons in the group.
29
- * @default true (when isAttached is true)
28
+ * The type of divider to display between the buttons in the group.
29
+ * @default 'full' (when isAttached is true)
30
30
  */
31
- showDivider?: boolean;
31
+ divider?: 'full' | 'middle' | 'none';
32
32
  /**
33
33
  * The content of the button group.
34
34
  */
35
35
  children: ReactNode;
36
36
  }
37
37
 
38
- export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
38
+ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProperties>(
39
39
  (
40
40
  {
41
41
  children,
@@ -46,31 +46,54 @@ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
46
46
  isDisabled,
47
47
  isAttached = true,
48
48
  isVertical = false,
49
- radius,
49
+ radius = 'md',
50
50
  fullWidth,
51
- disableAnimation = true,
52
- showDivider,
53
- ...props
51
+ disableAnimation = false,
52
+ divider = 'full',
53
+ isIconOnly,
54
+ ...properties
54
55
  },
55
- ref,
56
+ reference,
56
57
  ): JSX.Element => {
57
- const contextValue = {
58
- size,
59
- color,
60
- variant,
61
- isDisabled,
62
- isAttached,
63
- isVertical,
64
- radius,
65
- fullWidth,
66
- disableAnimation,
67
- showDivider,
68
- };
58
+ const contextValue = useMemo(
59
+ () => ({
60
+ size,
61
+ color,
62
+ variant,
63
+ isDisabled,
64
+ isAttached,
65
+ isVertical,
66
+ radius,
67
+ fullWidth,
68
+ disableAnimation,
69
+ divider,
70
+ isIconOnly,
71
+ }),
72
+ [
73
+ size,
74
+ color,
75
+ variant,
76
+ isDisabled,
77
+ isAttached,
78
+ isVertical,
79
+ radius,
80
+ fullWidth,
81
+ disableAnimation,
82
+ divider,
83
+ isIconOnly,
84
+ ],
85
+ );
86
+
87
+ let gapClass = 'isolate';
88
+
89
+ if (!isAttached) {
90
+ gapClass = isVertical ? 'gap-y-2' : 'gap-x-2';
91
+ }
69
92
 
70
93
  const groupClasses = cn(
71
94
  fullWidth ? 'flex' : 'inline-flex',
72
95
  isVertical ? 'flex-col' : 'flex-row',
73
- isAttached ? 'isolate' : isVertical ? 'gap-y-2' : 'gap-x-2',
96
+ gapClass,
74
97
  fullWidth && 'w-full',
75
98
  className,
76
99
  );
@@ -78,14 +101,13 @@ export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>(
78
101
  return (
79
102
  <ButtonGroupContext.Provider value={contextValue}>
80
103
  <div
81
- ref={ref}
104
+ ref={reference}
82
105
  className={groupClasses}
83
106
  data-attached={isAttached}
84
107
  data-full-width={fullWidth}
85
108
  data-slot="button-group"
86
109
  data-vertical={isVertical}
87
- role="group"
88
- {...props}
110
+ {...properties}
89
111
  >
90
112
  {children}
91
113
  </div>
package/src/button.tsx CHANGED
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import type {
4
- ButtonProps,
4
+ ButtonProps as ButtonProperties,
5
5
  ButtonComponent,
6
- ButtonLabelProps,
7
- ButtonIconProps,
8
- ButtonSpinnerProps,
9
- ButtonShortcutProps,
6
+ ButtonClassNames,
7
+ ButtonLabelProperties,
8
+ ButtonIconProperties,
9
+ ButtonSpinnerProperties,
10
+ ButtonShortcutProperties,
10
11
  } from './button.types';
11
12
  import type { ButtonReturnType } from '@ideasui/theme/recipes';
12
13
  import type { ButtonRenderProps } from 'react-aria-components';
@@ -29,7 +30,7 @@ import { ButtonGroup } from './button-group';
29
30
  */
30
31
  interface ButtonContextValue {
31
32
  styles: ButtonReturnType;
32
- classNames?: ButtonProps['classNames'];
33
+ classNames?: ButtonClassNames;
33
34
  }
34
35
 
35
36
  const ButtonContext = createContext<ButtonContextValue | null>(null);
@@ -53,8 +54,8 @@ function useButtonContext(): ButtonContextValue {
53
54
  * Button Slots
54
55
  * ---------------------------------------------------------------------------------------------*/
55
56
 
56
- const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
57
- ({ children, className, placement = 'start', ...props }, ref): JSX.Element => {
57
+ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProperties>(
58
+ ({ children, className, placement = 'start', ...properties }, reference): JSX.Element => {
58
59
  const { styles, classNames } = useButtonContext();
59
60
  const { icon } = styles;
60
61
 
@@ -62,7 +63,7 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
62
63
 
63
64
  return (
64
65
  <span
65
- ref={ref}
66
+ ref={reference}
66
67
  aria-hidden="true"
67
68
  className={cn(
68
69
  icon(),
@@ -71,7 +72,7 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
71
72
  className,
72
73
  )}
73
74
  data-slot="button-icon"
74
- {...props}
75
+ {...properties}
75
76
  >
76
77
  {children}
77
78
  </span>
@@ -81,14 +82,14 @@ const ButtonIcon = forwardRef<HTMLElement, ButtonIconProps>(
81
82
 
82
83
  ButtonIcon.displayName = 'IdeasUI.Button.Icon';
83
84
 
84
- const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProps>(
85
- ({ className, label = 'Loading', ...props }, ref): JSX.Element => {
85
+ const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProperties>(
86
+ ({ className, label = 'Loading', ...properties }, reference): JSX.Element => {
86
87
  const { styles, classNames } = useButtonContext();
87
88
  const { loader, icon } = styles;
88
89
 
89
90
  return (
90
91
  <span
91
- ref={ref}
92
+ ref={reference}
92
93
  className={cn(
93
94
  loader(),
94
95
  icon(),
@@ -97,8 +98,7 @@ const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProps>(
97
98
  className,
98
99
  )}
99
100
  data-slot="button-spinner"
100
- role="status"
101
- {...props}
101
+ {...properties}
102
102
  >
103
103
  <svg
104
104
  className="size-full animate-spin"
@@ -120,17 +120,17 @@ const ButtonSpinner = forwardRef<HTMLSpanElement, ButtonSpinnerProps>(
120
120
 
121
121
  ButtonSpinner.displayName = 'IdeasUI.Button.Spinner';
122
122
 
123
- const ButtonShortcut = forwardRef<HTMLSpanElement, ButtonShortcutProps>(
124
- ({ children, className, ...props }, ref): JSX.Element => {
123
+ const ButtonShortcut = forwardRef<HTMLSpanElement, ButtonShortcutProperties>(
124
+ ({ children, className, ...properties }, reference): JSX.Element => {
125
125
  const { styles, classNames } = useButtonContext();
126
126
  const { shortcut } = styles;
127
127
 
128
128
  return (
129
129
  <kbd
130
- ref={ref}
130
+ ref={reference}
131
131
  className={cn(shortcut(), classNames?.shortcut, className)}
132
132
  data-slot="button-shortcut"
133
- {...props}
133
+ {...properties}
134
134
  >
135
135
  {children}
136
136
  </kbd>
@@ -140,17 +140,17 @@ const ButtonShortcut = forwardRef<HTMLSpanElement, ButtonShortcutProps>(
140
140
 
141
141
  ButtonShortcut.displayName = 'IdeasUI.Button.Shortcut';
142
142
 
143
- const ButtonLabel = forwardRef<HTMLSpanElement, ButtonLabelProps>(
144
- ({ children, className, ...props }, ref): JSX.Element => {
143
+ const ButtonLabel = forwardRef<HTMLSpanElement, ButtonLabelProperties>(
144
+ ({ children, className, ...properties }, reference): JSX.Element => {
145
145
  const { styles, classNames } = useButtonContext();
146
146
  const { label } = styles;
147
147
 
148
148
  return (
149
149
  <span
150
- ref={ref}
150
+ ref={reference}
151
151
  className={cn(label(), classNames?.label, className)}
152
152
  data-slot="button-label"
153
- {...props}
153
+ {...properties}
154
154
  >
155
155
  {children}
156
156
  </span>
@@ -164,7 +164,7 @@ ButtonLabel.displayName = 'IdeasUI.Button.Label';
164
164
  * Button Render Helpers
165
165
  * ---------------------------------------------------------------------------------------------*/
166
166
 
167
- interface ButtonContentProps {
167
+ interface ButtonContentProperties {
168
168
  isLoading?: boolean;
169
169
  loadingIndicator?: React.ReactNode;
170
170
  loadingPosition?: 'start' | 'end' | 'center';
@@ -172,7 +172,7 @@ interface ButtonContentProps {
172
172
  startIcon?: React.ReactNode;
173
173
  endIcon?: React.ReactNode;
174
174
  shortcut?: React.ReactNode;
175
- children: ButtonProps['children'];
175
+ children: ButtonProperties['children'];
176
176
  renderProps: ButtonRenderProps;
177
177
  }
178
178
 
@@ -186,7 +186,8 @@ const ButtonContent = ({
186
186
  shortcut,
187
187
  children,
188
188
  renderProps,
189
- }: ButtonContentProps): ReactNode => {
189
+ // eslint-disable-next-line sonarjs/function-return-type
190
+ }: ButtonContentProperties): ReactNode => {
190
191
  const content = typeof children === 'function' ? children(renderProps) : children;
191
192
  const loader = loadingIndicator || <ButtonSpinner />;
192
193
 
@@ -224,86 +225,92 @@ const ButtonContent = ({
224
225
  * Button Base
225
226
  * ---------------------------------------------------------------------------------------------*/
226
227
 
227
- const ButtonBase = forwardRef<HTMLButtonElement, ButtonProps>((originalProps, ref): JSX.Element => {
228
- const {
229
- isLoading,
230
- loadingIndicator,
231
- loadingPosition,
232
- isIconOnly,
233
- startIcon,
234
- endIcon,
235
- shortcut,
236
- children,
237
- className,
238
- classNames,
239
- ...props
240
- } = originalProps;
241
-
242
- const groupContext = useButtonGroupContext();
243
- const merged = mergePropsWithContext(originalProps, groupContext);
244
-
245
- // Specific logic for Button
246
- const mergedDisabled = merged.isDisabled || isLoading;
247
-
248
- const styles = button({
249
- variant: merged.variant,
250
- size: merged.size,
251
- color: merged.color,
252
- radius: merged.radius,
253
- fullWidth: merged.fullWidth,
254
- isIconOnly,
255
- isDisabled: mergedDisabled,
256
- isLoading,
257
- disableAnimation: merged.disableAnimation,
258
- isAttached: merged.isAttached,
259
- isVertical: merged.isVertical,
260
- elevation: merged.elevation,
261
- showDivider: merged.showDivider ?? merged.isAttached,
262
- });
263
-
264
- // Ensure we have an accessible name when loading or icon-only
265
- const ariaLabel = getAccessibleName(merged, typeof children === 'function' ? null : children);
266
- const loadingLabel = typeof loadingIndicator === 'string' ? loadingIndicator : 'Loading';
228
+ const ButtonBase = forwardRef<HTMLButtonElement, ButtonProperties>(
229
+ (originalProperties, reference): JSX.Element => {
230
+ const {
231
+ isLoading,
232
+ loadingIndicator,
233
+ loadingPosition,
234
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
235
+ isIconOnly,
236
+ startIcon,
237
+ endIcon,
238
+ shortcut,
239
+ children,
240
+ className,
241
+ classNames,
242
+ ...properties
243
+ } = originalProperties;
244
+
245
+ const groupContext = useButtonGroupContext();
246
+ const merged = mergePropsWithContext(originalProperties, groupContext);
247
+
248
+ // Specific logic for Button
249
+ const mergedDisabled = merged.isDisabled || isLoading;
250
+
251
+ const styles = button({
252
+ variant: merged.variant,
253
+ size: merged.size,
254
+ color: merged.color,
255
+ radius: merged.radius,
256
+ fullWidth: merged.fullWidth,
257
+ isIconOnly: merged.isIconOnly,
258
+ isDisabled: mergedDisabled,
259
+ isLoading,
260
+ disableAnimation: merged.disableAnimation,
261
+ isAttached: merged.isAttached,
262
+ isVertical: merged.isVertical,
263
+ elevation: merged.elevation,
264
+ divider: merged.divider || (merged.isAttached ? 'full' : 'none'),
265
+ });
266
+
267
+ // Context value - React Compiler will optimize this automatically
268
+ const contextValue = { styles, classNames };
269
+
270
+ // Ensure we have an accessible name when loading or icon-only
271
+ const ariaLabel = getAccessibleName(merged, typeof children === 'function' ? null : children);
272
+ const loadingLabel = typeof loadingIndicator === 'string' ? loadingIndicator : 'Loading';
267
273
 
268
- return (
269
- <ButtonPrimitive
270
- ref={ref}
271
- aria-busy={isLoading}
272
- aria-label={isLoading && !ariaLabel ? loadingLabel : ariaLabel}
273
- className={(renderProps) =>
274
- styles.base({
275
- className: cn(
276
- typeof className === 'function' ? className(renderProps) : className,
277
- classNames?.base,
278
- ),
279
- })
280
- }
281
- data-attached={merged.isAttached}
282
- data-slot="button"
283
- data-vertical={merged.isVertical}
284
- isDisabled={mergedDisabled}
285
- isPending={isLoading}
286
- {...props}
287
- >
288
- {(renderProps) => (
289
- <ButtonContext.Provider value={{ styles, classNames }}>
290
- <ButtonContent
291
- endIcon={endIcon}
292
- isIconOnly={isIconOnly}
293
- isLoading={isLoading}
294
- loadingIndicator={loadingIndicator}
295
- loadingPosition={loadingPosition}
296
- renderProps={renderProps}
297
- shortcut={shortcut}
298
- startIcon={startIcon}
299
- >
300
- {children}
301
- </ButtonContent>
302
- </ButtonContext.Provider>
303
- )}
304
- </ButtonPrimitive>
305
- );
306
- });
274
+ return (
275
+ <ButtonPrimitive
276
+ ref={reference}
277
+ aria-busy={isLoading}
278
+ aria-label={isLoading && !ariaLabel ? loadingLabel : ariaLabel}
279
+ className={(renderProperties) =>
280
+ styles.base({
281
+ className: cn(
282
+ typeof className === 'function' ? className(renderProperties) : className,
283
+ classNames?.base,
284
+ ),
285
+ })
286
+ }
287
+ data-attached={merged.isAttached}
288
+ data-slot="button"
289
+ data-vertical={merged.isVertical}
290
+ isDisabled={mergedDisabled}
291
+ isPending={isLoading}
292
+ {...properties}
293
+ >
294
+ {(renderProperties) => (
295
+ <ButtonContext.Provider value={contextValue}>
296
+ <ButtonContent
297
+ endIcon={endIcon}
298
+ isIconOnly={merged.isIconOnly}
299
+ isLoading={isLoading}
300
+ loadingIndicator={loadingIndicator}
301
+ loadingPosition={loadingPosition}
302
+ renderProps={renderProperties}
303
+ shortcut={shortcut}
304
+ startIcon={startIcon}
305
+ >
306
+ {children}
307
+ </ButtonContent>
308
+ </ButtonContext.Provider>
309
+ )}
310
+ </ButtonPrimitive>
311
+ );
312
+ },
313
+ );
307
314
 
308
315
  ButtonBase.displayName = 'IdeasUI.Button';
309
316
 
@@ -1,7 +1,10 @@
1
- import type { ButtonProps as ButtonPrimitiveProps, ButtonRenderProps } from 'react-aria-components';
1
+ import type {
2
+ ButtonProps as ButtonPrimitiveProperties,
3
+ ButtonRenderProps,
4
+ } from 'react-aria-components';
2
5
  import type { ForwardRefExoticComponent, HTMLAttributes, ReactNode, RefAttributes } from 'react';
3
6
  import type { ButtonVariantProps } from '@ideasui/theme/recipes';
4
- import type { ButtonGroupProps } from './button-group';
7
+ import type { ButtonGroupProperties } from './button-group';
5
8
 
6
9
  /**
7
10
  * Granular class names for the Button component slots.
@@ -22,11 +25,11 @@ export interface ButtonClassNames {
22
25
  }
23
26
 
24
27
  /**
25
- * Props for the Button component.
28
+ * Base props for the Button component.
26
29
  */
27
- export interface ButtonProps
30
+ interface ButtonBaseProperties
28
31
  extends
29
- Omit<ButtonPrimitiveProps, 'className'>,
32
+ Omit<ButtonPrimitiveProperties, 'className'>,
30
33
  Omit<ButtonVariantProps, 'isLoading' | 'isDisabled'> {
31
34
  /**
32
35
  * Whether the button is in a loading state.
@@ -43,13 +46,6 @@ export interface ButtonProps
43
46
  * @default 'start'
44
47
  */
45
48
  loadingPosition?: 'start' | 'end' | 'center';
46
- /**
47
- * Whether the button should be square and optimized for icons.
48
- *
49
- * Note: When true, you MUST provide an `aria-label` or `aria-labelledby` for accessibility.
50
- * @default false
51
- */
52
- isIconOnly?: boolean;
53
49
  /**
54
50
  * Icon to display before the button content.
55
51
  */
@@ -63,10 +59,10 @@ export interface ButtonProps
63
59
  */
64
60
  shortcut?: ReactNode;
65
61
  /**
66
- * Whether to show a divider between the buttons in a group.
67
- * @default true (when isAttached is true)
62
+ * The type of divider to display between the buttons in a group.
63
+ * @default 'full' (when isAttached is true)
68
64
  */
69
- showDivider?: boolean;
65
+ divider?: 'full' | 'middle' | 'none';
70
66
  /**
71
67
  * Custom class names for individual button slots.
72
68
  */
@@ -75,12 +71,19 @@ export interface ButtonProps
75
71
  * The CSS class name for the button.
76
72
  * Can be a string or a function that receives the button render props.
77
73
  */
78
- className?: string | ((props: ButtonRenderProps) => string);
74
+ className?: string | ((properties: ButtonRenderProps) => string);
79
75
  /**
80
76
  * The content to display inside the button.
81
77
  * Can be a ReactNode or a function that receives the button render props.
82
78
  */
83
- children?: ReactNode | ((props: ButtonRenderProps) => ReactNode);
79
+ children?: ReactNode | ((properties: ButtonRenderProps) => ReactNode);
80
+ /**
81
+ * Whether the button should be square and optimized for icons.
82
+ *
83
+ * Note: When true, you MUST provide an `aria-label` or `aria-labelledby` for accessibility.
84
+ * @default false
85
+ */
86
+ isIconOnly?: boolean;
84
87
  /**
85
88
  * Accessibility label for the button.
86
89
  * Required if the button has no visible label (e.g., `isIconOnly`).
@@ -92,15 +95,67 @@ export interface ButtonProps
92
95
  'aria-labelledby'?: string;
93
96
  }
94
97
 
98
+ /**
99
+ * Props for the Button component.
100
+ *
101
+ * Enforces that if `isIconOnly` is true, either `aria-label` or `aria-labelledby` must be provided.
102
+ */
103
+ export type ButtonProps =
104
+ | (ButtonBaseProperties & {
105
+ /**
106
+ * Whether the button should be square and optimized for icons.
107
+ * @default false
108
+ */
109
+ isIconOnly?: false;
110
+ /**
111
+ * Accessibility label for the button.
112
+ */
113
+ 'aria-label'?: string;
114
+ /**
115
+ * ID of an element that serves as the accessible label for the button.
116
+ */
117
+ 'aria-labelledby'?: string;
118
+ })
119
+ | (ButtonBaseProperties & {
120
+ /**
121
+ * Whether the button should be square and optimized for icons.
122
+ */
123
+ isIconOnly: true;
124
+ /**
125
+ * Accessibility label for the button.
126
+ * Required if the button has no visible label (e.g., `isIconOnly`).
127
+ */
128
+ 'aria-label': string;
129
+ /**
130
+ * ID of an element that serves as the accessible label for the button.
131
+ */
132
+ 'aria-labelledby'?: string;
133
+ })
134
+ | (ButtonBaseProperties & {
135
+ /**
136
+ * Whether the button should be square and optimized for icons.
137
+ */
138
+ isIconOnly: true;
139
+ /**
140
+ * Accessibility label for the button.
141
+ */
142
+ 'aria-label'?: string;
143
+ /**
144
+ * ID of an element that serves as the accessible label for the button.
145
+ * Required if the button has no visible label (e.g., `isIconOnly`).
146
+ */
147
+ 'aria-labelledby': string;
148
+ });
149
+
95
150
  /**
96
151
  * Props for the Button.Label component.
97
152
  */
98
- export interface ButtonLabelProps extends HTMLAttributes<HTMLSpanElement> {}
153
+ export interface ButtonLabelProperties extends HTMLAttributes<HTMLSpanElement> {}
99
154
 
100
155
  /**
101
156
  * Props for the Button.Icon component.
102
157
  */
103
- export interface ButtonIconProps extends HTMLAttributes<HTMLElement> {
158
+ export interface ButtonIconProperties extends HTMLAttributes<HTMLElement> {
104
159
  /**
105
160
  * The icon content.
106
161
  */
@@ -115,7 +170,7 @@ export interface ButtonIconProps extends HTMLAttributes<HTMLElement> {
115
170
  /**
116
171
  * Props for the Button.Spinner component.
117
172
  */
118
- export interface ButtonSpinnerProps extends HTMLAttributes<HTMLSpanElement> {
173
+ export interface ButtonSpinnerProperties extends HTMLAttributes<HTMLSpanElement> {
119
174
  /**
120
175
  * Accessibility label for the spinner.
121
176
  * @default 'Loading'
@@ -126,7 +181,7 @@ export interface ButtonSpinnerProps extends HTMLAttributes<HTMLSpanElement> {
126
181
  /**
127
182
  * Props for the Button.Shortcut component.
128
183
  */
129
- export interface ButtonShortcutProps extends HTMLAttributes<HTMLSpanElement> {
184
+ export interface ButtonShortcutProperties extends HTMLAttributes<HTMLSpanElement> {
130
185
  /**
131
186
  * The shortcut keys to display.
132
187
  */
@@ -142,21 +197,21 @@ export interface ButtonComponent extends ForwardRefExoticComponent<
142
197
  /**
143
198
  * Component to display the button label.
144
199
  */
145
- Label: ForwardRefExoticComponent<ButtonLabelProps & RefAttributes<HTMLSpanElement>>;
200
+ Label: ForwardRefExoticComponent<ButtonLabelProperties & RefAttributes<HTMLSpanElement>>;
146
201
  /**
147
202
  * Component to display an icon inside the button.
148
203
  */
149
- Icon: ForwardRefExoticComponent<ButtonIconProps & RefAttributes<HTMLElement>>;
204
+ Icon: ForwardRefExoticComponent<ButtonIconProperties & RefAttributes<HTMLElement>>;
150
205
  /**
151
206
  * Component to display a shortcut indicator inside the button.
152
207
  */
153
- Shortcut: ForwardRefExoticComponent<ButtonShortcutProps & RefAttributes<HTMLSpanElement>>;
208
+ Shortcut: ForwardRefExoticComponent<ButtonShortcutProperties & RefAttributes<HTMLSpanElement>>;
154
209
  /**
155
210
  * Component to display a loading spinner inside the button.
156
211
  */
157
- Spinner: ForwardRefExoticComponent<ButtonSpinnerProps & RefAttributes<HTMLSpanElement>>;
212
+ Spinner: ForwardRefExoticComponent<ButtonSpinnerProperties & RefAttributes<HTMLSpanElement>>;
158
213
  /**
159
214
  * Component to group multiple buttons together.
160
215
  */
161
- Group: ForwardRefExoticComponent<ButtonGroupProps & RefAttributes<HTMLDivElement>>;
216
+ Group: ForwardRefExoticComponent<ButtonGroupProperties & RefAttributes<HTMLDivElement>>;
162
217
  }