react-proportion-slider 0.9.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.
- package/README.md +124 -0
- package/dev/App.css +13 -0
- package/dev/App.tsx +49 -0
- package/dev/index.html +17 -0
- package/dev/main.tsx +9 -0
- package/eslint.config.js +28 -0
- package/package.json +29 -0
- package/src/components/DynamicChildPositioner.tsx +143 -0
- package/src/components/Proportion.tsx +55 -0
- package/src/components/ProportionSlider.tsx +112 -0
- package/src/components/SliderKnob.tsx +68 -0
- package/src/index.ts +1 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +26 -0
package/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# React Proportion Slider
|
2
|
+
|
3
|
+
> **Note:** This package is currently in beta
|
4
|
+
|
5
|
+
A React component that allows users to adjust the proportion of two elements using a slider.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
(Not yet published to npm)
|
10
|
+
|
11
|
+
```bash
|
12
|
+
npm install react-proportion-slider
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
```tsx
|
18
|
+
import React from "react";
|
19
|
+
import { ProportionSlider } from "react-proportion-slider";
|
20
|
+
|
21
|
+
function App() {
|
22
|
+
const [proportions, setProportions] = React.useState<[number, number]>([
|
23
|
+
50, 50,
|
24
|
+
]);
|
25
|
+
return (
|
26
|
+
<div
|
27
|
+
style={{
|
28
|
+
height: "100%",
|
29
|
+
flex: 1,
|
30
|
+
padding: "20px 200px",
|
31
|
+
display: "flex",
|
32
|
+
flexDirection: "column",
|
33
|
+
justifyContent: "center",
|
34
|
+
}}
|
35
|
+
>
|
36
|
+
<ProportionSlider
|
37
|
+
value={proportions}
|
38
|
+
proportions={[
|
39
|
+
{
|
40
|
+
name: "Skill",
|
41
|
+
backgroundColor: "#31332E",
|
42
|
+
},
|
43
|
+
{
|
44
|
+
name: "3.7 Sonnet",
|
45
|
+
backgroundColor: "#5f625C",
|
46
|
+
},
|
47
|
+
]}
|
48
|
+
onChange={(change) => {
|
49
|
+
setProportions(change);
|
50
|
+
}}
|
51
|
+
sliderOptions={{
|
52
|
+
width: 5,
|
53
|
+
gap: 5,
|
54
|
+
backgroundColor: "#EC1308",
|
55
|
+
}}
|
56
|
+
options={{
|
57
|
+
height: 50,
|
58
|
+
displayValueType: "percentage",
|
59
|
+
}}
|
60
|
+
/>
|
61
|
+
</div>
|
62
|
+
);
|
63
|
+
}
|
64
|
+
```
|
65
|
+
|
66
|
+
## Future Features
|
67
|
+
|
68
|
+
1. Make it possible to adjust the proportion of more than two elements
|
69
|
+
2. Add more customization options
|
70
|
+
|
71
|
+
## React + TypeScript + Vite
|
72
|
+
|
73
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
74
|
+
|
75
|
+
Currently, two official plugins are available:
|
76
|
+
|
77
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
78
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
79
|
+
|
80
|
+
### Expanding the ESLint configuration
|
81
|
+
|
82
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
83
|
+
|
84
|
+
```js
|
85
|
+
export default tseslint.config({
|
86
|
+
extends: [
|
87
|
+
// Remove ...tseslint.configs.recommended and replace with this
|
88
|
+
...tseslint.configs.recommendedTypeChecked,
|
89
|
+
// Alternatively, use this for stricter rules
|
90
|
+
...tseslint.configs.strictTypeChecked,
|
91
|
+
// Optionally, add this for stylistic rules
|
92
|
+
...tseslint.configs.stylisticTypeChecked,
|
93
|
+
],
|
94
|
+
languageOptions: {
|
95
|
+
// other options...
|
96
|
+
parserOptions: {
|
97
|
+
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
98
|
+
tsconfigRootDir: import.meta.dirname,
|
99
|
+
},
|
100
|
+
},
|
101
|
+
});
|
102
|
+
```
|
103
|
+
|
104
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
105
|
+
|
106
|
+
```js
|
107
|
+
// eslint.config.js
|
108
|
+
import reactX from "eslint-plugin-react-x";
|
109
|
+
import reactDom from "eslint-plugin-react-dom";
|
110
|
+
|
111
|
+
export default tseslint.config({
|
112
|
+
plugins: {
|
113
|
+
// Add the react-x and react-dom plugins
|
114
|
+
"react-x": reactX,
|
115
|
+
"react-dom": reactDom,
|
116
|
+
},
|
117
|
+
rules: {
|
118
|
+
// other rules...
|
119
|
+
// Enable its recommended typescript rules
|
120
|
+
...reactX.configs["recommended-typescript"].rules,
|
121
|
+
...reactDom.configs.recommended.rules,
|
122
|
+
},
|
123
|
+
});
|
124
|
+
```
|
package/dev/App.css
ADDED
package/dev/App.tsx
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import "./App.css";
|
3
|
+
import { ProportionSlider } from "../src/components/ProportionSlider";
|
4
|
+
|
5
|
+
function App() {
|
6
|
+
const [proportions, setProportions] = React.useState<[number, number]>([
|
7
|
+
50, 50,
|
8
|
+
]);
|
9
|
+
return (
|
10
|
+
<div
|
11
|
+
style={{
|
12
|
+
height: "100%",
|
13
|
+
flex: 1,
|
14
|
+
padding: "20px 200px",
|
15
|
+
display: "flex",
|
16
|
+
flexDirection: "column",
|
17
|
+
justifyContent: "center",
|
18
|
+
}}
|
19
|
+
>
|
20
|
+
<ProportionSlider
|
21
|
+
value={proportions}
|
22
|
+
proportions={[
|
23
|
+
{
|
24
|
+
name: "Skill",
|
25
|
+
backgroundColor: "#31332E",
|
26
|
+
},
|
27
|
+
{
|
28
|
+
name: "3.7 Sonnet",
|
29
|
+
backgroundColor: "#5f625C",
|
30
|
+
},
|
31
|
+
]}
|
32
|
+
onChange={(change) => {
|
33
|
+
setProportions(change);
|
34
|
+
}}
|
35
|
+
sliderOptions={{
|
36
|
+
width: 5,
|
37
|
+
gap: 5,
|
38
|
+
backgroundColor: "#EC1308",
|
39
|
+
}}
|
40
|
+
options={{
|
41
|
+
height: 50,
|
42
|
+
displayValueType: "percentage",
|
43
|
+
}}
|
44
|
+
/>
|
45
|
+
</div>
|
46
|
+
);
|
47
|
+
}
|
48
|
+
|
49
|
+
export default App;
|
package/dev/index.html
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
|
4
|
+
<head>
|
5
|
+
<meta charset="UTF-8" />
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
8
|
+
<title>Dev Environment</title>
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@200;400;700&display=swap" rel="stylesheet">
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body>
|
13
|
+
<div id="root"></div>
|
14
|
+
<script type="module" src="/dev/main.tsx"></script>
|
15
|
+
</body>
|
16
|
+
|
17
|
+
</html>
|
package/dev/main.tsx
ADDED
package/eslint.config.js
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import js from '@eslint/js'
|
2
|
+
import globals from 'globals'
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
5
|
+
import tseslint from 'typescript-eslint'
|
6
|
+
|
7
|
+
export default tseslint.config(
|
8
|
+
{ ignores: ['dist'] },
|
9
|
+
{
|
10
|
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
12
|
+
languageOptions: {
|
13
|
+
ecmaVersion: 2020,
|
14
|
+
globals: globals.browser,
|
15
|
+
},
|
16
|
+
plugins: {
|
17
|
+
'react-hooks': reactHooks,
|
18
|
+
'react-refresh': reactRefresh,
|
19
|
+
},
|
20
|
+
rules: {
|
21
|
+
...reactHooks.configs.recommended.rules,
|
22
|
+
'react-refresh/only-export-components': [
|
23
|
+
'warn',
|
24
|
+
{ allowConstantExport: true },
|
25
|
+
],
|
26
|
+
},
|
27
|
+
},
|
28
|
+
)
|
package/package.json
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
{
|
2
|
+
"name": "react-proportion-slider",
|
3
|
+
"private": false,
|
4
|
+
"version": "0.9.0",
|
5
|
+
"type": "module",
|
6
|
+
"scripts": {
|
7
|
+
"dev": "vite",
|
8
|
+
"build": "tsc -b && vite build",
|
9
|
+
"lint": "eslint .",
|
10
|
+
"preview": "vite preview"
|
11
|
+
},
|
12
|
+
"dependencies": {
|
13
|
+
"react": "^19.0.0",
|
14
|
+
"react-dom": "^19.0.0"
|
15
|
+
},
|
16
|
+
"devDependencies": {
|
17
|
+
"@eslint/js": "^9.21.0",
|
18
|
+
"@types/react": "^19.0.10",
|
19
|
+
"@types/react-dom": "^19.0.4",
|
20
|
+
"@vitejs/plugin-react": "^4.3.4",
|
21
|
+
"eslint": "^9.21.0",
|
22
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
23
|
+
"eslint-plugin-react-refresh": "^0.4.19",
|
24
|
+
"globals": "^15.15.0",
|
25
|
+
"typescript": "~5.7.2",
|
26
|
+
"typescript-eslint": "^8.24.1",
|
27
|
+
"vite": "^6.2.0"
|
28
|
+
}
|
29
|
+
}
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import React, { useRef, useState, useMemo, useEffect } from "react";
|
2
|
+
|
3
|
+
type DynamicChildPositionerProps = {
|
4
|
+
rightNode: React.ReactNode;
|
5
|
+
leftNode: React.ReactNode;
|
6
|
+
options: {
|
7
|
+
primary: "left" | "right";
|
8
|
+
};
|
9
|
+
backgroundColor?: string;
|
10
|
+
width: number | string;
|
11
|
+
};
|
12
|
+
|
13
|
+
export const DynamicChildPositioner = ({
|
14
|
+
rightNode,
|
15
|
+
leftNode,
|
16
|
+
options: { primary },
|
17
|
+
width,
|
18
|
+
backgroundColor = "gray",
|
19
|
+
}: DynamicChildPositionerProps) => {
|
20
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
21
|
+
const refRight = useRef<HTMLDivElement | null>(null);
|
22
|
+
const refLeft = useRef<HTMLDivElement | null>(null);
|
23
|
+
const [fitStatus, setFitStatus] = useState<"both" | "primary" | "none">(
|
24
|
+
"both"
|
25
|
+
);
|
26
|
+
|
27
|
+
const rightStyle = useMemo(() => {
|
28
|
+
const rightNoSpace =
|
29
|
+
fitStatus === "none" || (fitStatus === "primary" && primary === "left");
|
30
|
+
if (primary === "right") {
|
31
|
+
return rightNoSpace ? STYLES["BOTTOM_RIGHT"] : STYLES["RIGHT"];
|
32
|
+
} else {
|
33
|
+
return rightNoSpace ? STYLES["TOP_LEFT"] : STYLES["RIGHT"];
|
34
|
+
}
|
35
|
+
}, [fitStatus, primary]);
|
36
|
+
|
37
|
+
const leftStyle = useMemo(() => {
|
38
|
+
const leftNoSpace =
|
39
|
+
fitStatus === "none" || (fitStatus === "primary" && primary === "right");
|
40
|
+
if (primary === "left") {
|
41
|
+
return leftNoSpace ? STYLES["BOTTOM_LEFT"] : STYLES["LEFT"];
|
42
|
+
} else {
|
43
|
+
return leftNoSpace ? STYLES["TOP_RIGHT"] : STYLES["LEFT"];
|
44
|
+
}
|
45
|
+
}, [fitStatus, primary]);
|
46
|
+
|
47
|
+
useEffect(() => {
|
48
|
+
const interval = setInterval(() => {
|
49
|
+
const textRight = refRight.current;
|
50
|
+
const textLeft = refLeft.current;
|
51
|
+
const container = ref.current;
|
52
|
+
if (!container || !textRight || !textLeft) {
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
const containerRectWidth = container.getBoundingClientRect().width;
|
56
|
+
const rightWidth = textRight.getBoundingClientRect().width;
|
57
|
+
const leftWidth = textLeft.getBoundingClientRect().width;
|
58
|
+
|
59
|
+
const { primaryWidth, secondaryWidth } =
|
60
|
+
primary === "left"
|
61
|
+
? { primaryWidth: leftWidth, secondaryWidth: rightWidth }
|
62
|
+
: { primaryWidth: rightWidth, secondaryWidth: leftWidth };
|
63
|
+
|
64
|
+
const primaryCanFit = primaryWidth + 2 * GAP <= containerRectWidth;
|
65
|
+
const secondaryCanFit =
|
66
|
+
secondaryWidth + primaryWidth + 3 * GAP <= containerRectWidth;
|
67
|
+
|
68
|
+
const fitStatus = primaryCanFit
|
69
|
+
? secondaryCanFit
|
70
|
+
? "both"
|
71
|
+
: "primary"
|
72
|
+
: "none";
|
73
|
+
|
74
|
+
setFitStatus(fitStatus);
|
75
|
+
}, 1000 / 30);
|
76
|
+
return () => clearInterval(interval);
|
77
|
+
}, [ref, refRight, refLeft, primary]);
|
78
|
+
|
79
|
+
return (
|
80
|
+
<div
|
81
|
+
ref={ref}
|
82
|
+
style={{
|
83
|
+
position: "relative",
|
84
|
+
width,
|
85
|
+
backgroundColor,
|
86
|
+
borderRadius: "5px",
|
87
|
+
color: "white",
|
88
|
+
}}
|
89
|
+
>
|
90
|
+
<div ref={refLeft} style={leftStyle}>
|
91
|
+
{leftNode}
|
92
|
+
</div>
|
93
|
+
<div ref={refRight} style={rightStyle}>
|
94
|
+
{rightNode}
|
95
|
+
</div>
|
96
|
+
</div>
|
97
|
+
);
|
98
|
+
};
|
99
|
+
|
100
|
+
const GAP = 5;
|
101
|
+
const COMMON_STYLES: React.CSSProperties = {
|
102
|
+
position: "absolute",
|
103
|
+
transition: "all 200ms cubic-bezier(.47,1.64,.41,.8)",
|
104
|
+
transitionProperty: "transform, top, left, bottom, right",
|
105
|
+
};
|
106
|
+
const STYLES: Record<string, React.CSSProperties> = {
|
107
|
+
TOP_LEFT: {
|
108
|
+
...COMMON_STYLES,
|
109
|
+
left: GAP,
|
110
|
+
top: -GAP,
|
111
|
+
transform: "translateY(-100%)",
|
112
|
+
},
|
113
|
+
LEFT: {
|
114
|
+
...COMMON_STYLES,
|
115
|
+
left: GAP,
|
116
|
+
top: "50%",
|
117
|
+
transform: "translateY(-50%)",
|
118
|
+
},
|
119
|
+
BOTTOM_LEFT: {
|
120
|
+
...COMMON_STYLES,
|
121
|
+
left: GAP,
|
122
|
+
bottom: -GAP,
|
123
|
+
transform: "translateY(100%)",
|
124
|
+
},
|
125
|
+
TOP_RIGHT: {
|
126
|
+
...COMMON_STYLES,
|
127
|
+
right: GAP,
|
128
|
+
top: -GAP,
|
129
|
+
transform: "translateY(-100%)",
|
130
|
+
},
|
131
|
+
RIGHT: {
|
132
|
+
...COMMON_STYLES,
|
133
|
+
right: GAP,
|
134
|
+
top: "50%",
|
135
|
+
transform: "translateY(-50%)",
|
136
|
+
},
|
137
|
+
BOTTOM_RIGHT: {
|
138
|
+
...COMMON_STYLES,
|
139
|
+
right: GAP,
|
140
|
+
bottom: -GAP,
|
141
|
+
transform: "translateY(100%)",
|
142
|
+
},
|
143
|
+
};
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { DynamicChildPositioner } from "./DynamicChildPositioner";
|
2
|
+
import { ProportionDetail, DisplayValueTypes } from "./ProportionSlider";
|
3
|
+
|
4
|
+
export type ProportionProp = {
|
5
|
+
value: number;
|
6
|
+
total: number;
|
7
|
+
detail: ProportionDetail;
|
8
|
+
width: number | string;
|
9
|
+
anchorName: "left" | "right";
|
10
|
+
displayValueType: DisplayValueTypes;
|
11
|
+
backgroundColor?: string;
|
12
|
+
};
|
13
|
+
export const Proportion = ({
|
14
|
+
value,
|
15
|
+
total,
|
16
|
+
detail,
|
17
|
+
width,
|
18
|
+
anchorName,
|
19
|
+
displayValueType,
|
20
|
+
backgroundColor = "gray",
|
21
|
+
}: ProportionProp) => {
|
22
|
+
const nameNode = (
|
23
|
+
<div
|
24
|
+
style={{
|
25
|
+
userSelect: "none",
|
26
|
+
whiteSpace: "nowrap",
|
27
|
+
}}
|
28
|
+
>
|
29
|
+
{detail.name}
|
30
|
+
</div>
|
31
|
+
);
|
32
|
+
const percentNode =
|
33
|
+
displayValueType === "percentage" ? (
|
34
|
+
<div
|
35
|
+
style={{
|
36
|
+
userSelect: "none",
|
37
|
+
whiteSpace: "nowrap",
|
38
|
+
}}
|
39
|
+
>
|
40
|
+
{`${Math.round((value * 100) / total)}%`}
|
41
|
+
</div>
|
42
|
+
) : null;
|
43
|
+
|
44
|
+
return (
|
45
|
+
<DynamicChildPositioner
|
46
|
+
width={width}
|
47
|
+
rightNode={anchorName === "right" ? nameNode : percentNode}
|
48
|
+
leftNode={anchorName === "left" ? nameNode : percentNode}
|
49
|
+
options={{
|
50
|
+
primary: anchorName,
|
51
|
+
}}
|
52
|
+
backgroundColor={backgroundColor}
|
53
|
+
></DynamicChildPositioner>
|
54
|
+
);
|
55
|
+
};
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import { useCallback, useRef } from "react";
|
2
|
+
import { SliderKnob } from "./SliderKnob";
|
3
|
+
import { Proportion } from "./Proportion";
|
4
|
+
|
5
|
+
export type ProportionDetail = {
|
6
|
+
name: string;
|
7
|
+
backgroundColor?: string;
|
8
|
+
};
|
9
|
+
export type DisplayValueTypes = "percentage" | "none";
|
10
|
+
|
11
|
+
export type ProportionSliderProps = {
|
12
|
+
value: [number, number];
|
13
|
+
proportions: [ProportionDetail, ProportionDetail];
|
14
|
+
onChange: (change: [number, number]) => void;
|
15
|
+
sliderOptions?: {
|
16
|
+
width: number;
|
17
|
+
gap: number;
|
18
|
+
backgroundColor?: string;
|
19
|
+
};
|
20
|
+
options?: {
|
21
|
+
height: number;
|
22
|
+
displayValueType: DisplayValueTypes;
|
23
|
+
};
|
24
|
+
};
|
25
|
+
export const ProportionSlider = ({
|
26
|
+
value,
|
27
|
+
proportions,
|
28
|
+
onChange,
|
29
|
+
sliderOptions = {
|
30
|
+
width: 5,
|
31
|
+
gap: 2,
|
32
|
+
},
|
33
|
+
options = {
|
34
|
+
height: 20,
|
35
|
+
displayValueType: "percentage",
|
36
|
+
},
|
37
|
+
}: ProportionSliderProps) => {
|
38
|
+
const refWidth = useRef<number | null>(null);
|
39
|
+
|
40
|
+
const total = value[0] + value[1];
|
41
|
+
|
42
|
+
const refStartX = useRef<number | null>(null);
|
43
|
+
const refValue1Start = useRef<number | null>(null);
|
44
|
+
const refValue2Start = useRef<number | null>(null);
|
45
|
+
const sliderWidth = sliderOptions.width + sliderOptions.gap * 2;
|
46
|
+
const onDragStart = useCallback(
|
47
|
+
(px: number): void => {
|
48
|
+
refStartX.current = px;
|
49
|
+
refValue1Start.current = value[0];
|
50
|
+
refValue2Start.current = value[1];
|
51
|
+
},
|
52
|
+
[value]
|
53
|
+
);
|
54
|
+
const onDragEnd = useCallback(() => {
|
55
|
+
refStartX.current = null;
|
56
|
+
refValue1Start.current = null;
|
57
|
+
refValue2Start.current = null;
|
58
|
+
}, []);
|
59
|
+
const onDrag = useCallback(
|
60
|
+
(px: number): void => {
|
61
|
+
if (refStartX.current === null) return;
|
62
|
+
const diffPx = px - refStartX.current;
|
63
|
+
const totalWidthPx = refWidth.current! - sliderWidth;
|
64
|
+
const total = refValue1Start.current! + refValue2Start.current!;
|
65
|
+
let newValue1 = refValue1Start.current! + (diffPx / totalWidthPx) * total;
|
66
|
+
newValue1 = Math.max(0, Math.min(total, newValue1));
|
67
|
+
onChange([newValue1, total - newValue1]);
|
68
|
+
},
|
69
|
+
[onChange, sliderWidth]
|
70
|
+
);
|
71
|
+
return (
|
72
|
+
<div
|
73
|
+
ref={(el) => {
|
74
|
+
if (el) {
|
75
|
+
refWidth.current = el.getBoundingClientRect().width;
|
76
|
+
}
|
77
|
+
}}
|
78
|
+
style={{
|
79
|
+
display: "flex",
|
80
|
+
flexDirection: "row",
|
81
|
+
height: options.height,
|
82
|
+
}}
|
83
|
+
>
|
84
|
+
<Proportion
|
85
|
+
value={value[0]}
|
86
|
+
backgroundColor={proportions[0].backgroundColor}
|
87
|
+
total={total}
|
88
|
+
width={`calc(${(value[0] * 100) / total}% - ${sliderWidth / 2}px)`}
|
89
|
+
detail={proportions[0]}
|
90
|
+
anchorName="left"
|
91
|
+
displayValueType={options.displayValueType}
|
92
|
+
/>
|
93
|
+
<SliderKnob
|
94
|
+
backgroundColor={sliderOptions.backgroundColor}
|
95
|
+
width={sliderOptions.width}
|
96
|
+
gap={sliderOptions.gap}
|
97
|
+
onDragStart={onDragStart}
|
98
|
+
onDrag={onDrag}
|
99
|
+
onDragEnd={onDragEnd}
|
100
|
+
/>
|
101
|
+
<Proportion
|
102
|
+
value={value[1]}
|
103
|
+
total={total}
|
104
|
+
backgroundColor={proportions[1].backgroundColor}
|
105
|
+
width={`calc(${(value[1] * 100) / total}% - ${sliderWidth / 2}px)`}
|
106
|
+
detail={proportions[1]}
|
107
|
+
anchorName="right"
|
108
|
+
displayValueType={options.displayValueType}
|
109
|
+
/>
|
110
|
+
</div>
|
111
|
+
);
|
112
|
+
};
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import { useRef, useEffect } from "react";
|
2
|
+
|
3
|
+
export type SliderKnobProps = {
|
4
|
+
width: number;
|
5
|
+
gap: number;
|
6
|
+
backgroundColor?: string;
|
7
|
+
onDragStart: (px: number) => void;
|
8
|
+
onDrag: (px: number) => void;
|
9
|
+
onDragEnd: () => void;
|
10
|
+
};
|
11
|
+
|
12
|
+
export const SliderKnob = ({
|
13
|
+
width,
|
14
|
+
gap,
|
15
|
+
backgroundColor = "red",
|
16
|
+
onDrag,
|
17
|
+
onDragStart,
|
18
|
+
onDragEnd,
|
19
|
+
}: SliderKnobProps) => {
|
20
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
21
|
+
const refIsDragging = useRef<boolean>(false);
|
22
|
+
useEffect(() => {
|
23
|
+
const onMouseDown = (e: MouseEvent) => {
|
24
|
+
if (e.target !== ref.current) {
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
refIsDragging.current = true;
|
28
|
+
onDragStart(e.clientX);
|
29
|
+
return true;
|
30
|
+
};
|
31
|
+
const onMouseMove = (e: MouseEvent) => {
|
32
|
+
if (!refIsDragging.current) {
|
33
|
+
return false;
|
34
|
+
}
|
35
|
+
onDrag(e.clientX);
|
36
|
+
return true;
|
37
|
+
};
|
38
|
+
const onMouseUp = () => {
|
39
|
+
if (!refIsDragging.current) {
|
40
|
+
return false;
|
41
|
+
}
|
42
|
+
refIsDragging.current = false;
|
43
|
+
onDragEnd();
|
44
|
+
return true;
|
45
|
+
};
|
46
|
+
window.addEventListener("mousedown", onMouseDown);
|
47
|
+
window.addEventListener("mousemove", onMouseMove);
|
48
|
+
window.addEventListener("mouseup", onMouseUp);
|
49
|
+
return () => {
|
50
|
+
window.removeEventListener("mousedown", onMouseDown);
|
51
|
+
window.removeEventListener("mousemove", onMouseMove);
|
52
|
+
window.removeEventListener("mouseup", onMouseUp);
|
53
|
+
};
|
54
|
+
}, [onDragStart, onDrag, onDragEnd]);
|
55
|
+
return (
|
56
|
+
<div
|
57
|
+
ref={ref}
|
58
|
+
style={{
|
59
|
+
width: `${width}px`,
|
60
|
+
margin: `${gap}px ${gap}px`,
|
61
|
+
alignSelf: "stretch",
|
62
|
+
background: backgroundColor,
|
63
|
+
borderRadius: "2px",
|
64
|
+
cursor: "ew-resize",
|
65
|
+
}}
|
66
|
+
></div>
|
67
|
+
);
|
68
|
+
};
|
package/src/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./components/ProportionSlider";
|
@@ -0,0 +1 @@
|
|
1
|
+
/// <reference types="vite/client" />
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
4
|
+
"target": "ES2020",
|
5
|
+
"useDefineForClassFields": true,
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
7
|
+
"module": "ESNext",
|
8
|
+
"skipLibCheck": true,
|
9
|
+
|
10
|
+
/* Bundler mode */
|
11
|
+
"moduleResolution": "bundler",
|
12
|
+
"allowImportingTsExtensions": true,
|
13
|
+
"isolatedModules": true,
|
14
|
+
"moduleDetection": "force",
|
15
|
+
"noEmit": true,
|
16
|
+
"jsx": "react-jsx",
|
17
|
+
|
18
|
+
/* Linting */
|
19
|
+
"strict": true,
|
20
|
+
"noUnusedLocals": true,
|
21
|
+
"noUnusedParameters": true,
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
23
|
+
"noUncheckedSideEffectImports": true
|
24
|
+
},
|
25
|
+
"include": ["src"]
|
26
|
+
}
|
package/tsconfig.json
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
4
|
+
"target": "ES2022",
|
5
|
+
"lib": ["ES2023"],
|
6
|
+
"module": "ESNext",
|
7
|
+
"skipLibCheck": true,
|
8
|
+
|
9
|
+
/* Bundler mode */
|
10
|
+
"moduleResolution": "bundler",
|
11
|
+
"allowImportingTsExtensions": true,
|
12
|
+
"isolatedModules": true,
|
13
|
+
"moduleDetection": "force",
|
14
|
+
"noEmit": true,
|
15
|
+
|
16
|
+
/* Linting */
|
17
|
+
"strict": true,
|
18
|
+
"noUnusedLocals": true,
|
19
|
+
"noUnusedParameters": true,
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
21
|
+
"noUncheckedSideEffectImports": true
|
22
|
+
},
|
23
|
+
"include": ["vite.config.ts"]
|
24
|
+
}
|
package/vite.config.ts
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
import { defineConfig } from "vite";
|
2
|
+
import react from "@vitejs/plugin-react";
|
3
|
+
|
4
|
+
// https://vite.dev/config/
|
5
|
+
export default defineConfig({
|
6
|
+
plugins: [react()],
|
7
|
+
build: {
|
8
|
+
rollupOptions: {
|
9
|
+
external: ["react", "react-dom"],
|
10
|
+
output: {
|
11
|
+
globals: {
|
12
|
+
react: "React",
|
13
|
+
"react-dom": "ReactDOM",
|
14
|
+
},
|
15
|
+
},
|
16
|
+
},
|
17
|
+
lib: {
|
18
|
+
entry: "src/index.ts",
|
19
|
+
name: "react-proportion-slider",
|
20
|
+
fileName: (format) => `my-react-library.${format}.js`,
|
21
|
+
},
|
22
|
+
},
|
23
|
+
server: {
|
24
|
+
open: "/dev/index.html", // Open the dev environment on `npm run dev`
|
25
|
+
},
|
26
|
+
});
|