refactor ElementPickerSegmented to inline styles with element-specific states

This commit is contained in:
Justin Edmund 2026-01-04 16:08:03 -08:00
parent 31528bdc1f
commit 55ac4df9f2
2 changed files with 282 additions and 100 deletions

View file

@ -4,7 +4,6 @@
import { ToggleGroup } from 'bits-ui'
import Tooltip from '../Tooltip.svelte'
import { ELEMENT_LABELS, getElementImage } from '$lib/utils/element'
import styles from './element-picker.module.scss'
// Element display order: Fire(2) → Water(3) → Earth(4) → Wind(1) → Light(6) → Dark(5)
const ELEMENT_DISPLAY_ORDER = [2, 3, 4, 1, 6, 5]
@ -16,6 +15,8 @@
includeAny?: boolean
contained?: boolean
disabled?: boolean
size?: 'small' | 'regular'
showClear?: boolean
class?: string
}
@ -26,9 +27,31 @@
includeAny = false,
contained = false,
disabled = false,
size = 'small',
showClear = false,
class: className = ''
}: Props = $props()
// Check if any elements 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)
}
}
// Build element list based on includeAny prop
const elements = $derived(includeAny ? [0, ...ELEMENT_DISPLAY_ORDER] : ELEMENT_DISPLAY_ORDER)
@ -64,54 +87,264 @@
}
const containerClasses = $derived(
[styles.container, contained && styles.contained, className].filter(Boolean).join(' ')
['container', contained && 'contained', size === 'regular' ? 'regular' : 'small', className]
.filter(Boolean)
.join(' ')
)
</script>
<div class={containerClasses}>
{#if multiple}
<ToggleGroup.Root
type="multiple"
value={stringValue as string[]}
onValueChange={handleMultipleChange}
class={styles.group}
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item value={String(element)} class={styles.item} {disabled}>
<img
src={getElementImage(element)}
alt={getLabel(element)}
class={styles.image}
/>
</ToggleGroup.Item>
{/snippet}
</Tooltip>
{/each}
</ToggleGroup.Root>
{:else}
<ToggleGroup.Root
type="single"
value={stringValue as string | undefined}
onValueChange={handleSingleChange}
class={styles.group}
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item value={String(element)} class={styles.item} {disabled}>
<img
src={getElementImage(element)}
alt={getLabel(element)}
class={styles.image}
/>
</ToggleGroup.Item>
{/snippet}
</Tooltip>
{/each}
</ToggleGroup.Root>
{/if}
</div>
{#if showClear}
<div class="wrapper">
<div class={containerClasses}>
{#if multiple}
<ToggleGroup.Root
type="multiple"
value={stringValue as string[]}
onValueChange={handleMultipleChange}
class="group"
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item
value={String(element)}
class="item"
{disabled}
>
<img
src={getElementImage(element)}
alt={getLabel(element)}
class="image"
/>
</ToggleGroup.Item>
{/snippet}
</Tooltip>
{/each}
</ToggleGroup.Root>
{:else}
<ToggleGroup.Root
type="single"
value={stringValue as string | undefined}
onValueChange={handleSingleChange}
class="group"
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item
value={String(element)}
class="item"
{disabled}
>
<img
src={getElementImage(element)}
alt={getLabel(element)}
class="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="group"
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item
value={String(element)}
class="item"
{disabled}
>
<img src={getElementImage(element)} alt={getLabel(element)} class="image" />
</ToggleGroup.Item>
{/snippet}
</Tooltip>
{/each}
</ToggleGroup.Root>
{:else}
<ToggleGroup.Root
type="single"
value={stringValue as string | undefined}
onValueChange={handleSingleChange}
class="group"
{disabled}
>
{#each elements as element}
<Tooltip content={getLabel(element)}>
{#snippet children()}
<ToggleGroup.Item
value={String(element)}
class="item"
{disabled}
>
<img src={getElementImage(element)} alt={getLabel(element)} class="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(.group) {
display: flex;
gap: $unit-half;
align-items: center;
}
:global(.item) {
all: unset;
cursor: pointer;
border-radius: $full-corner;
padding: $unit-half;
@include smooth-transition($duration-quick, background-color, opacity);
}
:global(.item:focus-visible) {
@include focus-ring($blue);
}
:global(.item:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
// Element-specific hover and selected states
:global(.item[data-value='0']:hover:not(:disabled)) {
background-color: var(--null-nav-hover-bg);
}
:global(.item[data-value='0'][data-state='on']:not(:hover)) {
background-color: var(--null-nav-selected-bg);
}
:global(.item[data-value='1']:hover:not(:disabled)) {
background-color: var(--wind-nav-hover-bg);
}
:global(.item[data-value='1'][data-state='on']:not(:hover)) {
background-color: var(--wind-nav-selected-bg);
}
:global(.item[data-value='2']:hover:not(:disabled)) {
background-color: var(--fire-nav-hover-bg);
}
:global(.item[data-value='2'][data-state='on']:not(:hover)) {
background-color: var(--fire-nav-selected-bg);
}
:global(.item[data-value='3']:hover:not(:disabled)) {
background-color: var(--water-nav-hover-bg);
}
:global(.item[data-value='3'][data-state='on']:not(:hover)) {
background-color: var(--water-nav-selected-bg);
}
:global(.item[data-value='4']:hover:not(:disabled)) {
background-color: var(--earth-nav-hover-bg);
}
:global(.item[data-value='4'][data-state='on']:not(:hover)) {
background-color: var(--earth-nav-selected-bg);
}
:global(.item[data-value='5']:hover:not(:disabled)) {
background-color: var(--dark-nav-hover-bg);
}
:global(.item[data-value='5'][data-state='on']:not(:hover)) {
background-color: var(--dark-nav-selected-bg);
}
:global(.item[data-value='6']:hover:not(:disabled)) {
background-color: var(--light-nav-hover-bg);
}
:global(.item[data-value='6'][data-state='on']:not(:hover)) {
background-color: var(--light-nav-selected-bg);
}
:global(.image) {
display: block;
}
// Size variants
.small {
:global(.item) {
padding: $unit-half;
}
:global(.image) {
width: $unit-3x;
height: $unit-3x;
}
}
.regular {
:global(.item) {
padding: $unit-half;
}
:global(.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>

View file

@ -1,51 +0,0 @@
@use '$src/themes/spacing' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/effects' as *;
@use '$src/themes/colors' as *;
.container {
display: inline-flex;
border-radius: $full-corner;
padding: $unit-half;
&.contained {
background-color: var(--segmented-control-background-bg);
}
}
.group {
display: flex;
gap: $unit-half;
align-items: center;
}
.item {
all: unset;
cursor: pointer;
border-radius: $full-corner;
padding: $unit-half;
@include smooth-transition($duration-quick, background-color, opacity);
&:hover:not(:disabled) {
background-color: var(--option-bg-hover);
}
&:focus-visible {
@include focus-ring($blue);
}
&[data-state='on'] {
background-color: var(--accent-subtle-bg);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.image {
width: $unit-3x;
height: $unit-3x;
display: block;
}