This commit is contained in:
Peaceultime 2025-01-29 22:53:05 +01:00
commit 8fc1855ae6
14 changed files with 2743 additions and 48 deletions

2656
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

View File

@ -59,12 +59,23 @@ import { Icon } from '@iconify/vue/dist/iconify.js';
import { clamp } from '#shared/general.util'; import { clamp } from '#shared/general.util';
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas'; import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
const canvas = defineModel<CanvasContent>({ required: true, }); const canvas = defineModel<CanvasContent>({ required: true });
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5); const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
const focusing = ref<Element>(), editing = ref<Element>(); const focusing = ref<Element>(), editing = ref<Element>();
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'); const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef');
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges'); const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
const canvasSettings = useCookie<{
snap: boolean,
size: number
}>('canvasPreference', { default: () => ({ snap: true, size: 32 }) });
const snap = computed({
get: () => canvasSettings.value.snap,
set: (value: boolean) => canvasSettings.value = { ...canvasSettings.value, snap: value },
}), gridSize = computed({
get: () => canvasSettings.value.size,
set: (value: number) => canvasSettings.value = { ...canvasSettings.value, size: value },
});
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined); const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
@ -614,7 +625,7 @@ useShortcuts({
</div> </div>
</div> </div>
<div> <div>
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" /> <CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom" @select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" :snapping="snap" :grid="gridSize" />
</div> </div>
<div> <div>
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" /> <CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" />

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="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" > <HoverCardContent :class="$attrs.class" :side="side" :align="align" 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>
@ -13,11 +13,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { delay = 500, disabled = false, side = 'bottom' } = defineProps<{ const { delay = 500, disabled = false, side = 'bottom', align = 'center', triggerKey } = defineProps<{
delay?: number delay?: number
disabled?: boolean disabled?: boolean
side?: 'top' | 'right' | 'bottom' | 'left' side?: 'top' | 'right' | 'bottom' | 'left'
align?: 'start' | 'center' | 'end'
triggerKey?: string
}>(); }>();
const emits = defineEmits(['open']) const emits = defineEmits(['open']);
</script> </script>

View File

@ -34,14 +34,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { Direction } from '#shared/canvas.util'; import { gridSnap, type Direction } from '#shared/canvas.util';
import type { Element } from '../CanvasEditor.vue'; import type { Element } from '../CanvasEditor.vue';
import FakeA from '../prose/FakeA.vue'; import FakeA from '../prose/FakeA.vue';
import type { CanvasNode } from '~/types/canvas'; import type { CanvasNode } from '~/types/canvas';
const { node, zoom } = defineProps<{ const { node, zoom, snapping, grid } = defineProps<{
node: CanvasNode node: CanvasNode
zoom: number zoom: number
snapping: boolean
grid: number
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -80,14 +82,20 @@ function resizeNode(e: MouseEvent, x: number, y: number, w: number, h: number) {
e.stopImmediatePropagation(); e.stopImmediatePropagation();
const startx = node.x, starty = node.y, startw = node.width, starth = node.height; const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
let realx = node.x, realy = node.y, realw = node.width, realh = node.height;
const resizemove = (e: MouseEvent) => { const resizemove = (e: MouseEvent) => {
if(e.button !== 0) if(e.button !== 0)
return; return;
node.x += (e.movementX / zoom) * x; realx += (e.movementX / zoom) * x;
node.y += (e.movementY / zoom) * y; realy += (e.movementY / zoom) * y;
node.width += (e.movementX / zoom) * w; realw += (e.movementX / zoom) * w;
node.height += (e.movementY / zoom) * h; realh += (e.movementY / zoom) * h;
node.x = snapping ? gridSnap(realx, grid) : realx;
node.y = snapping ? gridSnap(realy, grid) : realy;
node.width = snapping ? gridSnap(realw, grid) : realw;
node.height = snapping ? gridSnap(realh, grid) : realh;
}; };
const resizeend = (e: MouseEvent) => { const resizeend = (e: MouseEvent) => {
if(e.button !== 0) if(e.button !== 0)
@ -126,12 +134,16 @@ function unselect() {
} }
let lastx = 0, lasty = 0; let lastx = 0, lasty = 0;
let realx = 0, realy = 0;
const dragmove = (e: MouseEvent) => { const dragmove = (e: MouseEvent) => {
if(e.button !== 0) if(e.button !== 0)
return; return;
node.x += e.movementX / zoom; realx += e.movementX / zoom;
node.y += e.movementY / zoom; realy += e.movementY / zoom;
node.x = snapping ? gridSnap(realx, grid) : realx;
node.y = snapping ? gridSnap(realy, grid) : realy;
}; };
const dragend = (e: MouseEvent) => { const dragend = (e: MouseEvent) => {
if(e.button !== 0) if(e.button !== 0)
@ -148,6 +160,7 @@ const dragstart = (e: MouseEvent) => {
return; return;
lastx = node.x, lasty = node.y; lastx = node.x, lasty = node.y;
realx = node.x, realy = node.y;
window.addEventListener('mousemove', dragmove, { passive: true }); window.addEventListener('mousemove', dragmove, { passive: true });
window.addEventListener('mouseup', dragend, { passive: true }); window.addEventListener('mouseup', dragend, { passive: true });

View File

@ -35,7 +35,7 @@
<div class="flex justify-between items-center max-md:hidden"> <div class="flex justify-between items-center max-md:hidden">
<NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }"> <NuxtLink class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-md:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" /> <Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" /> <Avatar src="/logo.light.svg" class="block dark:hidden" />
</NuxtLink> </NuxtLink>
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip> <Tooltip message="Changer de theme" side="left"><ThemeSwitch /></Tooltip>

View File

@ -11,44 +11,45 @@
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@iconify/vue": "^4.3.0", "@iconify/vue": "^4.3.0",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/sitemap": "^7.0.1", "@nuxtjs/sitemap": "^7.2.3",
"@nuxtjs/tailwindcss": "^6.12.2", "@nuxtjs/tailwindcss": "^6.13.1",
"@vueuse/gesture": "^2.0.0", "@vueuse/gesture": "^2.0.0",
"@vueuse/math": "^11.3.0", "@vueuse/math": "^12.5.0",
"@vueuse/nuxt": "^11.3.0", "@vueuse/nuxt": "^12.5.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"drizzle-orm": "^0.35.3", "drizzle-orm": "^0.38.4",
"hast": "^1.0.0", "hast": "^1.0.0",
"hast-util-heading": "^3.0.0", "hast-util-heading": "^3.0.0",
"hast-util-heading-rank": "^3.0.0", "hast-util-heading-rank": "^3.0.0",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"mdast-util-find-and-replace": "^3.0.2", "mdast-util-find-and-replace": "^3.0.2",
"nodemailer": "^6.9.16", "nodemailer": "^6.10.0",
"nuxt": "^3.15.0", "nuxt": "3.15.1",
"nuxt-security": "^2.1.5", "nuxt-security": "^2.1.5",
"radix-vue": "^1.9.12", "radix-vue": "^1.9.12",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
"remark-frontmatter": "^5.0.0", "remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-ofm": "link:remark-ofm",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1", "remark-rehype": "^11.1.1",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-vue": "^6.0.0", "rollup-plugin-vue": "^6.0.0",
"unified": "^11.0.5", "unified": "^11.0.5",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vue": "latest", "vue": "^3.5.13",
"vue-router": "latest", "vue-router": "^4.5.0",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.1.14", "@types/bun": "^1.2.0",
"@types/lodash.capitalize": "^4.2.9", "@types/lodash.capitalize": "^4.2.9",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/unist": "^3.0.3", "@types/unist": "^3.0.3",
"better-sqlite3": "^11.7.0", "better-sqlite3": "^11.8.1",
"bun-types": "^1.1.42", "bun-types": "^1.2.0",
"drizzle-kit": "^0.26.2", "drizzle-kit": "^0.30.2",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"rehype-stringify": "^10.0.1" "rehype-stringify": "^10.0.1"
} }

View File

@ -36,7 +36,7 @@
<div class="flex flex-row justify-between items-center mb-4 px-6"> <div class="flex flex-row justify-between items-center mb-4 px-6">
<div class="flex flex-1 flex-row justify-start items-center gap-4"> <div class="flex flex-1 flex-row justify-start items-center gap-4">
<Tooltip side="top" message="Annuler (Ctrl+Shift+W)" ><Button icon @click="router.go(-1)"><Icon class="w-5 h-5" icon="radix-icons:arrow-left" /></Button></Tooltip> <Tooltip side="top" message="Annuler (Ctrl+Shift+W)" ><Button icon @click="router.go(-1)"><Icon class="w-5 h-5" icon="radix-icons:arrow-left" /></Button></Tooltip>
<Tooltip side="top" message="Enregistrer (Ctrl+S)" ><Button icon :loading="saveStatus === 'pending'" @click="save(true)"><Icon class="w-5 h-5" icon="radix-icons:check" /></Button></Tooltip> <Tooltip side="top" message="Enregistrer (Ctrl+S)" ><Button icon :loading="saveStatus === 'pending'" @click="save(true)"><Icon class="w-5 h-5" icon="ph:floppy-disk" /></Button></Tooltip>
<span v-if="edited" class="text-sm text-light-60 dark:text-dark-60 italic">Modifications non enregistrées</span> <span v-if="edited" class="text-sm text-light-60 dark:text-dark-60 italic">Modifications non enregistrées</span>
</div> </div>
<div class="flex flex-row justify-end items-center gap-4"> <div class="flex flex-row justify-end items-center gap-4">
@ -89,8 +89,8 @@
</div> </div>
<DraggableTree class="ps-4 pe-2 xl:text-base text-sm" <DraggableTree class="ps-4 pe-2 xl:text-base text-sm"
:items="navigation ?? undefined" :get-key="(item: Partial<TreeItemEditable>) => item.path !== undefined ? getPath(item as TreeItemEditable) : ''" @updateTree="drop" :items="navigation ?? undefined" :get-key="(item: Partial<TreeItemEditable>) => item.path !== undefined ? getPath(item as TreeItemEditable) : ''" @updateTree="drop"
v-model="selected" :defaultExpanded="defaultExpanded" > v-model="selected" :defaultExpanded="defaultExpanded" :get-children="(item: Partial<TreeItemEditable>) => item.type === 'folder' ? item.children : undefined" >
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, item }"> <template #default="{ handleToggle, handleSelect, isExpanded, isDragging, item }">
<div class="flex flex-1 items-center px-2 max-w-full pe-4" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }"> <div class="flex flex-1 items-center px-2 max-w-full pe-4" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" > <span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/> <Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
@ -148,7 +148,7 @@
<div class="flex flex-row justify-between items-center gap-x-4"> <div class="flex flex-row justify-between items-center gap-x-4">
<div v-if="selected.customPath" class="flex lg:items-center truncate"> <div v-if="selected.customPath" class="flex lg:items-center truncate">
<pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre> <pre class="md:text-base text-sm truncate" style="direction: rtl">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}</pre>
<TextInput v-model="selected.name" @input="(e) => { <TextInput v-model="selected.name" @input="(e: Event) => {
if(selected && selected.customPath) if(selected && selected.customPath)
{ {
selected.name = parsePath(selected.name); selected.name = parsePath(selected.name);
@ -192,7 +192,7 @@
<span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span> <span class="flex flex-1 justify-center items-center"><ProseH3>Editeur de carte en cours de développement</ProseH3></span>
</template> </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.target as HTMLInputElement).files?.length)" /> <span>Modifier le contenu :</span><input type="file" @change="(e: Event) => console.log((e.target as HTMLInputElement).files?.length)" />
</template> </template>
</div> </div>
</div> </div>
@ -206,8 +206,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item'; import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
import { iconByType, convertContentFromText, convertContentToText, parsePath } from '#shared/general.util'; import { iconByType, convertContentFromText, convertContentToText, DEFAULT_CONTENT,parsePath } from '#shared/general.util';
import type { ExploreContent, FileType, TreeItem } from '~/types/content'; import type { CanvasContent, ExploreContent, FileType, TreeItem } from '~/types/content';
import FakeA from '~/components/prose/FakeA.vue'; import FakeA from '~/components/prose/FakeA.vue';
export type TreeItemEditable = TreeItem & export type TreeItemEditable = TreeItem &
@ -417,7 +417,7 @@ function add(type: FileType): void
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i); const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`; const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false, content: undefined, owner: -1, timestamp: new Date(), visit: 0 }; const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
if(!selected.value) if(!selected.value)
{ {
@ -517,6 +517,7 @@ function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: st
} }
async function save(redirect: boolean): Promise<void> async function save(redirect: boolean): Promise<void>
{ {
//@ts-ignore
const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined })); const map = (e: TreeItemEditable[]): TreeItemEditable[] => e.map(f => ({ ...f, content: f.content ? convertContentToText(f.type, f.content) : undefined, children: f.children ? map(f.children) : undefined }));
saveStatus.value = 'pending'; saveStatus.value = 'pending';
try { try {
@ -531,6 +532,7 @@ async function save(redirect: boolean): Promise<void>
toaster.clear('error'); toaster.clear('error');
toaster.add({ type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000 }); toaster.add({ type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000 });
//@ts-ignore
complete.value = result as ExploreContent[]; complete.value = result as ExploreContent[];
if(redirect) router.go(-1); if(redirect) router.go(-1);
} catch(e: any) { } catch(e: any) {

View File

@ -3,8 +3,6 @@
<Title>d[any] - Accueil</Title> <Title>d[any] - Accueil</Title>
</Head> </Head>
<div class="h-full w-full flex flex-1 flex-col justify-center items-center"> <div class="h-full w-full flex flex-1 flex-col justify-center items-center">
<Avatar src="/logo.dark.svg" class="dark:block hidden w-48 h-48" />
<Avatar src="/logo.light.svg" class="block dark:hidden w-48 h-48" />
<h1 class="text-5xl font-thin font-mono">Bienvenue</h1> <h1 class="text-5xl font-thin font-mono">Bienvenue</h1>
</div> </div>
</template> </template>

View File

@ -8,8 +8,8 @@
<ProseH4>Modification de mon mot de passe</ProseH4> <ProseH4>Modification de mon mot de passe</ProseH4>
</div> </div>
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch"> <form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="password" label="Ancien mot de passe" autocomplete="currentPassword" v-model="oldPasswd"/> <TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ 'border-light-red dark:border-dark-red': error }"/> <TextInput type="password" label="Nouveau mot de passe" name="new-password" autocomplete="new-password" v-model="newPasswd" :class="{ 'border-light-red dark:border-dark-red': error }"/>
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none"> <div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
<span class="col-span-2">Prérequis de sécurité</span> <span class="col-span-2">Prérequis de sécurité</span>
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
@ -18,8 +18,8 @@
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
</div> </div>
<TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="newPassword" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/> <TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="new-password" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/>
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Mettre à jour mon mot de passe</Button> <Button type="submit" class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Mettre à jour mon mot de passe</Button>
</form> </form>
</div> </div>
</template> </template>

View File

@ -8,9 +8,9 @@
<ProseH4>Connexion</ProseH4> <ProseH4>Connexion</ProseH4>
</div> </div>
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch"> <form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="state.usernameOrEmail"/> <TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
<TextInput type="password" label="Mot de passe" autocomplete="current-password" v-model="state.password"/> <TextInput type="password" label="Mot de passe" name="password" autocomplete="current-password" v-model="state.password"/>
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button> <Button type="submit" class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-reset-password' }">Mot de passe oublié ?</NuxtLink> <NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-reset-password' }">Mot de passe oublié ?</NuxtLink>
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-register' }">Pas de compte ?</NuxtLink> <NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-register' }">Pas de compte ?</NuxtLink>
</form> </form>

View File

@ -8,9 +8,9 @@
<ProseH4>Inscription</ProseH4> <ProseH4>Inscription</ProseH4>
</div> </div>
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0"> <form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
<TextInput type="text" label="Nom d'utilisateur" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/> <TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
<TextInput type="email" label="Email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/> <TextInput type="email" label="Email" name="email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
<TextInput type="password" label="Mot de passe" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/> <TextInput type="password" label="Mot de passe" name="password" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none"> <div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
<span class="col-span-2">Prérequis de sécurité</span> <span class="col-span-2">Prérequis de sécurité</span>
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
@ -20,7 +20,7 @@
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span> <span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
</div> </div>
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/> <TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
<Button class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button> <Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span> <span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
</form> </form>
</div> </div>

View File

@ -82,4 +82,9 @@ export function getCenter(n: Position, i: Position, r: Position, o: Position, e:
x: s * n.x + l * r.x + c * o.x + u * i.x, x: s * n.x + l * r.x + c * o.x + u * i.x,
y: s * n.y + l * r.y + c * o.y + u * i.y y: s * n.y + l * r.y + c * o.y + u * i.y
}; };
}
export function gridSnap(value: number, grid: number): number
{
return Math.round(value / grid) * grid;
} }

View File

@ -1,6 +1,13 @@
import type { CanvasContent } from '~/types/canvas'; import type { CanvasContent } from '~/types/canvas';
import type { ContentMap, FileType } from '~/types/content'; import type { ContentMap, FileType } from '~/types/content';
export const DEFAULT_CONTENT: Record<FileType, ContentMap[FileType]['content']> = {
map: {},
canvas: { nodes: [], edges: []},
markdown: '',
file: '',
folder: null,
}
export function unifySlug(slug: string | string[]): string export function unifySlug(slug: string | string[]): string
{ {
return (Array.isArray(slug) ? slug.join('/') : slug); return (Array.isArray(slug) ? slug.join('/') : slug);