Compare commits
No commits in common. "1b2472bc1a7c8b68b687c6ab145282a42779b749" and "fa2d8e50353260708a6ef49370eb495b2646a715" have entirely different histories.
1b2472bc1a
...
fa2d8e5035
|
|
@ -5,6 +5,7 @@
|
|||
.nitro
|
||||
.cache
|
||||
dist
|
||||
content
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
|
@ -22,3 +23,5 @@ logs
|
|||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
db.sqlite-*
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "client: chrome",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "server: nuxt",
|
||||
"outputCapture": "std",
|
||||
"program": "${workspaceFolder}/node_modules/nuxi/bin/nuxi.mjs",
|
||||
"args": [
|
||||
"dev"
|
||||
],
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "fullstack: nuxt",
|
||||
"configurations": [
|
||||
"server: nuxt",
|
||||
"client: chrome"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
49
app.vue
49
app.vue
|
|
@ -1,12 +1,49 @@
|
|||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-light-40;
|
||||
@apply dark:bg-dark-40;
|
||||
@apply rounded-md;
|
||||
@apply border-2;
|
||||
@apply border-solid;
|
||||
@apply border-transparent;
|
||||
@apply bg-clip-padding;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-light-50;
|
||||
@apply dark:bg-dark-50;
|
||||
}
|
||||
|
||||
.loading {
|
||||
@apply w-full;
|
||||
@apply h-full;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply before:border-4;
|
||||
@apply before:border-accent-purple;
|
||||
@apply before:border-t-transparent;
|
||||
@apply before:rounded-full;
|
||||
@apply before:w-8;
|
||||
@apply before:h-8;
|
||||
@apply before:animate-spin;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const page = ref();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||
<NuxtRouteAnnouncer/>
|
||||
<NuxtRouteAnnouncer></NuxtRouteAnnouncer>
|
||||
<NuxtLayout>
|
||||
<TooltipProvider>
|
||||
<ToastProvider>
|
||||
<NuxtPage></NuxtPage>
|
||||
</ToastProvider>
|
||||
</TooltipProvider>
|
||||
<NuxtPage ref="page"></NuxtPage>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
### Librairies, framework et outils libres utilisés:
|
||||
- [Vuejs](https://vuejs.org/) - MIT License - Copyright (c) 2018-present, Yuxi (Evan) You and Vue contributors
|
||||
- [vue-router](https://router.vuejs.org/) - MIT License - Copyright (c) 2019-present Eduardo San Martin Morote
|
||||
- [Nuxt](https://nuxt.com/) - MIT License - Copyright (c) 2016-present - Nuxt Team
|
||||
- [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils) - MIT License - Copyright (c) 2023 Sébastien Chopin
|
||||
|
||||
Le logo a été créé grace aux icones de [Game Icons](https://game-icons.net).
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
<style>
|
||||
.editor
|
||||
{
|
||||
white-space: pre-line;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="editor">
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<MarkdownRenderer
|
||||
v-if="node"
|
||||
:key="key"
|
||||
:node="node"
|
||||
:proses="{
|
||||
'a': LiveA,
|
||||
'h1': LiveH1,
|
||||
'h2': LiveH2,
|
||||
'h3': LiveH3,
|
||||
'h4': LiveH4,
|
||||
'h5': LiveH5,
|
||||
'h6': LiveH6,
|
||||
'blockquote': LiveBlockquote,
|
||||
}"
|
||||
></MarkdownRenderer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LiveA from "~/components/prose/live/LiveA.vue";
|
||||
import LiveH1 from "~/components/prose/live/LiveH1.vue";
|
||||
import LiveH2 from "~/components/prose/live/LiveH2.vue";
|
||||
import LiveH3 from "~/components/prose/live/LiveH3.vue";
|
||||
import LiveH4 from "~/components/prose/live/LiveH4.vue";
|
||||
import LiveH5 from "~/components/prose/live/LiveH5.vue";
|
||||
import LiveH6 from "~/components/prose/live/LiveH6.vue";
|
||||
import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
||||
|
||||
import { hash } from 'ohash'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root, Node } from 'hast';
|
||||
/* import { diffLines as diff } from 'diff'; */
|
||||
|
||||
const model = defineModel<string>();
|
||||
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const node = ref<Root>(), changes = ref();
|
||||
|
||||
watch(model, update);
|
||||
update(model.value, "");
|
||||
|
||||
async function update(value: string | undefined, old: string | undefined) {
|
||||
if(value && old)
|
||||
{
|
||||
if(node.value)
|
||||
{
|
||||
/* const differences = diff(old, value, {
|
||||
newlineIsToken: true,
|
||||
});
|
||||
|
||||
let removeStart = 0, removeEnd = 0; //Character count
|
||||
let addStart = 0, addEnd = 0; //Character count
|
||||
|
||||
const needAdd = differences.find(e => e.added) !== undefined;
|
||||
const needRemove = differences.find(e => e.removed) !== undefined;
|
||||
|
||||
for(const difference of differences)
|
||||
{
|
||||
if(!difference.added && !difference.removed)
|
||||
{
|
||||
removeStart += difference.value.length;
|
||||
addStart += difference.value.length;
|
||||
}
|
||||
else if(difference.added)
|
||||
{
|
||||
addEnd = addStart + difference.value.length;
|
||||
}
|
||||
else if(difference.removed)
|
||||
{
|
||||
removeEnd = removeStart + difference.value.length;
|
||||
}
|
||||
|
||||
if((!needAdd || addEnd !== 0) && (!needRemove || removeEnd !== 0))
|
||||
break;
|
||||
}
|
||||
|
||||
const oldNodes = getNodes(node.value.children, removeStart - 1, removeEnd + 1);
|
||||
let newNodes;
|
||||
|
||||
if(oldNodes.length === 0)
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
const newStart = oldNodes[0].position?.start.offset;
|
||||
const newEnd = oldNodes[oldNodes.length - 1].position?.end.offset;
|
||||
|
||||
const lengthDiff = value.length - old.length;
|
||||
|
||||
newNodes = parser(value.substring(newStart ?? 0, (newEnd ?? 0) + lengthDiff));
|
||||
|
||||
const root = node.value;
|
||||
|
||||
node.value = parser(value);
|
||||
} */
|
||||
node.value = parser(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
else if(value)
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
function getNodes(nodes: Node[], start: number, end: number)
|
||||
{
|
||||
return nodes.filter(e => (e.position?.start.offset ?? 0) <= end && (e.position?.end.offset ?? 0) >= start);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<template>
|
||||
<ToastRoot class="ToastRoot bg-light-10 dark:bg-dark-10 p-3 border border-light-30 dark:border-dark-30" v-model:open="model">
|
||||
<ToastTitle asChild><slot name="title"></slot></ToastTitle>
|
||||
<ToastDescription asChild><slot></slot></ToastDescription>
|
||||
<ToastClose v-if="closeable"><button>×</button></ToastClose>
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-96 z-50 outline-none" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>();
|
||||
|
||||
const { closeable = true } = defineProps<{
|
||||
closeable?: boolean
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ToastRoot[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
.ToastRoot[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
.ToastRoot[data-swipe='end'] {
|
||||
animation: swipeRight 100ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes swipeRight {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<template>
|
||||
<TooltipRoot :delay-duration="delay">
|
||||
<TooltipTrigger asChild>
|
||||
<span tabindex="0"><slot></slot></span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent class="TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70" :side="side" :side-offset="['left', 'right'].includes(side ?? '') ? 8 : 0">
|
||||
{{ message }}
|
||||
<TooltipArrow class="fill-light-30 dark:fill-dark-30"></TooltipArrow>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { message, delay = 300, side } = defineProps<{
|
||||
message: string
|
||||
delay?: number
|
||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.TooltipContent {
|
||||
animation-duration: .3s;
|
||||
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.TooltipContent[data-side="top"] {
|
||||
animation-name: slideUp;
|
||||
}
|
||||
.TooltipContent[data-side="bottom"] {
|
||||
animation-name: slideDown;
|
||||
}
|
||||
.TooltipContent[data-side="left"] {
|
||||
animation-name: slideLeft;
|
||||
}
|
||||
.TooltipContent[data-side="right"] {
|
||||
animation-name: slideRight;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<script setup lang="ts">
|
||||
import type { CanvasColor } from "~/types/canvas";
|
||||
|
||||
type Direction = 'bottom' | 'top' | 'left' | 'right';
|
||||
interface Props
|
||||
{
|
||||
path: {
|
||||
path: string;
|
||||
from: { x: number; y: number };
|
||||
to: { x: number; y: number };
|
||||
side: Direction;
|
||||
};
|
||||
color?: CanvasColor;
|
||||
label?: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const rotation: Record<Direction, string> = {
|
||||
top: "180",
|
||||
bottom: "0",
|
||||
left: "90",
|
||||
right: "270"
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<g :style="{'--canvas-color': color?.hex}" class="z-0">
|
||||
<path :style="`stroke-linecap: butt; stroke-width: calc(3px * var(--zoom-multiplier));`" :class="color?.class ? `stroke-light-${color.class} dark:stroke-dark-${color.class}` : ((color && color?.hex !== undefined) ? 'stroke-[color:var(--canvas-color)]' : 'stroke-light-40 dark:stroke-dark-40')" class="fill-none stroke-[4px]" :d="path.path"></path>
|
||||
<g :style="`transform: translate(${path.to.x}px, ${path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path.side]}deg);`">
|
||||
<polygon :class="color?.class ? `fill-light-${color.class} dark:fill-dark-${color.class}` : ((color && color?.hex !== undefined) ? 'fill-[color:var(--canvas-color)]' : 'fill-light-40 dark:fill-dark-40')" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import type { CanvasNode } from '~/types/canvas';
|
||||
|
||||
interface Props {
|
||||
node: CanvasNode;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const size = Math.max(props.node.width, props.node.height);
|
||||
const colors = computed(() => {
|
||||
if(props.node.color)
|
||||
{
|
||||
const color = props.node.color;
|
||||
return color?.class ? { bg: `bg-light-${color?.class} dark:bg-dark-${color?.class}`, border: `border-light-${color?.class} dark:border-dark-${color?.class}`} : { bg: `bg-colored`, border: `border-[color:var(--canvas-color)]` };
|
||||
}
|
||||
else
|
||||
{
|
||||
return { border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` };
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bg-colored
|
||||
{
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
||||
<div :class="[colors.border]" class="border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex">
|
||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="colors.bg">
|
||||
<template v-if="node.type === 'group' || zoom > Math.min(0.4, 1000 / size)">
|
||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||
<Markdown :content="node.text" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-1 justify-center items-center bg-light-30 dark:bg-dark-30">
|
||||
<div class="">
|
||||
<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>
|
||||
<div v-if="node.type === 'group' && node.label !== undefined" :class="[colors.border]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
<script setup lang="ts">
|
||||
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||
import { clamp } from '#imports';
|
||||
|
||||
interface Props
|
||||
{
|
||||
canvas: CanvasContent;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||
const canvas = useTemplateRef('canvas');
|
||||
|
||||
const reset = (_: MouseEvent) => {
|
||||
zoom.value = minZoom.value;
|
||||
|
||||
dispX.value = 0;
|
||||
dispY.value = 0;
|
||||
}
|
||||
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, offset: number): { x: number, y: number } {
|
||||
switch (side) {
|
||||
case "left":
|
||||
return {
|
||||
x: pos.x - offset,
|
||||
y: pos.y
|
||||
};
|
||||
case "right":
|
||||
return {
|
||||
x: pos.x + offset,
|
||||
y: pos.y
|
||||
};
|
||||
case "top":
|
||||
return {
|
||||
x: pos.x,
|
||||
y: pos.y - offset
|
||||
};
|
||||
case "bottom":
|
||||
return {
|
||||
x: pos.x,
|
||||
y: pos.y + offset
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNode(id: string): CanvasNode | undefined
|
||||
{
|
||||
return props.canvas.nodes.find(e => e.id === id);
|
||||
}
|
||||
function posFromDir(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 getBbox(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: '',
|
||||
from: {},
|
||||
to: {},
|
||||
toSide: '',
|
||||
}
|
||||
}
|
||||
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||
return bezier(start, fromSide, end, 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}`,
|
||||
from: from,
|
||||
to: to,
|
||||
side: toSide,
|
||||
};
|
||||
}
|
||||
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
|
||||
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||
const len = Math.hypot(start.x - end.x, start.y - end.y), offset = clamp(len / 2, 70, 150), b = edgePos(fromSide, start, offset), s = edgePos(toSide, end, offset);
|
||||
const center = getCenter(start, end, b, s, 0.5);
|
||||
return `translate(${center.x}px, ${center.y}px)`;
|
||||
}
|
||||
function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r: { x: number, y: number }, o: { x: number, y: number }, e: number): { x: number, y: number } {
|
||||
const a = 1 - e, s = a * a * a, l = 3 * e * a * a, c = 3 * e * e * a, u = e * e * e;
|
||||
return {
|
||||
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
|
||||
};
|
||||
}
|
||||
/*
|
||||
|
||||
stroke-light-red
|
||||
stroke-light-orange
|
||||
stroke-light-yellow
|
||||
stroke-light-green
|
||||
stroke-light-cyan
|
||||
stroke-light-purple
|
||||
dark:stroke-dark-red
|
||||
dark:stroke-dark-orange
|
||||
dark:stroke-dark-yellow
|
||||
dark:stroke-dark-green
|
||||
dark:stroke-dark-cyan
|
||||
dark:stroke-dark-purple
|
||||
fill-light-red
|
||||
fill-light-orange
|
||||
fill-light-yellow
|
||||
fill-light-green
|
||||
fill-light-cyan
|
||||
fill-light-purple
|
||||
dark:fill-dark-red
|
||||
dark:fill-dark-orange
|
||||
dark:fill-dark-yellow
|
||||
dark:fill-dark-green
|
||||
dark:fill-dark-cyan
|
||||
dark:fill-dark-purple
|
||||
bg-light-red
|
||||
bg-light-orange
|
||||
bg-light-yellow
|
||||
bg-light-green
|
||||
bg-light-cyan
|
||||
bg-light-purple
|
||||
dark:bg-dark-red
|
||||
dark:bg-dark-orange
|
||||
dark:bg-dark-yellow
|
||||
dark:bg-dark-green
|
||||
dark:bg-dark-cyan
|
||||
dark:bg-dark-purple
|
||||
border-light-red
|
||||
border-light-orange
|
||||
border-light-yellow
|
||||
border-light-green
|
||||
border-light-cyan
|
||||
border-light-purple
|
||||
dark:border-dark-red
|
||||
dark:border-dark-orange
|
||||
dark:border-dark-yellow
|
||||
dark:border-dark-green
|
||||
dark:border-dark-cyan
|
||||
dark:border-dark-purple
|
||||
|
||||
*/
|
||||
|
||||
const dragHandler = useDrag(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||
event?.preventDefault();
|
||||
dispX.value += x / zoom.value;
|
||||
dispY.value += y / zoom.value;
|
||||
}, {
|
||||
domTarget: canvas,
|
||||
eventOptions: { passive: false, }
|
||||
})
|
||||
const pinchHandler = usePinch(({ event: Event, offset: [z] }: { event: Event, offset: number[] }) => {
|
||||
event?.preventDefault();
|
||||
console.log(z);
|
||||
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
||||
}, {
|
||||
domTarget: canvas,
|
||||
eventOptions: { passive: false, }
|
||||
})
|
||||
const wheelHandler = useWheel(({ event: Event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
||||
event?.preventDefault();
|
||||
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
||||
}, {
|
||||
domTarget: canvas,
|
||||
eventOptions: { passive: false, }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
||||
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-30 overflow-hidden">
|
||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer"
|
||||
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">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5v14"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" 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">
|
||||
<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="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" 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">
|
||||
<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="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer"
|
||||
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">
|
||||
<path d="M5 12h14"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none"
|
||||
:style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
||||
<div>
|
||||
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
||||
</div>
|
||||
<template v-for="edge of props.canvas.edges">
|
||||
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
||||
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
|
||||
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="loading"></div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
comments: {
|
||||
type: [Object],
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="comments !== undefined && comments !== null" class="site-body-right-column">
|
||||
<div class="site-body-right-column-inner">
|
||||
<div>
|
||||
{{ comments.length }} commentaire{{ comments.length === 1 ? '' : 's' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
const project = computed(() => parseInt(route.params.projectId as string));
|
||||
|
||||
const { data: navigation, refresh } = await useFetch(() => `/api/project/${project.value}/navigation`, { immediate: false });
|
||||
if(!isNaN(project.value))
|
||||
{
|
||||
await refresh();
|
||||
}
|
||||
|
||||
const emit = defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-auto">
|
||||
<div class="absolute top-0 bottom-0 left-0 right-0 overflow-auto xl:px-4 px-2">
|
||||
<NavigationLink @navigate="e => emit('navigate')" class="ps-2" v-if="!!navigation" v-for="link of navigation" :project="project" :link="link" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
import type { Navigation } from '~/types/api';
|
||||
|
||||
interface Props {
|
||||
link: Navigation;
|
||||
project?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const hasChildren = computed(() => {
|
||||
return props.link && props.link.children && props.link.children.length > 0 || false;
|
||||
});
|
||||
|
||||
const collapsed = ref(!unifySlug(useRoute().params.slug).startsWith(props.link.path));
|
||||
|
||||
const emit = defineEmits(['navigate']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="py-1" v-if="project && !isNaN(project)">
|
||||
<Accordion v-if="hasChildren" v-model="collapsed" class="data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]">
|
||||
<template #header>{{ link.title }}</template>
|
||||
<template #content >
|
||||
<NavigationLink @navigate="e => emit('navigate')" class="border-light-40 dark:border-dark-40 ms-2 ps-4 border-l" v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
|
||||
</template>
|
||||
</Accordion>
|
||||
<NuxtLink v-else @click="e => emit('navigate')" class="text-light-100 dark:text-dark-100 cursor-pointer hover:text-opacity-75 xl:text-base text-sm flex justify-between items-center
|
||||
data-[type]:after:content-[attr(data-type)] data-[type]:after:border data-[type]:after:border-light-35 data-[type]:dark:after:border-dark-35
|
||||
data-[type]:after:text-sm data-[type]:after:px-1 data-[type]:after:bg-light-20 data-[type]:dark:after:bg-dark-20 data-[type]:after:[font-variant:all-petite-caps]" :to="{ path: `/explorer/${project}/${link.path}`, force: true }"
|
||||
active-class="!text-accent-blue relative before:border-l-2 before:border-accent-blue before:absolute before:-top-1 before:-bottom-1 before:-left-4 before:-mx-px" :data-type="(link.type === 'Canvas' ? 'graph' : (link.private ? 'privé' : undefined))">
|
||||
<div>{{ link.title }}</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<HoverPopup @before-show="fetch">
|
||||
<template #content>
|
||||
<Suspense suspensible>
|
||||
<div :class="[{'is-loaded': fetched}, file?.type === 'Markdown' ? 'overflow-auto' : 'overflow-hidden']">
|
||||
<div v-if="pending" class="loading w-[550px] h-[450px]"></div>
|
||||
<template v-else-if="!!file">
|
||||
<div v-if="file.type === 'Markdown'" class="p-6 ms-6">
|
||||
<ProseH1>{{ file.title }}</ProseH1>
|
||||
<Markdown :content="file.content"></Markdown>
|
||||
</div>
|
||||
<div v-else-if="file.type === 'Canvas'" class="w-[550px] h-[450px] overflow-hidden">
|
||||
<CanvasRenderer :canvas="JSON.parse(file.content) " />
|
||||
</div>
|
||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback><div class="loading w-[550px] h-[450px]"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template #default><slot name="default"></slot></template>
|
||||
</HoverPopup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CommentedFile } from '~/types/api';
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
const file = ref<CommentedFile | null>();
|
||||
const pending = ref(false), fetched = ref(false);
|
||||
|
||||
async function fetch()
|
||||
{
|
||||
if(fetched.value)
|
||||
return;
|
||||
|
||||
fetched.value = true;
|
||||
pending.value = true;
|
||||
|
||||
const data = await $fetch(`/api/project/${props.project}/file/${encodeURIComponent(props.path)}`);
|
||||
|
||||
pending.value = false;
|
||||
file.value = data;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<Suspense suspensible>
|
||||
<NuxtLink no-prefetch class="text-accent-blue inline-flex items-center" v-if="data && data[0]"
|
||||
:to="{ path: `/explorer/${project}/${data[0].path}`, hash: hash }" :class="class">
|
||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
||||
<template #default>
|
||||
<slot v-bind="$attrs"></slot>
|
||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||
</template>
|
||||
</PreviewContent>
|
||||
</NuxtLink>
|
||||
<NuxtLink no-prefetch v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
||||
<slot v-bind="$attrs"></slot>
|
||||
<Icon class="w-4 h-4 inline-block" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
||||
:icon="`icons/link-${data[0].type.toLowerCase()}`" />
|
||||
</NuxtLink>
|
||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { parseURL } from 'ufo';
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
required: false,
|
||||
}
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { hash, pathname, protocol } = parseURL(props.href);
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
const data = ref();
|
||||
|
||||
if(!!pathname && !protocol)
|
||||
{
|
||||
data.value = await $fetch(`/api/project/${project.value}/file`, {
|
||||
query: {
|
||||
search: `%${pathname}`
|
||||
},
|
||||
ignoreResponseError: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<blockquote ref="el">
|
||||
<slot />
|
||||
</blockquote>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
||||
{
|
||||
title.value = el.value.querySelector('.callout-title');
|
||||
title.value?.addEventListener('click', toggle);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
title.value?.removeEventListener('click', toggle);
|
||||
})
|
||||
function toggle() {
|
||||
el.value?.classList?.toggle('is-collapsed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
blockquote:not(.callout)
|
||||
{
|
||||
@apply ps-4;
|
||||
@apply my-4;
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@apply before:-top-1;
|
||||
@apply before:-bottom-1;
|
||||
@apply before:left-0;
|
||||
@apply before:w-1;
|
||||
@apply before:bg-light-30;
|
||||
@apply dark:before:bg-dark-30;
|
||||
}
|
||||
blockquote:empty
|
||||
{
|
||||
@apply before:hidden;
|
||||
}
|
||||
.callout {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
}
|
||||
.callout.is-collapsible .callout-title
|
||||
{
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.callout .fold
|
||||
{
|
||||
@apply transition-transform;
|
||||
}
|
||||
.callout.is-collapsed .fold
|
||||
{
|
||||
@apply -rotate-90;
|
||||
}
|
||||
.callout.is-collapsed > p
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.callout[datacallout="abstract"],
|
||||
.callout[datacallout="summary"],
|
||||
.callout[datacallout="tldr"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="info"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="todo"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="important"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="tip"],
|
||||
.callout[datacallout="hint"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="success"],
|
||||
.callout[datacallout="check"],
|
||||
.callout[datacallout="done"] {
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[datacallout="question"],
|
||||
.callout[datacallout="help"],
|
||||
.callout[datacallout="faq"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="warning"],
|
||||
.callout[datacallout="caution"],
|
||||
.callout[datacallout="attention"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="failure"],
|
||||
.callout[datacallout="fail"],
|
||||
.callout[datacallout="missing"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="danger"],
|
||||
.callout[datacallout="error"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="bug"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="example"] {
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
.callout
|
||||
{
|
||||
@apply overflow-hidden;
|
||||
@apply my-4;
|
||||
@apply p-3;
|
||||
@apply ps-6;
|
||||
@apply bg-blend-lighten;
|
||||
@apply !bg-opacity-25;
|
||||
@apply border-l-4;
|
||||
@apply inline-block;
|
||||
@apply pe-8;
|
||||
}
|
||||
.callout-icon
|
||||
{
|
||||
@apply w-6;
|
||||
@apply h-6;
|
||||
@apply stroke-2;
|
||||
}
|
||||
.callout-title
|
||||
{
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply gap-2;
|
||||
}
|
||||
.callout-title-inner
|
||||
{
|
||||
@apply inline-block;
|
||||
@apply font-bold;
|
||||
}
|
||||
.callout > p
|
||||
{
|
||||
@apply mt-2;
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<code><slot /></code>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<em>
|
||||
<slot />
|
||||
</em>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2 relative sm:right-8 right-4">
|
||||
<slot />
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2 relative sm:right-8 right-4">
|
||||
<slot />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
|
||||
<slot />
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h6 :id="parseId(id)">
|
||||
<slot />
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<hr class="border-light-35 dark:border-dark-35 m-4">
|
||||
</template>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<img
|
||||
:src="refinedSrc"
|
||||
:alt="alt"
|
||||
:width="width"
|
||||
:height="height"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
|
||||
import { useRuntimeConfig, computed, resolveComponent } from '#imports'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const refinedSrc = computed(() => {
|
||||
if (props.src?.startsWith('/') && !props.src.startsWith('//')) {
|
||||
const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
|
||||
if (_base !== '/' && !props.src.startsWith(_base)) {
|
||||
return joinURL(_base, props.src)
|
||||
}
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<li class="before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4"><slot /></li>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<ol>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<p><slot /></p>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.text-comment
|
||||
{
|
||||
@apply text-light-50;
|
||||
@apply dark:text-dark-50;
|
||||
@apply text-sm;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<pre :class="$props.class"><slot /></pre>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
highlights: {
|
||||
type: Array as () => number[],
|
||||
default: () => []
|
||||
},
|
||||
meta: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
pre code .line{display:block}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div v-if="isDev">
|
||||
Rendering the <code>script</code> element is dangerous and is disabled by default. Consider implementing your own <code>ProseScript</code> element to have control over script rendering.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const isDev = import.meta.dev
|
||||
</script>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<strong>
|
||||
<slot />
|
||||
</strong>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<table class="mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35">
|
||||
<slot />
|
||||
</table>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<HoverPopup @before-show="fetch">
|
||||
<template #content>
|
||||
<Suspense suspensible>
|
||||
<div class="mw-[400px]">
|
||||
<div v-if="fetched === false" class="loading w-[400px] h-[150px]"></div>
|
||||
<template v-else-if="!!data">
|
||||
<div v-if="data.description" class="pb-4 pt-3 px-8">
|
||||
<span class="text-2xl font-semibold">#{{ data.tag }}</span>
|
||||
<Markdown :content="data.description"></Markdown>
|
||||
</div>
|
||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Fichier vide</div>
|
||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est vide</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center" v-else>
|
||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Impossible d'afficher</div>
|
||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page est impossible à traiter</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #fallback><div class="loading w-[400px] h-[150px]"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
<template #default>
|
||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
</HoverPopup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Tag } from '~/types/api';
|
||||
|
||||
const { tag } = defineProps({
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
const data = ref<Tag>(), fetched = ref(false);
|
||||
const route = useRoute();
|
||||
const project = computed(() => parseInt(Array.isArray(route.params.projectId) ? '0' : route.params.projectId));
|
||||
async function fetch()
|
||||
{
|
||||
if(fetched.value)
|
||||
return;
|
||||
|
||||
data.value = await $fetch(`/api/project/${project.value}/tags/${encodeURIComponent(tag)}`);
|
||||
fetched.value = true;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<tbody>
|
||||
<slot />
|
||||
</tbody>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<td class="border border-light-35 dark:border-dark-35 py-1 px-2">
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<th class="border border-light-35 dark:border-dark-35 px-4 first:pt-0">
|
||||
<slot />
|
||||
</th>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<thead>
|
||||
<slot />
|
||||
</thead>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<tr>
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<span class="text-accent-blue inline-flex items-center cursor-pointer">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<blockquote ref="el">
|
||||
<slot />
|
||||
</blockquote>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const attrs = useAttrs(), el = ref<HTMLQuoteElement>(), title = ref<Element | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if(el && el.value && attrs.hasOwnProperty("dataCalloutFold"))
|
||||
{
|
||||
title.value = el.value.querySelector('.callout-title');
|
||||
title.value?.addEventListener('click', toggle);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
title.value?.removeEventListener('click', toggle);
|
||||
})
|
||||
function toggle() {
|
||||
el.value?.classList?.toggle('is-collapsed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
blockquote:not(.callout)
|
||||
{
|
||||
@apply ps-4;
|
||||
@apply my-4;
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@apply before:-top-1;
|
||||
@apply before:-bottom-1;
|
||||
@apply before:left-0;
|
||||
@apply before:w-1;
|
||||
@apply before:bg-light-30;
|
||||
@apply dark:before:bg-dark-30;
|
||||
}
|
||||
blockquote:empty
|
||||
{
|
||||
@apply before:hidden;
|
||||
}
|
||||
.callout {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
}
|
||||
.callout.is-collapsible .callout-title
|
||||
{
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.callout .fold
|
||||
{
|
||||
@apply transition-transform;
|
||||
}
|
||||
.callout.is-collapsed .fold
|
||||
{
|
||||
@apply -rotate-90;
|
||||
}
|
||||
.callout.is-collapsed > p
|
||||
{
|
||||
@apply hidden;
|
||||
}
|
||||
.callout[datacallout="abstract"],
|
||||
.callout[datacallout="summary"],
|
||||
.callout[datacallout="tldr"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="info"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="todo"] {
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[datacallout="important"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="tip"],
|
||||
.callout[datacallout="hint"] {
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[datacallout="success"],
|
||||
.callout[datacallout="check"],
|
||||
.callout[datacallout="done"] {
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
.callout[datacallout="question"],
|
||||
.callout[datacallout="help"],
|
||||
.callout[datacallout="faq"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="warning"],
|
||||
.callout[datacallout="caution"],
|
||||
.callout[datacallout="attention"] {
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
.callout[datacallout="failure"],
|
||||
.callout[datacallout="fail"],
|
||||
.callout[datacallout="missing"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="danger"],
|
||||
.callout[datacallout="error"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="bug"] {
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[datacallout="example"] {
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
|
||||
.callout
|
||||
{
|
||||
@apply overflow-hidden;
|
||||
@apply my-4;
|
||||
@apply p-3;
|
||||
@apply ps-6;
|
||||
@apply bg-blend-lighten;
|
||||
@apply !bg-opacity-25;
|
||||
@apply border-l-4;
|
||||
@apply inline-block;
|
||||
@apply pe-8;
|
||||
}
|
||||
.callout-icon
|
||||
{
|
||||
@apply w-6;
|
||||
@apply h-6;
|
||||
@apply stroke-2;
|
||||
}
|
||||
.callout-title
|
||||
{
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@apply gap-2;
|
||||
}
|
||||
.callout-title-inner
|
||||
{
|
||||
@apply inline-block;
|
||||
@apply font-bold;
|
||||
}
|
||||
.callout > p
|
||||
{
|
||||
@apply mt-2;
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2">
|
||||
<slot />
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2">
|
||||
<slot />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
|
||||
<slot />
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<h6 :id="parseId(id)">
|
||||
<slot />
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
const collapsed = defineModel<boolean>({
|
||||
default: true,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div @click="collapsed = !collapsed" class="flex gap-2 items-center cursor-pointer hover:text-opacity-75 text-light-100 dark:text-dark-100" :class="$attrs.class">
|
||||
<div class="w-4 h-4 transition-transform" :class="{'-rotate-90': collapsed}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 8L12 17L21 8"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold xl:text-base text-sm flex-1"><slot name="header"></slot></div>
|
||||
</div>
|
||||
<div v-if="!collapsed">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<Teleport to="#teleports" v-if="visible">
|
||||
<div @click.self="() => !focused && hide()" class="z-[100] absolute top-0 bottom-0 left-0 right-0 bg-light-0 dark:bg-dark-0 !bg-opacity-80 flex justify-center items-center">
|
||||
<div class="relative border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 min-h-40 min-w-72">
|
||||
<span v-if="closeIcon" class="cursor-pointer absolute top-1 right-1 flex hover:opacity-75" @click="() => hide()"><Icon :width="20" :height="20" icon="icons/close" /></span>
|
||||
<div class="p-6" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
focused: {
|
||||
type: Boolean,
|
||||
defualt: false,
|
||||
},
|
||||
closeIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['show', 'hide']);
|
||||
const visible = ref(false);
|
||||
|
||||
function show()
|
||||
{
|
||||
visible.value = true;
|
||||
emit('show');
|
||||
}
|
||||
function hide()
|
||||
{
|
||||
visible.value = false;
|
||||
emit('hide');
|
||||
}
|
||||
|
||||
defineExpose({hide, show});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div>
|
||||
{{beforeText}}<span class="font-bold">{{matchedText}}</span>{{afterText}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
text: string;
|
||||
matched: string;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
|
||||
const beforeText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
||||
return props.text.substring(0, pos);
|
||||
})
|
||||
const matchedText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase());
|
||||
return props.text.substring(pos, pos + props.matched.length);
|
||||
})
|
||||
const afterText = computed(() => {
|
||||
const pos = props.text.toLowerCase().indexOf(props.matched.toLowerCase()) + props.matched.length;
|
||||
return props.text.substring(pos);
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<Teleport to="#teleports" v-if="display">
|
||||
<div class="absolute border-2 border-light-35 dark:border-dark-35 max-w-[550px] max-h-[450px] bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 overflow-hidden" :style="pos"
|
||||
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
<span ref="el" :class="$attrs.class" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => { emit('beforeHide'); display = false }, 250)">
|
||||
<slot name="default"></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const display = ref(false), fetched = ref(false);
|
||||
const el = useTemplateRef('el'), pos = ref<Record<string, string>>();
|
||||
const emit = defineEmits(['beforeShow', 'beforeHide']);
|
||||
|
||||
async function show()
|
||||
{
|
||||
if(display.value)
|
||||
return;
|
||||
|
||||
emit('beforeShow');
|
||||
|
||||
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>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
icon: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
defineProps<Prop>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="$attrs.class" class="inline-block bg-light-100 dark:bg-dark-100 [mask-size:100%] [mask-repeat:no-repeat] [mask-position:center]" :style="`width: ${width}px; height: ${height}px; mask-image: url(/${icon}.svg)`"></span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
const emit = defineEmits(['input', 'change', 'focus', 'blur']);
|
||||
const model = defineModel();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="model" v-bind="$attrs" @input="e => emit('input', e)" @change="e => emit('change', e)" @focus="e => emit('focus', e)" @blur="e => emit('blur', e)"
|
||||
class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35"/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
error?: string | boolean;
|
||||
title: string;
|
||||
}
|
||||
const props = defineProps<Prop>();
|
||||
const model = defineModel<string>();
|
||||
|
||||
const err = ref<string | boolean | undefined>(props.error);
|
||||
|
||||
watchEffect(() => err.value = props.error);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-1 flex justify-between items-center gap-8">
|
||||
<label v-if="title" class="pe-4">{{ title }}</label>
|
||||
<Input class="flex-1" @input="err = false" :class="{ 'input-has-error': !!err }" v-model="model" v-bind="$attrs" />
|
||||
</div>
|
||||
<span v-if="err && typeof err === 'string'" class="text-light-red dark:text-dark-red block pb-2">{{ err }}</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<template
|
||||
v-if="content && content.length > 0">
|
||||
<Suspense :timeout="0">
|
||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
<template #fallback><div class="loading"></div></template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { hash } from 'ohash'
|
||||
|
||||
const { content } = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(content));
|
||||
const node = computed(() => content ? parser(content) : undefined);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<script lang="ts">
|
||||
import type { RootContent, Root } from 'hast';
|
||||
import { Text, Comment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||
|
||||
const proseList = {
|
||||
"p": ProseP,
|
||||
"a": ProseA,
|
||||
"blockquote": ProseBlockquote,
|
||||
"code": ProseCode,
|
||||
"pre": ProsePre,
|
||||
"em": ProseEm,
|
||||
"h1": ProseH1,
|
||||
"h2": ProseH2,
|
||||
"h3": ProseH3,
|
||||
"h4": ProseH4,
|
||||
"h5": ProseH5,
|
||||
"h6": ProseH6,
|
||||
"hr": ProseHr,
|
||||
"img": ProseImg,
|
||||
"ul": ProseUl,
|
||||
"ol": ProseOl,
|
||||
"li": ProseLi,
|
||||
"strong": ProseStrong,
|
||||
"table": ProseTable,
|
||||
"tag": ProseTag,
|
||||
"thead": ProseThead,
|
||||
"tbody": ProseTbody,
|
||||
"td": ProseTd,
|
||||
"th": ProseTh,
|
||||
"tr": ProseTr,
|
||||
"script": ProseScript
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MarkdownRenderer',
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
proses: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
async setup(props) {
|
||||
if(props.proses)
|
||||
{
|
||||
for(const prose of Object.keys(props.proses))
|
||||
{
|
||||
if(typeof props.proses[prose] === 'string')
|
||||
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||
}
|
||||
}
|
||||
return { tags: Object.assign({}, proseList, props.proses) };
|
||||
},
|
||||
render(ctx: any) {
|
||||
const { node, tags } = ctx;
|
||||
|
||||
if(!node)
|
||||
return null;
|
||||
|
||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Text, node.value);
|
||||
}
|
||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||
{
|
||||
return h(Comment, node.value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const containerRef = useTemplateRef("container");
|
||||
const { loggedIn, user } = useUserSession();
|
||||
const { direction } = useSwipe(window, { threshold: 150 });
|
||||
|
||||
watch(direction, () => {
|
||||
if(direction.value === 'right')
|
||||
toggleNavigation(true);
|
||||
if(direction.value === 'left')
|
||||
toggleNavigation(false);
|
||||
});
|
||||
|
||||
function toggleNavigation(bool?: boolean)
|
||||
{
|
||||
containerRef.value?.setAttribute('aria-expanded', bool === undefined ? (containerRef.value?.getAttribute('aria-expanded') !== 'true').toString() : bool.toString());
|
||||
}
|
||||
const hideNavigation = () => toggleNavigation(false);
|
||||
onMounted(() => {
|
||||
const links = containerRef.value?.getElementsByTagName('a');
|
||||
if(links)
|
||||
{
|
||||
for(const link of links)
|
||||
{
|
||||
link.addEventListener("click", hideNavigation);
|
||||
}
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
const links = containerRef.value?.getElementsByTagName('a');
|
||||
if(links)
|
||||
{
|
||||
for(const link of links)
|
||||
{
|
||||
link.addEventListener("click", hideNavigation);
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" aria-expanded="false" class="group flex h-screen overflow-hidden">
|
||||
<div class="z-50 sm:hidden block absolute top-0 left-0 p-2 border-e border-b border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25" @click="e => toggleNavigation()">
|
||||
<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="w-4 h-4 fill-light-100 dark:fill-dark-100">
|
||||
<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 class="bg-light-0 sm:my-8 sm:py-3 dark:bg-dark-0 top-0 z-40 xl:w-96 sm:w-[15em] w-full border-r border-light-30 dark:border-dark-30 flex flex-col justify-between max-sm:absolute max-sm:-top-0 max-sm:-bottom-0 sm:left-0 max-sm:-left-full max-sm:group-aria-expanded:left-0 max-sm:transition-[left] py-8 max-sm:z-40">
|
||||
<div class="relative bottom-6 flex flex-1 flex-col gap-4 xl:px-6 px-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<NuxtLink @click="hideNavigation" class=" text-light-100 dark:text-dark-100 hover:text-opacity-70 max-sm:ps-6" aria-label="Accueil" :to="{ path: '/', force: true }"><ThemeIcon class="inline" icon="logo" :width=56 :height=56 /></NuxtLink>
|
||||
<div class="flex gap-4 items-center">
|
||||
<ThemeSwitch />
|
||||
<NuxtLink @click="hideNavigation" class="" :to="{ path: '/user/profile', force: true }"><div class=" hover:border-opacity-70 flex border p-px border-light-70 dark:border-dark-70">
|
||||
<Icon v-if="!loggedIn" icon="icons/user-login" :width=28 :height=28 />
|
||||
<Picture v-else :src="`/users/${user?.id}/small.jpg`" :width=28 :height=28 class="flex" >
|
||||
<Icon :icon="`icons/unknown`" :width=28 :height=28 ></Icon>
|
||||
</Picture>
|
||||
</div></NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex"><SearchView @navigate="hideNavigation" /></div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="text-center xl:text-sm text-xs text-light-70 dark:text-dark-70">
|
||||
<NuxtLink class="hover:underline italic" :to="{ path: '/third-party', force: true }">Mentions légales</NuxtLink>
|
||||
<p>Copyright Peaceultime - 2024</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<span>
|
||||
<img v-show="src && !fallback" @load="hideFallback" @error="showFallback" :src="src" :width="width" :height="height" />
|
||||
<span v-show="!src || fallback"><slot></slot></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
}
|
||||
});
|
||||
|
||||
const fallback = ref(false);
|
||||
|
||||
const showFallback = () => toggleFallback(true);
|
||||
const hideFallback = () => toggleFallback(false);
|
||||
|
||||
function toggleFallback(toggle: boolean): void
|
||||
{
|
||||
console.log("Something happened")
|
||||
fallback.value = toggle;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<script setup lang="ts">
|
||||
import type { Search, File, Project, User } from '~/types/api';
|
||||
|
||||
const input = ref(''), results = ref<Search>({ projects: [], files: [], users: [] });
|
||||
const pos = ref<DOMRect>(), loading = ref(false);
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
const emit = defineEmits(['navigate']);
|
||||
|
||||
function search(e: Event)
|
||||
{
|
||||
pos.value = (e.currentTarget as HTMLElement)?.getBoundingClientRect();
|
||||
|
||||
loading.value = true;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(debounced, 250);
|
||||
}
|
||||
async function debounced()
|
||||
{
|
||||
if(!input.value)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
const fetch = await useFetch(`/api/search`, {
|
||||
query: {
|
||||
"search": `%${input.value}%`
|
||||
}
|
||||
});
|
||||
|
||||
results.value = fetch.data.value;
|
||||
}
|
||||
catch (e) {
|
||||
results.value = { projects: [], files: [], users: [] };
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full border border-light-30 dark:border-dark-30">
|
||||
<Input class="w-full" type="text" placeholder="Recherche" v-model="input" @input="search" />
|
||||
</div>
|
||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
||||
<div class="absolute z-50 border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-0 text-light-100 dark:text-dark-100 divide-y divide-light-35 dark:divide-dark-35 max-h-96 overflow-auto
|
||||
scroll"
|
||||
:style="{top: (pos.bottom + 4) + 'px', left: pos.left + 'px', width: pos.width + 'px'}">
|
||||
<div class="loading" v-if="loading"></div>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.projects" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/explorer/${result.id}/${result.home}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.name" :matched="input" />
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="">{{ result.username }}</div>
|
||||
<div class="">{{ result.pages }} pages</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.files" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/explorer/${result.project}/${result.path}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.title" :matched="input" />
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="">{{ result.username }}</div>
|
||||
<div class="">{{ result.comments }} commentaires</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cursor-pointer hover:bg-light-25 dark:hover:bg-dark-25 px-4 py-1 " v-for="result of results.users" :key="result.id"
|
||||
@mouseenter="(e) => (e.target as HTMLElement).classList.add('is-selected')"
|
||||
@mouseleave="(e) => (e.target as HTMLElement).classList.remove('is-selected')"
|
||||
@mousedown.prevent="navigateTo(`/users/${result.id}`); input = ''; emit('navigate');">
|
||||
<div class="">
|
||||
<Highlight class="text-lg" :text="result.username" :matched="input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class=""
|
||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
||||
Aucun résultat
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
interface Prop
|
||||
{
|
||||
icon: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
defineProps<Prop>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<img :src="`/icons/${icon}.light.svg`" class="block dark:hidden" :width="width" :height="height" />
|
||||
<img :src="`/icons/${icon}.dark.svg`" class="dark:block hidden" :width="width" :height="height" />
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<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="flex relative">
|
||||
<span class="hidden dark:block absolute top-[3px] left-[2px] z-[1] px-[5px]">
|
||||
<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="w-4 h-4 stroke-light-100 dark:stroke-dark-100">
|
||||
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="
|
||||
before:absolute before:top-0 before:left-0 before:right-0 before:bottom-0 before:opacity-0 before:block before:z-10
|
||||
after:transition-all after:w-4 after:h-4 after:border after:border-light-30 dark:after:border-dark-30 after:block after:m-[3px] after:top-[-1px] after:pointer-events-none after:absolute after:left-0 after:bg-light-0 after:translate-x-[1px] dark:after:translate-x-[26px]
|
||||
inline-block relative cursor-pointer w-[50px] h-[22px] select-none border border-light-35 dark:border-dark-35 bg-light-0 dark:bg-dark-30 dark:hover:border-dark-35" @click="isDark = !isDark"></div>
|
||||
<span class="block dark:hidden absolute top-[3px] left-[22px] z-[1] px-[5px]">
|
||||
<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="w-4 h-4 stroke-light-100 dark:stroke-dark-100 ">
|
||||
<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>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<label>
|
||||
<slot></slot>
|
||||
<input ref="input" :accept="accept" :multiple="multiple" type="file" class="hidden" @change.self="(e) => files = [...(e.target as HTMLInputElement)?.files ?? []]"/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
accept: {
|
||||
type: String,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const input = useTemplateRef('input');
|
||||
const files = ref<File[]>([]);
|
||||
|
||||
watch([files], () => emit('change', files.value));
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
defineExpose({ files });
|
||||
</script>
|
||||
|
|
@ -1,13 +1,21 @@
|
|||
import ".dotenv/config";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
export default function useDatabase()
|
||||
let instance: Database | undefined;
|
||||
|
||||
export default function useDatabase(): Database {
|
||||
if(instance === undefined)
|
||||
instance = getDatabase();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
function getDatabase(): Database
|
||||
{
|
||||
const sqlite = new Database(process.env.DB_FILE);
|
||||
const db = drizzle({ client: sqlite });
|
||||
const { dbFile } = useRuntimeConfig();
|
||||
|
||||
db.run("PRAGMA journal_mode = WAL;");
|
||||
const db = new Database(dbFile);
|
||||
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
|
||||
return db;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { unified, type Processor } from "unified";
|
||||
import type { Root } from 'hast';
|
||||
import RemarkParse from "remark-parse";
|
||||
|
||||
import RemarkRehype from 'remark-rehype';
|
||||
import RemarkOfm from 'remark-ofm';
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
import RemarkFrontmatter from 'remark-frontmatter';
|
||||
import RehypeRaw from 'rehype-raw';
|
||||
|
||||
export default function useMarkdown(): (md: string) => Root
|
||||
{
|
||||
let processor: Processor;
|
||||
|
||||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
{
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
||||
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
||||
processor.use(RehypeRaw);
|
||||
}
|
||||
|
||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||
return processed;
|
||||
}
|
||||
|
||||
return parse;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type { Project } from "~/types/api";
|
||||
|
||||
export default function useProject()
|
||||
{
|
||||
const project = useCookie('project');
|
||||
|
||||
const id = useState<number>("projectId", () => parseInt(project.value ?? '0'));
|
||||
const name = useState<string>("projectName", undefined);
|
||||
const owner = useState<number>("projectOwner", undefined);
|
||||
const home = useState<string | null>("projectHomepage", () => null);
|
||||
const summary = useState<string | null>("projectSummary", () => null);
|
||||
|
||||
return {
|
||||
id, name, owner, home, summary, get, set
|
||||
};
|
||||
}
|
||||
|
||||
async function get(): Promise<boolean> {
|
||||
const id = useState<number>("projectId");
|
||||
|
||||
if (!id.value)
|
||||
return false;
|
||||
|
||||
try {
|
||||
const result = await $fetch(`/api/project/${id.value}`) as Project;
|
||||
|
||||
const name = useState<string>("projectName");
|
||||
const owner = useState<number>("projectOwner");
|
||||
const home = useState<string | null>("projectHomepage");
|
||||
const summary = useState<string | null>("projectSummary");
|
||||
|
||||
name.value = result.name;
|
||||
owner.value = result.owner;
|
||||
home.value = result.home;
|
||||
summary.value = result.summary;
|
||||
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function set(id: number): Promise<boolean> {
|
||||
const _id = useState<number>("projectId");
|
||||
|
||||
_id.value = id;
|
||||
const project = useCookie('project');
|
||||
project.value = id.toString();
|
||||
|
||||
return await get();
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
||||
|
||||
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
||||
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
||||
|
||||
/**
|
||||
* Composable to get back the user session and utils around it.
|
||||
* @see https://github.com/atinux/nuxt-auth-utils
|
||||
*/
|
||||
export function useUserSession(): UserSessionComposable {
|
||||
const sessionState = useSessionState()
|
||||
const authReadyState = useAuthReadyState()
|
||||
return {
|
||||
ready: computed(() => authReadyState.value),
|
||||
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
||||
user: computed(() => sessionState.value.user || null),
|
||||
session: sessionState,
|
||||
fetch,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
const authReadyState = useAuthReadyState()
|
||||
useSessionState().value = await useRequestFetch()('/api/auth/session', {
|
||||
headers: {
|
||||
Accept: 'text/json',
|
||||
},
|
||||
retry: false,
|
||||
}).catch(() => ({}))
|
||||
if (!authReadyState.value) {
|
||||
authReadyState.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
await $fetch('/api/auth/session', { method: 'DELETE' })
|
||||
useSessionState().value = {}
|
||||
useRouter().go(0);
|
||||
}
|
||||
33
db/schema.ts
33
db/schema.ts
|
|
@ -1,33 +0,0 @@
|
|||
import { int, text, sqliteTable, type SQLiteTableExtraConfig, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const usersTable = sqliteTable("users", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
username: text().notNull().unique(),
|
||||
email: text().notNull().unique(),
|
||||
hash: text().notNull().unique(),
|
||||
state: int().default(0),
|
||||
});
|
||||
|
||||
export const usersDataTable = sqliteTable("users_data", {
|
||||
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
});
|
||||
|
||||
export const userSessionsTable = sqliteTable("user_sessions", {
|
||||
id: int().notNull(),
|
||||
user_id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||
}, (table): SQLiteTableExtraConfig => {
|
||||
return {
|
||||
pk: primaryKey({ columns: [ table.id, table.user_id ] }),
|
||||
}
|
||||
});
|
||||
|
||||
export const explorerContentTable = sqliteTable("explorer_content", {
|
||||
path: text().primaryKey(),
|
||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
title: text().notNull(),
|
||||
type: text({ enum: ['file', 'folder', 'markdown', 'canvas'] }).notNull(),
|
||||
content: blob({ mode: 'buffer' }),
|
||||
navigable: int({ mode: 'boolean' }).default(true),
|
||||
private: int({ mode: 'boolean' }).default(false),
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import 'dotenv/config';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: process.env.DB_FILE!,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
CREATE TABLE `explorer_content` (
|
||||
`path` text PRIMARY KEY NOT NULL,
|
||||
`owner` integer NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`content` blob,
|
||||
`navigable` integer DEFAULT true,
|
||||
`private` integer DEFAULT false,
|
||||
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_sessions` (
|
||||
`id` integer NOT NULL,
|
||||
`user_id` integer NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
PRIMARY KEY(`id`, `user_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users_data` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`username` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`hash` text NOT NULL,
|
||||
`state` integer DEFAULT 0
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_hash_unique` ON `users` (`hash`);
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ddf5d5b3-bf1e-4d8d-89cb-230f8e90137a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"explorer_content": {
|
||||
"name": "explorer_content",
|
||||
"columns": {
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"navigable": {
|
||||
"name": "navigable",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"private": {
|
||||
"name": "private",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"explorer_content_owner_users_id_fk": {
|
||||
"name": "explorer_content_owner_users_id_fk",
|
||||
"tableFrom": "explorer_content",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_sessions": {
|
||||
"name": "user_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_sessions_user_id_users_id_fk": {
|
||||
"name": "user_sessions_user_id_users_id_fk",
|
||||
"tableFrom": "user_sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_sessions_id_user_id_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "user_sessions_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_data": {
|
||||
"name": "users_data",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"users_data_id_users_id_fk": {
|
||||
"name": "users_data_id_users_id_fk",
|
||||
"tableFrom": "users_data",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hash": {
|
||||
"name": "hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_hash_unique": {
|
||||
"name": "users_hash_unique",
|
||||
"columns": [
|
||||
"hash"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1730124775172,
|
||||
"tag": "0000_youthful_ma_gnuci",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
<template>
|
||||
<NavBar>
|
||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
||||
</NavBar>
|
||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<NavBar>
|
||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Projets" :to="{ path: `/explorer`, force: true }">Projets</NuxtLink>
|
||||
<NuxtLink class="xl:px-6 px-3 text-lg xl:tracking-wider flex items-center font-semibold text-light-100 dark:text-dark-100 hover:text-opacity-70" aria-label="Editeur" :to="{ path: '/editing', force: true }">Editeur</NuxtLink>
|
||||
<hr class="border-light-35 dark:border-dark-35"/>
|
||||
<ExplorerNavigation></ExplorerNavigation>
|
||||
</NavBar>
|
||||
<div class="flex-1 flex items-baseline overflow-auto py-8 sm:px-8 px-4 relative">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const { loggedIn, ready, fetch } = useUserSession();
|
||||
const meta = to.meta;
|
||||
|
||||
if(!ready)
|
||||
await fetch();
|
||||
|
||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||
{
|
||||
return navigateTo(meta.guestsGoesTo);
|
||||
}
|
||||
else if(meta.requireAuth && !loggedIn.value)
|
||||
{
|
||||
return abortNavigation();
|
||||
}
|
||||
else if(!!meta.usersGoesTo && loggedIn.value)
|
||||
{
|
||||
return navigateTo(meta.usersGoesTo);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { defineNuxtModule } from '@nuxt/kit'
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
interface Schema
|
||||
{
|
||||
type: string;
|
||||
name: string;
|
||||
tbl_name: string;
|
||||
sql: string;
|
||||
}
|
||||
interface Structure
|
||||
{
|
||||
cid: number;
|
||||
name: string;
|
||||
type: string;
|
||||
[key: string]: number | string | null;
|
||||
}
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup (options, nuxt) {
|
||||
nuxt.hook('build:done', () => {
|
||||
console.log("Building database");
|
||||
|
||||
const template = new Database(nuxt.options.runtimeConfig.templateFile);
|
||||
const schema = template.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
||||
const structure = template.query(`SELECT * FROM pragma_table_info(?1)`);
|
||||
|
||||
const db = new Database(nuxt.options.runtimeConfig.dbFile);
|
||||
db.exec(`PRAGMA foreign_keys = OFF; PRAGMA defer_foreign_keys = OFF;`);
|
||||
const oldSchema = db.query(`SELECT * FROM sqlite_schema WHERE type = 'table'`).all() as Schema[];
|
||||
const oldStructure = db.query(`SELECT * FROM pragma_table_info(?1)`);
|
||||
|
||||
(db.transaction((tables: Schema[], oldTables: Schema[]) => {
|
||||
for(const table of tables)
|
||||
{
|
||||
const oldIdx = oldTables.findIndex(e => e.name === table.name);
|
||||
|
||||
if(table.name === 'sqlite_sequence')
|
||||
{
|
||||
oldTables.splice(oldIdx, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const columns = structure.all(table.name) as Structure[];
|
||||
const oldColumns = oldStructure.all(table.name) as Structure[];
|
||||
|
||||
if(oldIdx !== -1)
|
||||
{
|
||||
oldTables.splice(oldIdx, 1);
|
||||
|
||||
const filteredColumns = oldColumns.filter(e => columns.find(f => e.name === f.name)).map(e => `"${e.name}"`).join(', ')
|
||||
|
||||
db.exec(table.sql.replace(`CREATE TABLE "${table.name}"`, `CREATE TABLE "${table.name}_new"`));
|
||||
db.exec(`INSERT INTO ${table.name}_new (${filteredColumns}) SELECT ${filteredColumns} FROM ${table.name}`);
|
||||
db.exec(`DROP TABLE ${table.name}`);
|
||||
db.exec(`ALTER TABLE ${table.name}_new RENAME TO ${table.name}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
db.exec(table.sql);
|
||||
}
|
||||
}
|
||||
for(const table of oldTables)
|
||||
{
|
||||
db.exec(`DROP TABLE ${table.name}`);
|
||||
}
|
||||
})).immediate(schema, oldSchema);
|
||||
|
||||
db.exec(`PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;`);
|
||||
|
||||
db.close();
|
||||
template.close();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -1,13 +1,21 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import DatabaseSync from './modules/database.sync/module';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
modules: [
|
||||
'@nuxtjs/color-mode',
|
||||
'nuxt-security',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@vueuse/nuxt',
|
||||
'radix-vue/nuxt',
|
||||
"@nuxtjs/color-mode",
|
||||
"nuxt-security",
|
||||
"@nuxtjs/tailwindcss",
|
||||
"@vueuse/nuxt",
|
||||
DatabaseSync
|
||||
],
|
||||
runtimeConfig: {
|
||||
dbFile: 'db.sqlite',
|
||||
templateFile: 'template.sqlite',
|
||||
session: {
|
||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
||||
}
|
||||
},
|
||||
tailwindcss: {
|
||||
viewer: false,
|
||||
config: {
|
||||
|
|
@ -15,7 +23,7 @@ export default defineNuxtConfig({
|
|||
extend: {
|
||||
boxShadow: {
|
||||
raw: '0 0 0 4px var(--tw-shadow-color)'
|
||||
},
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
|
|
@ -75,13 +83,21 @@ export default defineNuxtConfig({
|
|||
app: {
|
||||
pageTransition: false,
|
||||
layoutTransition: false
|
||||
}/*,
|
||||
},
|
||||
components: [
|
||||
{
|
||||
path: '~/components',
|
||||
pathPrefix: false,
|
||||
},
|
||||
]*/,
|
||||
],
|
||||
nitro: {
|
||||
experimental: {
|
||||
tasks: true,
|
||||
},
|
||||
scheduledTasks: {
|
||||
'0 */1 * * *': ['sync']
|
||||
}
|
||||
},
|
||||
security: {
|
||||
rateLimiter: false,
|
||||
headers: {
|
||||
|
|
@ -90,4 +106,5 @@ export default defineNuxtConfig({
|
|||
}
|
||||
}
|
||||
},
|
||||
compatibilityDate: '2024-07-25',
|
||||
})
|
||||
42
package.json
42
package.json
|
|
@ -1,22 +1,28 @@
|
|||
{
|
||||
"name": "d-any",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@vueuse/nuxt": "^11.1.0",
|
||||
"drizzle-orm": "^0.35.3",
|
||||
"nuxt": "^3.13.2",
|
||||
"nuxt-security": "^2.0.0",
|
||||
"radix-vue": "^1.9.8",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.12",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
"bun-types": "^1.1.33",
|
||||
"drizzle-kit": "^0.26.2"
|
||||
"@nuxtjs/color-mode": "^3.4.4",
|
||||
"@nuxtjs/tailwindcss": "^6.12.1",
|
||||
"@types/bun": "^1.1.8",
|
||||
"@types/diff": "^5.2.2",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/nuxt": "^11.0.3",
|
||||
"hast-util-to-html": "^9.0.2",
|
||||
"nuxt": "^3.13.1",
|
||||
"nuxt-security": "^2.0.0-rc.9",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-ofm": "link:remark-ofm",
|
||||
"vue": "^3.5.3",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": "^5.2.0",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"unified": "^11.0.5"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
<template>
|
||||
Page inconnue.
|
||||
<Head>
|
||||
<Title>Inconnu</Title>
|
||||
</Head>
|
||||
<div class="h-100 w-100 flex flex-1 flex-col justify-center items-center">
|
||||
<div class="text-3xl font-extralight tracking-wide text-light-60 dark:text-dark-60">Introuvable</div>
|
||||
<div class="text-lg text-light-60 dark:text-dark-60">Cette page n'existe pas</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
Administration
|
||||
</template>
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<style>
|
||||
.editor-container
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.editor
|
||||
{
|
||||
width: 45%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>Live Editing</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<h1 class="block flex-1 text-3xl">En cours de développement</h1>
|
||||
<div class="flex flex-1">
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<div class="loading"></div>
|
||||
</template>
|
||||
<EditableMarkdown class="editor-preview" v-if="input.length > 0" v-model="input"></EditableMarkdown>
|
||||
</Suspense>
|
||||
<textarea class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none w-1/2 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35" v-model="input"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const input = ref(`## Liste de sorts provisoire
|
||||
%% Equilibrage: Les sorts de dégâts plus cher ne doivent pas forcément proposer plus de dés de dégâts mais offrir plus d'options et avoir des dé de dégâts plus haut, pour synergiser avec les buffs de l'arbre de magie. %%
|
||||
|
||||
**Notation:**
|
||||
- Nom #element (coût, durée d'incantation, portée, prérequis d'incantation)
|
||||
> Effet
|
||||
### Rang 1
|
||||
- Trait de feu #element/feu (4 mana, tour, 12 cases, V/Ge/Gl)
|
||||
>Tire un faisceau de flamme, infligeant 2d8 dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- Echauffement #element/feu (2 mana, tour, V/Gl)
|
||||
>Chauffe à blanc une arme ou un projectile. Jusqu'au début de votre prochain tour, les coups portés avec l'objet infligent 1d6 dégâts supplémentaire. Les dégâts de l'arme deviennent des dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- #element/feu (mana, tour)
|
||||
>
|
||||
|
||||
- Corps ardent #element/feu(6 mana, tour, V/Ge/Gl/C)
|
||||
>Pendant 5 tours, toute personne terminant son tour à une case de vous subit 1d10 dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- #element/feu(mana, tour)
|
||||
>
|
||||
|
||||
- Protection supérieure #element/glace (2 mana, réaction, V/Gl)
|
||||
>L'armure subit l'intégralité des dégâts sur le prochain coup.
|
||||
|
||||
- Lames de glace #element/glace (4 mana, tour, 12 cases)
|
||||
>Tire 2 projectiles infligeant 1d8 dégâts de [[Les types de dégâts#Glace|glace]]. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- Chaine de foudre #element/foudre (4 mana, tour, 12 cases)
|
||||
>Frappe une cible visible puis rebondit sur jusqu'à 2 autres cibles à 1 case de la première. Inflige 1d8 dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
|
||||
- Vitesse lumière #element/foudre (3 mana, tour)
|
||||
>Se téléporte à 6 cases tant que vous pouvez voir et courir vers la destination.
|
||||
|
||||
- Décharge de foudre #element/foudre(3 mana, tour)
|
||||
>Tire une décharge foudroyante d'énergie, infligeant 4d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
|
||||
- Faisceau fulgurant #element/foudre(4 mana, tour)
|
||||
>Lance un faisceau électrique qui peut contourner les obstacles pour toucher une cible à couvert. Inflige 3d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
>*Vous pouvez viser une case que vous ne voyez pas, mais le MJ ne doit pas vous informer si l'attaque pourrait toucher une cible.*
|
||||
|
||||
- #element/foudre(mana, tour)
|
||||
>
|
||||
|
||||
- #element/terre(2 mana, tour)
|
||||
>Un pilier de matière est extirpé du sol pour aller frapper la cible, qui est alors déplacée d'une case. Si la cible est propulsée contre un mur, elle subit alors 3d12 dégâts [[Les types de dégâts#Contondant|contondant]].
|
||||
|
||||
- #element/terre(3 mana, tour)
|
||||
>Propulse un projectile de matière sur la cible, infligeant 1d12 dégâts [[Les types de dégâts#Contondant|contondant]] et appliquant un [[Les effets#L'étourdissement|étourdissement]] (2/12).
|
||||
|
||||
- Bouclier tortue #element/terre(3 mana, tour)
|
||||
>Vous gagnez un bonus de 2 en blocage, mais subissez également un malus de 2 en esquive et perdez 2 cases de vitesse de course durant 1 min.
|
||||
|
||||
- #element/terre(2 mana, réaction)
|
||||
> Vous gagnez une résistance aux dégâts [[Les types de dégâts#Les dégâts physiques|physiques]] jusqu'à la fin de votre prochain tour.
|
||||
|
||||
- #element/terre(mana, tour)
|
||||
>
|
||||
|
||||
- Enchantement mineur #element/arcane(2 mana, tour, V/Gl)
|
||||
> Condense de l'énergie magique dans une arme ou un projectile. Vous faites une attaque immédiatement après avoir lancé ce sort sans dépenser d'action, infligeant 1d8 dégâts supplémentaire. Les dégâts de l'arme deviennent [[Les types de dégâts#Neutre|magique]].
|
||||
|
||||
- Rupture de force #element/arcane(3 mana, tour, V/Ge/Gl)
|
||||
> Vous condensez une puissante énergie magique qui est propulsée directement sur votre cible. Vous lancez 2d20 et prenez le plus haut résultat pour infliger des dégâts [[Les types de dégâts#Neutre|magique]]. *Avoir un #avantage aux dégâts permet de lancer un autre d20.* *Augmenter les dégâts de ce sort permet d'infliger 5 dégâts [[Les types de dégâts#Neutre|magique]] supplémentaire.*
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- Foulée aérienne #element/air(3 mana, tour, 12 cases)
|
||||
>La vitesse de course de votre cible augmente de 2 cases pendant 1 minute. Vous gagnez également un bonus de +1 à l'esquive.
|
||||
|
||||
- Pression forcée #element/air(5 mana, tour, 18 cases)
|
||||
>Crée une imposante colonne d'air descendent de 3 cases de rayon sur 12 cases de haut. Les créatures à l'intérieur ont un malus de 1 à l'esquive. Les créatures volantes chutent de 3 cases par tour.
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- Conservation #element/nature (2 mana, 1 minute)
|
||||
>Permet à jusqu'à 5 herbes ou préparations médicinales de se conserver 1 jour de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- Absorption radieuse #element/lumiere (3 mana, tour)
|
||||
> Absorbe la lumière d'une zone de 4 cases de rayon, la faisant apparaitre comme plus sombre. #todo
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/psy(6 mana, tour)
|
||||
>Envenime l'esprit de la cible, brouillant sa perception de la réalité et lui faisant voir des images subliminales de chaos. Applique un effet de [[Les effets#La peur|peur]] (4/12).
|
||||
### Rang 2
|
||||
- Trait de feu 2 #element/feu (5 mana, tour, 15 cases, V/Ge/Gl)
|
||||
>Tire un faisceau de flamme, infligeant 3d8 de dégâts de feu.
|
||||
|
||||
- Lames de glace 2 #element/glace (5 mana, tour, 15 cases)
|
||||
>Tire 3 projectiles à 1d8 de glace. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
||||
|
||||
- Chaine de foudre 2 #element/foudre (5 mana, tour, 15 cases)
|
||||
>Frappe une cible visible puis rebondit sur jusqu'à 3 autres cibles à 2 cases de la première. 1d8+3 de foudre.
|
||||
|
||||
- Décharge de foudre 2 #element/foudre(3 mana, tour)
|
||||
>Tire une décharge foudroyante d'énergie, infligeant 6d4[[Glossaire#Jet explosif|!]] de dégâts de foudre.
|
||||
|
||||
- Conservation 2 #element/nature (4 mana, 1 minute)
|
||||
>Permet à jusqu'à 8 herbes ou préparations médicinales de se conserver 3 jours de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
||||
|
||||
- Boule de feu #element/feu (8 mana, tour, 12 cases)
|
||||
>Projette une imposante boule de flamme explosant au contact d'une surface, infligeant ainsi 4d10 de feu sur 3 cases de rayon.
|
||||
|
||||
- Détonation #element/feu (4 mana, tour, 8 cases)
|
||||
>Pointe un lieu visible. Une explosion de flamme jaillit subitement, infligeant 2d10 de feu sur 2 cases de rayon.
|
||||
|
||||
- Lance de givre #element/glace(4 mana, tour)
|
||||
>Une lame de glace vient grandir le long de votre arme. Augmente votre portée d'une case. L'arme inflige des dégâts tranchants. Dure 1 min, casse après 8 coups réussis.
|
||||
|
||||
- Téléportation #element/foudre (4 mana, tour)
|
||||
>Se téléporte à un point visible à 9 cases max.
|
||||
|
||||
- Apaisement #element/psy (3 mana, tour)
|
||||
>En touchant la cible, vous pouvez faire un jet d'intelligence. Guérit l'influence, le charme et la peur, mais augmente les chances de ces effet de 1 niveau pendant 3 tours.
|
||||
|
||||
- Painshock #element/psy (6 mana, tour)
|
||||
>*Ne fonctionne que si la cible touchée à subit des dégâts depuis votre dernier tour.* Vous touchez une plaie et intensifiez la douleur à l'extrême. Applique un effet d'[[Les effets#L'étourdissement|étourdissement]]. La difficulté est égale à 2/12 + 1 niveau pour chaque 10% de vie max retiré.
|
||||
|
||||
- Perturbateur #element/psy (4 mana, réaction, 9 cases, V/Ge)
|
||||
>Lorsqu'un lanceur de sort termine son incantation, vous pouvez perturber les flux magiques pour lui imposer un malus de 3 au jet.
|
||||
### Rang 3
|
||||
- Rejet pur #divin (spécial, tour, 3 cases, Ge)
|
||||
>Vous propulsez une énergie magique pure condensée sur votre adversaire avec une puissance absolue. Vous infligez 1d6!+4 dégâts [[Les types de dégâts#Neutre|magique]] tous les 3 mana dépensé. Vous pouvez dépenser jusqu'à 30 mana. Après avoir lancé ce sort, vous subissez un malus de 4 au lancer de sort pendant 1 tour.
|
||||
### Sorts unique
|
||||
Les sorts uniques sont des sorts obtenus uniquement avec des objets magiques ou en progressant dans l'arbre d'entrainement. Il n'existe **aucun** autre moyen d'obtenir des sorts.
|
||||
|
||||
- Dévastation #element/feu + #element/glace + #element/foudre (10 mana, tour, 12 cases)
|
||||
>Inflige 10+3d10 dégâts. Vous pouvez choisir le type de dégâts entre feu, glace et foudre. Ignore les résistances et réduit les immunités en résistance. ^484fc3
|
||||
|
||||
- Soin #element/nature (8 mana, tour, toucher)
|
||||
>Soigne 10+1d10 PV et guérit l'[[Les effets#L'étourdissement|étourdissement]], le [[Les effets#Le saignement|saignement]] et les [[Les effets#L'empoisonnement|poisons]]. ^068b55
|
||||
|
||||
- Contresort #element/arcane (4 mana, réaction, 12 cases)
|
||||
>Perturbe les flux magique pour interrompre une canalisation en cours. Vous pouvez augmenter le coût du sort pour augmenter les chances de réussite. La difficulté est égale à 6 - le cout du sort à interrompre + le cout du contresort. ^a8f46f
|
||||
|
||||
- Focalisation destructrice #element/arcane (12 mana, tour)
|
||||
>Vous focalisez les énergies magiques sur vous, rendant l'utilisation de sort plus complexe pour les autres. La densité d'énergie anormale vous fait subir 5 points de dégâts par tour. Pendant une minute, toute personne à 18 cases de vous essayant de lancer un sort ou de [[1.Règles/6.Les Aspects/index#Transformations|se transformer]] subit un malus de 4. ^73b8bd
|
||||
|
||||
- Domination mentale #element/psy (10 mana, tour, toucher)
|
||||
>Applique un effet de [[Les effets#La possession|possession]] (6/12). ^5b38b6
|
||||
### Sorts spéciaux
|
||||
Les sorts spéciaux sont une liste de sorts que les joueurs peuvent obtenir durant certaines aventures. Selon les cas, un joueur peut demander au maitre du jeu de commencer avec un sort spécial si ça correspond à son passé. Les sorts spéciaux peuvent aussi être des sorts que les PNJ ont et qu'ils peuvent apprendre aux joueurs.`);
|
||||
</script>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
Current path: {{ $route.params.path }}
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue