First version

This commit is contained in:
2024-01-08 01:00:37 +01:00
commit e70ab97b8b
88 changed files with 35689 additions and 0 deletions

35
components/CanvasEdge.vue Normal file
View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
interface Props
{
path: {
path: string;
to: { x: number; y: number };
side: 'bottom' | 'top' | 'left' | 'right';
};
color?: string;
}
const props = defineProps<Props>();
const rotation = {
top: "180",
bottom: "0",
left: "90",
right: "270"
};
function hexToRgb(hex: string): string {
return `${parseInt(hex.substring(1, 3), 16)}, ${parseInt(hex.substring(3, 5), 16)}, ${parseInt(hex.substring(5, 7), 16)}`;
}
const classes: any = { 'is-themed': props.color !== undefined, 'mod-canvas-color-custom': (props?.color?.startsWith('#') ?? false) };
if (props.color !== undefined) {
if (!props.color.startsWith('#'))
classes['mod-canvas-color-' + props.color] = true;
}
</script>
<template>
<g :class="classes" :style="{ '--canvas-color': props?.color?.startsWith('#') ? hexToRgb(props.color) : undefined }"><path class="canvas-display-path" :d="props.path.path"></path></g>
<g :class="classes" :style="{ '--canvas-color': props?.color?.startsWith('#') ? hexToRgb(props.color) : undefined, transform: `translate(${props.path.to.x}px, ${props.path.to.y}px) rotate(${rotation[props.path.side]}deg)` }"><polygon class="canvas-path-end" points="0,0 6.5,10.4 -6.5,10.4"></polygon></g>
</template>

53
components/CanvasNode.vue Normal file
View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { CanvasNode } from '~/types/canvas';
interface Props {
node: CanvasNode;
zoom: number;
}
function hexToRgb(hex: string): string
{
return `${parseInt(hex.substring(1, 3), 16)}, ${parseInt(hex.substring(3, 5), 16)}, ${parseInt(hex.substring(5, 7), 16)}`;
}
const props = defineProps<Props>();
const classes: any = { 'canvas-node-group': props.node.type === 'group', 'is-themed': props.node.color !== undefined, 'mod-canvas-color-custom': (props.node?.color?.startsWith('#') ?? false) };
if(props.node.color !== undefined)
{
if (!props.node.color.startsWith('#'))
classes['mod-canvas-color-' + props.node.color] = true;
}
</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': props.node?.color?.startsWith('#') ? hexToRgb(props.node.color) : undefined}">
<div class="canvas-node-container">
<template v-if="props.node.type === 'group' || props.zoom > 0.5">
<div class="canvas-node-content markdown-embed">
<div v-if="props.node.text?.body?.children?.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">
<ContentRenderer :value="props.node.text"/>
</div>
</div>
</div>
</div>
</template>
<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">
<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>
</svg>
</div>
</div>
</template>
</div>
<div v-if="props.node.type === 'group' && props.node.label !== undefined" class="canvas-group-label">{{ props.node.label }}</div>
</div>
</template>

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import("~/assets/canvas.css")
import type { CanvasNode, CanvasEdge } from '~/types/canvas';
interface CanvasProps
{
_id: string;
_type: string;
body: { nodes: CanvasNode[], edges: CanvasEdge[] };
}
interface Props
{
canvas: CanvasProps;
}
const props = defineProps<Props>();
let dragging = false, posX = 0, posY = 0, dispX = ref(0), dispY = ref(0), minZoom = ref(0.3), zoom = ref(1);
let centerX = ref(0), centerY = ref(0), canvas = ref<HTMLDivElement>();
onMounted(async () => {
await nextTick();
let minX = +Infinity, minY = +Infinity, maxX = -Infinity, maxY = -Infinity;
props.canvas.body.nodes.forEach((e) => {
minX = Math.min(minX, e.x);
minY = Math.min(minY, e.y);
maxX = Math.max(maxX, e.x + e.width);
maxY = Math.max(maxY, e.y + e.height);
});
minZoom.value = Math.min((canvas.value?.clientWidth ?? 0) / (maxX - minX), (canvas.value?.clientHeight ?? 0) / (maxY - minY)) * 0.9;
centerX.value = (canvas.value?.clientWidth ?? 0) / 2;
centerY.value = (canvas.value?.clientHeight ?? 0) / 2;
dispX.value = -(canvas.value?.clientWidth ?? 0) / 2;
dispY.value = -(canvas.value?.clientHeight ?? 0) / 2;
})
const onPointerDown = (event) => {
if (event.isPrimary === false) return;
event.target.setPointerCapture(event.pointerId);
dragging = true;
posX = event.clientX;
posY = event.clientY;
document.addEventListener('pointermove', onPointerMove);
document.addEventListener('pointerup', onPointerUp);
}
const onPointerMove = (event) => {
if (event.isPrimary === false) return;
dispX.value -= (posX - event.clientX) / zoom.value;
dispY.value -= (posY - event.clientY) / zoom.value;
posX = event.clientX;
posY = event.clientY;
}
const onPointerUp = (event) => {
if (event.isPrimary === false) return;
dragging = false;
document.removeEventListener('pointermove', onPointerMove);
document.removeEventListener('pointerup', onPointerUp);
}
const onWheel = (event: WheelEvent) => {
zoom.value *= 1 + (event.deltaY * -0.001);
if (zoom.value > 3)
zoom.value = 3;
if (zoom.value < minZoom.value)
zoom.value = minZoom.value;
}
function clamp(x: number, min: number, max: number): number {
if (x > max)
return max;
if (x < min)
return min;
return x;
}
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, n: number): { x: number, y: number } {
switch (side) {
case "left":
return {
x: pos.x - n,
y: pos.y
};
case "right":
return {
x: pos.x + n,
y: pos.y
};
case "top":
return {
x: pos.x,
y: pos.y - n
};
case "bottom":
return {
x: pos.x,
y: pos.y + n
}
}
}
function getNode(id: string): CanvasNode | undefined
{
return props.canvas.body.nodes.find(e => e.id === id);
}
function mK(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
switch (t) {
case "top":
return { x: (e.minX + e.maxX) / 2, y: e.minY };
case "right":
return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
case "bottom":
return { x: (e.minX + e.maxX) / 2, y: e.maxY };
case "left":
return { x: e.minX, y: (e.minY + e.maxY) / 2 };
}
}
function bbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
}
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
if(from === undefined || to === undefined)
{
return {
path: '',
to: {},
toSide: '',
}
}
const a = mK(bbox(from), fromSide),
l = mK(bbox(to), toSide);
return bezier(a, fromSide, l, toSide);
}
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
return {
path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
to: to,
side: toSide,
};
}
</script>
<template>
<div @pointerdown="onPointerDown" @wheel.passive="onWheel"
@touchstart.prevent="" @dragstart.prevent="" class="canvas-wrapper node-insert-event mod-zoomed-out">
<div class="canvas-controls" style="z-index: 421;">
<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="zoom = minZoom; dispX = -(canvas?.clientWidth ?? 0) / 2; dispY = -(canvas?.clientHeight ?? 0) / 2;" 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 ref="canvas" 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.body.edges" :key="edge.id" :path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)" :color="edge.color"/>
</svg>
<CanvasNode v-for="node of props.canvas.body.nodes" :key="node.id" :node="node" :zoom="zoom" />
</div>
</div>
</template>

View File

@@ -0,0 +1,26 @@
<template>
<div class="site-body-left-column">
<div class="site-body-left-column-inner">
<NuxtLink class="site-body-left-column-site-logo" aria-label="Developer Documentation logo" :href="'/Home'">
<img aria-hidden="true" src="https://publish-01.obsidian.md/access/caa27d6312fe5c26ebc657cc609543be/Assets/obsidian-lockup-docs.svg" style="">
</NuxtLink>
<NuxtLink class="site-body-left-column-site-name" aria-label="Accueil" :href="'/Home'">Accueil</NuxtLink>
<ThemeSwitch />
<SearchView />
<div class="nav-view-outer">
<div class="nav-view">
<div class="tree-item">
<div class="tree-item-self mod-root is-clickable" data-path="">
<div class="tree-item-inner"></div>
</div>
<div class="tree-item-children">
<ContentNavigation v-slot="{ navigation }">
<NavigationLink v-if="!!navigation" v-for="link of navigation" :link="link" />
</ContentNavigation>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,16 @@
<template>
<div class="site-body-center-column">
<div class="site-header">
<div class="clickable-icon">
<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-menu">
<line x1="4" y1="12" x2="20" y2="12"></line>
<line x1="4" y1="6" x2="20" y2="6"></line>
<line x1="4" y1="18" x2="20" y2="18"></line>
</svg>
</div>
</div>
<div class="render-container">
<NuxtPage />
</div>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
interface NavItem {
title: string
_path: string
_id?: string
_draft?: boolean
children?: NavItem[]
[key: string]: any
}
interface Props {
link: NavItem;
}
const props = defineProps<Props>();
const hasChildren = computed(() => {
return props.link && props.link.children && props.link.children.length > 0 || false;
});
const collapsed = ref(!useRoute().path.startsWith(props.link._path));
</script>
<template>
<div class="tree-item">
<template v-if="hasChildren">
<div class="tree-item-self" :class="{ 'is-collapsed': collapsed, 'mod-collapsible is-clickable': hasChildren }" data-path="{{ props.link.title }}" @click="collapsed = hasChildren && !collapsed">
<div v-if="hasChildren" class="tree-item-icon collapse-icon">
<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 right-triangle">
<path d="M3 8L12 17L21 8"></path>
</svg>
</div>
<div class="tree-item-inner">{{ props.link.title }}</div>
</div>
<div v-if="!collapsed" class="tree-item-children">
<NavigationLink v-if="hasChildren" v-for="link of props.link.children" :link="link"/>
</div>
</template>
<NuxtLink v-else class="tree-item-self" :to="props.link._path" :active-class="'mod-active'">
<div class="tree-item-inner">{{ props.link.title }}</div>
</NuxtLink>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
const { toc } = useContent()
</script>
<template>
<div class="site-body-right-column">
<div class="site-body-right-column-inner">
<div v-if="!!toc" class="outline-view-outer node-insert-event">
<div class="list-item published-section-header">
<span class="published-section-header-icon">
<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-list">
<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
</span>
<span>Sur cette page</span>
</div>
<div class="outline-view">
<TocLink v-for="link of toc.links" :link="link"/>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
const { data: nav } = await useAsyncData('search', () => fetchContentNavigation());
const input = ref('');
const pos = ref<DOMRect>();
function getPos(e: Event)
{
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
}
function flatten(val: NavItem[]): NavItem[] {
return val.flatMap ? val?.flatMap((e: NavItem) => e.children ? flatten(e.children) : e) : val;
}
function clear(text: string): string
{
return text.toLowerCase().trim().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const navigation = computed(() => {
return flatten(nav.value ?? []);
})
const results = computed(() => {
return navigation.value?.filter((e) => clear(e.title).includes(clear(input.value))) ?? [];
})
</script>
<template>
<div class="search-view-outer" ref="">
<div class="search-view-container">
<span class="published-search-icon"></span>
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="getPos">
</div>
</div>
<Teleport to="body" v-if="input !== ''">
<div class="search-results" :style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px'}">
<div class="suggestion-item" v-if="results.length > 0" v-for="result of results">
<div class="suggestion-content">
<div class="suggestion-title">
{{ result.title.substring(0, clear(result.title).indexOf(clear(input))) }}<span class="suggestion-highlight">{{ result.title.substring(clear(result.title).indexOf(clear(input)), clear(result.title).indexOf(clear(input)) + clear(input).length + 1) }}</span>{{ result.title.substring(clear(result.title).indexOf(clear(input)) + clear(input).length + 1) }}
</div>
</div>
</div>
<div class="suggestion-empty" v-else>
Aucun résultat
</div>
</div>
</Teleport>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
const colorMode = useColorMode()
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
</script>
<template>
<div class="site-body-left-column-site-theme-toggle" :class="{'is-dark': isDark}" style="">
<span class="option mod-dark">
<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-moon">
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
</svg>
</span>
<div class="checkbox-container" :class="{'is-enabled': isDark}" @click="isDark = !isDark"></div>
<span class="option mod-light">
<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-sun">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="m4.93 4.93 1.41 1.41"></path>
<path d="m17.66 17.66 1.41 1.41"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
<path d="m6.34 17.66-1.41 1.41"></path>
<path d="m19.07 4.93-1.41 1.41"></path>
</svg>
</span>
</div>
</template>

28
components/TocLink.vue Normal file
View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
interface TocItem {
text: string
id: string
children?: TocItem[]
[key: string]: any
}
interface Props {
link: TocItem;
}
const props = defineProps<Props>();
const hasChildren = computed(() => {
return props.link && props.link.children && props.link.children.length > 0 || false;
});
</script>
<template>
<div class="tree-item">
<div class="tree-item-self" :class="{'is-clickable': hasChildren}" data-path="{{ props.link.title }}">
<a class="tree-item-inner" :href="'#' + props.link.id">{{ props.link.text }}</a>
</div>
<div class="tree-item-children">
<TocLink v-if="hasChildren" v-for="link of props.link.children" :link="link" />
</div>
</div>
</template>

View File

@@ -0,0 +1,39 @@
import type { ContentNavigation } from '#build/components';
<script setup lang="ts">
const props = defineProps({
href: {
type: String,
default: ''
},
target: {
type: String,
default: undefined,
required: false
}
})
function sluggify(s: string): string {
return s
.split("/")
.map((segment) => segment.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/^\d\. */g, '').replace(/\s/g, "-").replace(/%/g, "-percent").replace(/\?/g, "-q").toLowerCase()) // slugify all segments
.filter(e => !!e)
.join("/") // always use / as sep
.replace(/\/$/, "")
}
const href = (props.href.includes('#') ? props.href.substring(0, props.href.indexOf('#')) : props.href).replace(/\..*$/, '');
const anchor = props.href.includes('#') ? props.href.substring(props.href.indexOf('#'), props.href.length) : '';
let content: any;
try {
content = await queryContent().where({ _path: new RegExp(sluggify(href) + '$', 'i') }).findOne();
} catch(e) {
}
</script>
<template >
<NuxtLink :href="(content?._path ?? href + anchor) ?? href" :target="target">
<slot />
</NuxtLink>
</template>