@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.
Files changed (124) hide show
  1. package/.eslintrc +54 -0
  2. package/declaration.d.ts +4 -0
  3. package/index.html +12 -0
  4. package/package.json +66 -0
  5. package/playground/App.tsx +166 -0
  6. package/playground/Example.tsx +14 -0
  7. package/playground/Test.tsx +44 -0
  8. package/playground/animation-test-1/index.scss +20 -0
  9. package/playground/animation-test-1/index.tsx +17 -0
  10. package/playground/animation-test-2/index.scss +62 -0
  11. package/playground/animation-test-2/index.tsx +32 -0
  12. package/playground/bootstrap.tsx +19 -0
  13. package/playground/buttons/index.tsx +132 -0
  14. package/playground/checkbox/index.tsx +64 -0
  15. package/playground/date-input/index.tsx +45 -0
  16. package/playground/date-picker/index.tsx +41 -0
  17. package/playground/dialog/index.tsx +92 -0
  18. package/playground/dialog-alert/index.tsx +47 -0
  19. package/playground/drawer/index.tsx +55 -0
  20. package/playground/index.css +33 -0
  21. package/playground/index.scss +270 -0
  22. package/playground/input/index.tsx +112 -0
  23. package/playground/number-inputs/index.tsx +50 -0
  24. package/playground/popovers/index.tsx +70 -0
  25. package/playground/radio-group/index.tsx +69 -0
  26. package/playground/select/index.tsx +72 -0
  27. package/playground/select-tags/index.tsx +36 -0
  28. package/playground/styles.scss +2 -0
  29. package/playground/switch/index.tsx +44 -0
  30. package/playground/tabs/index.tsx +16 -0
  31. package/playground/test.scss +0 -0
  32. package/playground/text-area/index.tsx +17 -0
  33. package/playground/text-input/index.tsx +12 -0
  34. package/playground/toaster/index.tsx +156 -0
  35. package/playground/tooltip/index.tsx +26 -0
  36. package/src/Button/Button.scss +128 -0
  37. package/src/Button/index.tsx +72 -0
  38. package/src/ButtonGroup/ButtonGroup.scss +18 -0
  39. package/src/ButtonGroup/index.tsx +20 -0
  40. package/src/Checkbox/Checkbox.scss +115 -0
  41. package/src/Checkbox/index.tsx +46 -0
  42. package/src/Countdown/index.tsx +54 -0
  43. package/src/DateInput/DateInput.scss +11 -0
  44. package/src/DateInput/index.tsx +96 -0
  45. package/src/DatePicker/Calendar.scss +125 -0
  46. package/src/DatePicker/Calendar.tsx +157 -0
  47. package/src/DatePicker/CalendarHeader.tsx +139 -0
  48. package/src/DatePicker/DatePicker.scss +0 -0
  49. package/src/DatePicker/index.tsx +177 -0
  50. package/src/Dialog/Dialog.scss +25 -0
  51. package/src/Dialog/Popup.scss +55 -0
  52. package/src/Dialog/index.tsx +31 -0
  53. package/src/DialogAlert/Alert.scss +52 -0
  54. package/src/DialogAlert/Alert.tsx +78 -0
  55. package/src/DialogAlert/Viewport.tsx +52 -0
  56. package/src/DialogAlert/index.tsx +37 -0
  57. package/src/Drawer/Drawer.scss +112 -0
  58. package/src/Drawer/index.tsx +46 -0
  59. package/src/File/index.tsx +60 -0
  60. package/src/Form/Form.scss +70 -0
  61. package/src/Form/Input.scss +24 -0
  62. package/src/Form/index.tsx +131 -0
  63. package/src/Icon/icon.scss +18 -0
  64. package/src/Icon/index.tsx +43 -0
  65. package/src/LoadButton/index.tsx +17 -0
  66. package/src/NumberInput/index.tsx +74 -0
  67. package/src/OptionItem/Option.scss +89 -0
  68. package/src/OptionItem/OptionItem.tsx +49 -0
  69. package/src/OptionItem/OptionsList.tsx +26 -0
  70. package/src/Popover/Popover.scss +80 -0
  71. package/src/Popover/index.tsx +117 -0
  72. package/src/Radio/Radio.scss +148 -0
  73. package/src/Radio/index.tsx +68 -0
  74. package/src/Scrollable/ImitateScroll.tsx +141 -0
  75. package/src/Scrollable/Scrollable.scss +50 -0
  76. package/src/Scrollable/index.tsx +141 -0
  77. package/src/Select/Select.scss +80 -0
  78. package/src/Select/SelectInput.tsx +131 -0
  79. package/src/Select/index.tsx +134 -0
  80. package/src/SelectTags/SelectTags.scss +66 -0
  81. package/src/SelectTags/index.tsx +192 -0
  82. package/src/Spinner/Spinner.scss +14 -0
  83. package/src/Spinner/index.tsx +19 -0
  84. package/src/Stepper/StepperInput.scss +35 -0
  85. package/src/Stepper/index.tsx +76 -0
  86. package/src/Switch/Switch.scss +102 -0
  87. package/src/Switch/index.tsx +49 -0
  88. package/src/Tabs/Tabs.scss +58 -0
  89. package/src/Tabs/index.tsx +89 -0
  90. package/src/TextArea/TextArea.scss +34 -0
  91. package/src/TextArea/index.tsx +51 -0
  92. package/src/Toaster/RemoveListener.tsx +11 -0
  93. package/src/Toaster/Toast.tsx +69 -0
  94. package/src/Toaster/Toaster.scss +151 -0
  95. package/src/Toaster/Viewport.tsx +117 -0
  96. package/src/Toaster/index.tsx +52 -0
  97. package/src/Tooltip/Tooltip.scss +28 -0
  98. package/src/Tooltip/index.tsx +33 -0
  99. package/src/__hooks/use-frooze-closing.ts +51 -0
  100. package/src/__hooks/use-loading.ts +34 -0
  101. package/src/__hooks/use-local-storage.ts +19 -0
  102. package/src/__hooks/use-popover-position.ts +24 -0
  103. package/src/__hooks/use-previos.ts +25 -0
  104. package/src/__hooks/use-resize.ts +41 -0
  105. package/src/__hooks/use-scrollbox.ts +45 -0
  106. package/src/__hooks/use-stepper-input.ts +82 -0
  107. package/src/__hooks/use-update.ts +19 -0
  108. package/src/__hooks/useCalendar.ts +104 -0
  109. package/src/__hooks/useCalendarOptions-copy.ts +87 -0
  110. package/src/__hooks/useCalendarOptions.ts +68 -0
  111. package/src/__libs/calendar.ts +175 -0
  112. package/src/__utils/utils.ts +137 -0
  113. package/src/css.scss +120 -0
  114. package/src/index.scss +22 -0
  115. package/src/index.ts +36 -0
  116. package/src/mixins.scss +99 -0
  117. package/src/theme.scss +103 -0
  118. package/src/types.ts +14 -0
  119. package/tailwind.config.js +91 -0
  120. package/themes/classic/animations.scss +179 -0
  121. package/themes/classic/classic.scss +493 -0
  122. package/tsconfig.json +27 -0
  123. package/vite.build.ts +35 -0
  124. 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 }