add Slider component with elemental styling for level selection

This commit is contained in:
Justin Edmund 2025-12-03 18:41:04 -08:00
parent a1bc125521
commit 23b1d091f5
3 changed files with 247 additions and 35 deletions

View file

@ -10,10 +10,11 @@
import { isQuirkArtifact, getSkillGroupForSlot } from '$lib/types/api/artifact'
import { createQuery } from '@tanstack/svelte-query'
import { artifactQueries } from '$lib/api/queries/artifact.queries'
import { usePaneStack, type PaneConfig } from '$lib/stores/paneStack.svelte'
import { usePaneStack, type PaneConfig, type ElementType } from '$lib/stores/paneStack.svelte'
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
import Select from '$lib/components/ui/Select.svelte'
import Slider from '$lib/components/ui/Slider.svelte'
import ArtifactSkillRow from './ArtifactSkillRow.svelte'
import ArtifactModifierList from './ArtifactModifierList.svelte'
import ArtifactGradeDisplay from './ArtifactGradeDisplay.svelte'
@ -64,6 +65,17 @@
{ value: 6, label: 'Light', color: '#e8d633' }
]
// Convert numeric element to ElementType string
const elementTypeMap: Record<number, ElementType> = {
1: 'wind',
2: 'fire',
3: 'water',
4: 'earth',
5: 'dark',
6: 'light'
}
const elementType = $derived(elementTypeMap[element] ?? undefined)
// Level options (1-5 for standard, fixed at 1 for quirk)
const levelOptions = $derived(
isQuirk
@ -189,25 +201,6 @@
<div class="artifact-edit-pane">
<DetailsSection title="Base Properties">
<DetailRow label="Element" noHover>
{#if disabled}
{@const elementOption = elementOptions.find((o) => o.value === element)}
<span class="element-display">
<span class="element-dot" style="background-color: {elementOption?.color}"></span>
{elementOption?.label ?? '—'}
</span>
{:else}
<Select
options={elementOptions}
value={element}
onValueChange={handleElementChange}
size="small"
contained
{disabled}
/>
{/if}
</DetailRow>
{#if canChangeProficiency}
<DetailRow label="Proficiency" noHover>
{#if disabled}
@ -228,18 +221,41 @@
<DetailRow label="Proficiency" value={getProficiencyName(artifactData.proficiency)} />
{/if}
<DetailRow label="Element" noHover>
{#if disabled}
{@const elementOption = elementOptions.find((o) => o.value === element)}
<span class="element-display">
<span class="element-dot" style="background-color: {elementOption?.color}"></span>
{elementOption?.label ?? '—'}
</span>
{:else}
<Select
class="element-select"
options={elementOptions}
value={element}
onValueChange={handleElementChange}
contained
{disabled}
/>
{/if}
</DetailRow>
<DetailRow label="Level" noHover>
{#if disabled || isQuirk}
<span>{level}</span>
{:else}
<Select
options={levelOptions}
value={level}
onValueChange={handleLevelChange}
size="small"
contained
{disabled}
/>
<div class="level-slider">
<Slider
value={level}
onValueChange={handleLevelChange}
min={1}
max={5}
step={1}
element={elementType}
{disabled}
/>
<span class="level-value">{level}</span>
</div>
{/if}
</DetailRow>
</DetailsSection>
@ -281,6 +297,10 @@
padding-bottom: spacing.$unit-4x;
}
:global(.artifact-edit-pane .select.medium) {
min-width: 120px;
}
.element-display {
display: flex;
align-items: center;
@ -294,6 +314,20 @@
flex-shrink: 0;
}
.level-slider {
display: flex;
align-items: center;
gap: spacing.$unit;
flex: 1;
.level-value {
font-size: typography.$font-regular;
font-weight: typography.$medium;
min-width: spacing.$unit-2x;
text-align: center;
}
}
.skills-list {
display: flex;
flex-direction: column;

View file

@ -22,6 +22,7 @@
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
import DetailRow from '$lib/components/sidebar/details/DetailRow.svelte'
import Select from '$lib/components/ui/Select.svelte'
import Slider from '$lib/components/ui/Slider.svelte'
import Input from '$lib/components/ui/Input.svelte'
import ArtifactSkillRow from '$lib/components/artifact/ArtifactSkillRow.svelte'
import ArtifactModifierList from '$lib/components/artifact/ArtifactModifierList.svelte'
@ -320,13 +321,21 @@
{#if selectedArtifact}
<DetailsSection title="Configuration">
<DetailRow label="Level" noHover>
<Select
options={levelOptions}
value={level}
onValueChange={(v) => v !== undefined && (level = v)}
size="small"
contained
/>
{#if isQuirk}
<span>1</span>
{:else}
<div class="level-slider">
<Slider
value={level}
onValueChange={(v) => (level = v)}
min={1}
max={5}
step={1}
element={elementType}
/>
<span class="level-value">{level}</span>
</div>
{/if}
</DetailRow>
<DetailRow label="Nickname" noHover>
@ -395,6 +404,20 @@
color: colors.$error;
}
.level-slider {
display: flex;
align-items: center;
gap: spacing.$unit;
flex: 1;
.level-value {
font-size: typography.$font-regular;
font-weight: typography.$medium;
min-width: spacing.$unit-2x;
text-align: center;
}
}
.skills-list {
display: flex;
flex-direction: column;

View file

@ -0,0 +1,155 @@
<svelte:options runes={true} />
<script lang="ts">
import { Slider as SliderPrimitive } from 'bits-ui'
interface Props {
/** Current value */
value?: number
/** Callback when value changes */
onValueChange?: (value: number) => void
/** Minimum value */
min?: number
/** Maximum value */
max?: number
/** Step increment */
step?: number
/** Whether the slider is disabled */
disabled?: boolean
/** Element color theme */
element?: 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | undefined
/** Additional CSS classes */
class?: string
}
const {
value = 0,
onValueChange,
min = 0,
max = 100,
step = 1,
disabled = false,
element,
class: className = ''
}: Props = $props()
function handleValueChange(values: number[]) {
const newValue = values[0] ?? min
onValueChange?.(newValue)
}
</script>
<SliderPrimitive.Root
type="single"
value={[value]}
onValueChange={handleValueChange}
{min}
{max}
{step}
{disabled}
class="slider {element ?? ''} {className}"
>
<SliderPrimitive.Range class="slider-range" />
<SliderPrimitive.Thumb index={0} class="slider-thumb" />
</SliderPrimitive.Root>
<style lang="scss">
@use '$src/themes/spacing' as *;
@use '$src/themes/colors' as *;
@use '$src/themes/effects' as *;
@use '$src/themes/layout' as *;
:global(.slider) {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: calc($unit * 2.5);
touch-action: none;
user-select: none;
// Track
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
height: $unit-half;
background: var(--slider-track-bg, var(--button-bg));
border-radius: $full-corner;
}
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
:global(.slider-range) {
position: absolute;
height: $unit-half;
background: var(--slider-range-bg, var(--accent-blue));
border-radius: $full-corner;
}
:global(.slider-thumb) {
display: block;
width: calc($unit * 2.5);
height: calc($unit * 2.5);
background: var(--slider-thumb-bg, white);
border: 2px solid var(--slider-thumb-border, var(--accent-blue));
border-radius: $full-corner;
cursor: grab;
@include smooth-transition($duration-quick, transform, box-shadow);
&:hover {
transform: scale(1.1);
}
&:active {
cursor: grabbing;
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--slider-focus-ring, rgba(59, 130, 246, 0.3));
}
}
// Element-specific colors
:global(.slider.wind) {
--slider-range-bg: var(--wind-button-bg);
--slider-thumb-border: var(--wind-button-bg);
--slider-focus-ring: var(--wind-nav-selected-bg);
}
:global(.slider.fire) {
--slider-range-bg: var(--fire-button-bg);
--slider-thumb-border: var(--fire-button-bg);
--slider-focus-ring: var(--fire-nav-selected-bg);
}
:global(.slider.water) {
--slider-range-bg: var(--water-button-bg);
--slider-thumb-border: var(--water-button-bg);
--slider-focus-ring: var(--water-nav-selected-bg);
}
:global(.slider.earth) {
--slider-range-bg: var(--earth-button-bg);
--slider-thumb-border: var(--earth-button-bg);
--slider-focus-ring: var(--earth-nav-selected-bg);
}
:global(.slider.dark) {
--slider-range-bg: var(--dark-button-bg);
--slider-thumb-border: var(--dark-button-bg);
--slider-focus-ring: var(--dark-nav-selected-bg);
}
:global(.slider.light) {
--slider-range-bg: var(--light-button-bg);
--slider-thumb-border: var(--light-button-bg);
--slider-focus-ring: var(--light-nav-selected-bg);
}
</style>