@qoretechnologies/reqore 0.42.0 → 0.43.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.
@@ -0,0 +1,358 @@
1
+ import * as Slider from '@radix-ui/react-slider';
2
+ import { useCallback, useMemo, useState } from 'react';
3
+ import styled, { css } from 'styled-components';
4
+ import { HALF_PADDING_FROM_SIZE, ICON_FROM_SIZE, TSizes } from '../../constants/sizes';
5
+ import { IReqoreCustomTheme, TReqoreIntent } from '../../constants/theme';
6
+ import { changeLightness, getReadableColor } from '../../helpers/colors';
7
+ import { useReqoreTheme } from '../../hooks/useTheme';
8
+ import { useTooltip } from '../../hooks/useTooltip';
9
+ import { TReqoreTooltipProp } from '../../types/global';
10
+ import { IReqoreIconName } from '../../types/icons';
11
+ import ReqoreControlGroup, { IReqoreControlGroupProps } from '../ControlGroup';
12
+ import { IReqoreEffect, IReqoreTextEffectProps, StyledEffect, TReqoreEffectColor } from '../Effect';
13
+ import ReqoreIcon, { IReqoreIconProps } from '../Icon';
14
+ import { IReqoreParagraphProps, ReqoreP } from '../Paragraph';
15
+ import ReqoreTag, { IReqoreTagProps } from '../Tag';
16
+
17
+ export interface ISliderProps<T extends number | [number, number] = number>
18
+ extends Omit<Slider.SliderProps, 'onChange' | 'value' | 'defaultValue' | 'onValueChange'> {
19
+ value: T;
20
+ onChange(values: T): void;
21
+ fluid?: boolean;
22
+
23
+ icon?: IReqoreIconName;
24
+ iconColor?: TReqoreEffectColor;
25
+ iconProps?: IReqoreIconProps;
26
+
27
+ rightIcon?: IReqoreIconName;
28
+ rightIconColor?: TReqoreEffectColor;
29
+ rightIconProps?: IReqoreIconProps;
30
+
31
+ showLabels?: boolean;
32
+ size?: TSizes;
33
+ labelsPosition: 'top' | 'bottom';
34
+ customTheme?: IReqoreCustomTheme;
35
+ intent?: TReqoreIntent;
36
+ effect?: IReqoreEffect;
37
+ tooltip: TReqoreTooltipProp;
38
+
39
+ trackProps?: Partial<IReqoreTextEffectProps>;
40
+ rangeProps?: Partial<IReqoreTextEffectProps>;
41
+ thumbProps?: Partial<IReqoreTextEffectProps>;
42
+
43
+ minLabelProps?: IReqoreParagraphProps;
44
+ currentMinLabelProps?: IReqoreTagProps;
45
+ maxLabelProps?: IReqoreParagraphProps;
46
+ currentMaxLabelProps?: IReqoreTagProps;
47
+ wrapperProps?: IReqoreControlGroupProps;
48
+
49
+ displayCurrentValueOverThumb?: boolean;
50
+ }
51
+
52
+ const StyledControlGroupWrapper = styled(ReqoreControlGroup)`
53
+ &[data-orientation='horizontal'] {
54
+ &[data-fluid='false'] {
55
+ max-width: 200px;
56
+ min-width: 200px;
57
+ }
58
+ }
59
+ &[data-orientation='vertical'] {
60
+ &[data-fluid='false'] {
61
+ max-height: 200px;
62
+ }
63
+ }
64
+ `;
65
+
66
+ const StyledTrack = styled(Slider.Track)`
67
+ background-color: ${(props) => props.background};
68
+ position: relative;
69
+ border-radius: 9999px;
70
+ `;
71
+ const StyledRange = styled(Slider.Range)`
72
+ position: absolute;
73
+ background-color: ${(props) => props.background};
74
+ border-radius: 9999px;
75
+ `;
76
+ const StyledThumb = styled(Slider.Thumb)`
77
+ display: block;
78
+ background-color: ${(props) => props.background};
79
+ border-radius: 9999px;
80
+ transition: all 0.2s ease-out;
81
+
82
+ &:focus {
83
+ box-shadow: 0 0 0 8px ${(props) => props.background}15;
84
+ outline: none;
85
+ }
86
+ `;
87
+ const StyledRoot = styled(Slider.Root)`
88
+ position: relative;
89
+ display: inline-flex;
90
+ align-items: center;
91
+ cursor: pointer;
92
+ flex-grow: 1;
93
+
94
+ &[aria-disabled='true'] {
95
+ opacity: 0.5;
96
+ cursor: not-allowed;
97
+ pointer-events: none;
98
+ }
99
+
100
+ ${({ 'data-size': size }) => css`
101
+ ${StyledThumb} {
102
+ width: ${ICON_FROM_SIZE[size]}px;
103
+ height: ${ICON_FROM_SIZE[size]}px;
104
+ min-width: ${ICON_FROM_SIZE[size]}px;
105
+ }
106
+ `}
107
+
108
+ &[data-orientation='horizontal'] {
109
+ width: 100%;
110
+
111
+ ${({ 'data-size': size }) => css`
112
+ height: ${ICON_FROM_SIZE[size]}px;
113
+
114
+ ${StyledTrack} {
115
+ height: ${HALF_PADDING_FROM_SIZE[size]}px;
116
+ }
117
+ `}
118
+
119
+ ${StyledTrack} {
120
+ flex-grow: 1;
121
+ }
122
+ ${StyledRange} {
123
+ height: 100%;
124
+ }
125
+ }
126
+
127
+ &[data-orientation='vertical'] {
128
+ flex-direction: column;
129
+ justify-content: center;
130
+ height: 200px;
131
+
132
+ ${({ 'data-size': size }) => css`
133
+ width: ${ICON_FROM_SIZE[size]}px;
134
+
135
+ ${StyledTrack} {
136
+ width: ${HALF_PADDING_FROM_SIZE[size]}px;
137
+ }
138
+ `}
139
+
140
+ ${StyledTrack} {
141
+ flex-grow: 1;
142
+ }
143
+ ${StyledRange} {
144
+ width: 100%;
145
+ }
146
+ }
147
+ `;
148
+
149
+ const StyledCurrentValue = styled(ReqoreTag)`
150
+ position: absolute;
151
+ ${({ position, isAboveThreshold, isBelowThreshold }) =>
152
+ css`
153
+ ${position}: ${isAboveThreshold || isBelowThreshold ? '-5px' : '-35px'};
154
+ ${position === 'left' || position === 'right' ? 'top' : 'left'}: 50%;
155
+ transform: ${position === 'left' || position === 'right'
156
+ ? 'translateY(-50%)'
157
+ : 'translateX(-50%)'};
158
+ `}
159
+ `;
160
+
161
+ const SliderRootWrapper = styled(ReqoreControlGroup)`
162
+ align-items: center;
163
+ flex-grow: 1;
164
+ width: 100%;
165
+ `;
166
+
167
+ export function ReqoreSlider<T extends number | [number, number] = number>({
168
+ value,
169
+ onChange,
170
+ step = 0.1,
171
+ orientation = 'horizontal',
172
+ fluid = true,
173
+ showLabels,
174
+ icon,
175
+ iconColor,
176
+ iconProps,
177
+ rightIcon,
178
+ rightIconColor,
179
+ rightIconProps,
180
+ customTheme,
181
+ intent,
182
+ effect,
183
+ trackProps,
184
+ rangeProps,
185
+ thumbProps,
186
+ minLabelProps,
187
+ maxLabelProps,
188
+ size = 'normal',
189
+ wrapperProps,
190
+ labelsPosition = 'top',
191
+ tooltip,
192
+ currentMaxLabelProps,
193
+ currentMinLabelProps,
194
+ displayCurrentValueOverThumb,
195
+ ...props
196
+ }: ISliderProps<T>) {
197
+ const [wrapperRef, setWrapperRef] = useState<HTMLDivElement>(undefined);
198
+ const theme = useReqoreTheme('main', customTheme, intent);
199
+
200
+ useTooltip(wrapperRef, tooltip);
201
+
202
+ const background = intent
203
+ ? changeLightness(theme.main, 0.2)
204
+ : getReadableColor(theme, undefined, undefined, true);
205
+ const trackBackground = intent
206
+ ? changeLightness(theme.main, 0.05)
207
+ : changeLightness(theme.main, 0.2);
208
+ const isRange = Array.isArray(value);
209
+
210
+ const renderLabels = useCallback(() => {
211
+ if (!showLabels) {
212
+ return null;
213
+ }
214
+
215
+ return (
216
+ <ReqoreControlGroup fluid spaceBetween vertical={orientation === 'vertical'} fill>
217
+ <ReqoreControlGroup fixed vertical={orientation === 'vertical'} horizontalAlign='center'>
218
+ {icon && (
219
+ <ReqoreIcon icon={icon} size={size} color={iconColor} intent={intent} {...iconProps} />
220
+ )}
221
+ {props.min || props.min === 0 ? (
222
+ <ReqoreP size={size} intent={intent} effect={effect} {...minLabelProps}>
223
+ {props.min}
224
+ </ReqoreP>
225
+ ) : null}
226
+ </ReqoreControlGroup>
227
+ <ReqoreControlGroup fixed vertical={orientation === 'vertical'} horizontalAlign='center'>
228
+ {props.max || props.max === 0 ? (
229
+ <ReqoreP size={size} intent={intent} effect={effect} {...maxLabelProps}>
230
+ {props.max}
231
+ </ReqoreP>
232
+ ) : null}
233
+ {rightIcon && (
234
+ <ReqoreIcon
235
+ icon={rightIcon}
236
+ size={size}
237
+ color={rightIconColor}
238
+ intent={intent}
239
+ {...rightIconProps}
240
+ />
241
+ )}
242
+ </ReqoreControlGroup>
243
+ </ReqoreControlGroup>
244
+ );
245
+ }, [
246
+ icon,
247
+ rightIcon,
248
+ iconProps,
249
+ rightIconProps,
250
+ minLabelProps,
251
+ maxLabelProps,
252
+ size,
253
+ intent,
254
+ props.min,
255
+ props.max,
256
+ showLabels,
257
+ orientation,
258
+ effect,
259
+ ]);
260
+
261
+ const currentValuePosition = useMemo(() => {
262
+ return orientation === 'horizontal'
263
+ ? labelsPosition === 'top'
264
+ ? 'top'
265
+ : 'bottom'
266
+ : labelsPosition === 'top'
267
+ ? 'left'
268
+ : 'right';
269
+ }, [orientation, labelsPosition]);
270
+
271
+ const isMinValueBelowThreshold = useMemo(() => {
272
+ const _value: number = isRange ? value[0] : value;
273
+
274
+ return _value < props.min + step * 5;
275
+ }, [value, props.min]);
276
+
277
+ const isMaxValueAboveThreshold = useMemo(() => {
278
+ if (!isRange) return false;
279
+
280
+ const _value: number = value[1];
281
+
282
+ return _value > props.max - step * 5;
283
+ }, [value, props.max]);
284
+
285
+ return (
286
+ <StyledControlGroupWrapper
287
+ ref={setWrapperRef}
288
+ data-orientation={orientation}
289
+ data-labels-position={labelsPosition}
290
+ data-fluid={fluid}
291
+ fluid={fluid}
292
+ vertical={orientation === 'horizontal'}
293
+ fill
294
+ gapSize={orientation === 'horizontal' ? 'big' : 'normal'}
295
+ {...wrapperProps}
296
+ >
297
+ {labelsPosition === 'top' && renderLabels()}
298
+ <SliderRootWrapper vertical={orientation === 'vertical'}>
299
+ <StyledRoot
300
+ data-size={size}
301
+ orientation={orientation}
302
+ minStepsBetweenThumbs={step}
303
+ inverted={orientation === 'vertical'}
304
+ step={step}
305
+ {...props}
306
+ value={isRange ? value : [value]}
307
+ onValueChange={(values) => {
308
+ onChange((isRange ? values : values[0]) as T);
309
+ }}
310
+ >
311
+ <StyledTrack background={trackBackground} asChild>
312
+ <StyledEffect effect={effect} {...trackProps}>
313
+ <StyledRange background={background} asChild>
314
+ <StyledEffect effect={effect} {...rangeProps} />
315
+ </StyledRange>
316
+ </StyledEffect>
317
+ </StyledTrack>
318
+
319
+ <StyledThumb background={background} asChild>
320
+ <StyledEffect effect={effect} {...thumbProps}>
321
+ {showLabels && (
322
+ <StyledCurrentValue
323
+ size={size}
324
+ effect={effect}
325
+ intent={intent}
326
+ position={currentValuePosition}
327
+ label={isRange ? value[0] : value}
328
+ isBelowThreshold={displayCurrentValueOverThumb || isMinValueBelowThreshold}
329
+ {...currentMinLabelProps}
330
+ />
331
+ )}
332
+ </StyledEffect>
333
+ </StyledThumb>
334
+ {isRange && (
335
+ <StyledThumb background={background} asChild>
336
+ <StyledEffect effect={effect} {...thumbProps}>
337
+ {showLabels && (
338
+ <StyledCurrentValue
339
+ size={size}
340
+ effect={effect}
341
+ intent={intent}
342
+ position={currentValuePosition}
343
+ isAboveThreshold={displayCurrentValueOverThumb || isMaxValueAboveThreshold}
344
+ label={value[1]}
345
+ {...currentMaxLabelProps}
346
+ />
347
+ )}
348
+ </StyledEffect>
349
+ </StyledThumb>
350
+ )}
351
+ </StyledRoot>
352
+ </SliderRootWrapper>
353
+ {labelsPosition === 'bottom' && renderLabels()}
354
+ </StyledControlGroupWrapper>
355
+ );
356
+ }
357
+
358
+ export default ReqoreSlider;
@@ -150,8 +150,8 @@ export const StyledTag = styled(StyledEffect)<IReqoreTagStyle>`
150
150
  color: ${minimal
151
151
  ? getReadableColor(theme, undefined, undefined, true)
152
152
  : color && color !== 'transparent'
153
- ? getReadableColorFrom(color)
154
- : getReadableColorFrom(changeLightness(theme.main, 0.1))};
153
+ ? getReadableColorFrom(color)
154
+ : getReadableColorFrom(changeLightness(theme.main, 0.1))};
155
155
 
156
156
  ${StyledTagKeyWrapper} {
157
157
  background-color: ${labelKey ? rgba('#000000', 0.2) : undefined};
@@ -175,8 +175,8 @@ export const StyledTag = styled(StyledEffect)<IReqoreTagStyle>`
175
175
  color: ${minimal
176
176
  ? getReadableColor(theme, undefined, undefined, false, theme.originalMain)
177
177
  : color
178
- ? getReadableColorFrom(color)
179
- : getReadableColor(theme, undefined, undefined)};
178
+ ? getReadableColorFrom(color)
179
+ : getReadableColor(theme, undefined, undefined)};
180
180
  `}
181
181
  }
182
182
  `
package/src/index.tsx CHANGED
@@ -46,6 +46,7 @@ export { ReqorePanel } from './components/Panel';
46
46
  export { ReqoreP, ReqoreP as ReqoreParagraph } from './components/Paragraph';
47
47
  export { default as ReqorePopover } from './components/Popover';
48
48
  export { default as ReqoreRadioGroup } from './components/RadioGroup';
49
+ export * from './components/Slider';
49
50
  export { ReqoreHorizontalSpacer, ReqoreSpacer, ReqoreVerticalSpacer } from './components/Spacer';
50
51
  export { ReqoreSpinner } from './components/Spinner';
51
52
  export { default as ReqoreTable } from './components/Table';
@@ -0,0 +1,173 @@
1
+ import { StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import ReqoreControlGroup from '../../components/ControlGroup';
4
+ import ReqoreSlider from '../../components/Slider';
5
+ import { StoryMeta } from '../utils';
6
+ import { DisabledArg, IconArg, MinimalArg, SizeArg } from '../utils/args';
7
+
8
+ const meta = {
9
+ title: 'Form/Slider/Stories',
10
+ component: ReqoreSlider,
11
+ argTypes: {
12
+ ...MinimalArg,
13
+ ...SizeArg,
14
+ ...DisabledArg,
15
+ ...IconArg('icon', 'Icon'),
16
+ ...IconArg('rightIcon', 'Right Icon'),
17
+ fluid: {
18
+ control: 'boolean',
19
+ defaultValue: true,
20
+ },
21
+ labelsPosition: {
22
+ control: 'select',
23
+ defaultValue: 'top',
24
+ options: ['top', 'bottom'],
25
+ },
26
+ },
27
+ args: {
28
+ value: [2, 9.6],
29
+ min: 0,
30
+ max: 10,
31
+ fluid: true,
32
+ },
33
+ render(args) {
34
+ const [value, setValue] = useState(args.value);
35
+ return (
36
+ <>
37
+ <ReqoreControlGroup style={{ width: '100%' }} vertical gapSize='huge'>
38
+ <ReqoreSlider {...args} value={value[0]} onChange={(min) => setValue([min, value[1]])} />
39
+
40
+ <ReqoreSlider {...args} value={value} onChange={setValue} />
41
+ <ReqoreControlGroup gapSize='huge'>
42
+ <ReqoreSlider
43
+ {...args}
44
+ orientation='vertical'
45
+ value={value[0]}
46
+ onChange={(min) => setValue([min, value[1]])}
47
+ />
48
+ <ReqoreSlider {...args} orientation='vertical' value={value} onChange={setValue} />
49
+ </ReqoreControlGroup>
50
+ </ReqoreControlGroup>
51
+ </>
52
+ );
53
+ },
54
+ } as StoryMeta<typeof ReqoreSlider>;
55
+ type Story = StoryObj<typeof meta>;
56
+
57
+ export default meta;
58
+ export const Default: Story = {};
59
+ export const Disabled: Story = {
60
+ args: {
61
+ disabled: true,
62
+ },
63
+ };
64
+
65
+ export const WithIcons: Story = {
66
+ args: {
67
+ icon: 'VolumeDownLine',
68
+ rightIcon: 'VolumeUpLine',
69
+ },
70
+ };
71
+ export const WithIntent: Story = {
72
+ args: {
73
+ intent: 'success',
74
+ },
75
+ render(args, ctx) {
76
+ return (
77
+ <ReqoreControlGroup vertical gapSize='huge'>
78
+ {meta.render({ ...args, intent: 'info' }, ctx)}
79
+ {meta.render({ ...args, intent: 'success', showLabels: true }, ctx)}
80
+ {meta.render(
81
+ {
82
+ ...args,
83
+ intent: 'warning',
84
+ showLabels: true,
85
+ minLabelProps: { intent: 'info' },
86
+ icon: 'WeiboFill',
87
+ },
88
+ ctx
89
+ )}
90
+ {meta.render({ ...args, intent: 'danger' }, ctx)}
91
+ </ReqoreControlGroup>
92
+ );
93
+ },
94
+ };
95
+ export const WithLabels: Story = {
96
+ args: {
97
+ showLabels: true,
98
+ },
99
+ };
100
+ export const WithLabelsBelow: Story = {
101
+ args: {
102
+ showLabels: true,
103
+ labelsPosition: 'bottom',
104
+ },
105
+ };
106
+ export const WithLabelsAndIcons: Story = {
107
+ args: {
108
+ ...WithLabels.args,
109
+ ...WithIcons.args,
110
+ },
111
+ };
112
+
113
+ export const WithCurrentValueOverThumb: Story = {
114
+ args: {
115
+ ...WithLabels.args,
116
+ ...WithIcons.args,
117
+ displayCurrentValueOverThumb: true,
118
+ },
119
+ };
120
+
121
+ export const WithSize: Story = {
122
+ args: {
123
+ ...WithLabels.args,
124
+ ...WithIcons.args,
125
+ size: 'small',
126
+ },
127
+ };
128
+
129
+ export const WithEffect: Story = {
130
+ args: {
131
+ showLabels: true,
132
+ effect: {
133
+ gradient: {
134
+ direction: 'to right bottom',
135
+ colors: { 0: '#ca74da', 100: '#521b6e' },
136
+ animate: 'active',
137
+ },
138
+ spaced: 2,
139
+ uppercase: true,
140
+ weight: 'thick',
141
+ textSize: 'small',
142
+ },
143
+
144
+ rangeProps: {
145
+ effect: {
146
+ gradient: {
147
+ direction: 'to right bottom',
148
+ colors: { 0: '#f9f9f9', 100: '#ca55a9' },
149
+ animate: 'always',
150
+ },
151
+ },
152
+ },
153
+ thumbProps: {
154
+ effect: {
155
+ gradient: {
156
+ direction: 'to right bottom',
157
+ colors: { 0: '#f9f9f9', 100: '#ca55a9' },
158
+ animate: 'active',
159
+ },
160
+ },
161
+ },
162
+
163
+ currentMaxLabelProps: {
164
+ effect: {
165
+ gradient: {
166
+ type: 'radial',
167
+ colors: { 0: '#41baea', 100: '#80ffd9' },
168
+ animate: 'hover',
169
+ },
170
+ },
171
+ },
172
+ },
173
+ };