implement visual party segmented control
This commit is contained in:
parent
ad2e04623f
commit
fed7b5ae50
6 changed files with 249 additions and 2 deletions
|
|
@ -8,5 +8,9 @@
|
||||||
"nav_settings": "Settings",
|
"nav_settings": "Settings",
|
||||||
"nav_logout": "Log out",
|
"nav_logout": "Log out",
|
||||||
"nav_account_aria": "Your account",
|
"nav_account_aria": "Your account",
|
||||||
"nav_menu_button": "Menu"
|
"nav_menu_button": "Menu",
|
||||||
|
|
||||||
|
"party_segmented_control_characters": "Characters",
|
||||||
|
"party_segmented_control_weapons": "Weapons",
|
||||||
|
"party_segmented_control_summons": "Summons"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,9 @@
|
||||||
"nav_settings": "設定",
|
"nav_settings": "設定",
|
||||||
"nav_logout": "ログアウト",
|
"nav_logout": "ログアウト",
|
||||||
"nav_account_aria": "アカウント",
|
"nav_account_aria": "アカウント",
|
||||||
"nav_menu_button": "メニュー"
|
"nav_menu_button": "メニュー",
|
||||||
|
|
||||||
|
"party_segmented_control_characters": "キャラ",
|
||||||
|
"party_segmented_control_weapons": "武器",
|
||||||
|
"party_segmented_control_summons": "召喚石"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
89
src/lib/components/party/PartySegmentedControl.svelte
Normal file
89
src/lib/components/party/PartySegmentedControl.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<!-- PartySegmentedControl Component -->
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte'
|
||||||
|
import type { Party, GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||||
|
import { GridType } from '$lib/types/enums'
|
||||||
|
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||||
|
import RepSegment from '$lib/components/ui/segmented-control/RepSegment.svelte'
|
||||||
|
import CharacterRep from '$lib/components/reps/CharacterRep.svelte'
|
||||||
|
import WeaponRep from '$lib/components/reps/WeaponRep.svelte'
|
||||||
|
import SummonRep from '$lib/components/reps/SummonRep.svelte'
|
||||||
|
import * as m from '$lib/paraglide/messages'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedTab?: GridType
|
||||||
|
onTabChange?: (tab: GridType) => void
|
||||||
|
party: Party
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
selectedTab = GridType.Character,
|
||||||
|
onTabChange,
|
||||||
|
party,
|
||||||
|
class: className
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
|
|
||||||
|
// Handle value changes
|
||||||
|
let value = $state(selectedTab)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
value = selectedTab
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleValueChange(newValue: string) {
|
||||||
|
value = newValue as GridType
|
||||||
|
onTabChange?.(newValue as GridType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user gender from context if available
|
||||||
|
// This would typically come from auth/account state
|
||||||
|
const accountContext = getContext<any>('account')
|
||||||
|
const userGender = $derived(accountContext?.user?.gender ?? 0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class={className}>
|
||||||
|
<SegmentedControl
|
||||||
|
bind:value
|
||||||
|
onValueChange={handleValueChange}
|
||||||
|
gap={true}
|
||||||
|
grow={true}
|
||||||
|
>
|
||||||
|
<RepSegment
|
||||||
|
value={GridType.Character}
|
||||||
|
label={m.party_segmented_control_characters()}
|
||||||
|
selected={value === GridType.Character}
|
||||||
|
>
|
||||||
|
<CharacterRep
|
||||||
|
jobId={party.job?.id}
|
||||||
|
element={party.element}
|
||||||
|
gender={userGender}
|
||||||
|
characters={party.characters}
|
||||||
|
/>
|
||||||
|
</RepSegment>
|
||||||
|
|
||||||
|
<RepSegment
|
||||||
|
value={GridType.Weapon}
|
||||||
|
label={m.party_segmented_control_weapons()}
|
||||||
|
selected={value === GridType.Weapon}
|
||||||
|
>
|
||||||
|
<WeaponRep weapons={party.weapons} />
|
||||||
|
</RepSegment>
|
||||||
|
|
||||||
|
<RepSegment
|
||||||
|
value={GridType.Summon}
|
||||||
|
label={m.party_segmented_control_summons()}
|
||||||
|
selected={value === GridType.Summon}
|
||||||
|
>
|
||||||
|
<SummonRep summons={party.summons} />
|
||||||
|
</RepSegment>
|
||||||
|
</SegmentedControl>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
nav {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
42
src/lib/components/ui/segmented-control/RepSegment.svelte
Normal file
42
src/lib/components/ui/segmented-control/RepSegment.svelte
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!-- RepSegment Component - A segment with visual content and label -->
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
|
||||||
|
import styles from './rep-segment.module.scss'
|
||||||
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
|
interface Props extends Omit<HTMLButtonAttributes, 'value'> {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
class?: string
|
||||||
|
selected?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
class: className,
|
||||||
|
selected = false,
|
||||||
|
children: content,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
{value}
|
||||||
|
class={`${styles.repSegment} ${selected ? styles.selected : ''} ${className || ''}`}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
{#if checked}
|
||||||
|
<div class={styles.indicator}></div>
|
||||||
|
{/if}
|
||||||
|
<div class={styles.wrapper}>
|
||||||
|
<div class={styles.content}>
|
||||||
|
{@render content?.()}
|
||||||
|
</div>
|
||||||
|
<div class={styles.label}>{label}</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/colors' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
|
.repSegment {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: $item-corner;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $grey-80; // Solid gray for hover state
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected .wrapper,
|
||||||
|
&:hover .wrapper {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: $grey-80;
|
||||||
|
border-radius: $item-corner;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
[data-state='checked'] & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
[data-state='checked'] & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
[data-state='checked'] & {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/lib/types/enums.ts
Normal file
27
src/lib/types/enums.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export enum GridType {
|
||||||
|
Character = 'character',
|
||||||
|
Weapon = 'weapon',
|
||||||
|
Summon = 'summon'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TeamElement {
|
||||||
|
Any = 0,
|
||||||
|
Wind = 1,
|
||||||
|
Fire = 2,
|
||||||
|
Water = 3,
|
||||||
|
Earth = 4,
|
||||||
|
Dark = 5,
|
||||||
|
Light = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElementClass(element: number): string | null {
|
||||||
|
switch (element) {
|
||||||
|
case TeamElement.Wind: return 'wind'
|
||||||
|
case TeamElement.Fire: return 'fire'
|
||||||
|
case TeamElement.Water: return 'water'
|
||||||
|
case TeamElement.Earth: return 'earth'
|
||||||
|
case TeamElement.Dark: return 'dark'
|
||||||
|
case TeamElement.Light: return 'light'
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue