Rework character editor with new Figma design

This commit is contained in:
Clément Pons 2025-07-07 17:55:20 +02:00
parent c33bd95b81
commit da5c1202ed
11 changed files with 5237 additions and 102 deletions

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { CharacterConfig, MainStat, TrainingLevel } from '~/types/character';
import PreviewA from './prose/PreviewA.vue';
const { config } = defineProps<{
config: CharacterConfig
}>();
const selection = ref<{
stat: MainStat;
level: TrainingLevel;
option: number;
}>();
function focusTraining(stat: MainStat, level: TrainingLevel, option: number)
{
const s = selection.value;
if(s !== undefined && s.stat === stat && s.level === level && s.option === option)
{
selection.value = undefined;
}
else
{
selection.value = {
stat, level, option
};
}
}
</script>
<template>
<TrainingViewer :config="config" progress>
<template #default="{ stat, level, option }">
<div @click.capture="console.log" class="border border-light-40 dark:border-dark-40 hover:border-light-70 dark:hover:border-dark-70 cursor-pointer px-2 py-1 w-96" :class="{ '!border-accent-blue': selection !== undefined && selection?.stat == stat && selection?.level == level && selection?.option == option }">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
</div>
</template>
</TrainingViewer>
</template>

View File

@ -58,7 +58,7 @@ onUnmounted(() => {
</div>
</template>
<template v-for="(option, i) in options">
<slot :stat="name" :level="level" :index="i" :option="option"></slot>
<slot :stat="name" :level="level" :option="i"></slot>
</template>
</div>
</div>

View File

@ -1,7 +1,7 @@
<template>
<Label class="flex justify-center items-center my-2">
<span class="md:text-base text-sm">{{ label }}</span>
<SwitchRoot v-model:checked="model" :disabled="disabled"
<SwitchRoot v-model:checked="model" :disabled="disabled" :default-checked="defaultValue"
class="group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative">
@ -21,6 +21,7 @@ const { label, disabled, onIcon, offIcon } = defineProps<{
disabled?: boolean
onIcon?: string
offIcon?: string
defaultValue?: boolean
}>();
const model = defineModel<boolean>();
</script>

View File

@ -0,0 +1,28 @@
<template>
<template v-if="model">
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center">
<TextInput label="Nom" v-model="model.name" class="flex-none"/>
<Switch label="Privé ?" :default-value="model.visibility === 'private'" @update:model-value="(e) => model!.visibility = e ? 'private' : 'public'" />
<Button @click="emit('next')">Suivant</Button>
</div>
<div class="flex flex-1 gap-4 p-2 overflow-x-auto justify-center">
<div v-for="(people, i) of config.peoples" @click="model.people = i" class="flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35
cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]" :class="{ '!border-accent-blue outline-2 outline outline-accent-blue': model.people === i }">
<span class="text-xl font-bold text-center">{{ people.name }}</span>
<Avatar :src="people.name" :text="`Image placeholder`" class="h-[320px]" />
<span class="text-wrap word-break">{{ people.description }}</span>
</div>
</div>
</template>
</template>
<script setup lang="ts">
import type { Character, CharacterConfig } from '~/types/character';
const { config } = defineProps<{
config: CharacterConfig,
}>();
const model = defineModel<Character>();
const emit = defineEmits(['next']);
</script>

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"predev": "bun i",
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev"
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 bunx --bun nuxi dev --no-f"
},
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",

View File

@ -58,6 +58,8 @@ const spellFilter = ref<{
tags: [],
});
const step = ref(0);
const raceOptions = computed(() => data.value.people !== undefined ? characterConfig.peoples[data.value.people!].options : undefined);
const selectedRaceOptions = computed(() => raceOptions.value ? data.value.leveling!.map(e => raceOptions.value![e[0]][e[1]]) : undefined);
const trainingPoints = computed(() => raceOptions.value ? data.value.leveling?.reduce((p, v) => p + (raceOptions.value![v[0]][v[1]].training ?? 0), 0) : 0);
@ -220,67 +222,25 @@ useShortcuts({
<Head>
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
</Head>
<div class="flex flex-1 max-w-full flex-col gap-8 align-center">
<div class="flex flex-row gap-4 align-center justify-between">
<div></div>
<div class="flex flex-row gap-4 align-center justify-center">
<Tooltip side="left" message="Developpement en cours"><Avatar src="" icon="radix-icons:person" size="large" /></Tooltip>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-2 md:p-0">Nom du personnage</span>
<input class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
type="text" v-model="data.name">
</Label>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-2 md:p-0">Niveau</span>
<NumberFieldRoot :min="1" :max="20" v-model="data.level" @update:model-value="updateLevel" 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-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
</NumberFieldRoot>
</Label>
<Label class="flex items-start justify-between flex-col gap-2">
<span class="pb-1 mx-6 md:p-0">Visibilité</span>
<Select class="!my-0" v-model="data.visibility">
<SelectItem label="Privé" value="private" />
<SelectItem label="Public" value="public" />
</Select>
</Label>
<div class="flex flex-1 max-w-full flex-col align-center">
<StepperRoot class="flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden" v-model="step">
<div class="flex w-full flex-row gap-4 self-center items-center justify-center relative px-4 bg-light-0 dark:bg-dark-0 z-20">
<StepperItem :step="0"><StepperTrigger class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</StepperTrigger></StepperItem>
<StepperItem :disabled="data.people === undefined" :step="1" class="group flex"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent" /><StepperTrigger 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">Niveaux</StepperTrigger></StepperItem>
<StepperItem :disabled="data.people === undefined" :step="2" class="group flex"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent" /><StepperTrigger 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</StepperTrigger></StepperItem>
<StepperItem :disabled="data.people === undefined" :step="3" class="group flex"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent" /><StepperTrigger 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">Compétences</StepperTrigger></StepperItem>
<StepperItem :disabled="data.people === undefined" :step="4" class="group flex"><Icon icon="radix-icons:chevron-right" class="w-6 h-6 group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent" /><StepperTrigger 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">Aspect</StepperTrigger></StepperItem>
</div>
<div class="self-center">
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 0">
<PeopleEditor v-model="data" :config="characterConfig" @next="step = 1" />
</div>
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 1">
</div>
<TabsRoot class="flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden" 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>
<TabsTrigger :disabled="data.people === undefined" value="abilities" 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">Compétences</TabsTrigger>
<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 outline-none max-w-full w-full overflow-y-auto" 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 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>
</div>
</div>
</template>
</div>
</TabsContent>
<TabsContent value="training" class="flex-1 outline-none max-w-full w-full h-full max-h-full overflow-y-auto" forceMount>
<div class="flex-1 outline-none max-w-full w-full h-full max-h-full overflow-y-auto" v-show="step === 2">
<TrainingViewer :config="characterConfig" progress>
<template #default="{ stat, level, option, index }">
<template #default="{ stat, level, option }">
<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')" />
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
</div>
</template>
<template #addin="{ stat }">
@ -289,8 +249,8 @@ useShortcuts({
</div>
</template>
</TrainingViewer>
</TabsContent>
<TabsContent value="abilities" class="flex-1 outline-none max-w-full w-fulloverflow-y-auto" forceMount>
</div>
<div class="flex-1 outline-none max-w-full w-fulloverflow-y-auto" v-show="step === 3">
<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>
@ -309,8 +269,8 @@ useShortcuts({
</div>
</div>
</div>
</TabsContent>
<TabsContent value="spells" class="flex-1 outline-none max-w-full w-full overflow-y-auto" forceMount>
</div>
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 4">
<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>
@ -340,10 +300,7 @@ useShortcuts({
</div>
</div>
</div>
</TabsContent>
<TabsContent value="notes" class="flex-1 outline-none max-w-full w-full overflow-y-auto" forceMount>
<Editor class="min-h-[400px] border border-light-30 dark:border-dark-30 my-4" :v-model="data.notes" />
</TabsContent>
</TabsRoot>
</div>
</StepperRoot>
</div>
</template>

View File

@ -1,38 +1,15 @@
<script setup lang="ts">
import characterConfig from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
import type { CharacterConfig, MainStat, TrainingLevel } from '~/types/character';
import type { CharacterConfig } from '~/types/character';
//@ts-ignore
const config = ref<CharacterConfig>(characterConfig);
const selection = ref<{
stat: MainStat;
level: TrainingLevel;
index: number;
}>();
function copy()
{
navigator.clipboard.writeText(JSON.stringify(config.value));
}
function focusTraining(stat: MainStat, level: TrainingLevel, index: number)
{
const s = selection.value;
if(s !== undefined && s.stat === stat && s.level === level && s.index === index)
{
selection.value = undefined;
}
else
{
selection.value = {
stat, level, index
};
}
console.log(selection.value);
}
</script>
<template>
@ -51,13 +28,7 @@ function focusTraining(stat: MainStat, level: TrainingLevel, index: number)
<TabsContent value="peoples" class="flex-1 outline-none max-w-full w-full">
</TabsContent>
<TabsContent value="training" class="flex-1 outline-none max-w-full w-full">
<TrainingViewer :config="config" progress>
<template #default="{ stat, level, option, index }">
<div class="border border-light-40 dark:border-dark-40 hover:border-light-70 dark:hover:border-dark-70 cursor-pointer px-2 py-1 w-96" :class="{ '!border-accent-blue': selection !== undefined && selection?.stat == stat && selection?.level == level && selection?.index == index }" @click="focusTraining(stat, level, index)">
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" />
</div>
</template>
</TrainingViewer>
<TrainingEditor :config="config" />
</TabsContent>
<TabsContent value="abilities" class="flex-1 outline-none max-w-full w-full">

File diff suppressed because one or more lines are too long