Rework Training Viewer rendering as vertical

This commit is contained in:
Clément Pons 2025-06-18 17:41:19 +02:00
parent 9a6f91a341
commit e78a60f771
7 changed files with 44 additions and 106 deletions

View File

@ -4,7 +4,7 @@
<NuxtLoadingIndicator />
<TooltipProvider>
<NuxtLayout>
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full relative *:overflow-x-hidden *:w-full *:h-full">
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full relative">
<NuxtPage />
</div>
</NuxtLayout>

View File

@ -2,11 +2,12 @@
import { clamp } from '#shared/general.util';
import { MAIN_STATS, mainStatTexts, type CharacterConfig } from '~/types/character';
const { config } = defineProps<{
const { config, progress = false } = defineProps<{
config: CharacterConfig,
progress?: boolean,
}>();
const dragger = useTemplateRef<HTMLElement | null>('dragger'), items = useTemplateRef<HTMLElement[] | null>('items');
const dragger = useTemplateRef<HTMLElement | null>('dragger');
const position = ref(0), id = ref<number>(0);
const dragging = ref(false), offset = ref(0);
@ -18,8 +19,8 @@ const dragend = () => {
};
const dragmove = (e: MouseEvent) => {
const box = dragger.value!.getBoundingClientRect();
offset.value = clamp(offset.value - e.movementX, 0, (320+32+2) * 16);
if(dragger.value) dragger.value.scrollLeft = offset.value;
offset.value = clamp(offset.value - e.movementY, 0, 100);
if(dragger.value) dragger.value.scrollTop = offset.value;
};
const dragstart = () => {
window.addEventListener('mousemove', dragmove);
@ -27,88 +28,36 @@ const dragstart = () => {
dragging.value = true;
};
const wheel = (e: WheelEvent) => {
if(dragging.value) return;
const box = dragger.value!.getBoundingClientRect();
offset.value = clamp(offset.value + e.deltaY, 0, (320+32+2) * 16);
if(dragger.value) dragger.value.scrollLeft = offset.value;
}
onMounted(() => {
dragger.value?.addEventListener('mousedown', dragstart);
dragger.value?.addEventListener('wheel', wheel);
transition(1, 0);
});
onUnmounted(() => {
dragger.value?.removeEventListener('mousedown', dragstart);
dragger.value?.removeEventListener('wheel', wheel);
});
function transition(from: number, to: number)
{
if(!items.value || from === to)
return;
position.value = to;
items.value![to].style.visibility = 'visible';
items.value![from].style.opacity = '0';
items.value![to].style.opacity = '1';
for(let i = 0; i < MAIN_STATS.length; i++)
{
if(i < to)
items.value![i].style.top = `-25%`;
else
items.value![i].style.top = `25%`;
}
items.value![to].style.top = `0%`;
clearTimeout(id.value);
//@ts-ignore
id.value = setTimeout(() => {
items.value![from].style.visibility = 'hidden';
}, 200);
}
</script>
<!-- <template>
<div class="flex flex-1 flex-row justify-start w-full max-h-full h-full overflow-hidden gap-8 relative">
<div class="flex flex-col gap-3 relative items-center">
<span v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="() => transition(position, i)"></span>
<span :style="{ 'top': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position]]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[top]
after:content-[attr(data-text)] after:absolute after:-top-2 after:left-4 after:p-px after:bg-light-0 dark:after:bg-dark-0"></span>
</div>
<div class="flex flex-1 flex-col justify-center relative cursor-grab" ref="dragger" :class="{ 'cursor-grabbing': dragging }">
<div v-for="(stat) of MAIN_STATS" :value="stat" class="flex-1 transition-[opacity,transform] items-center hidden absolute" ref="items" :style="{ 'left': `${-offset}%` }">
<div class="flex flex-row overflow-x-auto items-center w-full gap-4">
<div class="w-96 flex flex-col gap-4 justify-between" v-for="(level, i) of config.training[stat]">
<template v-for="(option) of level">
<slot :stat="stat" :level="i" :option="option"></slot>
</template>
</div>
</div>
</div>
</div>
</div>
</template> -->
<template>
<div class="w-full h-full flex gap-8 max-w-full relative">
<div class="flex flex-col gap-3 relative items-center">
<div class="flex flex-col relative max-w-full">
<div class="sticky top-8 w-full bg-light-0 dark:bg-dark-0 z-20 flex pt-2 pb-6">
<div class="flex flex-row gap-3 items-center relative w-48">
<span v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="() => transition(position, i)"></span>
<span :style="{ 'top': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position]]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[top]
after:content-[attr(data-text)] after:absolute after:-top-2 after:left-4 after:p-px after:bg-light-0 dark:after:bg-dark-0"></span>
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="position = i"></span>
<span :style="{ 'left': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position]]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[left]
after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center"></span>
</div>
<div class="absolute top-0 left-24 z-10">
<div>
<slot name="addin" :stat="MAIN_STATS[position]"></slot>
</div>
<div ref="dragger" class="flex flex-col gap-4 pb-4 cursor-grab active:cursor-grabbing select-none overflow-hidden h-full w-full relative">
<div v-for="(stat, name) in config.training" class="flex flex-1 gap-4 items-center absolute h-full z-0" ref="items" :style="{ 'opacity': '0', 'visibility': 'hidden', 'transition': 'opacity 200ms ease-in-out, top 200ms ease-in-out' }">
<div v-for="(options, level) in stat" class="flex-shrink-0 w-80">
<div class="space-y-2" @mousedown.stop>
</div>
<div ref="dragger" class="flex flex-1 px-8 overflow-hidden max-w-full">
<div class="relative cursor-grab active:cursor-grabbing select-none transition-[left] flex flex-1 flex-row max-w-full" >
<div class="flex flex-shrink-0 flex-col gap-4 relative w-full" v-for="(stat, name) in config.training" ><!-- v-for="(stat, name) in config.training" -->
<div class="flex flex-row gap-2 justify-center relative" v-for="(options, level) in stat">
<template v-if="progress">
<div class="absolute -left-8 -right-8 -top-2 h-px border-t border-light-30 dark:border-dark-30 border-dashed">
<span class="absolute right-0 p-1 text-end">{{ level }}</span>
</div>
</template>
<template v-for="(option, i) in options">
<slot :stat="name" :level="level" :index="i" :option="option"></slot>
</template>

Binary file not shown.

Binary file not shown.

View File

@ -39,7 +39,7 @@
</template>
</div>
</div>
<div class="flex flex-1 flex-row relative max-w-[100vw] h-screen overflow-hidden">
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
<CollapsibleContent asChild forceMount>
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden">

View File

@ -251,8 +251,8 @@ useShortcuts({
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
</div>
</div>
<TabsRoot class="flex flex-1 flex-col justify-start items-center gap-4 px-8 w-full overflow-hidden" default-value="people">
<TabsList class="flex flex-row gap-4 self-center relative px-4">
<TabsRoot class="flex flex-1 flex-col justify-start items-center px-8 w-full overflow-y-auto" default-value="people">
<TabsList class="flex w-full flex-row gap-4 self-center items-center justify-center relative px-4 sticky top-0 bg-light-0 dark:bg-dark-0 z-20">
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
<TabsTrigger value="people" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</TabsTrigger>
<TabsTrigger :disabled="data.people === undefined" value="training" class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent">Entrainement</TabsTrigger>
@ -260,14 +260,14 @@ useShortcuts({
<TabsTrigger :disabled="data.people === undefined" value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent">Sorts</TabsTrigger>
<TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
</TabsList>
<TabsContent value="people" class="flex-1 overflow-auto w-full h-full outline-none" forceMount>
<TabsContent value="people" class="flex-1 outline-none" forceMount>
<div class="m-2 overflow-auto">
<Combobox label="Peuple de votre personnage" v-model="data.people" :options="config.peoples.map((people, index) => [people.name, index])" @update:model-value="(index) => { data.people = index as number | undefined; data.leveling = [[1, 0]]}" />
<template v-if="data.people !== undefined">
<div class="w-full border-b border-light-30 dark:border-dark-30 pb-4">
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.people].description }}</span>
</div>
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative">
<div class="flex flex-col gap-4 pe-4 relative">
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.level - (data.leveling?.length ?? 0) }}</span>
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.people].options" :class="{ 'opacity-30': index > data.level }">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(parseInt(index as unknown as string, 10) as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.leveling?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
@ -276,32 +276,21 @@ useShortcuts({
</template>
</div>
</TabsContent>
<TabsContent value="training" class="flex-1 overflow-hidden w-full h-full outline-none" forceMount>
<TrainingViewer :config="characterConfig">
<TabsContent value="training" class="flex-1 outline-none max-w-full" forceMount>
<TrainingViewer :config="characterConfig" progress>
<template #default="{ stat, level, option, index }">
<div @click="switchTrainingOption(stat, parseInt(level as unknown as string, 10) as TrainingLevel, index)" :class="{ 'opacity-30': level > maxTraining[stat] + 1, 'hover:border-light-60 dark:hover:border-dark-60': level <= maxTraining[stat] + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': level == 0 || (data.training[stat]?.some(e => e[0] == level && e[1] === index) ?? false) }" class="border border-light-35 dark:border-dark-35 px-3 py-1 select-none cursor-pointer hover:border-light-50 dark:hover:border-dark-50">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 max-w-[26rem] hover:border-light-50 dark:hover:border-dark-50" @click="switchTrainingOption(stat, parseInt(level as unknown as string, 10) as TrainingLevel, index)" :class="{ 'opacity-30': level > maxTraining[stat] + 1, 'hover:border-light-60 dark:hover:border-dark-60': level <= maxTraining[stat] + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': level == 0 || (data.training[stat]?.some(e => e[0] == level && e[1] === index) ?? false) }">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" />
</div>
</template>
<template #addin="{ stat }">
<div class="bg-light-0 dark:bg-dark-0 z-10">
<span class="text-xl" :class="{ 'text-light-red dark:text-dark-red': (trainingPoints ?? 0) < trainingSpent }">Points d'entrainement restants: {{ (trainingPoints ?? 0) - trainingSpent }}</span>
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 font-bold flex flex-row justify-between">
<span class="flex gap-2" v-if="maxTraining[stat] >= 0">Niveau {{ maxTraining[stat] }}</span>
<span class="flex gap-6">
Modifieur: +{{ modifiers[stat] }}
<NumberFieldRoot :default-value="data.modifiers[stat] ?? 0" v-model="data.modifiers[stat]" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
<NumberFieldInput class="tabular-nums w-8 text-base font-normal bg-transparent px-2 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
</span>
</div>
<div class="py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 justify-center items-center" :class="{ 'text-light-red dark:text-dark-red': (modifierPoints ?? 0) < modifierSpent }">Modifieur bonus: {{ modifierPoints - modifierSpent }}</div>
</div>
</template>
</TrainingViewer>
</TabsContent>
<TabsContent value="abilities" class="flex-1 overflow-auto w-full h-full outline-none" forceMount>
<TabsContent value="abilities" class="flex-1 outline-none" forceMount>
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex justify-between">
<span class="text-xl -mx-2" :class="{ 'text-light-red dark:text-dark-red': (abilityPoints ?? 0) < abilitySpent }">Points d'entrainement restants: {{ (abilityPoints ?? 0) - abilitySpent }}</span>
@ -321,7 +310,7 @@ useShortcuts({
</div>
</div>
</TabsContent>
<TabsContent value="spells" class="flex-1 overflow-auto w-full h-full outline-none" forceMount>
<TabsContent value="spells" class="flex-1 outline-none" forceMount>
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 items-center">
<span class="text-xl pe-4" :class="{ 'text-light-red dark:text-dark-red': spellsPoints < (data.spells?.length ?? 0) }">Sorts: {{ data.spells?.length ?? 0 }}/{{ spellsPoints }}</span>
@ -352,7 +341,7 @@ useShortcuts({
</div>
</div>
</TabsContent>
<TabsContent value="notes" class="flex-1 overflow-auto w-full h-full outline-none" forceMount>
<TabsContent value="notes" class="flex-1 outline-none" forceMount>
<Editor class="min-h-[400px] border border-light-30 dark:border-dark-30" :v-model="data.notes" />
</TabsContent>
</TabsRoot>

View File

@ -18,7 +18,7 @@ function copy()
<Head>
<Title>d[any] - Edition de données</Title>
</Head>
<TabsRoot class="flex flex-1 flex-col justify-start items-center gap-4 px-8 w-full overflow-hidden" default-value="training">
<TabsRoot class="flex flex-1 flex-col justify-start items-center gap-4 px-8 max-w-full overflow-hidden" default-value="training">
<TabsList class="flex flex-row gap-4 self-center relative px-4">
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</TabsTrigger>
@ -27,21 +27,21 @@ function copy()
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
</TabsList>
<TabsContent value="peoples" class="flex-1 overflow-auto outline-none">
<TabsContent value="peoples" class="flex-1 outline-none">
</TabsContent>
<TabsContent value="training" class="flex-1 overflow-hidden w-full h-full outline-none">
<TrainingViewer :config="config">
<TabsContent value="training" class="flex-1 outline-none max-w-full">
<TrainingViewer :config="config" progress>
<template #default="{ stat, level, option }">
<div class="border border-light-35 dark:border-dark-35 px-3 py-1 select-none cursor-pointer hover:border-light-50 dark:hover:border-dark-50">
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-96">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" />
</div>
</template>
</TrainingViewer>
</TabsContent>
<TabsContent value="abilities" class="flex-1 overflow-auto outline-none">
<TabsContent value="abilities" class="flex-1 outline-none">
</TabsContent>
<TabsContent value="spells" class="flex-1 overflow-auto outline-none">
<TabsContent value="spells" class="flex-1 outline-none">
</TabsContent>
</TabsRoot>