Загрузка данных


import classNames from 'classnames';
import { type ChangeEvent, type PointerEvent, useId, useRef } from 'react';

import styles from './index.module.scss';

interface CommonRangeFieldProps {
  label?: string;
  min?: number;
  max?: number;
  step?: number;
  color?: string;
  suffix?: string;
  size?: 'sm' | 'md';
  disabled?: boolean;
}

interface SingleRangeFieldProps extends CommonRangeFieldProps {
  mode?: 'single';
  value: number;
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}

interface IntervalRangeFieldProps extends CommonRangeFieldProps {
  mode: 'range';
  value: readonly [number, number];
  startLabel: string;
  endLabel: string;
  onRangeChange: (value: [number, number]) => void;
}

interface RangeDragState {
  pointerX: number;
  from: number;
  to: number;
  trackWidth: number;
}

type RangeFieldProps = SingleRangeFieldProps | IntervalRangeFieldProps;

export const RangeField = (props: RangeFieldProps) => {
  const {
    label,
    min = 0,
    max = 100,
    step = 1,
    color = 'var(--blue-1)',
    suffix = '%',
    size = 'md',
    disabled = false,
  } = props;
  const inputId = useId();
  const trackRef = useRef<HTMLDivElement>(null);
  const dragRef = useRef<RangeDragState | null>(null);

  if (props.mode !== 'range') {
    return (
      <div className={classNames(styles.range, { [styles.range_sm]: size === 'sm' })}>
        {label && (
          <label htmlFor={inputId} className={styles.range_label}>
            {label}
          </label>
        )}
        <div className={styles.range_wrapper}>
          <input
            id={inputId}
            type="range"
            min={min}
            max={max}
            step={step}
            value={props.value}
            className={styles.range_input}
            style={{ ['--range-color' as string]: color }}
            onChange={props.onChange}
            disabled={disabled}
          />
          <span className={styles.range_value}>
            {props.value}
            {suffix}
          </span>
        </div>
      </div>
    );
  }

  const {
    value: [from, to],
    startLabel,
    endLabel,
    onRangeChange,
  } = props;
  const span = max - min;
  const left = span > 0 ? ((from - min) / span) * 100 : 0;
  const right = span > 0 ? ((to - min) / span) * 100 : 0;
  const isDisabled = disabled || span <= 0;

  const handleDragEnd = () => {
    dragRef.current = null;
  };
  const handleDragStart = (event: PointerEvent<HTMLDivElement>) => {
    if (isDisabled || event.button !== 0 || !trackRef.current) return;
    const { width } = trackRef.current.getBoundingClientRect();
    if (width <= 0) return;
    event.preventDefault();
    event.currentTarget.setPointerCapture(event.pointerId);
    dragRef.current = { pointerX: event.clientX, from, to, trackWidth: width };
  };
  const handleDragMove = (event: PointerEvent<HTMLDivElement>) => {
    const drag = dragRef.current;
    if (!drag || isDisabled) return;
    const pointerOffset = ((event.clientX - drag.pointerX) / drag.trackWidth) * span;
    const steppedOffset = Math.round(pointerOffset / step) * step;
    const offset = Math.max(min - drag.from, Math.min(max - drag.to, steppedOffset));
    onRangeChange([drag.from + offset, drag.to + offset]);
  };

  const handleFromChange = (event: ChangeEvent<HTMLInputElement>) => {
    onRangeChange([Math.min(event.currentTarget.valueAsNumber, to), to]);
  };
  const handleToChange = (event: ChangeEvent<HTMLInputElement>) => {
    onRangeChange([from, Math.max(event.currentTarget.valueAsNumber, from)]);
  };

  return (
    <div
      className={classNames(styles.range, styles.range_interval, {
        [styles.range_sm]: size === 'sm',
        [styles.range_disabled]: isDisabled,
      })}
    >
      {label && <span className={styles.range_label}>{label}</span>}
      <div ref={trackRef} className={styles.range_track} style={{ ['--range-color' as string]: color }}>
        <div className={styles.range_rail} />
        <div
          className={styles.range_selection}
          style={{ left: `${left}%`, width: `${right - left}%` }}
          onPointerDown={handleDragStart}
          onPointerMove={handleDragMove}
          onPointerUp={handleDragEnd}
          onPointerCancel={handleDragEnd}
          onLostPointerCapture={handleDragEnd}
        />
        <input
          id={`${inputId}-from`}
          type="range"
          min={min}
          max={max}
          step={step}
          value={from}
          aria-label={startLabel}
          disabled={isDisabled}
          className={classNames(styles.range_handle, styles.range_handle_start, {
            [styles.range_handle_behind]: from === to && from === min,
          })}
          onChange={handleFromChange}
        />
        <input
          id={`${inputId}-to`}
          type="range"
          min={min}
          max={max}
          step={step}
          value={to}
          aria-label={endLabel}
          disabled={isDisabled}
          className={classNames(styles.range_handle, styles.range_handle_end)}
          onChange={handleToChange}
        />
      </div>
      <div className={styles.range_values}>
        <span>
          {from}
          {suffix}
        </span>
        <span>
          {to}
          {suffix}
        </span>
      </div>
    </div>
  );
};


@use '../../theme/mixins' as m;

[data-theme='mxt'],
[data-theme='mb'][data-mode='light'] {
  --dd-item-text-selected: var(--neutral-2);
  --dd-item-bg-selected: var(--neutral-12);
  --color-field-border: var(--neutral-11);
  --color-field-square: #00000024;
}

[data-theme='tr'],
[data-theme='mb'][data-mode='dark'] {
  --dd-item-text-selected: var(--neutral-13);
  --dd-item-bg-selected: var(--neutral-5);
  --color-field-border: var(--neutral-9);
  --color-field-square: #ffffff14;
}

$button-color-bottom: 5px;

.input {
  color: var(--neutral-12);

  input {
    color: var(--neutral-12);
  }

  &_select {
    &:focus-within {
      background-color: transparent !important;
    }

    div {
      background-color: transparent;

      &:focus-within {
        background-color: transparent !important;
      }

      .input {
        background-color: transparent;
      }
    }
  }

  &_color {
    display: flex;
    flex-direction: column;
    gap: var(--space-0250);

    .input_wrapper {
      position: relative;
      width: 100%;
    }
  }
}

.dropdown {
  li {
    &[aria-selected='true'] {
      background-color: var(--dd-item-bg-selected);

      div[aria-label='Label'] {
        color: var(--dd-item-text-selected);
      }
    }

    svg {
      color: transparent;
    }
  }
}

.button_color {
  position: absolute;
  bottom: $button-color-bottom;
  right: var(--space-0500);
  width: var(--space-1375);
  height: var(--space-1375);
  border: 1px solid var(--color-field-border);
  border-radius: var(--space-0125);
}

.input_checkbox {
  padding: var(--space-0375) 0 !important;

  span {
    font-size: var(--space-0875);
    line-height: normal;
  }
}

.input_textarea {
  textarea {
    font-size: var(--space-0875);
    line-height: normal;
  }
}

.range {
  --range-input-height: var(--space-1000);
  --range-track-height: var(--space-0750);
  --range-thumb-size: var(--space-1000);
  --range-pattern-size: var(--space-0500);
  --range-label-font-size: var(--space-0875);
  --range-value-font-size: var(--space-0750);
  --range-wrapper-gap: var(--space-0500);

  display: flex;
  flex-direction: column;
  gap: var(--space-0250);

  &_sm {
    --range-input-height: var(--space-0875);
    --range-track-height: var(--space-0500);
    --range-thumb-size: var(--space-0875);
    --range-pattern-size: var(--space-0375);
    --range-label-font-size: var(--space-0750);
    --range-wrapper-gap: var(--space-0375);
  }

  &_label {
    color: var(--neutral-12);
    font-size: var(--range-label-font-size);
  }

  &_wrapper {
    display: grid;
    align-items: center;
    grid-template: 1fr / 1fr auto;
    gap: var(--range-wrapper-gap);
  }

  &_value {
    min-width: var(--space-2000);
    color: var(--neutral-12);
    font-size: var(--range-value-font-size);
    line-height: normal;
    text-align: right;
  }

  &_input {
    --range-color: var(--blue-1);

    width: 100%;
    height: var(--range-input-height);
    appearance: none;
    background: transparent;
    cursor: pointer;

    &::-webkit-slider-runnable-track {
      height: var(--range-track-height);
      border: 1px solid var(--color-field-border);
      border-radius: 999px;
      background:
        linear-gradient(to right, transparent 0%, var(--range-color) 100%),
        linear-gradient(45deg, var(--color-field-square) 25%, transparent 25%),
        linear-gradient(-45deg, var(--color-field-square) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--color-field-square) 75%),
        linear-gradient(-45deg, transparent 75%, var(--color-field-square) 75%);
      background-size:
        100% 100%,
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size);
      background-position:
        0 0,
        0 0,
        0 calc(var(--range-pattern-size) / 2),
        calc(var(--range-pattern-size) / 2) calc(var(--range-pattern-size) / -2),
        calc(var(--range-pattern-size) / -2) 0;
    }

    &::-webkit-slider-thumb {
      width: var(--range-thumb-size);
      height: var(--range-thumb-size);
      margin-top: calc((var(--range-track-height) - var(--range-thumb-size)) / 2 - 1px);
      appearance: none;
      border: 1px solid var(--color-field-border);
      border-radius: 50%;
      background-color: var(--neutral-panel);
    }

    &::-moz-range-track {
      height: var(--range-track-height);
      border: 1px solid var(--color-field-border);
      border-radius: 999px;
      background:
        linear-gradient(to right, transparent 0%, var(--range-color) 100%),
        linear-gradient(45deg, var(--color-field-square) 25%, transparent 25%),
        linear-gradient(-45deg, var(--color-field-square) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--color-field-square) 75%),
        linear-gradient(-45deg, transparent 75%, var(--color-field-square) 75%);
      background-size:
        100% 100%,
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size),
        var(--range-pattern-size) var(--range-pattern-size);
      background-position:
        0 0,
        0 0,
        0 calc(var(--range-pattern-size) / 2),
        calc(var(--range-pattern-size) / 2) calc(var(--range-pattern-size) / -2),
        calc(var(--range-pattern-size) / -2) 0;
    }

    &::-moz-range-progress {
      background: transparent;
    }

    &::-moz-range-thumb {
      width: var(--range-thumb-size);
      height: var(--range-thumb-size);
      border: 1px solid var(--color-field-border);
      border-radius: 50%;
      background-color: var(--neutral-panel);
    }
  }

  &_interval {
    min-width: 0;
  }

  &_disabled {
    opacity: 0.5;
  }

  &_track {
    position: relative;
    height: var(--range-thumb-size);
    margin-inline: calc(var(--range-thumb-size) / 2);
    touch-action: none;
  }

  &_rail,
  &_selection {
    position: absolute;
    top: 50%;
    height: var(--range-track-height);
    transform: translateY(-50%);
    border-radius: 999px;
  }

  &_rail {
    inset-inline: 0;
    background: var(--neutral-9);
  }

  &_selection {
    z-index: 1;
    background: var(--range-color);
    cursor: grab;
    touch-action: none;

    &:active {
      cursor: grabbing;
    }
  }

  &_handle {
    position: absolute;
    inset: 0;
    width: calc(100% + var(--range-thumb-size));
    height: var(--range-thumb-size);
    margin: 0;
    margin-left: calc(var(--range-thumb-size) / -2);
    appearance: none;
    background: transparent;
    pointer-events: none;

    &::-webkit-slider-runnable-track {
      height: var(--range-track-height);
      background: transparent;
    }

    &::-moz-range-track {
      height: var(--range-track-height);
      background: transparent;
    }

    &::-moz-range-progress {
      background: transparent;
    }

    &::-webkit-slider-thumb {
      width: var(--range-thumb-size);
      height: var(--range-thumb-size);
      margin-top: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
      appearance: none;
      border: 1px solid var(--color-field-border);
      border-radius: 50%;
      background: var(--neutral-panel);
      cursor: ew-resize;
      pointer-events: auto;
    }

    &::-moz-range-thumb {
      width: var(--range-thumb-size);
      height: var(--range-thumb-size);
      border: 1px solid var(--color-field-border);
      border-radius: 50%;
      background: var(--neutral-panel);
      cursor: ew-resize;
      pointer-events: auto;
    }

    &_start {
      z-index: 4;
    }

    &_end {
      z-index: 3;
    }

    &_behind {
      z-index: 2;
    }

    &:focus-visible {
      outline: none;
    }

    &:focus-visible::-webkit-slider-thumb {
      outline: 2px solid var(--range-color);
      outline-offset: 2px;
    }

    &:focus-visible::-moz-range-thumb {
      outline: 2px solid var(--range-color);
      outline-offset: 2px;
    }

    &:disabled::-webkit-slider-thumb,
    &:disabled::-moz-range-thumb {
      pointer-events: none;
      cursor: default;
    }
  }

  &_values {
    display: flex;
    justify-content: space-between;
    color: var(--neutral-12);
    font-size: var(--range-value-font-size);
  }
}

.color {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1000);

  &_label {
    font-size: var(--space-0875);
    color: var(--neutral-12);
    font-weight: var(--font-medium);
  }

  &_menu {
    --dropdown-width: max-content;
    padding: var(--space-1000);
  }

  &_trigger {
    min-width: none;
    width: var(--space-2000);
    height: var(--space-2000);
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: var(--space-0375);
    border: var(--borderWidth-md) solid var(--neutral-9);

    p {
      padding: 0;
    }
  }

  &_preview {
    width: var(--space-1375);
    height: var(--space-1375);
    border-radius: var(--space-0250);
  }
}