@@ -2,21 +2,22 @@ import { size } from 'lodash';
22import { rgba , saturate , tint } from 'polished' ;
33import React , { forwardRef , memo , useCallback , useMemo } from 'react' ;
44import { useHotkeys } from 'react-hotkeys-hook' ;
5- import styled , { css } from 'styled-components' ;
5+ import styled , { css , keyframes } from 'styled-components' ;
66import { CONTROL_ICON_OPACITY } from '../../constants/colors' ;
77import {
88 CONTROL_HORIZONTAL_PADDING_FROM_SIZE ,
99 CONTROL_TEXT_FROM_SIZE ,
1010 CONTROL_VERTICAL_PADDING_FROM_SIZE ,
1111 CONTROL_VERTICAL_PADDING_MODIFIER_FROM_SIZE ,
1212 ICON_FROM_SIZE ,
13+ INDICATOR_SIZE_TO_PX ,
1314 PADDING_FROM_SIZE ,
1415 PILL_RADIUS_MODIFIER ,
1516 resolveRadius ,
1617 SIZE_TO_PX ,
1718 TSizes ,
1819} from '../../constants/sizes' ;
19- import { IReqoreCustomTheme , IReqoreTheme } from '../../constants/theme' ;
20+ import { IReqoreCustomTheme , IReqoreTheme , TReqoreIntent } from '../../constants/theme' ;
2021import {
2122 changeLightness ,
2223 getGradientMix ,
@@ -57,6 +58,7 @@ import {
5758 IReqoreEffect ,
5859 ReqoreTextEffect ,
5960 StyledEffect ,
61+ TReqoreColor ,
6062 TReqoreEffectColor ,
6163 TReqoreHexColor ,
6264} from '../Effect' ;
@@ -69,6 +71,78 @@ import { ReqoreTooltipComponent } from '../TooltipComponent';
6971
7072export type TReqoreBadge = string | number | IReqoreTagProps ;
7173
74+ export type TReqoreButtonIndicatorPosition =
75+ | 'top-right'
76+ | 'top-left'
77+ | 'bottom-right'
78+ | 'bottom-left' ;
79+
80+ export interface IReqoreButtonIndicatorProps {
81+ /** Intent used to tint the dot. Resolved from the button's theme intents. Defaults to `danger`. */
82+ intent ?: TReqoreIntent ;
83+ /** Explicit color override. Takes precedence over `intent`. */
84+ color ?: TReqoreColor ;
85+ /** Animate the dot with an expanding, fading pulse ring. Defaults to `false` (static). */
86+ pulse ?: boolean ;
87+ /** Corner the dot is anchored to. Defaults to `top-right`. */
88+ position ?: TReqoreButtonIndicatorPosition ;
89+ }
90+
91+ const indicatorPulseKeyframes = keyframes `
92+ 0% {
93+ transform: scale(1);
94+ opacity: 0.55;
95+ }
96+ 70% {
97+ opacity: 0;
98+ }
99+ 100% {
100+ transform: scale(2.6);
101+ opacity: 0;
102+ }
103+ ` ;
104+
105+ export interface IReqoreButtonIndicatorStyle {
106+ $color : TReqoreColor ;
107+ $diameter : number ;
108+ $offset : number ;
109+ $position : TReqoreButtonIndicatorPosition ;
110+ $pulse ?: boolean ;
111+ }
112+
113+ export const StyledButtonIndicator = styled . span < IReqoreButtonIndicatorStyle > `
114+ position: absolute;
115+ z-index: 2;
116+ width: ${ ( { $diameter } ) => $diameter } px;
117+ height: ${ ( { $diameter } ) => $diameter } px;
118+ border-radius: 50%;
119+ background-color: ${ ( { $color } ) => $color } ;
120+ pointer-events: none;
121+
122+ ${ ( { $position, $offset } ) => {
123+ const [ vertical , horizontal ] = $position . split ( '-' ) ;
124+
125+ return css `
126+ ${ vertical } : ${ $offset } px;
127+ ${ horizontal } : ${ $offset } px;
128+ ` ;
129+ } }
130+
131+ ${ ( { $pulse, $color } ) =>
132+ $pulse
133+ ? css `
134+ &::after {
135+ content: '';
136+ position: absolute;
137+ inset: 0;
138+ border-radius: 50%;
139+ background-color: ${ $color } ;
140+ animation: ${ indicatorPulseKeyframes } 1.6s ease-out infinite;
141+ }
142+ `
143+ : undefined } ;
144+ ` ;
145+
72146/**
73147 * Button props for the primary Reqore action component.
74148 */
@@ -99,6 +173,17 @@ export interface IReqoreButtonProps
99173 wrap ?: boolean ;
100174 badge ?: TReqoreBadge | TReqoreBadge [ ] ;
101175
176+ /**
177+ * Renders a small dot in a corner of the button to signal importance or a
178+ * pending event (unread items, a required action, a live status). Distinct
179+ * from `badge`, which renders content tags inside the button.
180+ *
181+ * Pass `true` for a static, danger-tinted dot in the top-right corner, or an
182+ * object to customize the `intent`/`color`, enable the `pulse` animation, and
183+ * pick the `position`.
184+ */
185+ indicator ?: boolean | IReqoreButtonIndicatorProps ;
186+
102187 /**
103188 * Keyboard shortcut that triggers the button's `onClick`, following the
104189 * `react-hotkeys-hook` syntax (e.g. `'mod+k'`, `'ctrl+shift+s'`, or an array
@@ -514,6 +599,7 @@ const ReqoreButton = memo(
514599 wrap,
515600 readOnly,
516601 badge,
602+ indicator,
517603 description,
518604 maxWidth,
519605 textAlign = 'left' ,
@@ -598,6 +684,23 @@ const ReqoreButton = memo(
598684 [ fixedEffect , intent , readOnly , rest . disabled ]
599685 ) ;
600686
687+ const indicatorConfig = useMemo ( ( ) => {
688+ if ( ! indicator ) {
689+ return undefined ;
690+ }
691+
692+ const config : IReqoreButtonIndicatorProps = indicator === true ? { } : indicator ;
693+ const diameter = INDICATOR_SIZE_TO_PX [ size ] ;
694+
695+ return {
696+ color : config . color ?? theme . intents ?. [ config . intent ?? 'danger' ] ?? theme . intents ?. danger ,
697+ diameter,
698+ offset : Math . round ( diameter * 0.6 ) ,
699+ position : config . position ?? 'top-right' ,
700+ pulse : config . pulse ?? false ,
701+ } ;
702+ } , [ indicator , size , theme . intents ] ) ;
703+
601704 return (
602705 < ReqoreTooltipComponent
603706 { ...{
@@ -785,6 +888,18 @@ const ReqoreButton = memo(
785888 { description }
786889 </ ReqoreTextEffect >
787890 ) }
891+
892+ { indicatorConfig && (
893+ < StyledButtonIndicator
894+ className = 'reqore-button-indicator'
895+ $color = { indicatorConfig . color }
896+ $diameter = { indicatorConfig . diameter }
897+ $offset = { indicatorConfig . offset }
898+ $position = { indicatorConfig . position }
899+ $pulse = { indicatorConfig . pulse }
900+ aria-hidden = 'true'
901+ />
902+ ) }
788903 </ ReqoreTooltipComponent >
789904 ) ;
790905 }
0 commit comments