Files
frontend-svelte/src/shared/ui/Slider/Slider.svelte
T
Ilia Mashkov 5b7ec03973 refactor: sweep call sites onto design-system utilities + bug fixes
Replace inline class clusters with the design-system utilities and
tokens established in the prior two commits. No behavior changes
intended beyond two real bug fixes.

Bug fixes:
- SampleList.svelte: 'border-border-subtle bg-background-40' was a
  silent no-op (both classes mis-spelled). Now 'border-subtle
  bg-background/40' applies as intended.
- FontList.svelte: 'h-[44px]' → 'h-11' (44px = 2.75rem = spacing-11,
  no need for arbitrary value).

Sweeps:
- TypographyMenu: popover + floating bar now use surface-popover /
  surface-floating + shadow-popover.
- FontList + FilterGroup: tertiary list buttons use the new
  Button layout="block-list-row" variant; skeleton fills use
  the skeleton-fill utility.
- Footer / BreadcrumbHeader: surface-floating absorbs the
  bg-surface/blur/border cluster. Footer bumped to z-20 with a
  comment explaining the stacking against SidebarContainer (z-40/50).
- FontSampler: surface-card + hover shadow-stamp-card token.
- SliderArea: surface-canvas, flex-center, shadow-floating-panel
  tokens (light + dark variants).
- Sidebar / Header / ButtonGroup / Layout / SidebarContainer:
  bg-surface dark:bg-dark-bg → surface-canvas (8 sites);
  SidebarContainer mobile panel uses shadow-overlay.
- Loader / Thumb: flex items-center justify-center → flex-center;
  Thumb durations → duration-fast.
- ComboControl: trigger uses surface-card-elevated when open,
  popover uses surface-card-elevated, label cluster → text-label-mono,
  flex-center for the trigger interior.
- Slider: shadow-sm → shadow-rest, duration-150 → duration-fast.
- text-secondary → text-subtle across Input, Slider, ComboControl
  (matches the rename in the styles commit).
- Link: reverted earlier surface-floating attempt — Link's original
  bg-surface/80 backdrop-blur pattern was thinner than surface-floating
  (no border, smaller blur), and the Footer was overlaying its own
  border-subtle on top, fighting the utility. Kept the original style.
2026-05-25 10:20:40 +03:00

176 lines
4.6 KiB
Svelte

<!--
Component: Slider
Single-value slider using bits-ui Slider primitive.
Swiss design: 1px track, diamond thumb (rotate-45), brand accent.
-->
<script lang="ts">
import {
type Orientation,
Slider,
} from 'bits-ui';
interface Props {
/**
* Slider value
* @default 0
*/
value?: number;
/**
* Minimum value
* @default 0
*/
min?: number;
/**
* Maximum value
* @default 100
*/
max?: number;
/**
* Step increment
* @default 1
*/
step?: number;
/**
* Disabled state
* @default false
*/
disabled?: boolean;
/**
* Slider orientation
* @default 'horizontal'
*/
orientation?: Orientation;
/**
* Value formatter
* @default (v) => v
*/
format?: (v: number) => string | number;
/**
* Value change callback
*/
onValueChange?: (v: number) => void;
/**
* CSS classes
*/
class?: string;
}
let {
value = $bindable(0),
min = 0,
max = 100,
step = 1,
disabled = false,
orientation = 'horizontal',
format = (v: number) => v,
onValueChange,
class: className,
}: Props = $props();
const isVertical = $derived(orientation === 'vertical');
const labelClasses = `font-mono text-2xs tabular-nums shrink-0
text-subtle
group-hover:text-neutral-700 dark:group-hover:text-neutral-300
transition-colors`;
const thumbClasses = `block w-2.5 h-2.5 bg-brand
rotate-45 shadow-rest
hover:scale-125
focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-brand/20
data-active:scale-90
transition-transform duration-fast
disabled:pointer-events-none disabled:opacity-50
cursor-grab active:cursor-grabbing`;
</script>
{#if isVertical}
<div class="inline-flex flex-col items-center gap-3 group h-full {className ?? ''}">
<span class="{labelClasses} text-center">
{format(value)}
</span>
<Slider.Root
type="single"
orientation="vertical"
bind:value
{min}
{max}
{step}
{disabled}
onValueChange={(v => onValueChange?.(v))}
class="
relative flex flex-col items-center select-none touch-none
w-5 h-full grow cursor-row-resize
disabled:opacity-50 disabled:cursor-not-allowed
"
>
{#snippet children({ thumbItems })}
<span
class="
bg-neutral-200 dark:bg-neutral-800
relative grow w-px overflow-visible
group-hover:bg-neutral-300 dark:group-hover:bg-neutral-700
transition-colors
"
>
<Slider.Range class="absolute bg-brand w-full" />
</span>
{#each thumbItems as thumb (thumb)}
<Slider.Thumb
index={thumb.index}
class={thumbClasses}
aria-label="Value"
/>
{/each}
{/snippet}
</Slider.Root>
</div>
{:else}
<div class="flex items-center gap-4 group w-full {className ?? ''}">
<Slider.Root
type="single"
orientation="horizontal"
bind:value
{min}
{max}
{step}
{disabled}
onValueChange={(v => onValueChange?.(v))}
class="
relative flex items-center select-none touch-none
w-full h-5 cursor-col-resize
disabled:opacity-50 disabled:cursor-not-allowed
"
>
{#snippet children({ thumbItems })}
<span
class="
bg-neutral-200 dark:bg-neutral-800
relative grow h-px overflow-visible
group-hover:bg-neutral-300 dark:group-hover:bg-neutral-700
transition-colors
"
>
<Slider.Range class="absolute bg-brand h-full" />
</span>
{#each thumbItems as thumb (thumb)}
<Slider.Thumb
index={thumb.index}
class={thumbClasses}
aria-label="Value"
/>
{/each}
{/snippet}
</Slider.Root>
<!-- Label: right of slider -->
<span class="{labelClasses} w-12 text-right">
{format(value)}
</span>
</div>
{/if}