add Slider component with elemental styling for level selection
This commit is contained in:
parent
a1bc125521
commit
23b1d091f5
3 changed files with 247 additions and 35 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
155
src/lib/components/ui/Slider.svelte
Normal file
155
src/lib/components/ui/Slider.svelte
Normal 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>
|
||||
Loading…
Reference in a new issue