@companix/uikit 0.0.1
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/.eslintrc +54 -0
- package/declaration.d.ts +4 -0
- package/index.html +12 -0
- package/package.json +66 -0
- package/playground/App.tsx +166 -0
- package/playground/Example.tsx +14 -0
- package/playground/Test.tsx +44 -0
- package/playground/animation-test-1/index.scss +20 -0
- package/playground/animation-test-1/index.tsx +17 -0
- package/playground/animation-test-2/index.scss +62 -0
- package/playground/animation-test-2/index.tsx +32 -0
- package/playground/bootstrap.tsx +19 -0
- package/playground/buttons/index.tsx +132 -0
- package/playground/checkbox/index.tsx +64 -0
- package/playground/date-input/index.tsx +45 -0
- package/playground/date-picker/index.tsx +41 -0
- package/playground/dialog/index.tsx +92 -0
- package/playground/dialog-alert/index.tsx +47 -0
- package/playground/drawer/index.tsx +55 -0
- package/playground/index.css +33 -0
- package/playground/index.scss +270 -0
- package/playground/input/index.tsx +112 -0
- package/playground/number-inputs/index.tsx +50 -0
- package/playground/popovers/index.tsx +70 -0
- package/playground/radio-group/index.tsx +69 -0
- package/playground/select/index.tsx +72 -0
- package/playground/select-tags/index.tsx +36 -0
- package/playground/styles.scss +2 -0
- package/playground/switch/index.tsx +44 -0
- package/playground/tabs/index.tsx +16 -0
- package/playground/test.scss +0 -0
- package/playground/text-area/index.tsx +17 -0
- package/playground/text-input/index.tsx +12 -0
- package/playground/toaster/index.tsx +156 -0
- package/playground/tooltip/index.tsx +26 -0
- package/src/Button/Button.scss +128 -0
- package/src/Button/index.tsx +72 -0
- package/src/ButtonGroup/ButtonGroup.scss +18 -0
- package/src/ButtonGroup/index.tsx +20 -0
- package/src/Checkbox/Checkbox.scss +115 -0
- package/src/Checkbox/index.tsx +46 -0
- package/src/Countdown/index.tsx +54 -0
- package/src/DateInput/DateInput.scss +11 -0
- package/src/DateInput/index.tsx +96 -0
- package/src/DatePicker/Calendar.scss +125 -0
- package/src/DatePicker/Calendar.tsx +157 -0
- package/src/DatePicker/CalendarHeader.tsx +139 -0
- package/src/DatePicker/DatePicker.scss +0 -0
- package/src/DatePicker/index.tsx +177 -0
- package/src/Dialog/Dialog.scss +25 -0
- package/src/Dialog/Popup.scss +55 -0
- package/src/Dialog/index.tsx +31 -0
- package/src/DialogAlert/Alert.scss +52 -0
- package/src/DialogAlert/Alert.tsx +78 -0
- package/src/DialogAlert/Viewport.tsx +52 -0
- package/src/DialogAlert/index.tsx +37 -0
- package/src/Drawer/Drawer.scss +112 -0
- package/src/Drawer/index.tsx +46 -0
- package/src/File/index.tsx +60 -0
- package/src/Form/Form.scss +70 -0
- package/src/Form/Input.scss +24 -0
- package/src/Form/index.tsx +131 -0
- package/src/Icon/icon.scss +18 -0
- package/src/Icon/index.tsx +43 -0
- package/src/LoadButton/index.tsx +17 -0
- package/src/NumberInput/index.tsx +74 -0
- package/src/OptionItem/Option.scss +89 -0
- package/src/OptionItem/OptionItem.tsx +49 -0
- package/src/OptionItem/OptionsList.tsx +26 -0
- package/src/Popover/Popover.scss +80 -0
- package/src/Popover/index.tsx +117 -0
- package/src/Radio/Radio.scss +148 -0
- package/src/Radio/index.tsx +68 -0
- package/src/Scrollable/ImitateScroll.tsx +141 -0
- package/src/Scrollable/Scrollable.scss +50 -0
- package/src/Scrollable/index.tsx +141 -0
- package/src/Select/Select.scss +80 -0
- package/src/Select/SelectInput.tsx +131 -0
- package/src/Select/index.tsx +134 -0
- package/src/SelectTags/SelectTags.scss +66 -0
- package/src/SelectTags/index.tsx +192 -0
- package/src/Spinner/Spinner.scss +14 -0
- package/src/Spinner/index.tsx +19 -0
- package/src/Stepper/StepperInput.scss +35 -0
- package/src/Stepper/index.tsx +76 -0
- package/src/Switch/Switch.scss +102 -0
- package/src/Switch/index.tsx +49 -0
- package/src/Tabs/Tabs.scss +58 -0
- package/src/Tabs/index.tsx +89 -0
- package/src/TextArea/TextArea.scss +34 -0
- package/src/TextArea/index.tsx +51 -0
- package/src/Toaster/RemoveListener.tsx +11 -0
- package/src/Toaster/Toast.tsx +69 -0
- package/src/Toaster/Toaster.scss +151 -0
- package/src/Toaster/Viewport.tsx +117 -0
- package/src/Toaster/index.tsx +52 -0
- package/src/Tooltip/Tooltip.scss +28 -0
- package/src/Tooltip/index.tsx +33 -0
- package/src/__hooks/use-frooze-closing.ts +51 -0
- package/src/__hooks/use-loading.ts +34 -0
- package/src/__hooks/use-local-storage.ts +19 -0
- package/src/__hooks/use-popover-position.ts +24 -0
- package/src/__hooks/use-previos.ts +25 -0
- package/src/__hooks/use-resize.ts +41 -0
- package/src/__hooks/use-scrollbox.ts +45 -0
- package/src/__hooks/use-stepper-input.ts +82 -0
- package/src/__hooks/use-update.ts +19 -0
- package/src/__hooks/useCalendar.ts +104 -0
- package/src/__hooks/useCalendarOptions-copy.ts +87 -0
- package/src/__hooks/useCalendarOptions.ts +68 -0
- package/src/__libs/calendar.ts +175 -0
- package/src/__utils/utils.ts +137 -0
- package/src/css.scss +120 -0
- package/src/index.scss +22 -0
- package/src/index.ts +36 -0
- package/src/mixins.scss +99 -0
- package/src/theme.scss +103 -0
- package/src/types.ts +14 -0
- package/tailwind.config.js +91 -0
- package/themes/classic/animations.scss +179 -0
- package/themes/classic/classic.scss +493 -0
- package/tsconfig.json +27 -0
- package/vite.build.ts +35 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react'
|
|
2
|
+
import { OptionsList } from '../OptionItem/OptionsList'
|
|
3
|
+
import { OptionItem } from '../OptionItem/OptionItem'
|
|
4
|
+
import { Popover } from '../Popover'
|
|
5
|
+
import { useFroozeClosing } from '../__hooks/use-frooze-closing'
|
|
6
|
+
import { FormProps } from '../Form'
|
|
7
|
+
import type { Option } from '../types'
|
|
8
|
+
import { SelectInput } from './SelectInput'
|
|
9
|
+
import { useScrollListController } from '../__hooks/use-scrollbox'
|
|
10
|
+
|
|
11
|
+
interface SelectProps<T> extends Omit<FormProps, 'value' | 'onChange' | 'rightElement'> {
|
|
12
|
+
options: Option<T>[]
|
|
13
|
+
onChange: (event: T | null) => void
|
|
14
|
+
placeholder?: string
|
|
15
|
+
clearButton?: boolean
|
|
16
|
+
clearButtonIcon?: boolean
|
|
17
|
+
value: T | null
|
|
18
|
+
children?: React.ReactNode
|
|
19
|
+
minimalOptions?: boolean
|
|
20
|
+
matchTarget?: 'width' | 'min-width'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Select = <T,>(props: SelectProps<T>) => {
|
|
24
|
+
const {
|
|
25
|
+
options,
|
|
26
|
+
onChange,
|
|
27
|
+
minimalOptions,
|
|
28
|
+
clearButton,
|
|
29
|
+
clearButtonIcon,
|
|
30
|
+
matchTarget = 'width',
|
|
31
|
+
value,
|
|
32
|
+
children,
|
|
33
|
+
disabled,
|
|
34
|
+
...inputProps
|
|
35
|
+
} = props
|
|
36
|
+
|
|
37
|
+
const currentOption = useMemo(() => {
|
|
38
|
+
const index = options.findIndex((o) => o.value === value)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
index,
|
|
42
|
+
option: options[index] as Option<T> | undefined
|
|
43
|
+
}
|
|
44
|
+
}, [options, value])
|
|
45
|
+
|
|
46
|
+
const active = currentOption.option?.value ?? null
|
|
47
|
+
|
|
48
|
+
const { popoverRef, froozePopoverPosition, handleAnimationEnd } = useFroozeClosing()
|
|
49
|
+
const { scrollToElement, optionsWrapperRef, scrollBoxRef } = useScrollListController()
|
|
50
|
+
|
|
51
|
+
const handleChange = (value: T, close: () => void) => {
|
|
52
|
+
froozePopoverPosition()
|
|
53
|
+
onChange(value)
|
|
54
|
+
close()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleClear = (e: React.MouseEvent) => {
|
|
58
|
+
e.stopPropagation()
|
|
59
|
+
onChange(null)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const onOpened = () => {
|
|
63
|
+
if (currentOption.index !== -1) {
|
|
64
|
+
scrollToElement(currentOption.index, true)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Popover
|
|
70
|
+
minimal
|
|
71
|
+
ref={popoverRef}
|
|
72
|
+
sideOffset={0}
|
|
73
|
+
matchTarget={matchTarget}
|
|
74
|
+
onAnimationEnd={handleAnimationEnd}
|
|
75
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
76
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
content={({ close }) => (
|
|
79
|
+
<SelectPopover<T>
|
|
80
|
+
options={options}
|
|
81
|
+
active={active}
|
|
82
|
+
scrollboxRef={scrollBoxRef}
|
|
83
|
+
optionsWrapperRef={optionsWrapperRef}
|
|
84
|
+
minimalOptions={minimalOptions}
|
|
85
|
+
onOpened={onOpened}
|
|
86
|
+
onSelect={(value) => handleChange(value, close)}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{children ?? (
|
|
91
|
+
<SelectInput
|
|
92
|
+
{...inputProps}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
value={currentOption.option?.title ?? ''}
|
|
95
|
+
onClear={handleClear}
|
|
96
|
+
clearButton={clearButton}
|
|
97
|
+
clearButtonIcon={clearButtonIcon}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
</Popover>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface SelectPopoverProps<T> {
|
|
105
|
+
scrollboxRef?: React.Ref<HTMLDivElement>
|
|
106
|
+
optionsWrapperRef?: React.RefObject<HTMLDivElement>
|
|
107
|
+
options: Option<T>[]
|
|
108
|
+
minimalOptions?: boolean
|
|
109
|
+
active?: T | null
|
|
110
|
+
onSelect?: (value: T) => void
|
|
111
|
+
onOpened?: () => void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const SelectPopover = <T,>(props: SelectPopoverProps<T>) => {
|
|
115
|
+
const { active, onOpened, scrollboxRef, optionsWrapperRef, options, onSelect, minimalOptions } = props
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
onOpened?.()
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<OptionsList scrollboxRef={scrollboxRef} optionsWrapperRef={optionsWrapperRef} maxHeight={300}>
|
|
123
|
+
{options.map((option, i) => (
|
|
124
|
+
<OptionItem
|
|
125
|
+
key={`option-item-${option.value}-${i}`}
|
|
126
|
+
active={active === option.value}
|
|
127
|
+
onClick={() => onSelect?.(option.value)}
|
|
128
|
+
minimal={minimalOptions}
|
|
129
|
+
{...option}
|
|
130
|
+
/>
|
|
131
|
+
))}
|
|
132
|
+
</OptionsList>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
@use '../mixins.scss';
|
|
2
|
+
|
|
3
|
+
.select-tags {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
|
|
7
|
+
.form-input {
|
|
8
|
+
padding-left: var(--form_space, 0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&-empty {
|
|
12
|
+
@include mixins.use-styles(tag, empty);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&-container {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
cursor: text;
|
|
19
|
+
|
|
20
|
+
.expand-icon {
|
|
21
|
+
margin: 0px var(--form_space, 0);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tag {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
cursor: default;
|
|
30
|
+
user-select: none;
|
|
31
|
+
|
|
32
|
+
@include mixins.use-styles(tag);
|
|
33
|
+
|
|
34
|
+
&-name {
|
|
35
|
+
@include mixins.use-styles(tag, name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&-close-button {
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
@include mixins.use-styles(tag, close);
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
@include mixins.use-styles(tag, close, hover);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&-close-icon {
|
|
48
|
+
@include mixins.use-size(tag, close, size);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&-container {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: row;
|
|
54
|
+
align-items: center;
|
|
55
|
+
align-self: stretch;
|
|
56
|
+
flex-wrap: wrap;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
position: relative;
|
|
59
|
+
|
|
60
|
+
@include mixins.use-styles(tag, container);
|
|
61
|
+
|
|
62
|
+
&:not([data-readonly]) {
|
|
63
|
+
padding-bottom: 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useFroozeClosing } from '../__hooks/use-frooze-closing'
|
|
2
|
+
import type { Option } from '../types'
|
|
3
|
+
import { OptionItem, OptionsList, Popover } from '..'
|
|
4
|
+
import { useMemo, useRef, useState } from 'react'
|
|
5
|
+
import { Icon } from '../Icon'
|
|
6
|
+
import { faChevronDown, faClose } from '@fortawesome/free-solid-svg-icons'
|
|
7
|
+
import { attr, contains, getActiveElementByAnotherElement } from '@companix/utils-browser'
|
|
8
|
+
|
|
9
|
+
export interface SelectTagsProps<T> {
|
|
10
|
+
options: Option<T>[]
|
|
11
|
+
onChange: (event: T[]) => void
|
|
12
|
+
placeholder?: string
|
|
13
|
+
value: T[]
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
readOnly?: boolean
|
|
17
|
+
closeAfterSelect?: boolean
|
|
18
|
+
emptyText?: string
|
|
19
|
+
size?: 'sm' | 'md' | 'lg'
|
|
20
|
+
fill?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SelectTags = <T extends string | number>(props: SelectTagsProps<T>) => {
|
|
24
|
+
const {
|
|
25
|
+
options: optionsProp,
|
|
26
|
+
closeAfterSelect,
|
|
27
|
+
placeholder,
|
|
28
|
+
onChange,
|
|
29
|
+
emptyText = 'Ничего не найдено',
|
|
30
|
+
readOnly,
|
|
31
|
+
size = 'md',
|
|
32
|
+
value: values,
|
|
33
|
+
disabled
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
const [inputValue, setInputValue] = useState('')
|
|
37
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
38
|
+
const listboxRef = useRef<HTMLDivElement>(null)
|
|
39
|
+
const { popoverRef, froozePopoverPosition, handleAnimationEnd } = useFroozeClosing()
|
|
40
|
+
|
|
41
|
+
const store = useMemo(() => {
|
|
42
|
+
const store = {} as { [value in T]: Option<T> }
|
|
43
|
+
|
|
44
|
+
optionsProp.forEach((option) => {
|
|
45
|
+
store[option.value] = option
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return store
|
|
49
|
+
}, [optionsProp])
|
|
50
|
+
|
|
51
|
+
const add = (value: T) => {
|
|
52
|
+
if (values.includes(value)) {
|
|
53
|
+
return [...values]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [...values, value]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const remove = (value: T) => {
|
|
60
|
+
return values.filter((item) => value !== item)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleSelect = (value: T[], close: () => void) => {
|
|
64
|
+
if (closeAfterSelect) {
|
|
65
|
+
froozePopoverPosition()
|
|
66
|
+
onChange(value)
|
|
67
|
+
close()
|
|
68
|
+
} else {
|
|
69
|
+
onChange(value)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const options = useMemo(() => {
|
|
74
|
+
if (!inputValue.trim()) {
|
|
75
|
+
return optionsProp
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return optionsProp.filter(({ title }) => {
|
|
79
|
+
const normalizedTitle = title.toLowerCase()
|
|
80
|
+
const normalizedQuery = inputValue.trim().toLowerCase()
|
|
81
|
+
|
|
82
|
+
return normalizedTitle.indexOf(normalizedQuery) >= 0
|
|
83
|
+
})
|
|
84
|
+
}, [inputValue, optionsProp])
|
|
85
|
+
|
|
86
|
+
const handleRootClick = (event: React.MouseEvent) => {
|
|
87
|
+
if (disabled) return
|
|
88
|
+
|
|
89
|
+
// Предотвращаем закрытие Popover при клике на форму
|
|
90
|
+
if (popoverRef.current && popoverRef.current.getAttribute('data-state') === 'open') {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const activeElement = getActiveElementByAnotherElement(event.currentTarget)
|
|
95
|
+
|
|
96
|
+
if (event.defaultPrevented || contains(event.currentTarget, activeElement)) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (inputRef.current) {
|
|
101
|
+
inputRef.current.focus()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleRootMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
106
|
+
// Когда клик в сам инпут, не нужно делать preventDefault, так как не будет работать выделение текста
|
|
107
|
+
if (e.target === inputRef.current) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
// Делаем preventDefault
|
|
111
|
+
e.preventDefault()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const handleRemove = (e: React.MouseEvent, value: T) => {
|
|
115
|
+
e.stopPropagation()
|
|
116
|
+
onChange(remove(value))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// const
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Popover
|
|
123
|
+
minimal
|
|
124
|
+
ref={popoverRef}
|
|
125
|
+
sideOffset={0}
|
|
126
|
+
matchTarget="width"
|
|
127
|
+
onAnimationEnd={handleAnimationEnd}
|
|
128
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
129
|
+
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
130
|
+
content={({ close }) => (
|
|
131
|
+
<OptionsList maxHeight={300}>
|
|
132
|
+
{options.length === 0 && <div className="select-tags-empty">{emptyText}</div>}
|
|
133
|
+
{options.map(({ value, title, icon }, i) => (
|
|
134
|
+
<OptionItem
|
|
135
|
+
key={`option-item-${value}-${i}`}
|
|
136
|
+
active={values.includes(value)}
|
|
137
|
+
onClick={() => handleSelect(add(value), close)}
|
|
138
|
+
title={title}
|
|
139
|
+
icon={icon}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</OptionsList>
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
className="form"
|
|
147
|
+
onClick={handleRootClick}
|
|
148
|
+
onMouseDown={handleRootMouseDown}
|
|
149
|
+
data-size={size}
|
|
150
|
+
>
|
|
151
|
+
<div className="select-tags-container">
|
|
152
|
+
<div className="select-tags">
|
|
153
|
+
{values.length > 0 && (
|
|
154
|
+
<div
|
|
155
|
+
className="tag-container"
|
|
156
|
+
ref={listboxRef}
|
|
157
|
+
role="listbox"
|
|
158
|
+
data-readonly={attr(readOnly)}
|
|
159
|
+
>
|
|
160
|
+
{values.map((value, i) => (
|
|
161
|
+
<div key={`tag-option-${value}-${i}`} className="tag">
|
|
162
|
+
<span className="tag-name">{store[value].title}</span>
|
|
163
|
+
<button className="tag-close-button" onClick={(e) => handleRemove(e, value)}>
|
|
164
|
+
<Icon className="tag-close-icon" icon={faClose} size="xxxs" />
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
{(!readOnly || values.length === 0) && (
|
|
171
|
+
<input
|
|
172
|
+
ref={inputRef}
|
|
173
|
+
type="text"
|
|
174
|
+
autoCapitalize="none"
|
|
175
|
+
autoComplete="off"
|
|
176
|
+
autoCorrect="off"
|
|
177
|
+
className="form-input"
|
|
178
|
+
spellCheck={false}
|
|
179
|
+
value={inputValue}
|
|
180
|
+
disabled={disabled}
|
|
181
|
+
readOnly={readOnly}
|
|
182
|
+
placeholder={placeholder}
|
|
183
|
+
onChange={({ target }) => setInputValue(target.value)}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
<Icon className="expand-icon" icon={faChevronDown} size="xxxs" />
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</Popover>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@keyframes spinner-border {
|
|
2
|
+
to {
|
|
3
|
+
transform: rotate(360deg);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.spinner-border {
|
|
8
|
+
display: inline-block;
|
|
9
|
+
border-style: solid;
|
|
10
|
+
border-color: currentColor;
|
|
11
|
+
border-right-color: transparent;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
animation: 0.75s linear infinite spinner-border;
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './Spinner.scss'
|
|
2
|
+
import cn from 'classnames'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
size?: number
|
|
6
|
+
width?: number
|
|
7
|
+
color?: string
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Spinner = ({ size = 40, className, width = 4, color = 'inherit' }: Props) => {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
style={{ width: `${size}px`, height: `${size}px`, color, borderWidth: `${width}px` }}
|
|
15
|
+
className={cn('spinner-border', className)}
|
|
16
|
+
role="status"
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.number-input-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 4px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.number-stepper {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
height: 30px;
|
|
10
|
+
min-height: 30px;
|
|
11
|
+
line-height: 30px;
|
|
12
|
+
border: 1px solid var(--border-color);
|
|
13
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 47%);
|
|
14
|
+
border-radius: 4px;
|
|
15
|
+
|
|
16
|
+
&-splitter {
|
|
17
|
+
height: 1px;
|
|
18
|
+
background-color: var(--border-color);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&-slot {
|
|
22
|
+
border-radius: 0px;
|
|
23
|
+
height: 13.5px;
|
|
24
|
+
|
|
25
|
+
&[data-slot='decrement'] {
|
|
26
|
+
border-bottom-left-radius: 3px;
|
|
27
|
+
border-bottom-right-radius: 3px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&[data-slot='increment'] {
|
|
31
|
+
border-top-left-radius: 3px;
|
|
32
|
+
border-top-right-radius: 3px;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import './StepperInput.scss'
|
|
2
|
+
|
|
3
|
+
import { Form } from '../Form'
|
|
4
|
+
import { useStepperInput, StepperInputOptions } from '../__hooks/use-stepper-input'
|
|
5
|
+
import { usePress } from '@react-aria/interactions'
|
|
6
|
+
import { Button } from '../Button'
|
|
7
|
+
import { attr } from '@companix/utils-browser'
|
|
8
|
+
import { Icon } from '../Icon'
|
|
9
|
+
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
|
10
|
+
|
|
11
|
+
interface StepperInputProps extends StepperInputOptions {
|
|
12
|
+
buttons?: boolean
|
|
13
|
+
placeholder?: string
|
|
14
|
+
inputStyle?: React.CSSProperties
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const StepperInput = ({ inputStyle, placeholder, buttons, ...options }: StepperInputProps) => {
|
|
18
|
+
const { inputRef, increment, decrement, handleChange, value } = useStepperInput(options)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="number-input-container">
|
|
22
|
+
<Form
|
|
23
|
+
ref={inputRef}
|
|
24
|
+
placeholder={placeholder}
|
|
25
|
+
style={inputStyle}
|
|
26
|
+
value={value}
|
|
27
|
+
onChange={handleChange}
|
|
28
|
+
/>
|
|
29
|
+
{buttons && <Stepper increment={increment} decrement={decrement} />}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface StepperProps {
|
|
35
|
+
increment: () => void
|
|
36
|
+
decrement: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Stepper = ({ increment, decrement }: StepperProps) => {
|
|
40
|
+
return (
|
|
41
|
+
<div className="number-stepper">
|
|
42
|
+
<SlepButton slot="increment" onClick={increment}>
|
|
43
|
+
<Icon size="xxs" icon={faChevronUp} />
|
|
44
|
+
</SlepButton>
|
|
45
|
+
<div className="number-stepper-splitter" />
|
|
46
|
+
<SlepButton slot="decrement" onClick={decrement}>
|
|
47
|
+
<Icon size="xxs" icon={faChevronDown} />
|
|
48
|
+
</SlepButton>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ButtonProps {
|
|
54
|
+
children: React.ReactNode
|
|
55
|
+
slot: string
|
|
56
|
+
onClick: () => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SlepButton = ({ children, slot, onClick }: ButtonProps) => {
|
|
60
|
+
const { pressProps, isPressed } = usePress({ onClick, preventFocusOnPress: true })
|
|
61
|
+
const { onBeforeInput, ...rest } = pressProps
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Button
|
|
65
|
+
{...rest}
|
|
66
|
+
onBeforeInput={onBeforeInput as any}
|
|
67
|
+
data-slot={slot}
|
|
68
|
+
data-size={undefined}
|
|
69
|
+
data-pressed={attr(isPressed)}
|
|
70
|
+
style={{ border: 'none' }}
|
|
71
|
+
className="number-stepper-slot"
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</Button>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
@use '../mixins.scss';
|
|
2
|
+
|
|
3
|
+
.switch {
|
|
4
|
+
display: inline-table;
|
|
5
|
+
user-select: none;
|
|
6
|
+
touch-action: manipulation;
|
|
7
|
+
max-width: max-content;
|
|
8
|
+
|
|
9
|
+
&[data-disabled] {
|
|
10
|
+
@include mixins.use-styles(switch, disabled);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&-track {
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
outline: none;
|
|
16
|
+
position: relative;
|
|
17
|
+
|
|
18
|
+
&:after {
|
|
19
|
+
visibility: hidden;
|
|
20
|
+
content: '\00A0';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-disabled] {
|
|
24
|
+
cursor: default;
|
|
25
|
+
@include mixins.use-styles(switch, track, disabled);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-state='checked'] {
|
|
29
|
+
@include mixins.use-styles(switch, track, checked);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&[data-state='unchecked'] {
|
|
33
|
+
@include mixins.use-styles(switch, track, unchecked);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@include mixins.use-styles(switch, track);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&-thumb {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 0;
|
|
42
|
+
left: 0;
|
|
43
|
+
|
|
44
|
+
@include mixins.use-styles(switch, thumb);
|
|
45
|
+
@include mixins.use-size(switch, thumb, size);
|
|
46
|
+
|
|
47
|
+
border-radius: 9999px;
|
|
48
|
+
|
|
49
|
+
&[data-state='unchecked'] {
|
|
50
|
+
transform: translate(
|
|
51
|
+
var(--switch_track_outline_width),
|
|
52
|
+
calc(var(--switch_track_height) - var(--switch_thumb_size) - var(--switch_track_outline_width))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
.switch-thumb-icon {
|
|
56
|
+
@include mixins.use-styles(switch, icon, unchecked);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&[data-state='checked'] {
|
|
61
|
+
transform: translate(
|
|
62
|
+
calc(var(--switch_track_width) - var(--switch_thumb_size) - var(--switch_track_outline_width)),
|
|
63
|
+
calc(var(--switch_track_height) - var(--switch_thumb_size) - var(--switch_track_outline_width))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
.switch-thumb-icon {
|
|
67
|
+
@include mixins.use-styles(switch, icon, checked);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&-icon {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
|
|
78
|
+
@include mixins.use-styles(switch, icon);
|
|
79
|
+
|
|
80
|
+
&[data-visible] {
|
|
81
|
+
@include mixins.use-styles(switch, icon, visible);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
svg {
|
|
85
|
+
@include mixins.use-size(switch, icon, size);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&-label {
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
display: table-cell;
|
|
93
|
+
word-break: break-word;
|
|
94
|
+
|
|
95
|
+
&[data-disabled] {
|
|
96
|
+
cursor: default;
|
|
97
|
+
@include mixins.use-styles(switch, label, disabled);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@include mixins.use-styles(switch, label);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch'
|
|
2
|
+
import { attr } from '@companix/utils-browser'
|
|
3
|
+
import { useId } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface SwitchProps {
|
|
6
|
+
checked: boolean
|
|
7
|
+
onCheckedChange: (checked: boolean) => void
|
|
8
|
+
label?: React.ReactNode
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
checkedIcon?: React.ReactNode
|
|
11
|
+
uncheckedIcon?: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Switch = (props: SwitchProps) => {
|
|
15
|
+
const { checked, disabled, onCheckedChange, uncheckedIcon, checkedIcon, label } = props
|
|
16
|
+
const id = useId()
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="switch" data-disabled={attr(disabled)}>
|
|
20
|
+
<SwitchPrimitive.Root
|
|
21
|
+
className="switch-track"
|
|
22
|
+
checked={checked}
|
|
23
|
+
onCheckedChange={onCheckedChange}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
id={id}
|
|
26
|
+
>
|
|
27
|
+
<SwitchPrimitive.Thumb className="switch-thumb">
|
|
28
|
+
{uncheckedIcon && (
|
|
29
|
+
<span className="switch-thumb-icon" data-visible={attr(!checked)}>
|
|
30
|
+
{uncheckedIcon}
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
{checkedIcon && (
|
|
34
|
+
<span className="switch-thumb-icon" data-visible={attr(checked)}>
|
|
35
|
+
{checkedIcon}
|
|
36
|
+
</span>
|
|
37
|
+
)}
|
|
38
|
+
</SwitchPrimitive.Thumb>
|
|
39
|
+
</SwitchPrimitive.Root>
|
|
40
|
+
{label && (
|
|
41
|
+
<label className="switch-label" htmlFor={id} data-disabled={disabled}>
|
|
42
|
+
{label}
|
|
43
|
+
</label>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { Switch }
|