add RarityPicker component with segmented and dropdown modes
This commit is contained in:
parent
b2108dfaa9
commit
8f1e306d3c
2 changed files with 428 additions and 0 deletions
126
src/lib/components/ui/rarity-picker/RarityPicker.svelte
Normal file
126
src/lib/components/ui/rarity-picker/RarityPicker.svelte
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Select from '../Select.svelte'
|
||||||
|
import MultiSelect from '../MultiSelect.svelte'
|
||||||
|
import RarityPickerSegmented from './RarityPickerSegmented.svelte'
|
||||||
|
import { RARITY_LABELS, getRarityImage } from '$lib/utils/rarity'
|
||||||
|
|
||||||
|
// Rarity display order: R(1) → SR(2) → SSR(3)
|
||||||
|
const RARITY_DISPLAY_ORDER = [1, 2, 3]
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: number | number[]
|
||||||
|
onValueChange?: (value: number | number[]) => void
|
||||||
|
multiple?: boolean
|
||||||
|
mode?: 'auto' | 'segmented' | 'dropdown'
|
||||||
|
contained?: boolean
|
||||||
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
showClear?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable(),
|
||||||
|
onValueChange,
|
||||||
|
multiple = false,
|
||||||
|
mode = 'auto',
|
||||||
|
contained = false,
|
||||||
|
size = 'medium',
|
||||||
|
showClear = false,
|
||||||
|
disabled = false,
|
||||||
|
class: className = ''
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
// Map size to segmented control size (small stays small, medium/large become regular)
|
||||||
|
const segmentedSize = $derived(size === 'small' ? 'small' : 'regular')
|
||||||
|
|
||||||
|
// Responsive detection for auto mode
|
||||||
|
let isMobile = $state(false)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
const mq = window.matchMedia('(max-width: 640px)')
|
||||||
|
isMobile = mq.matches
|
||||||
|
|
||||||
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
|
isMobile = e.matches
|
||||||
|
}
|
||||||
|
|
||||||
|
mq.addEventListener('change', handler)
|
||||||
|
return () => mq.removeEventListener('change', handler)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Determine if we should use dropdown mode
|
||||||
|
const shouldUseDropdown = $derived(mode === 'dropdown' || (mode === 'auto' && isMobile))
|
||||||
|
|
||||||
|
// Build rarity options for Select/MultiSelect
|
||||||
|
const options = $derived.by(() => {
|
||||||
|
return RARITY_DISPLAY_ORDER.map((rarity) => ({
|
||||||
|
value: rarity,
|
||||||
|
label: RARITY_LABELS[rarity] ?? 'Unknown',
|
||||||
|
image: getRarityImage(rarity)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle value changes for single-select dropdown
|
||||||
|
function handleSingleChange(newValue: number | undefined) {
|
||||||
|
if (newValue !== undefined) {
|
||||||
|
value = newValue
|
||||||
|
onValueChange?.(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle value changes for multi-select dropdown
|
||||||
|
function handleMultipleChange(newValue: number[]) {
|
||||||
|
value = newValue
|
||||||
|
onValueChange?.(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle value changes for segmented control
|
||||||
|
function handleSegmentedChange(newValue: number | number[]) {
|
||||||
|
value = newValue
|
||||||
|
onValueChange?.(newValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if shouldUseDropdown}
|
||||||
|
{#if multiple}
|
||||||
|
<MultiSelect
|
||||||
|
{options}
|
||||||
|
value={Array.isArray(value) ? value : value !== undefined ? [value] : []}
|
||||||
|
onValueChange={handleMultipleChange}
|
||||||
|
size="medium"
|
||||||
|
{contained}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder="Select rarities..."
|
||||||
|
fullWidth={true}
|
||||||
|
class={className}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Select
|
||||||
|
{options}
|
||||||
|
value={typeof value === 'number' ? value : undefined}
|
||||||
|
onValueChange={handleSingleChange}
|
||||||
|
size="medium"
|
||||||
|
{contained}
|
||||||
|
disabled={disabled}
|
||||||
|
placeholder="Select rarity"
|
||||||
|
fullWidth={true}
|
||||||
|
class={className}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<RarityPickerSegmented
|
||||||
|
{value}
|
||||||
|
onValueChange={handleSegmentedChange}
|
||||||
|
{multiple}
|
||||||
|
{contained}
|
||||||
|
{showClear}
|
||||||
|
size={segmentedSize}
|
||||||
|
disabled={disabled}
|
||||||
|
class={className}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
302
src/lib/components/ui/rarity-picker/RarityPickerSegmented.svelte
Normal file
302
src/lib/components/ui/rarity-picker/RarityPickerSegmented.svelte
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ToggleGroup } from 'bits-ui'
|
||||||
|
import Tooltip from '../Tooltip.svelte'
|
||||||
|
import { RARITY_LABELS, getRarityImage } from '$lib/utils/rarity'
|
||||||
|
|
||||||
|
// Rarity display order: R(1) → SR(2) → SSR(3)
|
||||||
|
const RARITY_DISPLAY_ORDER = [1, 2, 3]
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: number | number[]
|
||||||
|
onValueChange?: (value: number | number[]) => void
|
||||||
|
multiple?: boolean
|
||||||
|
contained?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
size?: 'small' | 'regular'
|
||||||
|
showClear?: boolean
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable(),
|
||||||
|
onValueChange,
|
||||||
|
multiple = false,
|
||||||
|
contained = false,
|
||||||
|
disabled = false,
|
||||||
|
size = 'small',
|
||||||
|
showClear = false,
|
||||||
|
class: className = ''
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
// Check if any rarities are selected
|
||||||
|
const hasSelection = $derived.by(() => {
|
||||||
|
if (multiple) {
|
||||||
|
const arr = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
||||||
|
return arr.length > 0
|
||||||
|
}
|
||||||
|
return value !== undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
if (multiple) {
|
||||||
|
value = []
|
||||||
|
onValueChange?.([])
|
||||||
|
} else {
|
||||||
|
value = undefined
|
||||||
|
onValueChange?.(undefined as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get label for rarity
|
||||||
|
function getLabel(rarity: number): string {
|
||||||
|
return RARITY_LABELS[rarity] ?? 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert value to string format for ToggleGroup
|
||||||
|
const stringValue = $derived.by(() => {
|
||||||
|
if (multiple) {
|
||||||
|
const arr = Array.isArray(value) ? value : value !== undefined ? [value] : []
|
||||||
|
return arr.map(String)
|
||||||
|
} else {
|
||||||
|
return value !== undefined ? String(value) : undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle value changes from ToggleGroup
|
||||||
|
function handleSingleChange(newValue: string | undefined) {
|
||||||
|
if (newValue !== undefined) {
|
||||||
|
const numValue = Number(newValue)
|
||||||
|
value = numValue
|
||||||
|
onValueChange?.(numValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMultipleChange(newValue: string[]) {
|
||||||
|
const numValues = newValue.map(Number)
|
||||||
|
value = numValues
|
||||||
|
onValueChange?.(numValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerClasses = $derived(
|
||||||
|
['container', contained && 'contained', size === 'regular' ? 'regular' : 'small', className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showClear}
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class={containerClasses}>
|
||||||
|
{#if multiple}
|
||||||
|
<ToggleGroup.Root
|
||||||
|
type="multiple"
|
||||||
|
value={stringValue as string[]}
|
||||||
|
onValueChange={handleMultipleChange}
|
||||||
|
class="rarity-group"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{#each RARITY_DISPLAY_ORDER as rarity}
|
||||||
|
<Tooltip content={getLabel(rarity)}>
|
||||||
|
{#snippet children()}
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value={String(rarity)}
|
||||||
|
class="rarity-item"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getRarityImage(rarity)}
|
||||||
|
alt={getLabel(rarity)}
|
||||||
|
class="rarity-image"
|
||||||
|
/>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
{/snippet}
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
{:else}
|
||||||
|
<ToggleGroup.Root
|
||||||
|
type="single"
|
||||||
|
value={stringValue as string | undefined}
|
||||||
|
onValueChange={handleSingleChange}
|
||||||
|
class="rarity-group"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{#each RARITY_DISPLAY_ORDER as rarity}
|
||||||
|
<Tooltip content={getLabel(rarity)}>
|
||||||
|
{#snippet children()}
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value={String(rarity)}
|
||||||
|
class="rarity-item"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getRarityImage(rarity)}
|
||||||
|
alt={getLabel(rarity)}
|
||||||
|
class="rarity-image"
|
||||||
|
/>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
{/snippet}
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if hasSelection}
|
||||||
|
<button type="button" class="clearButton" onclick={handleClear}> Clear </button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class={containerClasses}>
|
||||||
|
{#if multiple}
|
||||||
|
<ToggleGroup.Root
|
||||||
|
type="multiple"
|
||||||
|
value={stringValue as string[]}
|
||||||
|
onValueChange={handleMultipleChange}
|
||||||
|
class="rarity-group"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{#each RARITY_DISPLAY_ORDER as rarity}
|
||||||
|
<Tooltip content={getLabel(rarity)}>
|
||||||
|
{#snippet children()}
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value={String(rarity)}
|
||||||
|
class="rarity-item"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<img src={getRarityImage(rarity)} alt={getLabel(rarity)} class="rarity-image" />
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
{/snippet}
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
{:else}
|
||||||
|
<ToggleGroup.Root
|
||||||
|
type="single"
|
||||||
|
value={stringValue as string | undefined}
|
||||||
|
onValueChange={handleSingleChange}
|
||||||
|
class="rarity-group"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
{#each RARITY_DISPLAY_ORDER as rarity}
|
||||||
|
<Tooltip content={getLabel(rarity)}>
|
||||||
|
{#snippet children()}
|
||||||
|
<ToggleGroup.Item
|
||||||
|
value={String(rarity)}
|
||||||
|
class="rarity-item"
|
||||||
|
{disabled}
|
||||||
|
>
|
||||||
|
<img src={getRarityImage(rarity)} alt={getLabel(rarity)} class="rarity-image" />
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
{/snippet}
|
||||||
|
</Tooltip>
|
||||||
|
{/each}
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/effects' as *;
|
||||||
|
@use '$src/themes/colors' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: $full-corner;
|
||||||
|
padding: $unit-half;
|
||||||
|
|
||||||
|
&.contained {
|
||||||
|
background-color: var(--segmented-control-background-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-group) {
|
||||||
|
display: flex;
|
||||||
|
gap: $unit-quarter;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-item) {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: $full-corner;
|
||||||
|
padding: $unit-half;
|
||||||
|
@include smooth-transition($duration-quick, background-color, opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-item:focus-visible) {
|
||||||
|
@include focus-ring($blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-item:disabled) {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple hover and selected states with gray background
|
||||||
|
:global(.rarity-item:hover:not(:disabled)) {
|
||||||
|
background-color: var(--picker-item-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-item[data-state='on']) {
|
||||||
|
background-color: var(--picker-item-bg-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-image) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size variants
|
||||||
|
.small {
|
||||||
|
:global(.rarity-item) {
|
||||||
|
padding: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-image) {
|
||||||
|
width: calc($unit * 3.25);
|
||||||
|
height: calc($unit * 3.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
:global(.rarity-item) {
|
||||||
|
padding: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.rarity-image) {
|
||||||
|
width: $unit-4x;
|
||||||
|
height: $unit-4x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearButton {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: $font-small;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: $unit-half $unit;
|
||||||
|
border-radius: $input-corner;
|
||||||
|
@include smooth-transition($duration-quick, background-color, color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--option-bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@include focus-ring($blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue