Improved slider.
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good
This commit is contained in:
parent
beae2ccff4
commit
7a6ec9ab33
@ -78,25 +78,33 @@ function Dashboard({
|
||||
const { isFullscreen, toggleFullscreen } = useFullscreen(dashboardRef);
|
||||
const [isEntityDashboardOpen, setIsEntityDashboardOpen] = useState(false);
|
||||
const [entityDashboardEntity, setEntityDashboardEntity] = useState(null);
|
||||
const entityDashboardEntityRef = useRef(null);
|
||||
const [isPreview, setIsPreview] = useState(false);
|
||||
|
||||
// Keep ref in sync with state for use in callbacks
|
||||
useEffect(() => {
|
||||
entityDashboardEntityRef.current = entityDashboardEntity;
|
||||
}, [entityDashboardEntity]);
|
||||
|
||||
// Entity subscription handler - updates local entity states from WebSocket
|
||||
const handleEntityUpdate = useCallback(
|
||||
(entityId, updatedEntity) => {
|
||||
setEntityStates((prev) => ({
|
||||
const handleEntityUpdate = useCallback((entityId, updatedEntity) => {
|
||||
setEntityStates((prev) => ({
|
||||
...prev,
|
||||
[entityId]: {
|
||||
...prev[entityId],
|
||||
...updatedEntity,
|
||||
},
|
||||
}));
|
||||
// Also update entityDashboardEntity if it matches the updated entity
|
||||
// Use ref to avoid recreating callback when entityDashboardEntity changes
|
||||
if (entityId === entityDashboardEntityRef.current?.entityId) {
|
||||
setEntityDashboardEntity((prev) => ({
|
||||
...prev,
|
||||
[entityId]: {
|
||||
...prev[entityId],
|
||||
...updatedEntity,
|
||||
},
|
||||
...updatedEntity,
|
||||
name: updatedEntity.name.replaceAll(title, ""),
|
||||
}));
|
||||
// Also update entityDashboardEntity if it matches the updated entity
|
||||
if (entityId === entityDashboardEntity?.entityId) {
|
||||
setEntityDashboardEntity((prev) => ({ ...prev, ...updatedEntity }));
|
||||
}
|
||||
},
|
||||
[entityDashboardEntity],
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Combine all entity IDs to subscribe to (from entities and slides)
|
||||
const allEntityIdsToSubscribe = useMemo(() => {
|
||||
@ -604,7 +612,10 @@ function Dashboard({
|
||||
};
|
||||
|
||||
const handleOpenEntityDashboard = useCallback((entity) => {
|
||||
setEntityDashboardEntity(entity);
|
||||
setEntityDashboardEntity({
|
||||
...entity,
|
||||
name: entity.name.replaceAll(title, ""),
|
||||
});
|
||||
setIsEntityDashboardOpen(true);
|
||||
}, []);
|
||||
|
||||
|
||||
@ -11,6 +11,14 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brightness-slider-unfilled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brightness-slider-filled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brightness-slider-handle {
|
||||
transition: transform 0.05s ease-out;
|
||||
}
|
||||
|
||||
@ -3,52 +3,73 @@ import "./BrightnessSlider.css";
|
||||
|
||||
/**
|
||||
* BrightnessSlider - Custom vertical brightness slider component
|
||||
* Displays a gradient from black to the current color based on hue/saturation
|
||||
* Displays a progress bar style with filled portion showing current color
|
||||
* and unfilled portion showing black with 0.1 opacity
|
||||
*/
|
||||
export const BrightnessSlider = ({
|
||||
value = 100, // 0-100 brightness value
|
||||
color = { h: 0, s: 100 }, // HS color for gradient
|
||||
color = { h: 0, s: 100 }, // HS color for filled portion
|
||||
onChange,
|
||||
height = 300,
|
||||
height = 300, // Can be number (px) or CSS string (e.g., "30vw")
|
||||
width = 60,
|
||||
handleRadius = 30,
|
||||
borderWidth = 2,
|
||||
borderColor = "rgba(255, 255, 255, 0.359)",
|
||||
}) => {
|
||||
const containerRef = useRef(null);
|
||||
const isDragging = useRef(false);
|
||||
const [handlePosition, setHandlePosition] = useState(0);
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
const [measuredHeight, setMeasuredHeight] = useState(0);
|
||||
|
||||
const sliderPadding = handleRadius;
|
||||
const effectiveHeight = height - sliderPadding * 2;
|
||||
// Constantly measure the container height like HSPicker does
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Convert value (0-100) to Y position
|
||||
const valueToPosition = useCallback(
|
||||
(val) => {
|
||||
// Invert: 100 = top, 0 = bottom
|
||||
return sliderPadding + (1 - val / 100) * effectiveHeight;
|
||||
},
|
||||
[sliderPadding, effectiveHeight]
|
||||
);
|
||||
let lastHeight = containerRef.current.clientHeight;
|
||||
setMeasuredHeight(lastHeight);
|
||||
|
||||
// Poll for size changes
|
||||
const checkSize = () => {
|
||||
if (containerRef.current) {
|
||||
const currentHeight = containerRef.current.clientHeight;
|
||||
if (currentHeight !== lastHeight) {
|
||||
lastHeight = currentHeight;
|
||||
setMeasuredHeight(currentHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after a short delay to ensure element is rendered
|
||||
const startId = setTimeout(() => {
|
||||
checkSize(); // Initial check
|
||||
}, 100);
|
||||
|
||||
const intervalId = setInterval(checkSize, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(startId);
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sliderPadding = 0;
|
||||
const effectiveHeight = measuredHeight - sliderPadding * 2;
|
||||
|
||||
// Convert Y position to value (0-100)
|
||||
const positionToValue = useCallback(
|
||||
(y) => {
|
||||
const clampedY = Math.max(sliderPadding, Math.min(y, height - sliderPadding));
|
||||
const clampedY = Math.max(
|
||||
sliderPadding,
|
||||
Math.min(y, measuredHeight - sliderPadding),
|
||||
);
|
||||
const normalizedY = (clampedY - sliderPadding) / effectiveHeight;
|
||||
// Invert: top = 100, bottom = 0
|
||||
return Math.round((1 - normalizedY) * 100);
|
||||
},
|
||||
[sliderPadding, effectiveHeight, height]
|
||||
[sliderPadding, effectiveHeight, measuredHeight],
|
||||
);
|
||||
|
||||
// Update handle position when value prop changes
|
||||
// Update current value when value prop changes
|
||||
useEffect(() => {
|
||||
const pos = valueToPosition(value);
|
||||
setHandlePosition(pos);
|
||||
setCurrentValue(value);
|
||||
}, [value, valueToPosition]);
|
||||
}, [value]);
|
||||
|
||||
// Get coordinates from mouse/touch event
|
||||
const getEventCoordinates = useCallback((e) => {
|
||||
@ -72,16 +93,13 @@ export const BrightnessSlider = ({
|
||||
const handleValueSelect = useCallback(
|
||||
(y) => {
|
||||
const newValue = positionToValue(y);
|
||||
const pos = valueToPosition(newValue);
|
||||
|
||||
setHandlePosition(pos);
|
||||
setCurrentValue(newValue);
|
||||
|
||||
if (onChange) {
|
||||
onChange(newValue);
|
||||
}
|
||||
},
|
||||
[positionToValue, valueToPosition, onChange]
|
||||
[positionToValue, onChange],
|
||||
);
|
||||
|
||||
// Mouse/touch event handlers
|
||||
@ -92,7 +110,7 @@ export const BrightnessSlider = ({
|
||||
const coords = getEventCoordinates(e);
|
||||
handleValueSelect(coords.y);
|
||||
},
|
||||
[getEventCoordinates, handleValueSelect]
|
||||
[getEventCoordinates, handleValueSelect],
|
||||
);
|
||||
|
||||
const handleMove = useCallback(
|
||||
@ -102,7 +120,7 @@ export const BrightnessSlider = ({
|
||||
const coords = getEventCoordinates(e);
|
||||
handleValueSelect(coords.y);
|
||||
},
|
||||
[getEventCoordinates, handleValueSelect]
|
||||
[getEventCoordinates, handleValueSelect],
|
||||
);
|
||||
|
||||
const handleEnd = useCallback(() => {
|
||||
@ -126,7 +144,9 @@ export const BrightnessSlider = ({
|
||||
document.addEventListener("mouseup", handleGlobalEnd);
|
||||
|
||||
// Touch events
|
||||
document.addEventListener("touchmove", handleGlobalMove, { passive: false });
|
||||
document.addEventListener("touchmove", handleGlobalMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", handleGlobalEnd);
|
||||
document.addEventListener("touchcancel", handleGlobalEnd);
|
||||
|
||||
@ -180,56 +200,16 @@ export const BrightnessSlider = ({
|
||||
};
|
||||
};
|
||||
|
||||
// Get HSV to RGB for handle color (includes brightness/value)
|
||||
const hsvToRgb = (h, s, v) => {
|
||||
const sNorm = s / 100;
|
||||
const vNorm = v / 100;
|
||||
const c = vNorm * sNorm;
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
const m = vNorm - c;
|
||||
// Color for the filled portion (current color at full brightness)
|
||||
const filledRgb = hsToRgb(color.h, color.s);
|
||||
const filledColor = `rgb(${filledRgb.r}, ${filledRgb.g}, ${filledRgb.b})`;
|
||||
|
||||
let r, g, b;
|
||||
if (h < 60) {
|
||||
r = c;
|
||||
g = x;
|
||||
b = 0;
|
||||
} else if (h < 120) {
|
||||
r = x;
|
||||
g = c;
|
||||
b = 0;
|
||||
} else if (h < 180) {
|
||||
r = 0;
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (h < 240) {
|
||||
r = 0;
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (h < 300) {
|
||||
r = x;
|
||||
g = 0;
|
||||
b = c;
|
||||
} else {
|
||||
r = c;
|
||||
g = 0;
|
||||
b = x;
|
||||
}
|
||||
// Calculate filled height based on current value (0-100)
|
||||
const filledHeight = (currentValue / 100) * effectiveHeight;
|
||||
const unfilledHeight = effectiveHeight - filledHeight;
|
||||
|
||||
return {
|
||||
r: Math.round((r + m) * 255),
|
||||
g: Math.round((g + m) * 255),
|
||||
b: Math.round((b + m) * 255),
|
||||
};
|
||||
};
|
||||
|
||||
const gradientRgb = hsToRgb(color.h, color.s);
|
||||
const gradientColor = `rgb(${gradientRgb.r}, ${gradientRgb.g}, ${gradientRgb.b})`;
|
||||
|
||||
const handleRgb = hsvToRgb(color.h, color.s, currentValue);
|
||||
const handleColorStyle = `rgb(${handleRgb.r}, ${handleRgb.g}, ${handleRgb.b})`;
|
||||
|
||||
// Gradient from color (top) to black (bottom)
|
||||
const gradientStyle = `linear-gradient(to bottom, ${gradientColor} 0%, rgb(0, 0, 0) 100%)`;
|
||||
const trackWidth = 60;
|
||||
const trackBorderRadius = 30;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -243,39 +223,44 @@ export const BrightnessSlider = ({
|
||||
onMouseDown={handleStart}
|
||||
onTouchStart={handleStart}
|
||||
>
|
||||
{/* Slider track with gradient */}
|
||||
{/* Slider track container */}
|
||||
<div
|
||||
className="brightness-slider-track"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: (width - 30) / 2,
|
||||
left: (width - trackWidth) / 2,
|
||||
top: sliderPadding,
|
||||
width: 30,
|
||||
width: trackWidth,
|
||||
height: effectiveHeight,
|
||||
borderRadius: 15,
|
||||
background: gradientStyle,
|
||||
boxShadow: "inset 0 0 3px rgba(0, 0, 0, 0.3)",
|
||||
borderRadius: trackBorderRadius,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Handle/cursor */}
|
||||
<div
|
||||
className="brightness-slider-handle"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: (width - handleRadius * 2) / 2,
|
||||
top: handlePosition - handleRadius,
|
||||
width: handleRadius * 2,
|
||||
height: handleRadius * 2,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: handleColorStyle,
|
||||
border: `${borderWidth}px solid ${borderColor}`,
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
|
||||
cursor: "grab",
|
||||
pointerEvents: "none",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{/* Unfilled portion (top) - black with 0.1 opacity */}
|
||||
<div
|
||||
className="brightness-slider-unfilled"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: unfilledHeight,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
/>
|
||||
{/* Filled portion (bottom) - current color */}
|
||||
<div
|
||||
className="brightness-slider-filled"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: filledHeight,
|
||||
backgroundColor: filledColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 380px;
|
||||
}
|
||||
|
||||
.entity-dashboard-info-title {
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
/* Brightness slider styling */
|
||||
.light-entity-dashboard-brightness-slider {
|
||||
margin-left: 5vw;
|
||||
margin-left: 2.5vw;
|
||||
}
|
||||
|
||||
/* iro.js slider handle styling */
|
||||
|
||||
@ -16,7 +16,7 @@ export const LightEntityDashboard = ({ entity }) => {
|
||||
const [localBrightness, setLocalBrightness] = useState(255);
|
||||
const brightnessTimeoutRef = useRef(null);
|
||||
const colorTimeoutRef = useRef(null);
|
||||
const DASHBOARD_HEIGHT = 460;
|
||||
const DASHBOARD_HEIGHT = "30vw";
|
||||
const isUpdatingFromEntity = useRef(false);
|
||||
|
||||
if (!entity) return null;
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import "./HSPicker.css";
|
||||
|
||||
// Import the color circle SVG as a React component
|
||||
@ -23,12 +29,47 @@ export const HSPicker = ({
|
||||
const pendingUpdate = useRef(null);
|
||||
const [handlePosition, setHandlePosition] = useState({ x: 0, y: 0 });
|
||||
const [currentColor, setCurrentColor] = useState(color);
|
||||
const [measuredSize, setMeasuredSize] = useState(0);
|
||||
|
||||
// Constantly measure the container size like SlideView does
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
let lastSize = containerRef.current.clientWidth;
|
||||
setMeasuredSize(lastSize);
|
||||
|
||||
// Poll for size changes
|
||||
const checkSize = () => {
|
||||
if (containerRef.current) {
|
||||
const currentSize = containerRef.current.clientWidth;
|
||||
if (currentSize !== lastSize) {
|
||||
lastSize = currentSize;
|
||||
setMeasuredSize(currentSize);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after a short delay to ensure element is rendered
|
||||
const startId = setTimeout(() => {
|
||||
checkSize(); // Initial check
|
||||
}, 100);
|
||||
|
||||
const intervalId = setInterval(checkSize, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(startId);
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Memoize computed values
|
||||
const { radius, effectiveRadius } = useMemo(() => ({
|
||||
radius: size / 2,
|
||||
effectiveRadius: size / 2 - handleRadius,
|
||||
}), [size, handleRadius]);
|
||||
const { radius, effectiveRadius } = useMemo(
|
||||
() => ({
|
||||
radius: measuredSize / 2,
|
||||
effectiveRadius: measuredSize / 2 - handleRadius,
|
||||
}),
|
||||
[measuredSize, handleRadius],
|
||||
);
|
||||
|
||||
// Convert HS to position on the wheel
|
||||
const hsToPosition = useCallback(
|
||||
@ -153,7 +194,14 @@ export const HSPicker = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[radius, effectiveRadius, positionToHS, hsToPosition, onChange, handleRadius],
|
||||
[
|
||||
radius,
|
||||
effectiveRadius,
|
||||
positionToHS,
|
||||
hsToPosition,
|
||||
onChange,
|
||||
handleRadius,
|
||||
],
|
||||
);
|
||||
|
||||
// Mouse/touch event handlers
|
||||
@ -177,13 +225,16 @@ export const HSPicker = ({
|
||||
[getEventCoordinates, handleColorSelect],
|
||||
);
|
||||
|
||||
const handleEnd = useCallback((e) => {
|
||||
if (isDragging.current && containerRef.current) {
|
||||
const coords = getEventCoordinates(e);
|
||||
handleColorSelect(coords.x, coords.y, true);
|
||||
}
|
||||
isDragging.current = false;
|
||||
}, [getEventCoordinates, handleColorSelect]);
|
||||
const handleEnd = useCallback(
|
||||
(e) => {
|
||||
if (isDragging.current && containerRef.current) {
|
||||
const coords = getEventCoordinates(e);
|
||||
handleColorSelect(coords.x, coords.y, true);
|
||||
}
|
||||
isDragging.current = false;
|
||||
},
|
||||
[getEventCoordinates, handleColorSelect],
|
||||
);
|
||||
|
||||
// Add global event listeners for drag
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user