Finish PreviewContent and add ProseA icon for none markdown files

This commit is contained in:
2024-08-20 23:51:00 +02:00
parent 04785ecf27
commit 7e318d4a12
22 changed files with 270 additions and 278 deletions

View File

@@ -9,6 +9,8 @@ defineProps<Prop>();
</script>
<template>
<img :src="`/icons/${icon}.light.svg`" class="dark-hidden light-block" :width="width" :height="height" />
<img :src="`/icons/${icon}.dark.svg`" class="dark-block light-hidden" :width="width" :height="height" />
<span>
<img :src="`/icons/${icon}.light.svg`" class="dark-hidden light-block" :width="width" :height="height" />
<img :src="`/icons/${icon}.dark.svg`" class="dark-block light-hidden" :width="width" :height="height" />
</span>
</template>

View File

@@ -36,14 +36,16 @@ if(props.node.color !== undefined)
</script>
<template>
<div class="canvas-node" :class="classes" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-node-width': `${node.width}px`, '--canvas-node-height': `${node.height}px`, '--canvas-color': node?.color?.startsWith('#') ? hexToRgb(node.color) : undefined}">
<div class="canvas-node" :class="classes"
:style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-node-width': `${node.width}px`, '--canvas-node-height': `${node.height}px`, '--canvas-color': node?.color?.startsWith('#') ? hexToRgb(node.color) : undefined}">
<div class="canvas-node-container">
<template v-if="node.type === 'group' || zoom > Math.min(0.38, 1000 / size)">
<div class="canvas-node-content markdown-embed">
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
<div class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
<div
class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
<div class="markdown-preview-sizer markdown-preview-section">
<Markdown v-model="node.text"/>
<Markdown v-model="node.text" />
</div>
</div>
</div>
@@ -52,7 +54,9 @@ if(props.node.color !== undefined)
<template v-else>
<div class="canvas-node-placeholder">
<div class="canvas-icon-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-align-left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="svg-icon lucide-align-left">
<line x1="21" y1="6" x2="3" y2="6"></line>
<line x1="15" y1="12" x2="3" y2="12"></line>
<line x1="17" y1="18" x2="3" y2="18"></line>
@@ -61,6 +65,8 @@ if(props.node.color !== undefined)
</div>
</template>
</div>
<div v-if="node.type === 'group' && node.label !== undefined" class="canvas-group-label" :class="{'mod-foreground-dark': darken(getColor(node?.color ?? '')), 'mod-foreground-light': !darken(getColor(node?.color ?? ''))}">{{ node.label }}</div>
<div v-if="node.type === 'group' && node.label !== undefined" class="canvas-group-label"
:class="{ 'mod-foreground-dark': darken(getColor(props.node?.color ?? '')), 'mod-foreground-light': !darken(getColor(props.node?.color ?? ''))}">
{{ node.label }}</div>
</div>
</template>

View File

@@ -19,6 +19,8 @@ let lastPinchLength = 0;
let _minX = +Infinity, _minY = +Infinity, _maxX = -Infinity, _maxY = -Infinity;
onMounted(async () => {
await nextTick();
props.canvas.nodes.forEach((e) => {
_minX = Math.min(_minX, e.x);
_minY = Math.min(_minY, e.y);
@@ -47,10 +49,10 @@ const onResize = (event?: Event) => {
maxX.value = _maxX = _maxX + 32;
maxY.value = _maxY = _maxY + 32;
minZoom.value = Math.min((canvas.value?.clientWidth ?? 0) / (_maxX - _minX), (canvas.value?.clientHeight ?? 0) / (_maxY - _minY)) * 0.9;
minZoom.value = Math.min((canvas.value?.clientWidth ?? 1920) / (_maxX - _minX), (canvas.value?.clientHeight ?? 1080) / (_maxY - _minY)) * 0.9;
zoom.value = clamp(zoom.value, minZoom.value, 3);
bbox.value = canvas.value?.getBoundingClientRect();
bbox.value = (canvas.value ?? document.getElementById('canvas'))?.getBoundingClientRect();
centerX.value = (bbox.value?.width ?? 0) / 2;
centerY.value = (bbox.value?.height ?? 0) / 2;
@@ -213,62 +215,71 @@ function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r:
</script>
<template>
<div ref="canvas" @pointerdown="onPointerDown" @wheel.passive="onWheel" @touchstart.passive="onTouchStart" @dragstart.prevent=""
class="canvas-wrapper node-insert-event mod-zoomed-out"
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
<div class="canvas-controls" style="z-index: 999;">
<div class="canvas-control-group">
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="canvas-control-item" aria-label="Zoom in"
data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="svg-icon lucide-plus">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<Suspense>
<template #default>
<div id="canvas" ref="canvas" @pointerdown="onPointerDown" @wheel.passive="onWheel" @touchstart.passive="onTouchStart"
@dragstart.prevent="" class="canvas-wrapper node-insert-event mod-zoomed-out"
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
<div class="canvas-controls" style="z-index: 999;">
<div class="canvas-control-group">
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="canvas-control-item"
aria-label="Zoom in" data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="svg-icon lucide-plus">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
</div>
<div @click="zoom = 1" class="canvas-control-item" aria-label="Reset zoom"
data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="svg-icon lucide-rotate-cw">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
</svg>
</div>
<div @click="reset" class="canvas-control-item" aria-label="Zoom to fit"
data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="svg-icon lucide-maximize">
<path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
<path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
<path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
<path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
</svg>
</div>
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="canvas-control-item"
aria-label="Zoom out" data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="svg-icon lucide-minus">
<path d="M5 12h14"></path>
</svg>
</div>
</div>
</div>
<div @click="zoom = 1" class="canvas-control-item" aria-label="Reset zoom" data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="svg-icon lucide-rotate-cw">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
<path d="M21 3v5h-5"></path>
</svg>
</div>
<div @click="reset" class="canvas-control-item" aria-label="Zoom to fit" data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="svg-icon lucide-maximize">
<path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
<path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
<path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
<path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
</svg>
</div>
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="canvas-control-item" aria-label="Zoom out"
data-tooltip-position="left">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="svg-icon lucide-minus">
<path d="M5 12h14"></path>
<div class="canvas"
:style="{transform: `translate(${centerX}px, ${centerY}px) scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
<svg class="canvas-edges">
<CanvasEdge v-for="edge of props.canvas.edges" :key="edge.id"
:path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
:color="edge.color" :label="edge.label" />
</svg>
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
<template v-for="edge of props.canvas.edges">
<div :key="edge.id" v-if="edge.label" class="canvas-path-label-wrapper"
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
<div class="canvas-path-label">{{ edge.label }}</div>
</div>
</template>
</div>
</div>
</div>
<div class="canvas"
:style="{transform: `translate(${centerX}px, ${centerY}px) scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
<svg class="canvas-edges">
<CanvasEdge v-for="edge of props.canvas.edges" :key="edge.id"
:path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
:color="edge.color" :label="edge.label" />
</svg>
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
<template v-for="edge of props.canvas.edges">
<div :key="edge.id" v-if="edge.label" class="canvas-path-label-wrapper"
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
<div class="canvas-path-label">{{ edge.label }}</div>
</div>
</template>
</div>
</div>
</template>
<template #fallback>
<div class="loading"></div>
</template>
</Suspense>
</template>

View File

@@ -1,11 +1,14 @@
<script setup lang="ts">
const route = useRoute();
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
const project = computed(() => parseInt(route.params.projectId as string));
const { data: navigation, execute, status, error } = await useFetch(() => `/api/project/${project.value}/navigation`, {
immediate: false,
});
const { data: navigation, refresh } = await useLazyFetch(() => `/api/project/${project.value}/navigation`, { immediate: false });
if(!isNaN(project.value))
{
await refresh();
}
</script>
<template>

View File

@@ -1,76 +0,0 @@
<template>
<Teleport to="#teleports">
<div class="popover hover-popover" :class="{'is-loaded': pending === false && content !== ''}" :style="{'top': (pos?.y ?? 0) - 4, 'left': (pos?.y ?? 0) + 4}">
<template v-if="display">
<div v-if="pending" class="loading"></div>
<div class="markdown-embed" v-else-if="content !== ''">
<div class="markdown-preview-view markdown-rendered">
<div class="markdown-embed-content">
<h1>{{ title }}</h1>
<Markdown v-model="content"></Markdown>
</div>
</div>
</div>
<div class="not-found-container" v-else>
<div class="not-found-image"></div>
<div class="not-found-title">Impossible d'afficher (ou vide)</div>
<div class="not-found-description">Cette page est actuellement vide ou impossible à traiter</div>
</div>
</template>
</div>
</Teleport>
<span ref="el" @mouseenter="show" @mouseleave="hide">
<slot></slot>
</span>
</template>
<script setup lang="ts">
import { useDebounceFn as debounce } from '@vueuse/core';
const props = defineProps({
project: {
type: Number,
required: true,
},
path: {
type: String,
required: true,
},
anchor: {
type: String,
required: false,
}
})
const content = ref(''), title = ref(''), display = ref(false), pending = ref(false);
const el = ref(), pos = ref<DOMRect>();
watch(display, () => display.value && !pending.value && content.value === '' && fetch());
onMounted(() => {
if(el && el.value)
{
pos.value = (el.value as HTMLDivElement).getBoundingClientRect();
debugger;
}
})
async function fetch()
{
pending.value = true;
const data = await $fetch(`/api/project/${props.project}/file`, {
method: 'get',
query: {
path: props.path,
},
});
pending.value = false;
if(data && data[0])
{
content.value = data[0].content;
title.value = data[0].title;
}
}
const show = debounce(() => display.value = true, 250), hide = debounce(() => display.value = false, 250);
</script>

View File

@@ -0,0 +1,112 @@
<template>
<Teleport to="#teleports" v-if="display && (!fetched || loaded)">
<div class="popover hover-popover" :class="{'is-loaded': fetched}" :style="pos"
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => display = false, 250)">
<div v-if="pending" class="loading"></div>
<template v-else-if="content !==''">
<div v-if="type === 'Markdown'" class=" markdown-embed">
<div class="markdown-embed-content">
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
<div class="markdown-preview-sizer markdown-preview-section">
<h1>{{ title }}</h1>
<Markdown v-model="content"></Markdown>
</div>
</div>
</div>
</div>
<div v-else-if="type === 'Canvas'" class="canvas-embed is-loaded">
<CanvasRenderer :canvas="JSON.parse(content) " />
</div>
<div class="not-found-container" v-else>
<div class="not-found-image"></div>
<div class="not-found-title">Fichier vide</div>
<div class="not-found-description">Cette page est vide</div>
</div>
</template>
<div class="not-found-container" v-else>
<div class="not-found-image"></div>
<div class="not-found-title">Impossible d'afficher</div>
<div class="not-found-description">Cette page est impossible à traiter</div>
</div>
</div>
</Teleport>
<span ref="el" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => display = false, 250)">
<slot></slot>
</span>
</template>
<script setup lang="ts">
const props = defineProps({
project: {
type: Number,
required: true,
},
path: {
type: String,
required: true,
},
anchor: {
type: String,
required: false,
}
})
const content = ref(''), title = ref(''), type = ref(''), display = ref(false), pending = ref(false), fetched = ref(false);
const el = ref(), pos = ref<Record<string, string>>();
const loaded = computed(() => !pending.value && fetched.value && content.value !== '');
async function fetch()
{
fetched.value = true;
pending.value = true;
const data = await $fetch(`/api/project/${props.project}/file`, {
method: 'get',
query: {
path: props.path,
},
});
pending.value = false;
if(data && data[0])
{
content.value = data[0].content;
title.value = data[0].title;
type.value = data[0].type;
}
}
async function show()
{
if(display.value)
return;
if(!fetched.value)
await fetch();
const rect = (el.value as HTMLDivElement)?.getBoundingClientRect();
if(!rect)
return;
const r: Record<string, string> = {};
if (rect.bottom + 450 < window.innerHeight)
r.top = (rect.bottom + 4) + "px";
else
r.bottom = (window.innerHeight - rect.top + 4) + "px";
if (rect.right + 550 < window.innerWidth)
r.left = rect.left + "px";
else
r.right = (window.innerWidth - rect.right + 4) + "px";
pos.value = r;
display.value = true;
}
let debounceFn: () => void, timeout: NodeJS.Timeout;
function debounce(fn: () => void, ms: number)
{
debounceFn = fn;
clearTimeout(timeout);
timeout = setTimeout(debounceFn, ms);
}
</script>

View File

@@ -1,11 +1,16 @@
<template>
<NuxtLink v-if="data && data[0] && status !== 'pending'" :to="{ path: `/explorer/${project}${data[0].path}`, hash: hash }" :class="class">
<NuxtLink class="preview-link" v-if="data && data[0] && status !== 'pending'"
:to="{ path: `/explorer/${project}${data[0].path}`, hash: hash }" :class="class">
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
<slot v-bind="$attrs"></slot>
<ThemeIcon class="link-icon" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
:icon="`link-${data[0].type.toLowerCase()}`" />
</PreviewContent>
</NuxtLink>
<NuxtLink v-else-if="href" :to="{ path: href }" :class="class">
<slot v-bind="$attrs"></slot>
<ThemeIcon class="link-icon" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
:icon="`link-${data[0].type.toLowerCase()}`" />
</NuxtLink>
<slot :class="class" v-else v-bind="$attrs"></slot>
</template>
@@ -32,7 +37,7 @@ const { data, status } = await useFetch(`/api/project/${project.value}/file`, {
query: {
search: `%${pathname}`
},
transform: (data) => data?.map(e => ({ path: e.path })),
transform: (data) => data?.map(e => ({ path: e.path, type: e.type })),
key: `file:${project.value}:%${pathname}`,
dedupe: 'defer'
});