From e55f4c2fe8a6e1d62a0b005777b46c80e360d37e Mon Sep 17 00:00:00 2001
From: memdmp <memdmp@estrogen.zone>
Date: Thu, 31 Jul 2025 22:48:01 +0200
Subject: feat: initial commit

---
 .../vendor/svelte-range-slider/range-slider.svelte | 1026 ++++++++++++++++++++
 1 file changed, 1026 insertions(+)
 create mode 100644 src/lib/vendor/svelte-range-slider/range-slider.svelte

(limited to 'src/lib/vendor/svelte-range-slider/range-slider.svelte')

diff --git a/src/lib/vendor/svelte-range-slider/range-slider.svelte b/src/lib/vendor/svelte-range-slider/range-slider.svelte
new file mode 100644
index 0000000..7f522e2
--- /dev/null
+++ b/src/lib/vendor/svelte-range-slider/range-slider.svelte
@@ -0,0 +1,1026 @@
+<script lang="ts" module>
+	export type ChangeEvent = {
+		activeHandle: number;
+		startValue: number;
+		previousValue: number;
+		value: number;
+		values: number[];
+	};
+
+	export type StartEvent = { activeHandle: number; value: number; values: number[] };
+
+	export type StopEvent = {
+		activeHandle: number;
+		startValue: number;
+		value: number;
+		values: number[];
+	};
+
+	export interface RangeSliderProps {
+		range?: boolean | 'min' | 'max';
+		onchange?: (event: ChangeEvent) => void;
+		onstart?: (event: StartEvent) => void;
+		onstop?: (event: StopEvent) => void;
+		pushy?: boolean;
+		min?: number;
+		max?: number;
+		ariaLabels?: string[];
+		precision?: number;
+		springOptions?: { stiffness: number; damping: number };
+		id?: string;
+		prefix?: string;
+		suffix?: string;
+		pips?: boolean;
+		pipstep?: number;
+		all?: boolean | 'pip' | 'label';
+		first?: boolean | 'pip' | 'label';
+		last?: boolean | 'pip' | 'label';
+		rest?: boolean | 'pip' | 'label';
+		step?: number;
+		value?: number;
+		values?: number[];
+		vertical?: boolean;
+		float?: boolean;
+		reversed?: boolean;
+		hoverable?: boolean;
+		disabled?: boolean;
+		formatter?: (value: number, index: number, percent: number) => string;
+		handleFormatter?: (value: number, index: number, percent: number) => string;
+	}
+</script>
+
+<script lang="ts">
+	import { spring } from 'svelte/motion';
+	import RangePips from './range-pips.svelte';
+
+	let {
+		range = false,
+		pushy = false,
+		min = 0,
+		max = 100,
+		ariaLabels = [],
+		precision = 2,
+		springOptions = { stiffness: 0.15, damping: 0.4 },
+		id = '',
+		prefix = '',
+		suffix = '',
+		pips = false,
+		pipstep,
+		all,
+		first,
+		last,
+		rest,
+		step = 1,
+		value = $bindable(0),
+		values = $bindable([(max + min) / 2]),
+		vertical = false,
+		float = false,
+		reversed = false,
+		hoverable = true,
+		disabled = false,
+		onchange,
+		onstart,
+		onstop,
+		formatter = (value: { toString: () => string }) => value.toString(),
+		handleFormatter = formatter
+	}: RangeSliderProps = $props();
+
+	if (value) {
+		values = [value];
+	}
+
+	let slider: Element | undefined = $state(undefined);
+	let valueLength = $state(0);
+	let focus = $state(false);
+	let handleActivated = $state(false);
+	let handlePressed = $state(false);
+	let keyboardActive = $state(false);
+	let activeHandle = $state(values.length - 1);
+
+	let startValue: number | undefined = $state();
+
+	let previousValue: number | undefined = $state();
+
+	/**
+	 * make sure the value is coerced to a float value
+	 * @param {number} v the value to fix
+	 * @return {number} a float version of the input
+	 **/
+	const fixFloat = (v: number): number => parseFloat((+v).toFixed(precision));
+
+	$effect(() => {
+		// check that "values" is an array, or set it as array to prevent any errors in springs, or range trimming
+		if (!Array.isArray(values)) {
+			values = [(max + min) / 2];
+			console.error(
+				"'values' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)"
+			);
+		}
+
+		// trim the range so it remains as a min/max (only 2 handles)
+		// and also align the handles to the steps
+		const trimmedAlignedValues = trimRange(values.map((v) => alignValueToStep(v)));
+		if (
+			!(values.length === trimmedAlignedValues.length) ||
+			!values.every((element, index) => fixFloat(element) === trimmedAlignedValues[index])
+		) {
+			values = trimmedAlignedValues;
+		}
+
+		// check if the valueLength (length of values[]) has changed,
+		// because if so we need to re-seed the spring function with the new values array.
+		if (valueLength !== values.length) {
+			// set the initial spring values when the slider initialises, or when values array length has changed
+			springPositions = spring(
+				values.map((v) => percentOf(v)),
+				springOptions
+			);
+		} else {
+			// update the value of the spring function for animated handles whenever the values has updated
+			springPositions.set(values.map((v) => percentOf(v)));
+		}
+		// set the valueLength for the next check
+		valueLength = values.length;
+
+		if (values.length > 1 && !Array.isArray(ariaLabels)) {
+			console.warn(
+				`'ariaLabels' prop should be an Array (https://github.com/simeydotme/svelte-range-slider-pips#slider-props)`
+			);
+		}
+	});
+
+	/**
+	 * take in a value, and then calculate that value's percentage
+	 * of the overall range (min-max);
+	 * @param {number} val the value we're getting percent for
+	 * @return {number} the percentage value
+	 **/
+	const percentOf = (/** @type {number} */ val: number): number => {
+		let percent = ((val - min) / (max - min)) * 100;
+
+		if (isNaN(percent) || percent <= 0) {
+			return 0;
+		}
+
+		if (percent >= 100) {
+			return 100;
+		}
+
+		return fixFloat(percent);
+	};
+
+	/**
+	 * clamp a value from the range so that it always
+	 * falls within the min/max values
+	 * @param {number} val the value to clamp
+	 * @return {number} the value after it's been clamped
+	 **/
+	const clampValue = (/** @type {number} */ val: number): number => {
+		// return the min/max if outside of that range
+		return val <= min ? min : val >= max ? max : val;
+	};
+
+	/**
+	 * align the value with the steps so that it
+	 * always sits on the closest (above/below) step
+	 * @param {number} val the value to align
+	 * @return {number} the value after it's been aligned
+	 **/
+	const alignValueToStep = (/** @type {number} */ val: number): number => {
+		// sanity check for performance
+		if (val <= min) {
+			return fixFloat(min);
+		}
+
+		if (val >= max) {
+			return fixFloat(max);
+		}
+
+		val = fixFloat(val);
+
+		// find the middle-point between steps and see if the value is closer to the next step, or previous step
+		let remainder = (val - min) % step;
+		let aligned = val - remainder;
+
+		if (Math.abs(remainder) * 2 >= step) {
+			aligned += remainder > 0 ? step : -step;
+		}
+
+		aligned = clampValue(aligned); // make sure the value is within acceptable limits
+
+		// make sure the returned value is set to the precision desired
+		// this is also because javascript often returns weird floats
+		// when dealing with odd numbers and percentages
+		return fixFloat(aligned);
+	};
+
+	/**
+	 * the orientation of the handles/pips based on the
+	 * input values of vertical and reversed
+	 * @type {"top"|"bottom"|"left"|"right"} orientationStart
+	 **/
+	let orientationStart: 'top' | 'bottom' | 'left' | 'right' = $derived(
+		vertical ? (reversed ? 'top' : 'bottom') : reversed ? 'right' : 'left'
+	);
+	let orientationEnd = $derived(
+		vertical ? (reversed ? 'bottom' : 'top') : reversed ? 'left' : 'right'
+	);
+
+	/**
+	 * helper function to get the index of an element in it's DOM container
+	 * @param {Element|null} el dom object reference we want the index of
+	 * @returns {number} the index of the input element
+	 **/
+	function index(el: Element | null): number {
+		if (!el) {
+			return -1;
+		}
+
+		let i = 0;
+		while ((el = el.previousElementSibling)) {
+			i++;
+		}
+		return i;
+	}
+
+	/**
+	 * normalise a mouse or touch event to return the
+	 * client (x/y) object for that event
+	 * @param {MouseEvent|TouchEvent} e a mouse/touch event to normalise
+	 * @returns {{ x: number, y: number }} normalised event client object (x,y)
+	 **/
+	function normalisedClient(e: MouseEvent | TouchEvent): { x: number; y: number } {
+		if (e.type.includes('touch')) {
+			const touchEvent = e as TouchEvent;
+			const touch = touchEvent.touches[0] || touchEvent.changedTouches[0];
+			return { x: touch.clientX, y: touch.clientY };
+		} else {
+			const mouseEvent = e as MouseEvent;
+			return { x: mouseEvent.clientX, y: mouseEvent.clientY };
+		}
+	}
+
+	/**
+	 * check if an element is a handle on the slider
+	 * @param {Element} el dom object reference we want to check
+	 * @returns {boolean}
+	 **/
+	function targetIsHandle(el: Element): boolean {
+		if (!slider) return false;
+		const handles = [...slider.querySelectorAll('.handle')];
+		const isHandle = handles.includes(el);
+		const isChild = handles.some((handle) => handle.contains(el));
+		return isHandle || isChild;
+	}
+
+	/**
+	 * trim the values array based on whether the property
+	 * for 'range' is 'min', 'max', or truthy. This is because we
+	 * do not want more than one handle for a min/max range, and we do
+	 * not want more than two handles for a true range.
+	 * @param {number[]} values the input values for the rangeSlider
+	 * @return {number[]} the range array for creating a rangeSlider
+	 **/
+	function trimRange(values: number[]): number[] {
+		if (range === 'min' || range === 'max') {
+			return values.slice(0, 1);
+		}
+		if (range) {
+			return values.slice(0, 2);
+		}
+
+		return values;
+	}
+
+	/**
+	 * helper to return the slider dimensions for finding
+	 * the closest handle to user interaction
+	 * @return {DOMRect} the range slider DOM client rect
+	 **/
+	function getSliderDimensions(): DOMRect | undefined {
+		return slider?.getBoundingClientRect();
+	}
+
+	/**
+	 * helper to return closest handle to user interaction
+	 * @param {{ x: number, y: number }} clientPos the client{x,y} positions to check against
+	 * @return {number} the index of the closest handle to clientPos
+	 **/
+	function getClosestHandle(clientPos: { x: number; y: number }): number {
+		// first make sure we have the latest dimensions
+		// of the slider, as it may have changed size
+		const dims = getSliderDimensions();
+		if (!dims) throw new Error('No Slider Dimensions yet.');
+		// calculate the interaction position, percent and value
+		let handlePos = 0;
+		let handlePercent = 0;
+		let handleVal = 0;
+		if (vertical) {
+			handlePos = clientPos.y - dims.top;
+			handlePercent = (handlePos / dims.height) * 100;
+			handlePercent = reversed ? handlePercent : 100 - handlePercent;
+		} else {
+			handlePos = clientPos.x - dims.left;
+			handlePercent = (handlePos / dims.width) * 100;
+			handlePercent = reversed ? 100 - handlePercent : handlePercent;
+		}
+		handleVal = ((max - min) / 100) * handlePercent + min;
+
+		// if we have a range, and the handles are at the same
+		// position, we want a simple check if the interaction
+		// value is greater than return the second handle
+		if (range === true && values[0] === values[1]) {
+			if (handleVal > values[1]) {
+				return 1;
+			}
+
+			return 0;
+
+			// if there are multiple handles, and not a range, then
+			// we sort the handles values, and return the first one closest
+			// to the interaction value
+		}
+
+		return values.indexOf(
+			[...values].sort((a, b) => Math.abs(handleVal - a) - Math.abs(handleVal - b))[0]
+		);
+	}
+
+	/**
+	 * take the interaction position on the slider, convert
+	 * it to a value on the range, and then send that value
+	 * through to the moveHandle() method to set the active
+	 * handle's position
+	 * @param {{ x: number, y: number }} clientPos the client{x,y} of the interaction
+	 **/
+	function handleInteract(clientPos: { x: number; y: number }) {
+		// first make sure we have the latest dimensions
+		// of the slider, as it may have changed size
+		const dims = getSliderDimensions();
+		if (!dims) throw new Error('No Slider Dimensions yet.');
+		// calculate the interaction position, percent and value
+		let handlePos = 0;
+		let handlePercent = 0;
+		let handleVal = 0;
+		if (vertical) {
+			handlePos = clientPos.y - dims.top;
+			handlePercent = (handlePos / dims.height) * 100;
+			handlePercent = reversed ? handlePercent : 100 - handlePercent;
+		} else {
+			handlePos = clientPos.x - dims.left;
+			handlePercent = (handlePos / dims.width) * 100;
+			handlePercent = reversed ? 100 - handlePercent : handlePercent;
+		}
+		handleVal = ((max - min) / 100) * handlePercent + min;
+		// move handle to the value
+		moveHandle(activeHandle, handleVal);
+	}
+
+	let lastSetValue = NaN;
+	/**
+	 * move a handle to a specific value, respecting the clamp/align rules
+	 * @param {number} index the index of the handle we want to move
+	 * @param {number} handleValue the value to move the handle to
+	 * @return {number} the value that was moved to (after alignment/clamping)
+	 **/
+	function moveHandle(index: number | undefined, handleValue: number): number {
+		// align & clamp the value so we're not doing extra
+		// calculation on an out-of-range value down below
+		handleValue = alignValueToStep(handleValue);
+		// use the active handle if handle index is not provided
+		if (typeof index === 'undefined') {
+			index = activeHandle;
+		}
+		// if this is a range slider perform special checks
+		if (range) {
+			// restrict the handles of a range-slider from
+			// going past one-another unless "pushy" is true
+			if (index === 0 && handleValue > values[1]) {
+				if (pushy) {
+					values[1] = handleValue;
+				} else {
+					handleValue = values[1];
+				}
+			} else if (index === 1 && handleValue < values[0]) {
+				if (pushy) {
+					values[0] = handleValue;
+				} else {
+					handleValue = values[0];
+				}
+			}
+		}
+
+		// if the value has changed, update it
+		if (values[index] !== handleValue) {
+			values[index] = handleValue;
+		}
+
+		// fire the change event when the handle moves,
+		// and store the previous value for the next time
+		if (previousValue !== handleValue) {
+			handleOnChange();
+			previousValue = handleValue;
+		}
+		lastSetValue = handleValue;
+		value = handleValue;
+		return handleValue;
+	}
+	$effect(() => {
+		if (value !== lastSetValue) moveHandle(undefined, value);
+	});
+
+	/**
+	 * helper to find the beginning range value for use with css style
+	 * @param {number[]} values the input values for the rangeSlider
+	 * @return {number} the beginning of the range
+	 **/
+	function rangeStart(values: number[]): number {
+		if (range === 'min') {
+			return 0;
+		}
+
+		return values[0];
+	}
+
+	/**
+	 * helper to find the ending range value for use with css style
+	 * @param {array} values the input values for the rangeSlider
+	 * @return {number} the end of the range
+	 **/
+	function rangeEnd(values: Array<any>): number {
+		if (range === 'max') {
+			return 0;
+		}
+
+		if (range === 'min') {
+			return 100 - values[0];
+		}
+
+		return 100 - values[1];
+	}
+
+	/**
+	 * helper to take a string of html and return only the text
+	 * @param {string} possibleHtml the string that may contain html
+	 * @return {string} the text from the input
+	 */
+	function pureText(possibleHtml: string): string {
+		return `${possibleHtml}`.replace(/<[^>]*>/g, '');
+	}
+
+	/**
+	 * when the user has unfocussed (blurred) from the
+	 * slider, deactivate all handles
+	 **/
+	function sliderBlurHandle() {
+		if (!keyboardActive) {
+			return;
+		}
+
+		focus = false;
+		handleActivated = false;
+		handlePressed = false;
+	}
+
+	/**
+	 * when the user focusses the handle of a slider
+	 * set it to be active
+	 * @param {Event} e the event from browser
+	 **/
+	function sliderFocusHandle(e: Event) {
+		if (disabled) {
+			return;
+		}
+
+		const target = e.target as HTMLElement;
+		activeHandle = index(target);
+		focus = true;
+	}
+
+	/**
+	 * handle the keyboard accessible features by checking the
+	 * input type, and modfier key then moving handle by appropriate amount
+	 * @param {KeyboardEvent} e the event from browser
+	 **/
+	function sliderKeydown(e: KeyboardEvent) {
+		if (disabled) {
+			return;
+		}
+
+		const target = e.target as HTMLElement;
+		const handle = index(target);
+		let jump = e.ctrlKey || e.metaKey || e.shiftKey ? step * 10 : step;
+		let prevent = false;
+
+		if (e.key === 'PageDown' || e.key === 'PageUp') {
+			jump *= 10;
+		}
+
+		if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
+			moveHandle(handle, values[handle] + jump);
+			prevent = true;
+		}
+
+		if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
+			moveHandle(handle, values[handle] - jump);
+			prevent = true;
+		}
+
+		if (e.key === 'Home') {
+			moveHandle(handle, min);
+			prevent = true;
+		}
+
+		if (e.key === 'End') {
+			moveHandle(handle, max);
+			prevent = true;
+		}
+
+		if (prevent) {
+			e.preventDefault();
+			e.stopPropagation();
+		}
+	}
+
+	/**
+	 * function to run when the user touches the slider element anywhere
+	 * @param {MouseEvent|TouchEvent} e the event from browser
+	 **/
+	function sliderInteractStart(e: MouseEvent | TouchEvent) {
+		if (disabled) {
+			return;
+		}
+
+		const element = e.target as HTMLElement;
+		const clientPos = normalisedClient(e);
+		// set the closest handle as active
+		focus = true;
+		handleActivated = true;
+		handlePressed = true;
+		activeHandle = getClosestHandle(clientPos);
+
+		// fire the start event
+		startValue = previousValue = alignValueToStep(values[activeHandle]);
+		handleOnStart();
+
+		// for touch devices we want the handle to instantly
+		// move to the position touched for more responsive feeling
+		if (e.type === 'touchstart' && !element.matches('.pipVal')) {
+			handleInteract(clientPos);
+		}
+	}
+
+	/**
+	 * function to run when the user stops touching
+	 * down on the slider element anywhere
+	 * @param {Event} e the event from browser
+	 **/
+	function sliderInteractEnd(e: Event) {
+		// fire the stop event for touch devices
+		if (e.type === 'touchend') {
+			handleOnStop();
+		}
+		handlePressed = false;
+	}
+
+	/**
+	 * unfocus the slider if the user clicked off of
+	 * it, somewhere else on the screen
+	 * @param {MouseEvent|TouchEvent} e the event from browser
+	 **/
+	function bodyInteractStart(e: MouseEvent | TouchEvent) {
+		keyboardActive = false;
+		const target = e.target as HTMLElement;
+		if (slider && focus && e.target !== slider && !slider.contains(target)) {
+			focus = false;
+		}
+	}
+
+	/**
+	 * send the clientX through to handle the interaction
+	 * whenever the user moves across screen while active
+	 * @param {MouseEvent|TouchEvent} e the event from browser
+	 **/
+	function bodyInteract(e: MouseEvent | TouchEvent) {
+		if (!disabled) {
+			if (handleActivated) {
+				handleInteract(normalisedClient(e));
+			}
+		}
+	}
+
+	/**
+	 * if user triggers mouseup on the body while
+	 * a handle is active (without moving) then we
+	 * trigger an interact event there
+	 * @param {MouseEvent|TouchEvent} e the event from browser
+	 **/
+	function bodyMouseUp(e: MouseEvent | TouchEvent) {
+		if (!disabled) {
+			const el = e.target as HTMLElement;
+			// this only works if a handle is active, which can
+			// only happen if there was sliderInteractStart triggered
+			// on the slider, already
+			if (handleActivated) {
+				if (slider && (el === slider || slider.contains(el))) {
+					focus = true;
+					// don't trigger interact if the target is a handle (no need) or
+					// if the target is a label (we want to move to that value from rangePips)
+					if (!targetIsHandle(el) && !el.matches('.pipVal')) {
+						handleInteract(normalisedClient(e));
+					}
+				}
+				// fire the stop event for mouse device
+				// when the body is triggered with an active handle
+				handleOnStop();
+			}
+		}
+		handleActivated = false;
+		handlePressed = false;
+	}
+
+	/**
+	 * @param {KeyboardEvent} e
+	 */
+	function bodyKeyDown(e: KeyboardEvent) {
+		if (disabled) {
+			return;
+		}
+
+		const target = e.target as HTMLElement;
+
+		if (slider && (e.target === slider || slider.contains(target))) {
+			keyboardActive = true;
+		}
+	}
+
+	function handleOnStop() {
+		if (disabled || !onstop || typeof onstop !== 'function') {
+			return;
+		}
+
+		onstop({
+			activeHandle,
+			startValue: startValue ?? 0,
+			value: values[activeHandle],
+			values: values.map((v) => alignValueToStep(v))
+		});
+	}
+
+	function handleOnStart() {
+		if (disabled || !onstart || typeof onstart !== 'function') {
+			return;
+		}
+
+		onstart({
+			activeHandle,
+			value: startValue ?? 0,
+			values: values.map((v) => alignValueToStep(v))
+		});
+	}
+
+	function handleOnChange() {
+		if (disabled || !onchange || typeof onchange !== 'function') {
+			return;
+		}
+
+		onchange({
+			activeHandle,
+			startValue: startValue ?? 0,
+			previousValue: typeof previousValue === 'undefined' ? (startValue ?? 0) : previousValue,
+			value: values[activeHandle],
+			values: values.map((v) => alignValueToStep(v))
+		});
+	}
+
+	/** @type {import('svelte/motion').Spring<number[]>} */
+	let springPositions: import('svelte/motion').Spring<number[]> = spring(
+		values.map((v) => percentOf(v)),
+		springOptions
+	);
+</script>
+
+<div
+	{id}
+	bind:this={slider}
+	role="none"
+	class="_rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28"
+	class:range
+	class:disabled
+	class:hoverable
+	class:vertical
+	class:reversed
+	class:focus
+	class:min={range === 'min'}
+	class:max={range === 'max'}
+	class:pips
+	class:pip-labels={all === 'label' || first === 'label' || last === 'label' || rest === 'label'}
+	onmousedown={sliderInteractStart}
+	onmouseup={sliderInteractEnd}
+>
+	{#each values as value, index}
+		<span
+			role="slider"
+			class="rangeHandle"
+			class:active={focus && activeHandle === index}
+			class:press={handlePressed && activeHandle === index}
+			data-handle={index}
+			onblur={sliderBlurHandle}
+			onfocus={sliderFocusHandle}
+			onkeydown={sliderKeydown}
+			style="{orientationStart}: {$springPositions[index]}%; z-index: {activeHandle === index
+				? 3
+				: 2};"
+			aria-label={ariaLabels[index]}
+			aria-valuemin={range === true && index === 1 ? values[0] : min}
+			aria-valuemax={range === true && index === 0 ? values[1] : max}
+			aria-valuenow={value}
+			aria-valuetext="{prefix}{pureText(handleFormatter(value, index, percentOf(value)))}{suffix}"
+			aria-orientation={vertical ? 'vertical' : 'horizontal'}
+			aria-disabled={disabled}
+			tabindex={disabled ? -1 : 0}
+		>
+			<span class="rangeNub"></span>
+			{#if float}
+				<span class="rangeFloat">
+					{prefix}{handleFormatter(value, index, percentOf(value))}{suffix}
+				</span>
+			{/if}
+		</span>
+	{/each}
+
+	{#if range}
+		<span
+			class="rangeBar"
+			style="{orientationStart}: {rangeStart($springPositions)}%; 
+             {orientationEnd}: {rangeEnd($springPositions)}%;"
+		></span>
+	{/if}
+
+	{#if pips}
+		<RangePips
+			{values}
+			{min}
+			{max}
+			{step}
+			{range}
+			{vertical}
+			{reversed}
+			{orientationStart}
+			{hoverable}
+			{disabled}
+			{all}
+			{first}
+			{last}
+			{rest}
+			{pipstep}
+			{prefix}
+			{suffix}
+			{formatter}
+			{focus}
+			{percentOf}
+			{moveHandle}
+			{fixFloat}
+			{normalisedClient}
+		/>
+	{/if}
+</div>
+
+<svelte:window
+	onmousedown={bodyInteractStart}
+	onmousemove={bodyInteract}
+	onmouseup={bodyMouseUp}
+	onkeydown={bodyKeyDown}
+/>
+
+<style>
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+		--slider: var(--range-slider, #d7dada);
+		--handle-inactive: var(--range-handle-inactive, #99a2a2);
+		--handle: var(--range-handle, #838de7);
+		--handle-focus: var(--range-handle-focus, #4a40d4);
+		--handle-border: var(--range-handle-border, var(--handle));
+		--range-inactive: var(--range-range-inactive, var(--handle-inactive));
+		--range: var(--range-range, var(--handle-focus));
+		--float-inactive: var(--range-float-inactive, var(--handle-inactive));
+		--float: var(--range-float, var(--handle-focus));
+		--float-text: var(--range-float-text, white);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+		position: relative;
+		border-radius: 100px;
+		height: 0.5em;
+		margin: 1em;
+		transition: opacity 0.2s ease;
+		user-select: none;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 *) {
+		user-select: none;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pips) {
+		margin-bottom: 1.8em;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.pip-labels) {
+		margin-bottom: 2.8em;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical) {
+		display: inline-block;
+		border-radius: 100px;
+		width: 0.5em;
+		min-height: 200px;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pips) {
+		margin-right: 1.8em;
+		margin-bottom: 1em;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.pip-labels) {
+		margin-right: 2.8em;
+		margin-bottom: 1em;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle) {
+		position: absolute;
+		display: block;
+		height: 1.4em;
+		width: 1.4em;
+		top: 0.25em;
+		bottom: auto;
+		transform: translateY(-50%) translateX(-50%);
+		z-index: 2;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.reversed .rangeHandle) {
+		transform: translateY(-50%) translateX(50%);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeHandle) {
+		left: 0.25em;
+		top: auto;
+		transform: translateY(50%) translateX(-50%);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical.reversed .rangeHandle) {
+		transform: translateY(-50%) translateX(-50%);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub),
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
+		position: absolute;
+		left: 0;
+		top: 0;
+		display: block;
+		border-radius: 10em;
+		height: 100%;
+		width: 100%;
+		transition: box-shadow 0.2s ease;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle:before) {
+		content: '';
+		left: 1px;
+		top: 1px;
+		bottom: 1px;
+		right: 1px;
+		height: auto;
+		width: auto;
+		box-shadow: 0 0 0 0px var(--handle-border);
+		opacity: 0;
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+			.rangeHandle:hover:before
+	) {
+		box-shadow: 0 0 0 8px var(--handle-border);
+		opacity: 0.2;
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+			.rangeHandle.press:before
+	),
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable:not(.disabled)
+			.rangeHandle.press:hover:before
+	) {
+		box-shadow: 0 0 0 12px var(--handle-border);
+		opacity: 0.4;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range:not(.min):not(.max) .rangeNub) {
+		border-radius: 10em 10em 10em 1.6em;
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(1) .rangeNub
+	) {
+		transform: rotate(-135deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range .rangeHandle:nth-of-type(2) .rangeNub
+	) {
+		transform: rotate(45deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
+			.rangeHandle:nth-of-type(1)
+			.rangeNub
+	) {
+		transform: rotate(45deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.reversed
+			.rangeHandle:nth-of-type(2)
+			.rangeNub
+	) {
+		transform: rotate(-135deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
+			.rangeHandle:nth-of-type(1)
+			.rangeNub
+	) {
+		transform: rotate(135deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical
+			.rangeHandle:nth-of-type(2)
+			.rangeNub
+	) {
+		transform: rotate(-45deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
+			.rangeHandle:nth-of-type(1)
+			.rangeNub
+	) {
+		transform: rotate(-45deg);
+	}
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.range.vertical.reversed
+			.rangeHandle:nth-of-type(2)
+			.rangeNub
+	) {
+		transform: rotate(135deg);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
+		display: block;
+		position: absolute;
+		left: 50%;
+		top: -0.5em;
+		transform: translate(-50%, -100%);
+		font-size: 1em;
+		text-align: center;
+		opacity: 0;
+		pointer-events: none;
+		white-space: nowrap;
+		transition: all 0.2s ease;
+		font-size: 0.9em;
+		padding: 0.2em 0.4em;
+		border-radius: 0.2em;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeFloat),
+	:global(
+		._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.hoverable .rangeHandle:hover .rangeFloat
+	) {
+		opacity: 1;
+		top: -0.2em;
+		transform: translate(-50%, -100%);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
+		position: absolute;
+		display: block;
+		transition: background 0.2s ease;
+		border-radius: 1em;
+		height: 0.5em;
+		top: 0;
+		user-select: none;
+		z-index: 1;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.vertical .rangeBar) {
+		width: 0.5em;
+		height: auto;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28) {
+		background-color: var(--slider, #d7dada);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeBar) {
+		background-color: var(--range-inactive, #99a2a2);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeBar) {
+		background-color: var(--range, #838de7);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeNub) {
+		background-color: var(--handle-inactive, #99a2a2);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeNub) {
+		background-color: var(--handle, #838de7);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeHandle.active .rangeNub) {
+		background-color: var(--handle-focus, #4a40d4);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28 .rangeFloat) {
+		color: white;
+		color: var(--float-text);
+		background-color: var(--float-inactive, #99a2a2);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.focus .rangeFloat) {
+		background-color: var(--float, #4a40d4);
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled) {
+		opacity: 0.5;
+	}
+	:global(._rangeslider-0f6d4a99-47b0-4108-8415-b2aefa867e28.disabled .rangeNub) {
+		background-color: var(--slider, #d7dada);
+	}
+</style>
-- 
cgit v1.2.3

