Create Slider

This commit is contained in:
Jeremy Thomas 2024-06-24 03:25:58 +01:00
parent 7d92a2b872
commit 4a165737df
12 changed files with 339 additions and 41 deletions

View File

@ -8,6 +8,7 @@
"name": "bulma-customizer", "name": "bulma-customizer",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"classnames": "^2.5.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
@ -1560,6 +1561,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",

View File

@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"classnames": "^2.5.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },

View File

@ -3,8 +3,22 @@ import "../../../../css/bulma.css";
import "./App.css"; import "./App.css";
import Slider from "./components/Slider"; import Slider from "./components/Slider";
const KEYS = ["scheme-h", "primary-h", "primary-s", "primary-l"]; // const COLORS = ["primary", "link", "info", "success", "warning", "danger"];
const KEYS = [
"scheme-h",
"primary-h",
"primary-s",
"primary-l",
"skeleton-lines-gap",
];
const UNITS = ["deg", "rem", "em", "%"]; const UNITS = ["deg", "rem", "em", "%"];
const SUFFIX_TO_KIND = {
"-h": "hue",
"-s": "saturation",
"-l": "lightness",
"-gap": "gap",
};
function App() { function App() {
const [vars, setVars] = useState([]); const [vars, setVars] = useState([]);
@ -14,11 +28,15 @@ function App() {
const cssvars = KEYS.map((key) => { const cssvars = KEYS.map((key) => {
const original = rootStyle.getPropertyValue(`--bulma-${key}`); const original = rootStyle.getPropertyValue(`--bulma-${key}`);
const suffix = Object.keys(SUFFIX_TO_KIND).find((kind) =>
key.endsWith(kind),
);
const unit = UNITS.find((unit) => original.endsWith(unit)) || ""; const unit = UNITS.find((unit) => original.endsWith(unit)) || "";
const value = unit !== "" ? original.split(unit)[0] : original; const value = unit !== "" ? original.split(unit)[0] : original;
return { return {
id: key, id: key,
kind: SUFFIX_TO_KIND[suffix] || "any",
original, original,
unit, unit,
start: Number(value), start: Number(value),
@ -35,11 +53,18 @@ function App() {
<div className="card"> <div className="card">
<div className="card-content"> <div className="card-content">
{vars.map((v) => { {vars.map((v) => {
const { id, original, unit, start } = v; const { id, kind, original, unit, start } = v;
return ( return (
<div key={id} className="block"> <div key={id} className="block">
<Slider id={id} original={original} start={start} unit={unit} /> <code>{id}</code>
<Slider
id={id}
kind={kind}
original={original}
start={start}
unit={unit}
/>
</div> </div>
); );
})} })}

View File

@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import cn from "./Slider.module.css";
const RANGES = {
deg: [0, 360, 1],
"%": [0, 100, 1],
};
function Picker({ id, kind, start, unit }) {
const [value, setValue] = useState(start);
let min = 0;
let max = 360;
let step = 1;
if (unit in RANGES) {
[min, max, step] = RANGES[unit];
}
const handleChange = (event) => {
setValue(event.target.value);
};
const handleReset = () => {
setValue(start);
};
useEffect(() => {
const computedValue = `${value}${unit}`;
if (value === start) {
document.documentElement.style.removeProperty(`--bulma-${id}`);
} else {
document.documentElement.style.setProperty(
`--bulma-${id}`,
computedValue,
);
}
}, [id, start, unit, value]);
const mainCN = classNames({
[cn.main]: true,
[cn[kind]]: kind,
});
return (
<div className={mainCN}>
<code>{id}</code>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
/>
<code>
{value}
{unit}
</code>
<button className="button is-small" onClick={handleReset}>
Reset
</button>
</div>
);
}
Picker.propTypes = {
id: PropTypes.string,
kind: PropTypes.string,
original: PropTypes.string,
start: PropTypes.number,
unit: PropTypes.string,
};
export default Picker;

View File

@ -1,28 +1,40 @@
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import classNames from "classnames";
import cn from "./Slider.module.css";
const RANGES = { const RANGES = {
deg: [0, 360, 1], hue: [0, 360, 1],
"%": [0, 100, 1], saturation: [0, 100, 1],
lightness: [0, 100, 1],
gap: [0, 100, 1],
any: [0, 100, 1],
}; };
function Slider({ id, start, unit }) { function Slider({ id, kind, start, unit }) {
const [value, setValue] = useState(start); const [value, setValue] = useState(start);
const [isMoving, setMoving] = useState(false);
const [x, setX] = useState(0);
const sliderRef = useRef(null);
const handleRef = useRef(null);
let min = 0; const [min, max, step] = RANGES[kind];
let max = 360;
let step = 1;
if (unit in RANGES) { const handleMouseDown = (event) => {
[min, max, step] = RANGES[unit]; setMoving(true);
} const slider = sliderRef.current;
const sliderRect = slider.getBoundingClientRect();
const handleChange = (event) => { const target = event.clientX - sliderRect.left;
setValue(event.target.value); setX(target);
}; };
const handleReset = () => { const docMouseLeave = () => {
setValue(start); setMoving(false);
};
const docMouseUp = () => {
setMoving(false);
}; };
useEffect(() => { useEffect(() => {
@ -38,30 +50,74 @@ function Slider({ id, start, unit }) {
} }
}, [id, start, unit, value]); }, [id, start, unit, value]);
useEffect(() => {
const docMouseMove = (event) => {
if (!isMoving || !sliderRef.current || !handleRef.current) {
return;
}
const slider = sliderRef.current;
const sliderRect = slider.getBoundingClientRect();
let target = event.clientX - sliderRect.left;
if (target < 0) {
target = 0;
} else if (target > sliderRect.width) {
target = sliderRect.width;
}
setX(target);
};
window.addEventListener("mousemove", docMouseMove);
return () => {
window.removeEventListener("mousemove", docMouseMove);
};
}, [isMoving, min, max, x]);
useEffect(() => {
window.addEventListener("mouseleave", docMouseLeave);
return () => {
window.removeEventListener("mouseleave", docMouseLeave);
};
}, []);
useEffect(() => {
window.addEventListener("mouseup", docMouseUp);
return () => {
window.removeEventListener("mouseup", docMouseUp);
};
}, []);
const mainCN = classNames({
[cn.main]: true,
[cn.moving]: isMoving,
});
const backgroundCN = classNames({
[cn.background]: true,
[cn[kind]]: true,
});
const handleStyle = {
transform: `translateX(${x}px)`,
};
return ( return (
<div> <div className={mainCN} ref={sliderRef} onMouseDown={handleMouseDown}>
<code>{id}</code> <div className={backgroundCN}>
<input <span ref={handleRef} className={cn.handle} style={handleStyle} />
type="range" </div>
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
/>
<code>
{value}
{unit}
</code>
<button className="button is-small" onClick={handleReset}>
Reset
</button>
</div> </div>
); );
} }
Slider.propTypes = { Slider.propTypes = {
id: PropTypes.string, id: PropTypes.string,
kind: PropTypes.string,
original: PropTypes.string, original: PropTypes.string,
start: PropTypes.number, start: PropTypes.number,
unit: PropTypes.string, unit: PropTypes.string,

View File

@ -0,0 +1,50 @@
.main {
position: relative;
width: 15rem;
padding: 0.375rem 0;
box-shadow: 0 0 0 1px green;
cursor: grab;
}
.moving {
cursor: grabbing;
}
.handle {
background-color: rgba(255, 255, 255, 0.05);
box-shadow:
rgba(0, 0, 0, 0.15) 0 0 0 1px,
rgba(0, 0, 0, 0.05) 0 10px 10px -5px;
height: 1.25rem;
width: 1.25rem;
border-radius: 9999px;
position: absolute;
top: 0rem;
cursor: grab;
border: 0.25rem solid white;
left: -0.625rem;
}
.moving .handle,
.handle:active {
cursor: grabbing;
}
.background {
border-radius: 0.125rem;
background-color: white;
height: 0.5rem;
}
.hue {
background-image: linear-gradient(
to right,
rgb(255, 0, 0),
rgb(255, 255, 0),
rgb(0, 255, 0),
rgb(0, 255, 255),
rgb(0, 0, 255),
rgb(255, 0, 255),
rgb(255, 0, 0)
);
}

View File

@ -1,7 +1,16 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react' import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
build: {
outDir: "../../assets/javascript/bulma-customizer",
rollupOptions: {
output: {
assetFileNames: "[name].css",
entryFileNames: "[name].js",
},
},
},
plugins: [react()], plugins: [react()],
}) });

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" crossorigin href="/index.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

16
docs/customizer.html Normal file
View File

@ -0,0 +1,16 @@
---
layout: default
theme: customizer
route: customizer
---
{% include global/header.html %}
{%
include docs/hero.html
title="The Bulma Customizer"
subtitle="Gorgeous websites built with Bulma."
%}
<div id="root"></div>
<script src="{{ site.url }}/assets/javascript/bulma-customizer/index.js"></script>

View File

@ -31,9 +31,9 @@ breadcrumb:
</div> </div>
<div class="media-content"> <div class="media-content">
<p class="title is-5">Use the <strong>HTML5 doctype</strong></p> <p class="title is-5">Use the <strong>HTML5 doctype</strong></p>
{% highlight html %} {%- highlight html -%}
<!doctype html> <!DOCTYPE html>
{% endhighlight %} {%- endhighlight -%}
</div> </div>
</article> </article>