Starting new file format "Map". Preparing editor for Map and Canvas editor with metadata folding UI. Fix comments filtering.

This commit is contained in:
Peaceultime 2025-01-06 17:46:31 +01:00
parent 896af11fa7
commit 6f305397a8
13 changed files with 71 additions and 45 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<template v-for="(item, idx) of options"> <template v-for="(item, idx) of options">
<template v-if="item.type === 'item'"> <template v-if="item.type === 'item'">
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'!pe-1': item.kbd}" class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35"> <DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'!pe-1': item.kbd}" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<Icon v-if="item.icon" :icon="item.icon" class="absolute left-1.5" /> <Icon v-if="item.icon" :icon="item.icon" class="absolute left-1.5" />
<div class="flex flex-1 justify-between"> <div class="flex flex-1 justify-between">
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
@ -10,14 +10,17 @@
</DropdownMenuItem> </DropdownMenuItem>
</template> </template>
<!-- TODO -->
<template v-else-if="item.type === 'checkbox'"> <template v-else-if="item.type === 'checkbox'">
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" @update:checked="item.select"> <DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" v-model:checked="item.checked" @update:checked="item.select" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<DropdownMenuItemIndicator> <span class="w-6 flex items-center justify-center">
<Icon icon="radix-icons:check" /> <DropdownMenuItemIndicator>
</DropdownMenuItemIndicator> <Icon icon="radix-icons:check" />
<span>{{ item.label }}</span> </DropdownMenuItemIndicator>
<span v-if="item.kbd"> {{ item.kbd }} </span> </span>
<div class="flex flex-1 justify-between">
<span>{{ item.label }}</span>
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
</div>
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
</template> </template>

View File

@ -4,7 +4,7 @@
<slot></slot> <slot></slot>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardPortal v-if="!disabled"> <HoverCardPortal v-if="!disabled">
<HoverCardContent :class="$attrs.class" :side="side" class="data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" > <HoverCardContent :class="$attrs.class" :side="side" class="max-h-[var(--radix-hover-card-content-available-height)] data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
<slot name="content"></slot> <slot name="content"></slot>
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" /> <HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
</HoverCardContent> </HoverCardContent>

View File

@ -2,7 +2,7 @@
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class"> <NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class">
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview"> <HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
<template #content> <template #content>
<Markdown v-if="overview?.type === 'markdown'" class="!px-10" :path="pathname" :filter="hash.substring(1)" popover /> <Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="pathname" :filter="hash.substring(1)" popover />
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template> <template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template>
</template> </template>
<span> <span>

View File

@ -2,8 +2,8 @@
<CollapsibleRoot :disabled="disabled" :defaultOpen="fold === true || fold === undefined" class="callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue" :data-type="type"> <CollapsibleRoot :disabled="disabled" :defaultOpen="fold === true || fold === undefined" class="callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue" :data-type="type">
<CollapsibleTrigger> <CollapsibleTrigger>
<div :class="{ 'cursor-pointer': fold !== undefined }" class="flex flex-row items-center justify-start ps-2"> <div :class="{ 'cursor-pointer': fold !== undefined }" class="flex flex-row items-center justify-start ps-2">
<Icon :icon="calloutIconByType[type] ?? defaultCalloutIcon" class="w-6 h-6 stroke-2 float-start me-2" /> <Icon :icon="calloutIconByType[type] ?? defaultCalloutIcon" inline class="w-6 h-6 stroke-2 float-start me-2 flex-shrink-0" />
<span v-if="title" class="block font-bold">{{ title }}</span> <span v-if="title" class="block font-bold text-start">{{ title }}</span>
<Icon icon="radix-icons:caret-right" v-if="fold !== undefined" class="transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6" /> <Icon icon="radix-icons:caret-right" v-if="fold !== undefined" class="transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6" />
</div> </div>
</CollapsibleTrigger> </CollapsibleTrigger>

View File

@ -5,6 +5,7 @@ import RemarkParse from "remark-parse";
import RemarkRehype from 'remark-rehype'; import RemarkRehype from 'remark-rehype';
import RemarkOfm from 'remark-ofm'; import RemarkOfm from 'remark-ofm';
import RemarkGfm from 'remark-gfm'; import RemarkGfm from 'remark-gfm';
import RemarkBreaks from 'remark-breaks';
import RemarkFrontmatter from 'remark-frontmatter'; import RemarkFrontmatter from 'remark-frontmatter';
export default function useMarkdown(): (md: string) => Root export default function useMarkdown(): (md: string) => Root
@ -14,7 +15,7 @@ export default function useMarkdown(): (md: string) => Root
const parse = (markdown: string) => { const parse = (markdown: string) => {
if (!processor) if (!processor)
{ {
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkFrontmatter]); processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
processor.use(RemarkRehype); processor.use(RemarkRehype);
} }

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -39,7 +39,7 @@ export const explorerContentTable = sqliteTable("explorer_content", {
path: text().primaryKey(), path: text().primaryKey(),
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
title: text().notNull(), title: text().notNull(),
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(), type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
content: blob({ mode: 'buffer' }), content: blob({ mode: 'buffer' }),
navigable: int({ mode: 'boolean' }).notNull().default(true), navigable: int({ mode: 'boolean' }).notNull().default(true),
private: int({ mode: 'boolean' }).notNull().default(false), private: int({ mode: 'boolean' }).notNull().default(false),

View File

@ -47,7 +47,7 @@
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100 flex md:flex-row flex-col gap-4 items-center"> <AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100 flex md:flex-row flex-col gap-4 items-center">
<AlertDialogTitle class="text-xl font-semibold">Supprimer <span>{{ selected.title }}</span><span v-if="selected.children"> et tous ces enfants</span> ?</AlertDialogTitle> <AlertDialogTitle class="text-xl font-semibold">Supprimer <span>{{ selected.title }}</span><span v-if="selected.children"> et tous ces enfants</span> ?</AlertDialogTitle>
<div class="flex flex-1 flex-row gap-4 justify-end"> <div class="flex flex-1 flex-row gap-4 justify-end">
<AlertDialogAction asChild @click="navigation = tree.remove(navigation, getPath(selected))"><Button class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction> <AlertDialogAction asChild @click="() => { navigation = tree.remove(navigation, getPath(selected!)); selected = undefined; }"><Button class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel> <AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
</div> </div>
</AlertDialogContent> </AlertDialogContent>
@ -58,7 +58,7 @@
type: 'item', type: 'item',
label: 'Markdown', label: 'Markdown',
kbd: 'Ctrl+N', kbd: 'Ctrl+N',
icon: 'radix-icons:file', icon: 'radix-icons:file-text',
select: () => add('markdown'), select: () => add('markdown'),
}, { }, {
type: 'item', type: 'item',
@ -71,10 +71,15 @@
label: 'Canvas', label: 'Canvas',
icon: 'ph:graph-light', icon: 'ph:graph-light',
select: () => add('canvas'), select: () => add('canvas'),
}, {
type: 'item',
label: 'Carte',
icon: 'lucide:map',
select: () => add('map'),
}, { }, {
type: 'item', type: 'item',
label: 'Fichier', label: 'Fichier',
icon: 'radix-icons:file-text', icon: 'radix-icons:file',
select: () => add('file'), select: () => add('file'),
}]"> }]">
<Button icon><Icon class="w-5 h-5" icon="radix-icons:plus" /></Button> <Button icon><Icon class="w-5 h-5" icon="radix-icons:plus" /></Button>
@ -120,27 +125,45 @@
</div> </div>
</CollapsibleContent> </CollapsibleContent>
<div class="flex flex-1 flex-row max-h-full overflow-hidden"> <div class="flex flex-1 flex-row max-h-full overflow-hidden">
<div v-if="selected" class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 flex-col items-start justify-start max-h-full relative"> <div v-if="selected" class="flex flex-1 flex-col items-start justify-start max-h-full relative">
<Head> <Head>
<Title>d[any] - Modification de {{ selected.title }}</Title> <Title>d[any] - Modification de {{ selected.title }}</Title>
</Head> </Head>
<div> <CollapsibleRoot v-model:open="topOpen" class="my-4 w-full relative">
<input type="text" v-model="selected.title" placeholder="Titre" class="inline md:h-16 h-12 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none py-1 md:text-5xl text-4xl font-thin bg-transparent" /> <CollapsibleTrigger asChild>
<div class="flex flex-col justify-start items-start"> <Button class="absolute left-1/2 -translate-x-1/2 -bottom-3" icon>
<Switch label="Chemin personnalisé" v-model="selected.customPath" /> <Icon v-if="topOpen" icon="radix-icons:caret-up" class="h-4 w-4" />
<span> <Icon v-else icon="radix-icons:caret-down" class="h-4 w-4" />
<pre v-if="selected.customPath" class="flex md:items-center md:text-base md:text-nowrap text-wrap md:flex-nowrap flex-wrap text-sm">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}<TextInput v-model="selected.name" @input="(e) => { </Button>
if(selected && selected.customPath) </CollapsibleTrigger>
{ <CollapsibleContent class="xl:px-12 lg:px-8 px-6">
selected.name = parsePath(selected.name); <div class="pb-2 grid lg:grid-cols-2 grid-cols-1 lg:items-center justify-between gap-x-4 flex-1 border-b border-light-35 dark:border-dark-35">
rebuildPath(selected.children, getPath(selected)); <input type="text" v-model="selected.title" placeholder="Titre" style="line-height: normal;" class="flex-1 md:text-5xl text-4xl md:h-14 h-12 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none pb-3 font-thin bg-transparent"/>
} <div class="flex flex-row justify-between items-center gap-x-4">
}" class="mx-0"/></pre> <div v-if="selected.customPath" class="flex lg:items-center truncate">
<pre v-else class="md:text-base text-smmd:text-nowrap text-wrap ">/{{ getPath(selected) }}</pre> <pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre>
</span> <TextInput v-model="selected.name" @input="(e) => {
</div> if(selected && selected.customPath)
</div> {
<div class="py-4 flex-1 w-full max-h-full flex overflow-hidden"> selected.name = parsePath(selected.name);
rebuildPath(selected.children, getPath(selected));
}
}" class="mx-0 font-mono"/>
</div>
<pre v-else class="md:text-base text-sm truncate" style="direction: rtl">{{ getPath(selected) }}/</pre>
<DropdownMenu align="end" :options="[{
type: 'checkbox',
label: 'URL custom',
select: (state: boolean) => selected!.customPath = state,
checked: selected.customPath
}]">
<Button class="" icon><Icon icon="radix-icons:dots-vertical"/></Button>
</DropdownMenu>
</div>
</div>
</CollapsibleContent>
</CollapsibleRoot>
<div class="py-4 flex-1 w-full max-h-full flex overflow-hidden xl:px-12 lg:px-8 px-6">
<template v-if="selected.type === 'markdown'"> <template v-if="selected.type === 'markdown'">
<div v-if="contentStatus === 'pending'" class="flex flex-1 justify-center items-center"> <div v-if="contentStatus === 'pending'" class="flex flex-1 justify-center items-center">
<Loading /> <Loading />
@ -159,8 +182,11 @@
<template v-else-if="selected.type === 'canvas'"> <template v-else-if="selected.type === 'canvas'">
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de graphe en cours de développement</ProseH3></span> <span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de graphe en cours de développement</ProseH3></span>
</template> </template>
<template v-else-if="selected.type === 'map'">
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
</template>
<template v-else-if="selected.type === 'file'"> <template v-else-if="selected.type === 'file'">
<span>Modifier le contenu :</span><input type="file" @change="(e) => console.log(e)" /> <span>Modifier le contenu :</span><input type="file" @change="(e) => console.log((e.target as HTMLInputElement).files?.length)" />
</template> </template>
</div> </div>
</div> </div>
@ -193,7 +219,7 @@ definePageMeta({
}); });
const router = useRouter(); const router = useRouter();
const open = ref(true); const open = ref(true), topOpen = ref(true);
const toaster = useToast(); const toaster = useToast();
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle'); const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');

View File

@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas']); export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas', 'map']);
export const schema = z.object({ export const schema = z.object({
path: z.string(), path: z.string(),
owner: z.number().finite(), owner: z.number().finite(),

View File

@ -24,12 +24,7 @@ export default defineEventHandler(async (e) => {
{ {
const session = await getUserSession(e); const session = await getUserSession(e);
if(content.private && (!session || !session.user)) if(!session || !session.user || session.user.id !== content.owner)
{
setResponseStatus(e, 404);
return;
}
if(session && session.user && session.user.id !== content.owner)
{ {
if(content.private) if(content.private)
{ {

View File

@ -51,5 +51,6 @@ export const iconByType: Record<FileType, string> = {
'folder': 'lucide:folder', 'folder': 'lucide:folder',
'canvas': 'ph:graph-light', 'canvas': 'ph:graph-light',
'file': 'radix-icons:file', 'file': 'radix-icons:file',
'markdown': 'radix-icons:file', 'markdown': 'radix-icons:file-text',
'map': 'lucide:map',
} }