New default layout without vuejs rendering (still needs some fixes)

This commit is contained in:
Peaceultime 2025-10-19 23:35:11 +02:00
parent df9ae95890
commit feb2fb56c6
8 changed files with 73 additions and 99 deletions

View File

@ -1,13 +1,11 @@
<template> <template>
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden"> <div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
<NuxtRouteAnnouncer/> <NuxtRouteAnnouncer/>
<TooltipProvider>
<NuxtLayout> <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 max-w-full relative" id="mainContainer"> <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 max-w-full relative" id="mainContainer">
<NuxtPage /> <NuxtPage />
</div> </div>
</NuxtLayout> </NuxtLayout>
</TooltipProvider>
</div> </div>
</template> </template>

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,87 +1,20 @@
<template> <template>
<CollapsibleRoot class="flex flex-1 flex-col" v-model:open="open"> <div ref="container"></div>
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2"> <div ref="slotContainer"><slot></slot></div>
<div class="flex items-center px-2 gap-4">
<CollapsibleTrigger asChild>
<Button icon class="!bg-transparent group md:hidden">
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
</Button>
</CollapsibleTrigger>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl max-md:hidden">d[any]</span>
</NuxtLink>
</div>
<NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem>
<NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70 border-b-[2px] border-transparent hover:border-accent-blue py-2 hover:!text-opacity-70 flex items-center" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
</NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center">
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenuRoot>
<div class="flex items-center px-2 gap-4">
<template v-if="!loggedIn">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
</template>
<template v-else>
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
</template>
</div>
</div>
<div class="flex flex-1 flex-row relative h-screen w-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" ref="treeParent">
<div v-if="user" class="flex flex-1 py-4 px-2 flex-row flex-1 justify-between items-center">
<NuxtLink v-if="hasPermissions(user.permissions, ['admin', 'editor'])" :to="{ name: 'explore-edit' }"><Button icon><Icon icon="radix-icons:pencil-2" /></Button></NuxtLink>
</div>
</div>
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
<p>Copyright Peaceultime - 2025</p>
</div>
</div>
<!-- </CollapsibleContent> -->
<slot></slot>
</div>
</CollapsibleRoot>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { link, optionmenu } from '~/shared/components.util';
import type { DropdownOption } from '~/components/base/DropdownMenu.vue'; import { Content, iconByType } from '~/shared/content.util';
import { hasPermissions } from '#shared/auth.util'; import { div, dom, icon, span, text } from '~/shared/dom.util';
import { TreeDOM } from '#shared/tree'; import { popper, tooltip } from '~/shared/floating.util';
import { Content, iconByType } from '#shared/content.util'; import { unifySlug } from '~/shared/general.util';
import { dom, icon } from '#shared/dom.util'; import { TreeDOM } from '~/shared/tree';
import { unifySlug } from '#shared/general.util';
import { tooltip } from '#shared/floating.util';
import { link } from '#shared/components.util';
const options = ref<DropdownOption[]>([{ const container = useTemplateRef('container'), slots = useTemplateRef('slotContainer');
type: 'item',
label: 'Mon profil',
select: () => useRouter().push({ name: 'user-profile' }),
}, {
type: 'item',
label: 'Deconnexion',
select: () => clear(),
}]);
const open = ref(false); const open = ref(false);
const { loggedIn, user, clear } = useUserSession(); const { loggedIn, user, clear: logout } = useUserSession();
const { fetch } = useContent(); const { fetch } = useContent();
await fetch(false); await fetch(false);
@ -97,33 +30,72 @@ const tree = new TreeDOM((item, depth) => {
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined, item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
])]); ])]);
}, (item, depth) => { }, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link({ class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined, [ return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }), icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }), dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined, item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
])]); ], { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined)]);
}, (item) => item.navigable); }, (item) => item.navigable);
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true)); const toggleByPath = (path: string | undefined) => (path?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
const treeParent = useTemplateRef('treeParent'); toggleByPath(path.value);
const unmount = useRouter().afterEach((to, from, failure) => { const unmount = useRouter().afterEach((to, from, failure) => {
if(failure) if(failure)
return; return;
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true)); to.name === 'explore-path' && toggleByPath(unifySlug(to.params.path ?? ''));
}); });
watch(route, () => { watch(route, () => {
open.value = false; open.value = false;
}); });
const getUserDom = () => user.value ? [popper(link([ text(user.value.username), icon('radix-icons:caret-down', { width: 12, height: 12 }) ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-profile' }), {
placement: 'bottom', delay: 0, content: () => [div('flex flex-1 flex-col', [
dom('span', { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 cursor-pointer select-none', listeners: { click: logout } }, [ text('Se déconnecter') ], ),
])], class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full'
}).container] : [link([ text('S\'inscrire') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-register' }), link([ text('Se connecter') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'user-login' })];
const slotContainer = div('flex flex-1');
const content = () => div('flex flex-row w-full h-full', [
div('bg-light-0 dark:bg-dark-0 w-[300px] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2', [
link([ dom('img', { attributes: { src: '/logo.dark.svg', width: 52, height: 41 } }), span('text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70', 'd[any]') ], { class: 'flex flex-row items-center justify-center group gap-2 my-2' }, { name: 'index' }),
div('flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden', [ tree.container ]),
div('flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60', [
link([ text('Mentions légales') ], { class: 'hover:underline' }, { name: 'legal' }),
link([ text('Conditions d\'utilisations') ], { class: 'hover:underline' }, { name: 'usage' }),
text('Copyright 2025 - Peaceultime & d[any]')
])
]),
div('flex flex-col flex-1 h-full', [
div('flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8', [
div('flex flex-row gap-16 items-center', [
popper(link([ text('Personnages'), icon('radix-icons:caret-down', { width: 12, height: 12 }) ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'character' }), {
placement: 'bottom', delay: 0, content: () => [div('flex flex-1 flex-col', [
link([ text('Personnages publics') ], { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none' }, { name: 'character-list' }),
link([ text('Nouveau personnage') ], { class: 'hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none' }, { name: 'character-id-edit', params: { id: 'new' } })
])], class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full'
}).container,
link([ text('Campagnes') ], { class: 'flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none' }, { name: 'character' })
]),
div('flex flex-row gap-16 items-center', getUserDom())
]),
slotContainer,
])
]);
onMounted(() => { onMounted(() => {
if(treeParent.value) if(container.value && slots.value)
{ {
treeParent.value.appendChild(tree.container); slotContainer.replaceChildren(...slots.value.childNodes);
container.value!.replaceWith(content());
} }
});
onUpdated(() => {
}) })
onUnmounted(() => { onUnmounted(() => {
unmount(); unmount();
}) });
</script> </script>

View File

@ -26,12 +26,12 @@ onMounted(() => {
if(container.value && id) if(container.value && id)
{ {
const character = new CharacterSheet(id, user); const character = new CharacterSheet(id, user);
container.value.appendChild(character.container); container.value.replaceWith(character.container);
} }
}); });
}); });
</script> </script>
<template> <template>
<div ref="container"></div> <div class="flex h-full" ref="container"></div>
</template> </template>

View File

@ -374,7 +374,7 @@ export class CharacterCompiler
} }
saveNotes() saveNotes()
{ {
useRequestFetch()(`/api/character/${this.character.id}/notes`, { return useRequestFetch()(`/api/character/${this.character.id}/notes`, {
method: 'POST', method: 'POST',
body: this._character.notes, body: this._character.notes,
}).then(() => {}).catch(() => { }).then(() => {}).catch(() => {
@ -1233,7 +1233,7 @@ export class CharacterSheet
{ {
user: ComputedRef<User | null>; user: ComputedRef<User | null>;
character?: CharacterCompiler; character?: CharacterCompiler;
container: HTMLElement = div(); container: HTMLElement = div('flex h-full');
tabs?: HTMLDivElement & { refresh: () => void }; tabs?: HTMLDivElement & { refresh: () => void };
constructor(id: string, user: ComputedRef<User | null>) constructor(id: string, user: ComputedRef<User | null>)
{ {
@ -1274,6 +1274,10 @@ export class CharacterSheet
const publicNotes = new MarkdownEditor(); const publicNotes = new MarkdownEditor();
const privateNotes = new MarkdownEditor(); const privateNotes = new MarkdownEditor();
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
const saveLoading = loading('small');
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.character?.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
publicNotes.onChange = (v) => this.character!.character.notes!.public = v; publicNotes.onChange = (v) => this.character!.character.notes!.public = v;
privateNotes.onChange = (v) => this.character!.character.notes!.private = v; privateNotes.onChange = (v) => this.character!.character.notes!.private = v;
publicNotes.content = this.character!.character.notes!.public!; publicNotes.content = this.character!.character.notes!.public!;
@ -1292,8 +1296,8 @@ export class CharacterSheet
{ id: 'notes', title: [ text('Notes') ], content: () => [ { id: 'notes', title: [ text('Notes') ], content: () => [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(icon('radix-icons:paper-plane', { width: 16, height: 16 }), () => this.character!.saveNotes(), 'p-1'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 p-1', [ publicNotes.dom ]) ]), div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ publicNotes.dom ]) ]),
div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 p-1', [ privateNotes.dom ]) ]), div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
]) ])
] }, ] },
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }); ], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } });

View File

@ -5,7 +5,7 @@ import { clamp } from "./general.util";
import { Tree } from "./tree"; import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom"; import type { Placement } from "@floating-ui/dom";
export function link(properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>, children?: NodeChildren) export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
{ {
const router = useRouter(); const router = useRouter();
const nav = link ? router.resolve(link) : undefined; const nav = link ? router.resolve(link) : undefined;