@hellboy/ds 0.1.2
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 +111 -0
- package/dist/index.css +3699 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +1087 -0
- package/dist/index.d.ts +1087 -0
- package/dist/index.js +3391 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3287 -0
- package/dist/index.mjs.map +1 -0
- package/dist/theme.css +55 -0
- package/hellboy-ds-0.1.2.tgz +0 -0
- package/package.json +42 -0
- package/src/components/badge/Badge.tsx +29 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/banner/Banner.tsx +48 -0
- package/src/components/banner/banner.css +44 -0
- package/src/components/banner/index.ts +1 -0
- package/src/components/button/button.tsx +127 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +57 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/Checkbox.tsx +98 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/code-block/code-block.tsx +44 -0
- package/src/components/code-block/index.ts +1 -0
- package/src/components/color-control/color-control.tsx +322 -0
- package/src/components/color-control/index.ts +1 -0
- package/src/components/drag-handle/DragHandle.tsx +78 -0
- package/src/components/drag-handle/index.ts +1 -0
- package/src/components/drawer/drawer.tsx +82 -0
- package/src/components/drawer/index.ts +1 -0
- package/src/components/floating-bar/floating-bar.tsx +52 -0
- package/src/components/floating-bar/index.ts +2 -0
- package/src/components/footer/footer.tsx +28 -0
- package/src/components/footer/index.ts +1 -0
- package/src/components/grid/Grid.tsx +53 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/header/header.tsx +57 -0
- package/src/components/header/index.ts +1 -0
- package/src/components/icons/icons.tsx +44 -0
- package/src/components/icons/index.ts +1 -0
- package/src/components/index.ts +29 -0
- package/src/components/input/DatePicker.tsx +133 -0
- package/src/components/input/Input.tsx +220 -0
- package/src/components/input/InputDate.tsx +10 -0
- package/src/components/input/InputDateTime.tsx +10 -0
- package/src/components/input/InputEmail.tsx +10 -0
- package/src/components/input/InputField.tsx +137 -0
- package/src/components/input/InputNumber.tsx +10 -0
- package/src/components/input/InputPassword.tsx +10 -0
- package/src/components/input/InputSearch.tsx +10 -0
- package/src/components/input/InputTel.tsx +10 -0
- package/src/components/input/InputText.tsx +10 -0
- package/src/components/input/InputTime.tsx +10 -0
- package/src/components/input/InputUrl.tsx +10 -0
- package/src/components/input/TimePicker.tsx +151 -0
- package/src/components/input/index.ts +11 -0
- package/src/components/layout/Layout.tsx +244 -0
- package/src/components/layout/index.ts +1 -0
- package/src/components/list/List.tsx +159 -0
- package/src/components/list/index.ts +1 -0
- package/src/components/navbar/MenuCategory.tsx +20 -0
- package/src/components/navbar/MenuGroup.tsx +288 -0
- package/src/components/navbar/MenuItem.tsx +65 -0
- package/src/components/navbar/Navbar.tsx +23 -0
- package/src/components/navbar/index.ts +4 -0
- package/src/components/page/index.ts +1 -0
- package/src/components/page/page.tsx +46 -0
- package/src/components/page-index/PageIndex.tsx +275 -0
- package/src/components/page-index/index.ts +1 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.tsx +199 -0
- package/src/components/radio/Radio.tsx +176 -0
- package/src/components/radio/index.ts +1 -0
- package/src/components/section/index.ts +1 -0
- package/src/components/section/section.tsx +66 -0
- package/src/components/select/Select.tsx +212 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/slider/Slider.tsx +267 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +99 -0
- package/src/components/table/Table.tsx +147 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/theme-control/index.ts +1 -0
- package/src/components/theme-control/theme-control.tsx +78 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +207 -0
- package/src/contexts/NavbarTooltipContext.tsx +48 -0
- package/src/contexts/index.ts +1 -0
- package/src/foundations/motion.md +136 -0
- package/src/index.ts +40 -0
- package/src/style/_shared/field.css +69 -0
- package/src/style/components/badge/badge.css +74 -0
- package/src/style/components/button/button.css +244 -0
- package/src/style/components/card/card.css +69 -0
- package/src/style/components/checkbox.css +142 -0
- package/src/style/components/code-block/code-block.css +34 -0
- package/src/style/components/color-control/color-control.css +126 -0
- package/src/style/components/drag-handle/drag-handle.css +68 -0
- package/src/style/components/drawer/drawer.css +210 -0
- package/src/style/components/floating-bar/floating-bar.css +39 -0
- package/src/style/components/footer/footer.css +108 -0
- package/src/style/components/grid/grid.css +33 -0
- package/src/style/components/header/header.css +44 -0
- package/src/style/components/icons/icons.css +44 -0
- package/src/style/components/input/input.css +393 -0
- package/src/style/components/layout/layout.css +205 -0
- package/src/style/components/list/list.css +140 -0
- package/src/style/components/navbar/navbar.css +342 -0
- package/src/style/components/page/page.css +46 -0
- package/src/style/components/page-index/page-index.css +158 -0
- package/src/style/components/popover/popover.css +44 -0
- package/src/style/components/radio.css +178 -0
- package/src/style/components/section/section.css +67 -0
- package/src/style/components/select/select.css +143 -0
- package/src/style/components/slider/slider.css +159 -0
- package/src/style/components/switch/switch.css +267 -0
- package/src/style/components/table/table.css +108 -0
- package/src/style/components/theme-control/theme-control.css +35 -0
- package/src/style/components/tooltip/tooltip.css +52 -0
- package/src/style/foundations/global.css +316 -0
- package/src/style/foundations/motion.css +164 -0
- package/src/style/foundations/spacing.css +51 -0
- package/src/style/foundations/typography.css +39 -0
- package/src/style/foundations/z-index.css +81 -0
- package/src/style/modes/dark.css +146 -0
- package/src/style/modes/light.css +147 -0
- package/src/style/semantic.css +52 -0
- package/src/style/styles.css +51 -0
- package/src/style/themes/theme.json +37 -0
- package/src/utils/README.md +305 -0
- package/src/utils/USER_PREFERENCES.md +558 -0
- package/src/utils/theme.ts +127 -0
- package/src/utils/user-preferences.ts +577 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +52 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { ReactNode, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
export type SectionSize = 'sm' | 'md' | 'lg';
|
|
4
|
+
|
|
5
|
+
export interface SectionProps {
|
|
6
|
+
/**
|
|
7
|
+
* Section title
|
|
8
|
+
*/
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Section content
|
|
13
|
+
*/
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Section size/padding
|
|
18
|
+
* @default 'md'
|
|
19
|
+
*/
|
|
20
|
+
size?: SectionSize;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Custom class name
|
|
24
|
+
*/
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Section component for content areas
|
|
30
|
+
* Provides consistent spacing and styling for content sections
|
|
31
|
+
*/
|
|
32
|
+
export const Section: React.FC<SectionProps> = ({ title, children, size = 'md', className }) => {
|
|
33
|
+
const sectionClasses = [
|
|
34
|
+
'section',
|
|
35
|
+
size !== 'md' && `section--${size}`,
|
|
36
|
+
className,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(' ');
|
|
40
|
+
|
|
41
|
+
// Generate ID from title for page index navigation
|
|
42
|
+
const sectionId = useMemo(() => {
|
|
43
|
+
if (typeof title === 'string' && title.trim()) {
|
|
44
|
+
return title
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
47
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
48
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}, [title]);
|
|
53
|
+
|
|
54
|
+
const titleText = typeof title === 'string' ? title : undefined;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<section
|
|
58
|
+
className={sectionClasses}
|
|
59
|
+
id={sectionId}
|
|
60
|
+
data-section-title={titleText}
|
|
61
|
+
>
|
|
62
|
+
{title && <h2 className="section__title">{title}</h2>}
|
|
63
|
+
<div className="section__content">{children}</div>
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Icon } from '../icons/icons';
|
|
3
|
+
import { Popover } from '../popover/popover';
|
|
4
|
+
import { List, ListItem } from '../list/List';
|
|
5
|
+
import '../../style/components/select/select.css';
|
|
6
|
+
|
|
7
|
+
export interface SelectOption {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectProps {
|
|
15
|
+
/**
|
|
16
|
+
* Select options
|
|
17
|
+
*/
|
|
18
|
+
options: SelectOption[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Current value
|
|
22
|
+
*/
|
|
23
|
+
value?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Change handler
|
|
27
|
+
*/
|
|
28
|
+
onChange?: (value: string) => void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Label text
|
|
32
|
+
*/
|
|
33
|
+
label?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Placeholder when no value selected
|
|
37
|
+
*/
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Helper text shown below the select
|
|
42
|
+
*/
|
|
43
|
+
helperText?: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Error message - when present, select is in error state
|
|
47
|
+
*/
|
|
48
|
+
error?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether the select is disabled
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Full width select
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
fullWidth?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Size variant
|
|
63
|
+
* @default 'md'
|
|
64
|
+
*/
|
|
65
|
+
size?: 'sm' | 'md' | 'lg';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom class name
|
|
69
|
+
*/
|
|
70
|
+
className?: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ID for accessibility
|
|
74
|
+
*/
|
|
75
|
+
id?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const Select: React.FC<SelectProps> = ({
|
|
79
|
+
options,
|
|
80
|
+
value,
|
|
81
|
+
onChange,
|
|
82
|
+
label,
|
|
83
|
+
placeholder = 'Select an option',
|
|
84
|
+
helperText,
|
|
85
|
+
error,
|
|
86
|
+
disabled = false,
|
|
87
|
+
fullWidth = false,
|
|
88
|
+
size = 'md',
|
|
89
|
+
className = '',
|
|
90
|
+
id,
|
|
91
|
+
}) => {
|
|
92
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
93
|
+
const selectId = id || `select-${React.useId()}`;
|
|
94
|
+
|
|
95
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
96
|
+
|
|
97
|
+
const handleSelect = (optionValue: string) => {
|
|
98
|
+
onChange?.(optionValue);
|
|
99
|
+
setIsOpen(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
103
|
+
if (disabled) return;
|
|
104
|
+
|
|
105
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
setIsOpen(!isOpen);
|
|
108
|
+
} else if (e.key === 'Escape') {
|
|
109
|
+
setIsOpen(false);
|
|
110
|
+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (!isOpen) {
|
|
113
|
+
setIsOpen(true);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const containerClasses = [
|
|
119
|
+
'select-container',
|
|
120
|
+
fullWidth && 'select-container--full-width',
|
|
121
|
+
className,
|
|
122
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join(' ');
|
|
125
|
+
|
|
126
|
+
const triggerClasses = [
|
|
127
|
+
'select__trigger',
|
|
128
|
+
`select__trigger--${size}`,
|
|
129
|
+
error && 'select__trigger--error',
|
|
130
|
+
disabled && 'select__trigger--disabled',
|
|
131
|
+
isOpen && 'select__trigger--open',
|
|
132
|
+
]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join(' ');
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className={containerClasses}>
|
|
138
|
+
{label && (
|
|
139
|
+
<label htmlFor={selectId} className="select__label">
|
|
140
|
+
{label}
|
|
141
|
+
</label>
|
|
142
|
+
)}
|
|
143
|
+
<Popover
|
|
144
|
+
trigger={
|
|
145
|
+
<button
|
|
146
|
+
id={selectId}
|
|
147
|
+
type="button"
|
|
148
|
+
className={triggerClasses}
|
|
149
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
150
|
+
onKeyDown={handleKeyDown}
|
|
151
|
+
disabled={disabled}
|
|
152
|
+
aria-haspopup="listbox"
|
|
153
|
+
aria-expanded={isOpen}
|
|
154
|
+
aria-labelledby={label ? `${selectId}-label` : undefined}
|
|
155
|
+
aria-describedby={
|
|
156
|
+
error ? `${selectId}-error` : helperText ? `${selectId}-helper` : undefined
|
|
157
|
+
}
|
|
158
|
+
aria-invalid={error ? 'true' : 'false'}
|
|
159
|
+
>
|
|
160
|
+
<span className="select__trigger-content">
|
|
161
|
+
{selectedOption?.icon && (
|
|
162
|
+
<Icon
|
|
163
|
+
name={selectedOption.icon}
|
|
164
|
+
size={size === 'sm' ? 16 : size === 'lg' ? 24 : 20}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
<span className="select__trigger-text">
|
|
168
|
+
{selectedOption ? selectedOption.label : placeholder}
|
|
169
|
+
</span>
|
|
170
|
+
</span>
|
|
171
|
+
<Icon
|
|
172
|
+
name="chevron-down"
|
|
173
|
+
size={size === 'sm' ? 16 : size === 'lg' ? 24 : 20}
|
|
174
|
+
className="select__trigger-icon"
|
|
175
|
+
/>
|
|
176
|
+
</button>
|
|
177
|
+
}
|
|
178
|
+
isOpen={isOpen}
|
|
179
|
+
onToggle={() => !disabled && setIsOpen(!isOpen)}
|
|
180
|
+
placement="bottom"
|
|
181
|
+
>
|
|
182
|
+
<div className="select__dropdown" role="listbox">
|
|
183
|
+
<List>
|
|
184
|
+
{options.map((option) => (
|
|
185
|
+
<ListItem
|
|
186
|
+
key={option.value}
|
|
187
|
+
icon={option.icon}
|
|
188
|
+
selected={option.value === value}
|
|
189
|
+
disabled={option.disabled}
|
|
190
|
+
onClick={() => !option.disabled && handleSelect(option.value)}
|
|
191
|
+
role="option"
|
|
192
|
+
aria-selected={option.value === value}
|
|
193
|
+
>
|
|
194
|
+
{option.label}
|
|
195
|
+
</ListItem>
|
|
196
|
+
))}
|
|
197
|
+
</List>
|
|
198
|
+
</div>
|
|
199
|
+
</Popover>
|
|
200
|
+
{error && (
|
|
201
|
+
<p id={`${selectId}-error`} className="select__message select__message--error">
|
|
202
|
+
{error}
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
{helperText && !error && (
|
|
206
|
+
<p id={`${selectId}-helper`} className="select__message">
|
|
207
|
+
{helperText}
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Select, type SelectProps, type SelectOption } from './Select';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { Icon } from '../icons/icons';
|
|
3
|
+
import '../../style/components/slider/slider.css';
|
|
4
|
+
|
|
5
|
+
export interface SliderProps {
|
|
6
|
+
/**
|
|
7
|
+
* Minimum value
|
|
8
|
+
* @default 0
|
|
9
|
+
*/
|
|
10
|
+
min?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum value
|
|
13
|
+
* @default 100
|
|
14
|
+
*/
|
|
15
|
+
max?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Current value
|
|
18
|
+
*/
|
|
19
|
+
value: number;
|
|
20
|
+
/**
|
|
21
|
+
* Step increment
|
|
22
|
+
* @default 1
|
|
23
|
+
*/
|
|
24
|
+
step?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Callback when value changes
|
|
27
|
+
*/
|
|
28
|
+
onChange: (value: number) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Callback when drag starts
|
|
31
|
+
*/
|
|
32
|
+
onDragStart?: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Callback when drag ends (value committed)
|
|
35
|
+
*/
|
|
36
|
+
onChangeEnd?: (value: number) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Label for the slider
|
|
39
|
+
*/
|
|
40
|
+
label?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Show value display
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
showValue?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Custom class name
|
|
48
|
+
*/
|
|
49
|
+
className?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Disabled state
|
|
52
|
+
* @default false
|
|
53
|
+
*/
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Slider type for different backgrounds
|
|
57
|
+
* @default 'default'
|
|
58
|
+
*/
|
|
59
|
+
type?: 'default' | 'hue' | 'saturation' | 'lightness';
|
|
60
|
+
/**
|
|
61
|
+
* Base color for saturation/lightness gradients (hsl values)
|
|
62
|
+
*/
|
|
63
|
+
baseHue?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Base saturation for lightness gradient (0-100)
|
|
66
|
+
*/
|
|
67
|
+
baseSaturation?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Slider: React.FC<SliderProps> = ({
|
|
71
|
+
min = 0,
|
|
72
|
+
max = 100,
|
|
73
|
+
value,
|
|
74
|
+
step = 1,
|
|
75
|
+
onChange,
|
|
76
|
+
onDragStart,
|
|
77
|
+
onChangeEnd,
|
|
78
|
+
label,
|
|
79
|
+
showValue = true,
|
|
80
|
+
className = '',
|
|
81
|
+
disabled = false,
|
|
82
|
+
type = 'default',
|
|
83
|
+
baseHue = 0,
|
|
84
|
+
baseSaturation = 70,
|
|
85
|
+
}) => {
|
|
86
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
87
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
88
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
|
+
const valueRef = useRef(value);
|
|
90
|
+
|
|
91
|
+
// Keep value ref in sync
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
valueRef.current = value;
|
|
94
|
+
}, [value]);
|
|
95
|
+
|
|
96
|
+
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
97
|
+
const newValue = parseFloat(event.target.value);
|
|
98
|
+
onChange(newValue);
|
|
99
|
+
}, [onChange]);
|
|
100
|
+
|
|
101
|
+
const getValueFromPosition = useCallback((clientX: number) => {
|
|
102
|
+
if (!containerRef.current) return value;
|
|
103
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
104
|
+
const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
105
|
+
const rawValue = min + percentage * (max - min);
|
|
106
|
+
const steppedValue = Math.round(rawValue / step) * step;
|
|
107
|
+
return Math.max(min, Math.min(max, steppedValue));
|
|
108
|
+
}, [min, max, step, value]);
|
|
109
|
+
|
|
110
|
+
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
|
111
|
+
if (disabled) return;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
setIsDragging(true);
|
|
114
|
+
onDragStart?.();
|
|
115
|
+
const newValue = getValueFromPosition(event.clientX);
|
|
116
|
+
onChange(newValue);
|
|
117
|
+
}, [disabled, onDragStart, getValueFromPosition, onChange]);
|
|
118
|
+
|
|
119
|
+
const handleMouseMove = useCallback((event: MouseEvent) => {
|
|
120
|
+
if (!isDragging) return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
const newValue = getValueFromPosition(event.clientX);
|
|
123
|
+
onChange(newValue);
|
|
124
|
+
}, [isDragging, getValueFromPosition, onChange]);
|
|
125
|
+
|
|
126
|
+
const handleMouseUp = useCallback(() => {
|
|
127
|
+
if (!isDragging) return;
|
|
128
|
+
setIsDragging(false);
|
|
129
|
+
onChangeEnd?.(valueRef.current);
|
|
130
|
+
}, [isDragging, onChangeEnd]);
|
|
131
|
+
|
|
132
|
+
// Add global mouse listeners for dragging
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
if (!isDragging) return;
|
|
135
|
+
|
|
136
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
137
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
141
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
142
|
+
};
|
|
143
|
+
}, [isDragging, handleMouseMove, handleMouseUp]);
|
|
144
|
+
|
|
145
|
+
// Handle touch events
|
|
146
|
+
const handleTouchStart = useCallback((event: React.TouchEvent) => {
|
|
147
|
+
if (disabled) return;
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
setIsDragging(true);
|
|
150
|
+
onDragStart?.();
|
|
151
|
+
const touch = event.touches[0];
|
|
152
|
+
const newValue = getValueFromPosition(touch.clientX);
|
|
153
|
+
onChange(newValue);
|
|
154
|
+
}, [disabled, onDragStart, getValueFromPosition, onChange]);
|
|
155
|
+
|
|
156
|
+
const handleTouchMove = useCallback((event: TouchEvent) => {
|
|
157
|
+
if (!isDragging) return;
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
const touch = event.touches[0];
|
|
160
|
+
const newValue = getValueFromPosition(touch.clientX);
|
|
161
|
+
onChange(newValue);
|
|
162
|
+
}, [isDragging, getValueFromPosition, onChange]);
|
|
163
|
+
|
|
164
|
+
const handleTouchEnd = useCallback(() => {
|
|
165
|
+
if (!isDragging) return;
|
|
166
|
+
setIsDragging(false);
|
|
167
|
+
onChangeEnd?.(valueRef.current);
|
|
168
|
+
}, [isDragging, onChangeEnd]);
|
|
169
|
+
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
if (!isDragging) return;
|
|
172
|
+
|
|
173
|
+
document.addEventListener('touchmove', handleTouchMove);
|
|
174
|
+
document.addEventListener('touchend', handleTouchEnd);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
document.removeEventListener('touchmove', handleTouchMove);
|
|
178
|
+
document.removeEventListener('touchend', handleTouchEnd);
|
|
179
|
+
};
|
|
180
|
+
}, [isDragging, handleTouchMove, handleTouchEnd]);
|
|
181
|
+
|
|
182
|
+
const sliderClasses = [
|
|
183
|
+
'slider',
|
|
184
|
+
`slider--${type}`,
|
|
185
|
+
isDragging && 'slider--dragging',
|
|
186
|
+
disabled && 'slider--disabled',
|
|
187
|
+
className
|
|
188
|
+
].filter(Boolean).join(' ');
|
|
189
|
+
|
|
190
|
+
// Generate background based on type
|
|
191
|
+
const getBackground = () => {
|
|
192
|
+
switch (type) {
|
|
193
|
+
case 'hue':
|
|
194
|
+
return 'linear-gradient(to right, hsl(0, 70%, 50%), hsl(60, 70%, 50%), hsl(120, 70%, 50%), hsl(180, 70%, 50%), hsl(240, 70%, 50%), hsl(300, 70%, 50%), hsl(360, 70%, 50%))';
|
|
195
|
+
case 'saturation':
|
|
196
|
+
return `linear-gradient(to right, hsl(${baseHue}, 0%, 50%), hsl(${baseHue}, 100%, 50%))`;
|
|
197
|
+
case 'lightness':
|
|
198
|
+
return `linear-gradient(to right, hsl(${baseHue}, ${baseSaturation}%, 0%), hsl(${baseHue}, ${baseSaturation}%, 50%), hsl(${baseHue}, ${baseSaturation}%, 100%))`;
|
|
199
|
+
default:
|
|
200
|
+
return 'var(--color-action-primary)';
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const inputStyle = { background: getBackground() };
|
|
205
|
+
|
|
206
|
+
// Generate step marks
|
|
207
|
+
const stepMarks = step > 1 ? Array.from({ length: Math.floor((max - min) / step) + 1 }, (_, i) => min + i * step) : [];
|
|
208
|
+
|
|
209
|
+
// Calculate thumb position as percentage
|
|
210
|
+
const thumbPosition = ((value - min) / (max - min)) * 100;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className={sliderClasses}>
|
|
214
|
+
{(label || showValue) && (
|
|
215
|
+
<div className="slider__header">
|
|
216
|
+
{label && <label className="slider__label">{label}</label>}
|
|
217
|
+
{showValue && (
|
|
218
|
+
<span className="slider__value">{value}</span>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<div className="slider__container" ref={containerRef} onMouseDown={handleMouseDown} onTouchStart={handleTouchStart}>
|
|
224
|
+
<input
|
|
225
|
+
ref={inputRef}
|
|
226
|
+
type="range"
|
|
227
|
+
min={min}
|
|
228
|
+
max={max}
|
|
229
|
+
step={step}
|
|
230
|
+
value={value}
|
|
231
|
+
onChange={handleChange}
|
|
232
|
+
className="slider__input"
|
|
233
|
+
style={inputStyle}
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
readOnly
|
|
236
|
+
/>
|
|
237
|
+
|
|
238
|
+
{/* Custom thumb with icon */}
|
|
239
|
+
<div
|
|
240
|
+
className="slider__thumb"
|
|
241
|
+
style={{
|
|
242
|
+
left: `${thumbPosition}%`,
|
|
243
|
+
transform: 'translateX(-50%)',
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<div className="slider__thumb-icon">
|
|
247
|
+
<Icon name="arrow-down" size={16} />
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{stepMarks.length > 0 && (
|
|
252
|
+
<div className="slider__marks">
|
|
253
|
+
{stepMarks.map((mark) => (
|
|
254
|
+
<div
|
|
255
|
+
key={mark}
|
|
256
|
+
className="slider__mark"
|
|
257
|
+
style={{
|
|
258
|
+
left: `${((mark - min) / (max - min)) * 100}%`,
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Slider";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Switch, type SwitchProps, type SwitchSize } from './switch';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from '../icons';
|
|
3
|
+
import '../../style/components/switch/switch.css';
|
|
4
|
+
|
|
5
|
+
export type SwitchSize = 'sm' | 'md' | 'lg';
|
|
6
|
+
|
|
7
|
+
export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
8
|
+
/**
|
|
9
|
+
* Size of the switch
|
|
10
|
+
* @default 'md'
|
|
11
|
+
*/
|
|
12
|
+
size?: SwitchSize;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Label text for the switch
|
|
16
|
+
*/
|
|
17
|
+
label?: React.ReactNode;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Icon to display when switch is ON
|
|
21
|
+
*/
|
|
22
|
+
onIcon?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Icon to display when switch is OFF
|
|
26
|
+
*/
|
|
27
|
+
offIcon?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Error message to display below the switch
|
|
31
|
+
*/
|
|
32
|
+
error?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Helper text to display below the switch
|
|
36
|
+
*/
|
|
37
|
+
helperText?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
size = 'md',
|
|
44
|
+
label,
|
|
45
|
+
onIcon,
|
|
46
|
+
offIcon,
|
|
47
|
+
error,
|
|
48
|
+
helperText,
|
|
49
|
+
disabled = false,
|
|
50
|
+
className,
|
|
51
|
+
checked,
|
|
52
|
+
...props
|
|
53
|
+
},
|
|
54
|
+
ref
|
|
55
|
+
) => {
|
|
56
|
+
const switchClasses = [
|
|
57
|
+
'switch',
|
|
58
|
+
`switch--${size}`,
|
|
59
|
+
checked && 'switch--checked',
|
|
60
|
+
disabled && 'switch--disabled',
|
|
61
|
+
error && 'switch--error',
|
|
62
|
+
className,
|
|
63
|
+
]
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(' ');
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="switch__wrapper">
|
|
69
|
+
<label className="switch__label">
|
|
70
|
+
<input
|
|
71
|
+
type="checkbox"
|
|
72
|
+
ref={ref}
|
|
73
|
+
checked={checked}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
className="switch__input"
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
<span className={switchClasses}>
|
|
79
|
+
<span className="switch__track">
|
|
80
|
+
<span className="switch__thumb">
|
|
81
|
+
{onIcon && <Icon name={onIcon} size={16} className="switch__icon switch__icon--on" />}
|
|
82
|
+
{offIcon && <Icon name={offIcon} size={16} className="switch__icon switch__icon--off" />}
|
|
83
|
+
</span>
|
|
84
|
+
</span>
|
|
85
|
+
</span>
|
|
86
|
+
{label && <span className="switch__text">{label}</span>}
|
|
87
|
+
</label>
|
|
88
|
+
{(error || helperText) && (
|
|
89
|
+
<div className="switch__message">
|
|
90
|
+
{error && <span className="switch__error-text">{error}</span>}
|
|
91
|
+
{helperText && !error && <span className="switch__helper-text">{helperText}</span>}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
Switch.displayName = 'Switch';
|