349 lines
23 KiB
Vue
349 lines
23 KiB
Vue
<script lang="ts">
|
|
import config from '#shared/character-config.json';
|
|
|
|
function raceOptionToText(option: RaceOption): string
|
|
{
|
|
const text = [];
|
|
if(option.training) text.push(`+${option.training} point${option.training > 1 ? 's' : ''} de statistique${option.training > 1 ? 's' : ''}.`);
|
|
if(option.shaping) text.push(`+${option.shaping} transformation${option.shaping > 1 ? 's' : ''} par jour.`);
|
|
if(option.modifier) text.push(`+${option.modifier} au modifieur de votre choix.`);
|
|
if(option.abilities) text.push(`+${option.abilities} point${option.abilities > 1 ? 's' : ''} de compétence${option.abilities > 1 ? 's' : ''}.`);
|
|
if(option.health) text.push(`+${option.health} PV max.`);
|
|
if(option.mana) text.push(`+${option.mana} mana max.`);
|
|
if(option.spellslots) text.push(`+${option.spellslots} sort${option.spellslots > 1 ? 's' : ''} maitrisé${option.spellslots > 1 ? 's' : ''}.`);
|
|
return text.join('\n');
|
|
}
|
|
function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
|
|
{
|
|
const characterData = config as CharacterConfig;
|
|
return progression.map(e => characterData.training[stat][e[0]][e[1]]);
|
|
}
|
|
|
|
|
|
|
|
function abilitySpecialFeatures(type: "points" | "max", curiosity: DoubleIndex<TrainingLevel>[], value: number): number
|
|
{
|
|
if(type === 'points')
|
|
{
|
|
if(curiosity.find(e => e[0] == 6 && e[1] === 0))
|
|
return Math.max(6, value);
|
|
if(curiosity.find(e => e[0] == 6 && e[1] === 2))
|
|
return value + 1;
|
|
}
|
|
return value;
|
|
}
|
|
</script>
|
|
<script setup lang="ts">
|
|
import PreviewA from '~/components/prose/PreviewA.vue';
|
|
import { defaultCharacter, elementTexts, mainStatTexts, spellTypeTexts, type Ability, type Character, type CharacterConfig, type DoubleIndex, type Level, type MainStat, type RaceOption, type SpellConfig, type SpellElement, type SpellType, type TrainingLevel, type TrainingOption } from '~/types/character';
|
|
|
|
definePageMeta({
|
|
guestsGoesTo: '/user/login',
|
|
});
|
|
let id = useRouter().currentRoute.value.params.id;
|
|
const { add } = useToast();
|
|
const characterConfig = config as CharacterConfig;
|
|
const data = ref<Character>({ ...defaultCharacter });
|
|
const spellFilter = ref<{
|
|
ranks: Array<1 | 2 | 3>,
|
|
types: Array<SpellType>,
|
|
text: string,
|
|
elements: Array<SpellElement>,
|
|
tags: string[],
|
|
}>({
|
|
ranks: [],
|
|
types: [],
|
|
text: "",
|
|
elements: [],
|
|
tags: [],
|
|
});
|
|
|
|
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);
|
|
const training = computed(() => Object.entries(characterConfig.training).map(e => [e[0], getFeaturesOf(e[0] as MainStat, data.value.training[e[0] as MainStat])]) as [MainStat, TrainingOption[]][]);
|
|
const maxTraining = computed(() => Object.entries(data.value.training).reduce((p, v) => { p[v[0] as MainStat] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<MainStat, number>));
|
|
const trainingSpent = computed(() => Object.values(maxTraining.value).reduce((p, v) => p + v, 0));
|
|
const modifiers = computed(() => Object.entries(maxTraining.value).reduce((p, v) => { p[v[0] as MainStat] = Math.floor(v[1] / 3) + (data.value.modifiers ? (data.value.modifiers[v[0] as MainStat] ?? 0) : 0); return p; }, {} as Record<MainStat, number>))
|
|
const modifierPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.modifier ?? 0), 0) : 0) + training.value.reduce((p, v) => p + v[1].reduce((_p, _v) => _p + (_v?.modifier ?? 0), 0), 0));
|
|
const modifierSpent = computed(() => Object.values(data.value.modifiers ?? {}).reduce((p, v) => p + v, 0));
|
|
const abilityPoints = computed(() => (selectedRaceOptions.value ? selectedRaceOptions.value.reduce((p, v) => p + (v?.abilities ?? 0), 0) : 0) + training.value.flatMap(e => e[1].filter(_e => _e.ability !== undefined)).reduce((p, v) => p + v.ability!, 0));
|
|
const abilityMax = computed(() => Object.entries(characterConfig.abilities).reduce((p, v) => { p[v[0] as Ability] = abilitySpecialFeatures("max", data.value.training.curiosity, Math.floor(maxTraining.value[v[1].max[0]] / 3) + Math.floor(maxTraining.value[v[1].max[1]] / 3)); return p; }, {} as Record<Ability, number>));
|
|
const abilitySpent = computed(() => Object.values(data.value.abilities ?? {}).reduce((p, v) => p + v[0], 0));
|
|
const spellranks = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellrank !== undefined)).reduce((p, v) => { p[v.spellrank!]++; return p; }, { instinct: 0, precision: 0, knowledge: 0 } as Record<SpellType, 0 | 1 | 2 | 3>));
|
|
const spellsPoints = computed(() => training.value.flatMap(e => e[1].filter(_e => _e.spellslot !== undefined)).reduce((p, v) => p + (modifiers.value.hasOwnProperty(v.spellslot as MainStat) ? modifiers.value[v.spellslot as MainStat] : v.spellslot as number), 0));
|
|
|
|
if(id !== 'new')
|
|
{
|
|
const character = await useRequestFetch()(`/api/character/${id}`);
|
|
|
|
if(!character)
|
|
{
|
|
throw new Error('Donnée du personnage introuvables');
|
|
}
|
|
|
|
data.value = Object.assign(defaultCharacter, data.value, character);
|
|
}
|
|
|
|
function selectRaceOption(level: Level, choice: number)
|
|
{
|
|
const character = data.value;
|
|
if(level > character.level)
|
|
return;
|
|
|
|
if(character.leveling === undefined)
|
|
character.leveling = [[1, 0]];
|
|
|
|
if(level == 1)
|
|
return;
|
|
|
|
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
|
{
|
|
if(!character.leveling.some(e => e[0] == i))
|
|
return;
|
|
}
|
|
|
|
if(character.leveling.some(e => e[0] == level))
|
|
{
|
|
character.leveling.splice(character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
|
|
}
|
|
else
|
|
{
|
|
character.leveling.push([level, choice]);
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
function switchTrainingOption(stat: MainStat, level: TrainingLevel, choice: number)
|
|
{
|
|
const character = data.value;
|
|
|
|
if(level == 0)
|
|
return;
|
|
|
|
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
|
{
|
|
if(!character.training[stat].some(e => e[0] == i))
|
|
return;
|
|
}
|
|
|
|
if(character.training[stat].some(e => e[0] == level))
|
|
{
|
|
if(character.training[stat].some(e => e[0] == level && e[1] === choice))
|
|
{
|
|
for(let i = 15; i >= level; i --) //Invalidate higher levels
|
|
{
|
|
const index = character.training[stat].findIndex(e => e[0] == i);
|
|
if(index !== -1)
|
|
character.training[stat].splice(index, 1);
|
|
}
|
|
}
|
|
else
|
|
character.training[stat].splice(character.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
|
|
}
|
|
else if(trainingPoints.value && trainingPoints.value > 0)
|
|
{
|
|
character.training[stat].push([level, choice]);
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
function updateLevel()
|
|
{
|
|
const character = data.value;
|
|
|
|
if(character.leveling) //Invalidate higher levels
|
|
{
|
|
for(let level = 20; level > character.level; level--)
|
|
{
|
|
const index = character.leveling.findIndex(e => e[0] == level);
|
|
if(index !== -1)
|
|
character.leveling.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
function filterSpells(spells: SpellConfig[])
|
|
{
|
|
const filter = spellFilter.value
|
|
let list = [...spells];
|
|
list = list.filter(e => spellranks.value[e.type] >= e.rank);
|
|
if(filter.text.length > 0) list = list.filter(e => e.name.toLowerCase().includes(filter.text.toLowerCase()));
|
|
if(filter.types.length > 0) list = list.filter(e => filter.types.includes(e.type));
|
|
if(filter.ranks.length > 0) list = list.filter(e => filter.ranks.includes(e.rank));
|
|
if(filter.elements.length > 0) list = list.filter(e => filter.elements.some(f => e.elements.includes(f)));
|
|
if(filter.tags.length > 0) list = list.filter(e => !e.tags || filter.tags.some(f => e.tags!.includes(f)));
|
|
|
|
return list;
|
|
}
|
|
async function save(leave: boolean)
|
|
{
|
|
if(data.value.name === '' || data.value.people === undefined || data.value.people === -1)
|
|
{
|
|
add({ title: 'Données manquantes', content: "Merci de saisir un nom et une race avant de pouvoir enregistrer votre personnage", type: 'error', duration: 25000, timer: true });
|
|
return;
|
|
}
|
|
if(id === 'new')
|
|
{
|
|
id = await useRequestFetch()(`/api/character`, {
|
|
method: 'post',
|
|
body: data.value,
|
|
onResponseError: (e) => {
|
|
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
|
}
|
|
});
|
|
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
|
useRouter().replace({ name: 'character-id-edit', params: { id: id } })
|
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
|
}
|
|
else
|
|
{
|
|
await useRequestFetch()(`/api/character/${id}`, {
|
|
method: 'post',
|
|
body: data.value,
|
|
onResponseError: (e) => {
|
|
add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
|
}
|
|
});
|
|
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
|
}
|
|
}
|
|
|
|
useShortcuts({
|
|
"Meta_S": () =>save(false),
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Head>
|
|
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
|
|
</Head>
|
|
<div class="flex 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>
|
|
<div class="self-center">
|
|
<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 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>
|
|
<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" 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" forceMount>
|
|
<TrainingViewer :config="characterConfig" progress>
|
|
<template #default="{ stat, level, option, index }">
|
|
<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>
|
|
</template>
|
|
</TrainingViewer>
|
|
</TabsContent>
|
|
<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>
|
|
</div>
|
|
<div class="grid gap-4 grid-cols-6">
|
|
<div v-for="(ability, index) of characterConfig.abilities" class="flex flex-col items-center border border-light-30 dark:border-dark-30 p-2">
|
|
<div class="flex items-center justify-center gap-4">
|
|
<NumberFieldRoot :min="0" :default-value="data.abilities[index] ? data.abilities[index][0] : 0" @update:model-value="(value) => { data.abilities[index] = [value, data.abilities[index] ? data.abilities[index][1] : 0]; }" class="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 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
|
</NumberFieldRoot>
|
|
<span class="font-bold col-span-4">/{{ abilityMax[index] }}</span>
|
|
</div>
|
|
<span class="text-xl font-bold flex-2">{{ ability.name }}</span>
|
|
<span class="text-sm text-light-70 dark:text-dark-70 flex-1">({{ mainStatTexts[ability.max[0]] }} + {{ mainStatTexts[ability.max[1]] }})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
<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>
|
|
<TextInput label="Nom" v-model="spellFilter.text" />
|
|
<Combobox label="Rang" v-model="spellFilter.ranks" multiple :options="[['Rang 1', 1], ['Rang 2', 2], ['Rang 3', 3]]" />
|
|
<Combobox label="Type" v-model="spellFilter.types" multiple :options="[['Précision', 'precision'], ['Savoir', 'knowledge'], ['Instinct', 'instinct']]" />
|
|
<Combobox label="Element" v-model="spellFilter.elements" multiple :options="[['Feu', 'fire'], ['Glace', 'ice'], ['Foudre', 'thunder'], ['Terre', 'earth'], ['Arcane', 'arcana'], ['Air', 'air'], ['Nature', 'nature'], ['Lumière', 'light'], ['Psy', 'psyche']]" />
|
|
</div>
|
|
<div class="grid gap-4 grid-cols-2">
|
|
<div class="py-1 px-2 border border-light-30 dark:border-dark-30 flex flex-col hover:border-light-50 dark:hover:border-dark-50 cursor-pointer" v-for="spell of filterSpells(characterConfig.spells)" :class="{ '!border-accent-blue bg-accent-blue bg-opacity-20': data.spells?.find(e => e === spell.id) }"
|
|
@click="() => data.spells?.includes(spell.id) ? data.spells.splice(data.spells.findIndex((e: string) => e === spell.id), 1) : data.spells!.push(spell.id)">
|
|
<div class="flex flex-row justify-between">
|
|
<span class="text-lg font-bold">{{ spell.name }}</span>
|
|
<div class="flex flex-row items-center gap-6">
|
|
<div class="flex flex-row text-sm gap-2">
|
|
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
|
|
</div>
|
|
<div class="flex flex-row text-sm gap-1">
|
|
<span class="">Rang {{ spell.rank }}</span><span>/</span>
|
|
<span class="">{{ spellTypeTexts[spell.type] }}</span><span>/</span>
|
|
<span class="">{{ spell.cost }} mana</span><span>/</span>
|
|
<span class="">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<MarkdownRenderer :content="spell.effect" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
<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>
|
|
</div>
|
|
</template> |