mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
color light support in HA panel
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D0OyPG8n.js"></script>
|
<script type="module" crossorigin src="/assets/index-U-JdJMLL.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CVqWOsms.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C0HFz2ll.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ function loadEntityConfig() {
|
|||||||
function buildState(meta, raw) {
|
function buildState(meta, raw) {
|
||||||
if (!meta) return null;
|
if (!meta) return null;
|
||||||
const name = meta.name || raw?.attributes?.friendly_name || meta.id;
|
const name = meta.name || raw?.attributes?.friendly_name || meta.id;
|
||||||
|
const supportedColorModes = Array.isArray(raw?.attributes?.supported_color_modes)
|
||||||
|
? raw.attributes.supported_color_modes.map((mode) => String(mode))
|
||||||
|
: [];
|
||||||
|
const rgbColor = Array.isArray(raw?.attributes?.rgb_color) ? raw.attributes.rgb_color : null;
|
||||||
|
const hsColor = Array.isArray(raw?.attributes?.hs_color) ? raw.attributes.hs_color : null;
|
||||||
|
const supportsColor =
|
||||||
|
meta.type === 'light' &&
|
||||||
|
(rgbColor ||
|
||||||
|
hsColor ||
|
||||||
|
supportedColorModes.some((mode) => mode === 'hs' || mode === 'rgb' || mode === 'xy'));
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {
|
return {
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
@@ -90,6 +100,11 @@ function buildState(meta, raw) {
|
|||||||
available: false,
|
available: false,
|
||||||
lastChanged: null,
|
lastChanged: null,
|
||||||
lastUpdated: null,
|
lastUpdated: null,
|
||||||
|
supportedColorModes,
|
||||||
|
colorMode: null,
|
||||||
|
rgbColor: null,
|
||||||
|
hsColor: null,
|
||||||
|
supportsColor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const rawState = raw.state;
|
const rawState = raw.state;
|
||||||
@@ -103,6 +118,11 @@ function buildState(meta, raw) {
|
|||||||
available: !unavailable,
|
available: !unavailable,
|
||||||
lastChanged: raw.last_changed || null,
|
lastChanged: raw.last_changed || null,
|
||||||
lastUpdated: raw.last_updated || null,
|
lastUpdated: raw.last_updated || null,
|
||||||
|
supportedColorModes,
|
||||||
|
colorMode: raw?.attributes?.color_mode || null,
|
||||||
|
rgbColor,
|
||||||
|
hsColor,
|
||||||
|
supportsColor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +239,29 @@ async function toggleEntity(entityId) {
|
|||||||
return setEntityState(entityId, nextState);
|
return setEntityState(entityId, nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setLightColor(entityId, rgbColor) {
|
||||||
|
if (!enabled) {
|
||||||
|
throw new Error('Home Assistant not configured');
|
||||||
|
}
|
||||||
|
const meta = entityConfig.get(entityId);
|
||||||
|
if (!meta || meta.type !== 'light') {
|
||||||
|
throw new Error('Home Assistant light required');
|
||||||
|
}
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error('Home Assistant not connected');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(rgbColor) || rgbColor.length !== 3) {
|
||||||
|
throw new Error('rgbColor required');
|
||||||
|
}
|
||||||
|
const normalized = rgbColor.map((value) => {
|
||||||
|
const next = Number(value);
|
||||||
|
if (Number.isNaN(next)) return 0;
|
||||||
|
return Math.max(0, Math.min(255, Math.round(next)));
|
||||||
|
});
|
||||||
|
await callService(connection, 'light', 'turn_on', { entity_id: entityId, rgb_color: normalized });
|
||||||
|
logger.info('Issued Home Assistant color command', { entityId, rgbColor: normalized });
|
||||||
|
}
|
||||||
|
|
||||||
function getState() {
|
function getState() {
|
||||||
const entities = Array.from(entityConfig.values()).map(
|
const entities = Array.from(entityConfig.values()).map(
|
||||||
(meta) => entityState.get(meta.id) || buildState(meta, null),
|
(meta) => entityState.get(meta.id) || buildState(meta, null),
|
||||||
@@ -265,11 +308,30 @@ io.on('connection', (socket) => {
|
|||||||
cb({ error: err.message });
|
cb({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('homeAssistant:lightColor', async ({ entityId, rgbColor } = {}, cb = () => {}) => {
|
||||||
|
const mode = getMode();
|
||||||
|
if (
|
||||||
|
(mode === 'admin' && isAdmin(socket) !== true) ||
|
||||||
|
(mode === 'lockdown' && isLockdownAdmin(socket) !== true)
|
||||||
|
) {
|
||||||
|
return cb({ error: 'Insufficient permissions to control Home Assistant' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!entityId) throw new Error('entityId required');
|
||||||
|
await setLightColor(entityId, rgbColor);
|
||||||
|
cb({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
cb({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getState,
|
getState,
|
||||||
toggleEntity,
|
toggleEntity,
|
||||||
setEntityState,
|
setEntityState,
|
||||||
|
setLightColor,
|
||||||
homeAssistantEvents: events,
|
homeAssistantEvents: events,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSession } from '../context/SessionContext.jsx';
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
import { useControlSystem } from '../controls/index.js';
|
import { useControlSystem } from '../controls/index.js';
|
||||||
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
import { formatKeyLabel } from '../controls/keymapUtils.js';
|
||||||
@@ -11,42 +11,194 @@ function StatusBadge({ label, tone = 'muted' }) {
|
|||||||
? 'bg-amber-900 text-amber-100'
|
? 'bg-amber-900 text-amber-100'
|
||||||
: 'bg-slate-800 text-slate-200';
|
: 'bg-slate-800 text-slate-200';
|
||||||
return (
|
return (
|
||||||
<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-tight ${styles}`}>
|
<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-none ${styles}`}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EntityRow({ entity, connected, onToggle }) {
|
function clampHue(value) {
|
||||||
|
if (!Number.isFinite(value)) return 0;
|
||||||
|
const wrapped = value % 360;
|
||||||
|
return wrapped < 0 ? wrapped + 360 : wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHue(rgb) {
|
||||||
|
if (!Array.isArray(rgb) || rgb.length < 3) return 0;
|
||||||
|
const [rRaw, gRaw, bRaw] = rgb;
|
||||||
|
const r = Math.max(0, Math.min(255, Number(rRaw))) / 255;
|
||||||
|
const g = Math.max(0, Math.min(255, Number(gRaw))) / 255;
|
||||||
|
const b = Math.max(0, Math.min(255, Number(bRaw))) / 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const delta = max - min;
|
||||||
|
if (delta === 0) return 0;
|
||||||
|
let hue = 0;
|
||||||
|
if (max === r) {
|
||||||
|
hue = ((g - b) / delta) % 6;
|
||||||
|
} else if (max === g) {
|
||||||
|
hue = (b - r) / delta + 2;
|
||||||
|
} else {
|
||||||
|
hue = (r - g) / delta + 4;
|
||||||
|
}
|
||||||
|
return clampHue(Math.round(hue * 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hueToRgb(hue) {
|
||||||
|
const h = clampHue(hue);
|
||||||
|
const c = 1;
|
||||||
|
const x = 1 - Math.abs(((h / 60) % 2) - 1);
|
||||||
|
let r = 0;
|
||||||
|
let g = 0;
|
||||||
|
let b = 0;
|
||||||
|
if (h < 60) {
|
||||||
|
r = c;
|
||||||
|
g = x;
|
||||||
|
} else if (h < 120) {
|
||||||
|
r = x;
|
||||||
|
g = c;
|
||||||
|
} else if (h < 180) {
|
||||||
|
g = c;
|
||||||
|
b = x;
|
||||||
|
} else if (h < 240) {
|
||||||
|
g = x;
|
||||||
|
b = c;
|
||||||
|
} else if (h < 300) {
|
||||||
|
r = x;
|
||||||
|
b = c;
|
||||||
|
} else {
|
||||||
|
r = c;
|
||||||
|
b = x;
|
||||||
|
}
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEntityHue(entity) {
|
||||||
|
if (!entity) return 0;
|
||||||
|
if (Array.isArray(entity.hsColor)) {
|
||||||
|
return clampHue(Number(entity.hsColor[0]));
|
||||||
|
}
|
||||||
|
if (Array.isArray(entity.rgbColor)) {
|
||||||
|
return rgbToHue(entity.rgbColor);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EntityRow({ entity, connected, onToggle, onSetColor }) {
|
||||||
const unavailable = entity.state === 'unavailable' || !entity.available;
|
const unavailable = entity.state === 'unavailable' || !entity.available;
|
||||||
const isOn = entity.state === 'on';
|
const isOn = entity.state === 'on';
|
||||||
|
const supportsColor = entity.type === 'light' && entity.supportsColor;
|
||||||
const statusTone = unavailable ? 'warn' : isOn ? 'success' : 'muted';
|
const statusTone = unavailable ? 'warn' : isOn ? 'success' : 'muted';
|
||||||
const statusLabel = unavailable ? 'Unavailable' : isOn ? 'On' : 'Off';
|
const statusLabel = unavailable ? 'Unavailable' : isOn ? 'On' : 'Off';
|
||||||
const disableToggle = !connected || unavailable;
|
const disableToggle = !connected || unavailable;
|
||||||
|
const disableColor = disableToggle || !supportsColor;
|
||||||
|
const [hue, setHue] = useState(() => getEntityHue(entity));
|
||||||
|
const hueRef = useRef(hue);
|
||||||
|
const draggingRef = useRef(false);
|
||||||
const toneStyles = unavailable
|
const toneStyles = unavailable
|
||||||
? 'border-slate-800 bg-slate-900 text-slate-400 cursor-not-allowed'
|
? 'border-slate-800 bg-slate-900 text-slate-400 cursor-not-allowed'
|
||||||
: isOn
|
: isOn
|
||||||
? 'border-emerald-700 bg-emerald-900/80 text-emerald-50 hover:bg-emerald-800'
|
? 'border-emerald-700 bg-emerald-900/80 text-emerald-50 hover:bg-emerald-800'
|
||||||
: 'border-rose-800 bg-rose-900/80 text-rose-50 hover:bg-rose-800';
|
: 'border-rose-800 bg-rose-900/80 text-rose-50 hover:bg-rose-800';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!supportsColor || draggingRef.current) return;
|
||||||
|
const nextHue = getEntityHue(entity);
|
||||||
|
hueRef.current = nextHue;
|
||||||
|
setHue(nextHue);
|
||||||
|
}, [entity.rgbColor, entity.hsColor, supportsColor]);
|
||||||
|
|
||||||
|
const handleHueChange = (event) => {
|
||||||
|
const nextHue = clampHue(Number(event.target.value));
|
||||||
|
hueRef.current = nextHue;
|
||||||
|
setHue(nextHue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitHue = () => {
|
||||||
|
if (disableColor || !onSetColor) return;
|
||||||
|
onSetColor(entity.id, hueToRgb(hueRef.current));
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPropagation = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayRgb = supportsColor ? hueToRgb(hueRef.current) : [255, 255, 255];
|
||||||
|
const displayColor = `rgb(${displayRgb.join(',')})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggle(entity.id)}
|
onClick={() => onToggle(entity.id)}
|
||||||
disabled={disableToggle}
|
disabled={disableToggle}
|
||||||
className={`flex min-w-[10rem] flex-1 items-center justify-between gap-0.5 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`}
|
className={`relative flex min-w-[10rem] flex-1 items-start justify-between gap-0.5 rounded px-1 py-0.5 text-left transition-colors ${toneStyles} disabled:opacity-60 disabled:hover:bg-inherit`}
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-0.5 text-sm">
|
<div className="flex items-center gap-0.5 text-sm leading-normal">
|
||||||
<span className="truncate font-semibold text-white">{entity.name || entity.id}</span>
|
<span className="truncate font-semibold text-white">{entity.name || entity.id}</span>
|
||||||
{/* <StatusBadge label={entity.type === 'light' ? 'Light' : 'Switch'} /> */}
|
{supportsColor && (
|
||||||
</div>
|
<>
|
||||||
<div className="flex items-center gap-0.5 text-xs text-slate-400">
|
<StatusBadge label={statusLabel} tone={statusTone} />
|
||||||
<StatusBadge label={statusLabel} tone={statusTone} />
|
{!connected && <span className="text-xs text-amber-200">Offline</span>}
|
||||||
{!connected && <span className="text-amber-200"> · Offline</span>}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{supportsColor ? (
|
||||||
|
<div
|
||||||
|
className="-mt-0.5 w-full"
|
||||||
|
onClick={stopPropagation}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
stopPropagation(event);
|
||||||
|
draggingRef.current = true;
|
||||||
|
}}
|
||||||
|
onPointerUp={(event) => {
|
||||||
|
stopPropagation(event);
|
||||||
|
draggingRef.current = false;
|
||||||
|
commitHue();
|
||||||
|
}}
|
||||||
|
onPointerCancel={(event) => {
|
||||||
|
stopPropagation(event);
|
||||||
|
draggingRef.current = false;
|
||||||
|
commitHue();
|
||||||
|
}}
|
||||||
|
onKeyUp={(event) => {
|
||||||
|
stopPropagation(event);
|
||||||
|
commitHue();
|
||||||
|
}}
|
||||||
|
onBlur={commitHue}
|
||||||
|
>
|
||||||
|
<div className="relative w-full">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
step="1"
|
||||||
|
value={hue}
|
||||||
|
onChange={handleHueChange}
|
||||||
|
disabled={disableColor}
|
||||||
|
aria-label={`Set color for ${entity.name || entity.id}`}
|
||||||
|
className="ha-hue-slider w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-0.5 text-xs text-slate-400">
|
||||||
|
<StatusBadge label={statusLabel} tone={statusTone} />
|
||||||
|
{!connected && <span className="text-amber-200"> · Offline</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs font-semibold text-white/90">{isOn ? 'Turn off' : 'Turn on'}</div>
|
{supportsColor && (
|
||||||
|
<span
|
||||||
|
className="pointer-events-none absolute right-1 top-1 h-2.5 w-2.5 rounded-full border border-white/60"
|
||||||
|
style={{ backgroundColor: displayColor }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!supportsColor && (
|
||||||
|
<div className="self-center text-xs font-semibold text-white/90">
|
||||||
|
{isOn ? 'Turn off' : 'Turn on'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,7 +207,7 @@ export default function HomeAssistantControls() {
|
|||||||
const {
|
const {
|
||||||
state: { keymap },
|
state: { keymap },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const { session, homeAssistantToggle } = useSession();
|
const { session, homeAssistantToggle, homeAssistantSetLightColor } = useSession();
|
||||||
const ha = session?.homeAssistant;
|
const ha = session?.homeAssistant;
|
||||||
const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
|
const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
|
||||||
const onKeyLabel = formatKeyLabel(keymap?.homeAssistantOn?.[0]);
|
const onKeyLabel = formatKeyLabel(keymap?.homeAssistantOn?.[0]);
|
||||||
@@ -107,6 +259,7 @@ export default function HomeAssistantControls() {
|
|||||||
entity={entity}
|
entity={entity}
|
||||||
connected={connected}
|
connected={connected}
|
||||||
onToggle={homeAssistantToggle}
|
onToggle={homeAssistantToggle}
|
||||||
|
onSetColor={homeAssistantSetLightColor}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const SessionContext = createContext({
|
|||||||
subscribeAll: async () => {},
|
subscribeAll: async () => {},
|
||||||
homeAssistantToggle: async () => {},
|
homeAssistantToggle: async () => {},
|
||||||
homeAssistantSetState: async () => {},
|
homeAssistantSetState: async () => {},
|
||||||
|
homeAssistantSetLightColor: async () => {},
|
||||||
setNickname: async () => {},
|
setNickname: async () => {},
|
||||||
triggerReplay: async () => {},
|
triggerReplay: async () => {},
|
||||||
setCommunityGoal: async () => {},
|
setCommunityGoal: async () => {},
|
||||||
@@ -109,6 +110,8 @@ export function SessionProvider({ children }) {
|
|||||||
homeAssistantToggle: (entityId) => emitWithAck('homeAssistant:toggle', { entityId }),
|
homeAssistantToggle: (entityId) => emitWithAck('homeAssistant:toggle', { entityId }),
|
||||||
homeAssistantSetState: (entityId, state) =>
|
homeAssistantSetState: (entityId, state) =>
|
||||||
emitWithAck('homeAssistant:setState', { entityId, state }),
|
emitWithAck('homeAssistant:setState', { entityId, state }),
|
||||||
|
homeAssistantSetLightColor: (entityId, rgbColor) =>
|
||||||
|
emitWithAck('homeAssistant:lightColor', { entityId, rgbColor }),
|
||||||
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
|
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
|
||||||
triggerReplay: (sources = []) => emitWithAck('replay:trigger', { sources }),
|
triggerReplay: (sources = []) => emitWithAck('replay:trigger', { sources }),
|
||||||
setCommunityGoal: (text) => emitWithAck('communityGoal:set', { text }),
|
setCommunityGoal: (text) => emitWithAck('communityGoal:set', { text }),
|
||||||
|
|||||||
@@ -137,3 +137,74 @@ body {
|
|||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.45rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#ff0000,
|
||||||
|
#ffff00,
|
||||||
|
#00ff00,
|
||||||
|
#00ffff,
|
||||||
|
#0000ff,
|
||||||
|
#ff00ff,
|
||||||
|
#ff0000
|
||||||
|
);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider::-webkit-slider-runnable-track {
|
||||||
|
height: 0.45rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#ff0000,
|
||||||
|
#ffff00,
|
||||||
|
#00ff00,
|
||||||
|
#00ffff,
|
||||||
|
#0000ff,
|
||||||
|
#ff00ff,
|
||||||
|
#ff0000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider::-moz-range-thumb {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ha-hue-slider::-moz-range-track {
|
||||||
|
height: 0.45rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#ff0000,
|
||||||
|
#ffff00,
|
||||||
|
#00ff00,
|
||||||
|
#00ffff,
|
||||||
|
#0000ff,
|
||||||
|
#ff00ff,
|
||||||
|
#ff0000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user