This commit is contained in:
2025-01-29 22:53:05 +01:00
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

@@ -83,3 +83,8 @@ export function getCenter(n: Position, i: Position, r: Position, o: Position, e:
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);