All files / web/src/components/studio StudioSlider.tsx

100% Statements 111/111
100% Branches 8/8
100% Functions 2/2
100% Lines 111/111

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 1121x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x  
'use client'
 
// StudioSlider — the studio's numeric slider (Gitea #43). Same shape and props
// as the toy debug panel's `DebugSlider`, which the design and fabrication rails
// borrowed wholesale; the borrowing is why a printing studio wore the debug
// panel's indigo. This is the studio's own copy, styled from theme.ts: one cyan
// accent, a 4 px track, and a 16 px thumb that is a plain accent disc inside an
// accent halo rather than a white-ringed dot.
//
// The header row is the reason a slider needs a component at all: the LABEL and
// the VALUE sit on one line above the track, label muted and value primary, so a
// column of sliders reads as a table of settings you can scan without touching
// anything.
 
import * as Slider from '@radix-ui/react-slider'
import { STUDIO } from './theme'
 
export interface StudioSliderProps {
  label: string
  value: number
  min: number
  max: number
  step?: number
  onChange: (value: number) => void
  /** Custom value formatter for display (e.g. toFixed(3) for floats) */
  formatValue?: (value: number) => string
  dataElement?: string
  dataAction?: string
  disabled?: boolean
}
 
export function StudioSlider({
  label,
  value,
  min,
  max,
  step = 1,
  onChange,
  formatValue,
  dataElement = 'debug-slider',
  dataAction,
  disabled = false,
}: StudioSliderProps) {
  return (
    <div
      data-component="studio-slider"
      data-element={dataElement}
      style={{
        display: 'flex',
        flexDirection: 'column',
        gap: STUDIO.space.field,
        opacity: disabled ? 0.55 : 1,
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
        <span style={STUDIO.type.label}>{label}</span>
        <span style={STUDIO.type.value}>{formatValue ? formatValue(value) : value}</span>
      </div>
      <Slider.Root
        value={[value]}
        min={min}
        max={max}
        step={step}
        disabled={disabled}
        data-action={dataAction}
        onValueChange={([v]) => onChange(v)}
        style={{
          position: 'relative',
          display: 'flex',
          alignItems: 'center',
          width: '100%',
          height: 16,
          touchAction: 'none',
          userSelect: 'none',
        }}
      >
        <Slider.Track
          style={{
            position: 'relative',
            flexGrow: 1,
            height: 4,
            borderRadius: 2,
            background: STUDIO.color.track,
          }}
        >
          <Slider.Range
            style={{
              position: 'absolute',
              height: '100%',
              borderRadius: 2,
              background: STUDIO.color.accent,
            }}
          />
        </Slider.Track>
        <Slider.Thumb
          aria-label={label}
          style={{
            display: 'block',
            width: 16,
            height: 16,
            borderRadius: '50%',
            background: STUDIO.color.accent,
            border: 'none',
            boxShadow: `${STUDIO.shadow.thumb}, 0 0 0 4px ${STUDIO.color.accentRing}`,
            cursor: disabled ? 'default' : 'grab',
          }}
        />
      </Slider.Root>
    </div>
  )
}