You've already forked obsidian-visualiser
Compare commits
221 Commits
ed1c338fee
...
dev_fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25bd165f1d | ||
|
|
5c1f41b0b7 | ||
| feb2fb56c6 | |||
|
|
df9ae95890 | ||
|
|
72843f2425 | ||
|
|
443612cc58 | ||
|
|
a577e3ccfc | ||
|
|
48e767944a | ||
| d187957915 | |||
|
|
16cc3ee438 | ||
|
|
26aa0847d9 | ||
| b19d2d1b41 | |||
|
|
89c4476ffb | ||
|
|
3113d8b0f3 | ||
| 2b39f26722 | |||
| d2a807694b | |||
|
|
eb0c33deae | ||
|
|
61d2d144b7 | ||
|
|
1642cd513f | ||
|
|
b1ac379f1a | ||
| 81f191d5f6 | |||
|
|
423df7bc42 | ||
| c93cc4078c | |||
|
|
17bc232602 | ||
|
|
042d4479ee | ||
|
|
da93fcd82d | ||
|
|
80a94bee86 | ||
|
|
5387dc66c3 | ||
|
|
6fe3746df4 | ||
| 893247e1eb | |||
|
|
69ee62c08e | ||
| 247b14b2c8 | |||
|
|
658499749d | ||
|
|
06276b3fbc | ||
|
|
72982a4ea9 | ||
|
|
4e5ea504ea | ||
| 920ce2e1b6 | |||
|
|
86556ec604 | ||
|
|
7d6f9162ed | ||
| 3ef98df5d2 | |||
|
|
ba8c7b05e6 | ||
|
|
a8dcc47a1b | ||
| 996b9711e4 | |||
|
|
da5c1202ed | ||
| c33bd95b81 | |||
|
|
d5851499cd | ||
|
|
e78a60f771 | ||
|
|
9a6f91a341 | ||
|
|
218b68db60 | ||
|
|
42915d699f | ||
|
|
df3577f673 | ||
|
|
871861e66e | ||
| 1ee895ab42 | |||
| 3f58114091 | |||
|
|
878bcc0a16 | ||
| e924fdfe38 | |||
| 5e6f296c56 | |||
| ab2778c626 | |||
| 7a11c5382c | |||
|
|
4885479ac6 | ||
|
|
fd0603f916 | ||
| 0771d5ebd1 | |||
|
|
598cf54bc5 | ||
|
|
9352b3f0a1 | ||
|
|
a30f394ef7 | ||
|
|
32439b41f6 | ||
|
|
b8f547d3e9 | ||
|
|
3c412d1cbe | ||
|
|
1de2439a8a | ||
|
|
308c2974f1 | ||
|
|
cb00c093ff | ||
|
|
735dfb6980 | ||
|
|
9ca546f490 | ||
| f599b561af | |||
| 7beeed8a61 | |||
| 403a65158a | |||
| fef7c8f57c | |||
| e5b53585aa | |||
|
|
2c80cb2456 | ||
|
|
6100fd9411 | ||
| 1d41514b26 | |||
| 227d7224e5 | |||
| e7d0d69e55 | |||
| f49fdaac79 | |||
| 41c19b4bfb | |||
|
|
c0e625a8cb | ||
| f2d00097d6 | |||
| 0b97e9a295 | |||
| 6abc467a43 | |||
| 939b9cbd28 | |||
| e2c18ff406 | |||
| 154584e175 | |||
| af317cb0e3 | |||
| 8fc1855ae6 | |||
| f3c453b1b2 | |||
| 62b2f3bbfb | |||
| 0b1809c3f6 | |||
| 3f04bb3d0c | |||
| 685bd47fc4 | |||
| f32c51ca38 | |||
| 348c991c54 | |||
| 76db788192 | |||
| 4433cf0e00 | |||
| 9439dd2d95 | |||
| 823f3d7730 | |||
| 62950be032 | |||
| b1a9eb859e | |||
| 83ac9b1f36 | |||
| 7403515f80 | |||
| 3839b003dc | |||
| e7412f6768 | |||
| 6f305397a8 | |||
| 896af11fa7 | |||
| 9515132659 | |||
| 031a51c2fe | |||
| 7bdf6ccd13 | |||
| cb2c19fada | |||
| 0abf0b11e6 | |||
| ec0afa9686 | |||
| b24a083d2e | |||
| ad61dc8897 | |||
| 1e8afe90dd | |||
| 8439d3444f | |||
| 36909c5d66 | |||
| fea37e2f59 | |||
| a3d9e466a5 | |||
| 9c69ff2903 | |||
| 3b919075ef | |||
| 4150b69ba3 | |||
| 298f47a280 | |||
| 161f0d856a | |||
| 51a5d501be | |||
| ecdfa947ac | |||
| fd951c294f | |||
| 602b0af212 | |||
| f7094f7ce1 | |||
| 429f1d4b38 | |||
| 5062d52667 | |||
| c4bf95e48b | |||
| 7fc7998a4b | |||
| fdaf765e2d | |||
| e99a5f15b4 | |||
| 5fb708051b | |||
| 9a69a92ef8 | |||
| f22e63bd4d | |||
| e83d8e802f | |||
| 3e463ea286 | |||
| 4125cbb3a2 | |||
| 4df9297d47 | |||
| d71e8b7910 | |||
| 20ab51a66c | |||
| 2855d4ba2e | |||
| 4f2fc31695 | |||
| 6e7243982b | |||
| 9c52494f8e | |||
| d0de943df2 | |||
| 1c239f161b | |||
| a9363e8c06 | |||
| d708e9ceb6 | |||
| 0c17dbf7bc | |||
| ac17134b7e | |||
| adb37b255a | |||
| b54402fc19 | |||
| 0882eb1dd0 | |||
| 60f1fbb4aa | |||
| 42658558c5 | |||
| 057efb848c | |||
| 721e7ff3db | |||
| 41951d7603 | |||
| a392841012 | |||
| b3fae0b5db | |||
| 1af78e5ab7 | |||
| 83ddaf19d4 | |||
| e8b521f122 | |||
| 0105a6aaea | |||
| 633231f821 | |||
| 5ce2d3e236 | |||
| 8a19448a38 | |||
| bd32d176b1 | |||
| cbce979aa9 | |||
| f80c6d5326 | |||
| a5a9086eb7 | |||
| f37c3e4cc9 | |||
| 4c8fb0ff77 | |||
| f4f4be6b27 | |||
| 97f8ca499a | |||
| 1b2472bc1a | |||
| fef8c092a9 | |||
| fa2d8e5035 | |||
| 8fee6ca5f1 | |||
| 0fb4246d26 | |||
| 6856d3b9af | |||
| e904f28b3b | |||
| fa1a13d411 | |||
| f2261c3f35 | |||
| 6c0bfb9e4c | |||
| 6550042751 | |||
| cbef6d1289 | |||
| 09f4f3c482 | |||
| 0d1332dcd4 | |||
| d7a8087c6c | |||
| 094f27d9ad | |||
| 85c8fbae0f | |||
| 6d3efea784 | |||
| 2b293a0c1a | |||
| c091a6d261 | |||
| 4efc0afffb | |||
| 145a46eba6 | |||
| 77aea62bfa | |||
| 175f6c3fcb | |||
| 35d356ae22 | |||
| e757682ed1 | |||
| 6981b32a3a | |||
| 2a8abb4796 | |||
| c694d28982 | |||
| d1450a1c99 | |||
| cc095af0c4 | |||
| da610022b4 | |||
| 4871510eea | |||
| 903d4f842e | |||
| 821ad1f759 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,7 +5,6 @@
|
|||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
content
|
|
||||||
|
|
||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
@@ -24,4 +23,7 @@ logs
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
db.sqlite*
|
bun.lockb
|
||||||
|
db.sqlite
|
||||||
|
db.sqlite-wal
|
||||||
|
db.sqlite-shm
|
||||||
31
.vscode/launch.json
vendored
31
.vscode/launch.json
vendored
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
210
app.vue
210
app.vue
@@ -1,28 +1,190 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const layout = ref();
|
|
||||||
const toggled = ref(false);
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<template>
|
<template>
|
||||||
<div class="published-container" :class="{'has-navigation': layout?.layoutRef?.navigation ?? false}">
|
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||||
<div class="site-nav-bar">
|
<NuxtRouteAnnouncer/>
|
||||||
<div>
|
<NuxtLayout>
|
||||||
<div class="gapx-3 flex align-stretch">
|
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
|
||||||
<NuxtLink class="site-nav-bar-text" aria-label="Accueil" :to="{ path: '/', force: true }"><ThemeIcon icon="logo" :width=40 :height=40 /></NuxtLink>
|
<NuxtPage />
|
||||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Projets" :to="{ path: `/explorer`, force: true }" active-class="mod-active">Projets</NuxtLink>
|
|
||||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Editeur" :to="{ path: '/editing', force: true }" :class="{ 'mod-active': $route.path.startsWith('/editing') }">Editeur</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mobile-bigger"><SearchView /></div>
|
|
||||||
<div class="ps-1 gapx-1 flex align-center">
|
|
||||||
<ThemeSwitch class="mobile-hidden" />
|
|
||||||
<NuxtLink class="site-login" :to="{ path: '/user/profile', force: true }"><ThemeIcon icon="user" :width=32 :height=32 /></NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NuxtLayout ref="layout"><NuxtPage/></NuxtLayout>
|
|
||||||
<div class="site-footer">
|
|
||||||
<p>Copyright Peaceultime - 2024</p>
|
|
||||||
<NuxtLink :to="{ path: '/third-party', force: true }">Applications tierces et crédits</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
|
</NuxtLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Content } from '#shared/content.util';
|
||||||
|
import * as Floating from '#shared/floating.util';
|
||||||
|
import { Toaster } from '#shared/components.util';
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
Content.init();
|
||||||
|
Floating.init();
|
||||||
|
Toaster.init();
|
||||||
|
|
||||||
|
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||||
|
if(failure) return;
|
||||||
|
|
||||||
|
document.querySelectorAll(`a[href="${from.path}"][data-active]`).forEach(e => e.classList.remove(e.getAttribute('data-active') ?? ''));
|
||||||
|
document.querySelectorAll(`a[href="${to.path}"][data-active]`).forEach(e => e.classList.add(e.getAttribute('data-active') ?? ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unmount();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ToastRoot[data-type='error'] {
|
||||||
|
@apply border-light-red;
|
||||||
|
@apply dark:border-dark-red;
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply !bg-opacity-50;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-type='success'] {
|
||||||
|
@apply border-light-green;
|
||||||
|
@apply dark:border-dark-green;
|
||||||
|
@apply bg-light-green;
|
||||||
|
@apply dark:bg-dark-green;
|
||||||
|
@apply !bg-opacity-50;
|
||||||
|
}
|
||||||
|
::-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;
|
||||||
|
}
|
||||||
|
.callout[data-type="abstract"],
|
||||||
|
.callout[data-type="summary"],
|
||||||
|
.callout[data-type="tldr"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="info"]
|
||||||
|
{
|
||||||
|
@apply bg-light-blue;
|
||||||
|
@apply dark:bg-dark-blue;
|
||||||
|
@apply text-light-blue;
|
||||||
|
@apply dark:text-dark-blue;
|
||||||
|
}
|
||||||
|
.callout[data-type="todo"]
|
||||||
|
{
|
||||||
|
@apply bg-light-blue;
|
||||||
|
@apply dark:bg-dark-blue;
|
||||||
|
@apply text-light-blue;
|
||||||
|
@apply dark:text-dark-blue;
|
||||||
|
}
|
||||||
|
.callout[data-type="important"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="tip"],
|
||||||
|
.callout[data-type="hint"]
|
||||||
|
{
|
||||||
|
@apply bg-light-cyan;
|
||||||
|
@apply dark:bg-dark-cyan;
|
||||||
|
@apply text-light-cyan;
|
||||||
|
@apply dark:text-dark-cyan;
|
||||||
|
}
|
||||||
|
.callout[data-type="success"],
|
||||||
|
.callout[data-type="check"],
|
||||||
|
.callout[data-type="done"]
|
||||||
|
{
|
||||||
|
@apply bg-light-green;
|
||||||
|
@apply dark:bg-dark-green;
|
||||||
|
@apply text-light-green;
|
||||||
|
@apply dark:text-dark-green;
|
||||||
|
}
|
||||||
|
.callout[data-type="question"],
|
||||||
|
.callout[data-type="help"],
|
||||||
|
.callout[data-type="faq"]
|
||||||
|
{
|
||||||
|
@apply bg-light-orange;
|
||||||
|
@apply dark:bg-dark-orange;
|
||||||
|
@apply text-light-orange;
|
||||||
|
@apply dark:text-dark-orange;
|
||||||
|
}
|
||||||
|
.callout[data-type="warning"],
|
||||||
|
.callout[data-type="caution"],
|
||||||
|
.callout[data-type="attention"]
|
||||||
|
{
|
||||||
|
@apply bg-light-orange;
|
||||||
|
@apply dark:bg-dark-orange;
|
||||||
|
@apply text-light-orange;
|
||||||
|
@apply dark:text-dark-orange;
|
||||||
|
}
|
||||||
|
.callout[data-type="failure"],
|
||||||
|
.callout[data-type="fail"],
|
||||||
|
.callout[data-type="missing"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="danger"],
|
||||||
|
.callout[data-type="error"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="bug"]
|
||||||
|
{
|
||||||
|
@apply bg-light-red;
|
||||||
|
@apply dark:bg-dark-red;
|
||||||
|
@apply text-light-red;
|
||||||
|
@apply dark:text-dark-red;
|
||||||
|
}
|
||||||
|
.callout[data-type="example"]
|
||||||
|
{
|
||||||
|
@apply bg-light-purple;
|
||||||
|
@apply dark:bg-dark-purple;
|
||||||
|
@apply text-light-purple;
|
||||||
|
@apply dark:text-dark-purple;
|
||||||
|
}
|
||||||
|
.variant-cap
|
||||||
|
{
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
.cm-editor
|
||||||
|
{
|
||||||
|
@apply bg-transparent;
|
||||||
|
@apply flex-1 h-full;
|
||||||
|
@apply font-sans;
|
||||||
|
|
||||||
|
@apply text-light-100 dark:text-dark-100;
|
||||||
|
}
|
||||||
|
.cm-editor .cm-content
|
||||||
|
{
|
||||||
|
@apply caret-light-100 dark:caret-dark-100;
|
||||||
|
}
|
||||||
|
.cm-line
|
||||||
|
{
|
||||||
|
@apply text-base;
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1062
assets/canvas.css
1062
assets/canvas.css
File diff suppressed because it is too large
Load Diff
@@ -1,387 +0,0 @@
|
|||||||
.ps-1
|
|
||||||
{
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
.pe-1
|
|
||||||
{
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
.pt-1
|
|
||||||
{
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
.pb-1
|
|
||||||
{
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ps-2
|
|
||||||
{
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
.pe-2
|
|
||||||
{
|
|
||||||
padding-right: 2em;
|
|
||||||
}
|
|
||||||
.pt-2
|
|
||||||
{
|
|
||||||
padding-top: 2em;
|
|
||||||
}
|
|
||||||
.pb-2
|
|
||||||
{
|
|
||||||
padding-bottom: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ps-3
|
|
||||||
{
|
|
||||||
padding-left: 3em;
|
|
||||||
}
|
|
||||||
.pe-3
|
|
||||||
{
|
|
||||||
padding-right: 3em;
|
|
||||||
}
|
|
||||||
.pt-3
|
|
||||||
{
|
|
||||||
padding-top: 3em;
|
|
||||||
}
|
|
||||||
.pb-3
|
|
||||||
{
|
|
||||||
padding-bottom: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.block
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.inline
|
|
||||||
{
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.hidden
|
|
||||||
{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.row
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.column
|
|
||||||
{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.gapx-1 > *
|
|
||||||
{
|
|
||||||
margin-left: .4em;
|
|
||||||
margin-right: .4em;
|
|
||||||
}
|
|
||||||
.gapx-1 > *:first-child, .gapx-2 > *:first-child, .gapx-3 > *:first-child
|
|
||||||
{
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
.gapx-1 > *:last-child, .gapx-2 > *:last-child, .gapx-3 > *:last-child
|
|
||||||
{
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.gapx-2 > *
|
|
||||||
{
|
|
||||||
margin-left: .8em;
|
|
||||||
margin-right: .8em;
|
|
||||||
}
|
|
||||||
.gapx-3 > *
|
|
||||||
{
|
|
||||||
margin-left: 1.2em;
|
|
||||||
margin-right: 1.2em;
|
|
||||||
}
|
|
||||||
.gapy-1 > *
|
|
||||||
{
|
|
||||||
margin-top: .4em;
|
|
||||||
margin-bottom: .4em;
|
|
||||||
}
|
|
||||||
.gapy-1 > *:first-child, .gapy-2 > *:first-child, .gapy-3 > *:first-child
|
|
||||||
{
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.gapy-1 > *:last-child, .gapy-2 > *:last-child, .gapy-3 > *:last-child
|
|
||||||
{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.gapy-2 > *
|
|
||||||
{
|
|
||||||
margin-top: .8em;
|
|
||||||
margin-bottom: .8em;
|
|
||||||
}
|
|
||||||
.gapy-3 > *
|
|
||||||
{
|
|
||||||
margin-top: 1.2em;
|
|
||||||
margin-bottom: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .dark-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .dark-block {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light-mode .light-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.light-mode .light-block {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-baseline {
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
.align-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.align-stretch {
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.justify-evenly {
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
.justify-around {
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 750px) {
|
|
||||||
.mobile-bigger {
|
|
||||||
flex: 3 1 0 !important;
|
|
||||||
}
|
|
||||||
.mobile-smaller {
|
|
||||||
flex: 1 3 0 !important;
|
|
||||||
}
|
|
||||||
.mobile-hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.mobile-block {
|
|
||||||
display: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (min-width: 750px) {
|
|
||||||
.desktop-bigger {
|
|
||||||
flex: 3 1 0 !important;
|
|
||||||
}
|
|
||||||
.desktop-smaller {
|
|
||||||
flex: 1 3 0 !important;
|
|
||||||
}
|
|
||||||
.desktop-hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.desktop-block {
|
|
||||||
display: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.input-form.input-form-wide {
|
|
||||||
width: 600px;
|
|
||||||
min-height: 300px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-form {
|
|
||||||
width: 400px;
|
|
||||||
min-height: 200px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 2em 2em 2em 2em;
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-form h1 {
|
|
||||||
margin-top: 0px;
|
|
||||||
font-size: x-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-form button {
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group .input-label {
|
|
||||||
padding: 4px 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-error {
|
|
||||||
padding: .5em 1em 4px;
|
|
||||||
color: var(--text-error);
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group .input-input.input-has-error {
|
|
||||||
border-color: var(--text-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-validation-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: .5em 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-validation-title {
|
|
||||||
font-style: italic;
|
|
||||||
font-size: small;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-validation-item {
|
|
||||||
font-size: small;
|
|
||||||
padding-left: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-validation-item pre {
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-validation-item.validation-error {
|
|
||||||
color: var(--text-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading:after {
|
|
||||||
content: '';
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border: 4px solid var(--color-purple);
|
|
||||||
border-right-color: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-sizing: content-box;
|
|
||||||
animation: rotate 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-form a {
|
|
||||||
padding: .5em;
|
|
||||||
text-align: center;
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: var(--font-extrabold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-self[data-type]:after {
|
|
||||||
content: attr(data-type);
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
background-color: var(--background-primary-alt);
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: var(--font-smaller);
|
|
||||||
font-variant: all-petite-caps;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-subtext {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: var(--font-smaller);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-down {
|
|
||||||
border: 2px solid var(--background-modifier-border-focus);
|
|
||||||
border-top-color: transparent;
|
|
||||||
border-left-color: transparent;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
position: relative;
|
|
||||||
left: 8px;
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-container {
|
|
||||||
flex: 1 1;
|
|
||||||
padding: 2em 4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-list {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item {
|
|
||||||
margin: 1em;
|
|
||||||
padding: .5em;
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
width: 45%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-title {
|
|
||||||
font-size: var(--font-ui-large);
|
|
||||||
font-weight: var(--font-bold);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-user {
|
|
||||||
font-size: var(--font-small);
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-pages {
|
|
||||||
display: inline;
|
|
||||||
float: inline-end;
|
|
||||||
font-size: var(--font-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-group {
|
|
||||||
position: absolute;
|
|
||||||
left: -1em;
|
|
||||||
top: calc(100% + 7px);
|
|
||||||
border: 1px solid var(--background-modifier-border);
|
|
||||||
background-color: var(--background-primary);
|
|
||||||
z-index: 99;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-down.active + .arrow-group {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-group .arrow-group-item {
|
|
||||||
padding: 8px 1em;
|
|
||||||
font-weight: var(--font-medium);
|
|
||||||
color: var(--text-normal);
|
|
||||||
border-bottom: 1px solid var(--background-modifier-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-group .arrow-group-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-group .arrow-group-item:hover {
|
|
||||||
background-color: var(--background-primary-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-icon {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
5008
assets/global.css
5008
assets/global.css
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
### 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).
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{beforeText}}<span class="highlight">{{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>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<style>
|
|
||||||
.editor
|
|
||||||
{
|
|
||||||
white-space: pre-line;
|
|
||||||
overflow: auto;
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="editor" contenteditable>
|
|
||||||
<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,22 +0,0 @@
|
|||||||
<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="input-group">
|
|
||||||
<label v-if="title" class="input-label">{{ title }}</label>
|
|
||||||
<input @input="err = false" class="input-input" :class="{ 'input-has-error': !!err }" v-model="model"
|
|
||||||
v-bind="$attrs" />
|
|
||||||
<span v-if="err && typeof err === 'string'" class="input-error">{{ err }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<template>
|
|
||||||
<template
|
|
||||||
v-if="model && model.length > 0">
|
|
||||||
<Suspense>
|
|
||||||
<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'
|
|
||||||
import { watch, computed } from 'vue'
|
|
||||||
import type { Root } from 'hast';
|
|
||||||
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const parser = useMarkdown();
|
|
||||||
const key = computed(() => hash(model.value));
|
|
||||||
|
|
||||||
const node = ref<Root>();
|
|
||||||
|
|
||||||
watch(model, async () => {
|
|
||||||
if(model.value && model.value)
|
|
||||||
{
|
|
||||||
node.value = parser(model.value);
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
</script>
|
|
||||||
@@ -1,117 +1,20 @@
|
|||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RootContent, Root } from 'hast';
|
import render, { type MDProperties } from '#shared/markdown.util'
|
||||||
import { Text, Comment } from 'vue';
|
const { content, filter, properties } = defineProps<{
|
||||||
|
content?: string,
|
||||||
|
filter?: string,
|
||||||
|
properties?: MDProperties
|
||||||
|
}>();
|
||||||
|
|
||||||
import ProseP from '~/components/prose/ProseP.vue';
|
const container = useTemplateRef('container');
|
||||||
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 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 = {
|
content && onMounted(() => {
|
||||||
"p": ProseP,
|
queueMicrotask(() => {
|
||||||
"a": ProseA,
|
container.value && content && container.value.replaceChildren(render(content, filter, properties));
|
||||||
"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,
|
|
||||||
"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: () => ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
node: {
|
|
||||||
handler: function(val, old) {
|
|
||||||
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="container"></div>
|
||||||
|
</template>
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<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;
|
|
||||||
|
|
||||||
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="search-view-outer">
|
|
||||||
<div class="search-view-container">
|
|
||||||
<span class="published-search-icon"></span>
|
|
||||||
<input class="search-bar" type="text" placeholder="Recherche" v-model="input" @input="search">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Teleport to="#teleports" v-if="input !== '' && !!pos">
|
|
||||||
<div class="search-results"
|
|
||||||
: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="suggestion-item suggestion-project" 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 = ''">
|
|
||||||
<div class="suggestion-content">
|
|
||||||
<BoldContent class="suggestion-title" :text="result.name" :matched="input" />
|
|
||||||
<div class="suggestion-subtext">
|
|
||||||
<div class="suggestion-text">{{ result.username }}</div>
|
|
||||||
<div class="suggestion-text">{{ result.pages }} pages</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-item suggestion-file" 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 = ''">
|
|
||||||
<div class="suggestion-content">
|
|
||||||
<BoldContent class="suggestion-title" :text="result.title" :matched="input" />
|
|
||||||
<div class="suggestion-subtext">
|
|
||||||
<div class="suggestion-text">{{ result.username }}</div>
|
|
||||||
<div class="suggestion-text">{{ result.comments }} commentaires</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-item suggestion-user" 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(`/user/${result.id}`); input = ''">
|
|
||||||
<div class="suggestion-content">
|
|
||||||
<BoldContent class="suggestion-title" :text="result.username" :matched="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-empty"
|
|
||||||
v-if="results.projects.length === 0 && results.files.length === 0 && results.users.length === 0">
|
|
||||||
Aucun résultat
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
interface Prop
|
|
||||||
{
|
|
||||||
icon: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
defineProps<Prop>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<img :src="`/icons/${icon}.light.svg`" class="dark-hidden light-block" :width="width" :height="height" />
|
|
||||||
<img :src="`/icons/${icon}.dark.svg`" class="dark-block light-hidden" :width="width" :height="height" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
@@ -12,25 +12,5 @@ const isDark = computed({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="site-body-left-column-site-theme-toggle" :class="{'is-dark': isDark}" style="">
|
<Switch v-model="isDark" onIcon="radix-icons:moon" offIcon="radix-icons:sun" />
|
||||||
<span class="option mod-dark">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-moon">
|
|
||||||
<path d="M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<div class="checkbox-container" :class="{'is-enabled': isDark}" @click="isDark = !isDark"></div>
|
|
||||||
<span class="option mod-light">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-sun">
|
|
||||||
<circle cx="12" cy="12" r="4"></circle>
|
|
||||||
<path d="M12 2v2"></path>
|
|
||||||
<path d="M12 20v2"></path>
|
|
||||||
<path d="m4.93 4.93 1.41 1.41"></path>
|
|
||||||
<path d="m17.66 17.66 1.41 1.41"></path>
|
|
||||||
<path d="M2 12h2"></path>
|
|
||||||
<path d="M20 12h2"></path>
|
|
||||||
<path d="m6.34 17.66-1.41 1.41"></path>
|
|
||||||
<path d="m19.07 4.93-1.41 1.41"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
30
components/base/Avatar.vue
Normal file
30
components/base/Avatar.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<AvatarRoot class="inline-flex select-none items-center justify-center overflow-hidden align-middle" :class="SIZES[size]">
|
||||||
|
<AvatarImage class="h-full w-full object-cover" :src="src" asChild @loading-status-change="(status) => loading = status === 'loading'">
|
||||||
|
<img :src="src" />
|
||||||
|
</AvatarImage>
|
||||||
|
<AvatarFallback :delay-ms="0" class="text-light-100 dark:text-dark-100 leading-1 flex h-full w-full p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<Icon v-else-if="!!icon" :icon="icon" class="w-full h-full" />
|
||||||
|
<span v-else-if="!!text">{{ text }}</span>
|
||||||
|
</AvatarFallback>
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { src, icon, text, size = 'medium' } = defineProps<{
|
||||||
|
src: string
|
||||||
|
icon?: string
|
||||||
|
text?: string
|
||||||
|
size?: keyof typeof SIZES
|
||||||
|
}>();
|
||||||
|
const loading = ref(true);
|
||||||
|
</script>
|
||||||
|
<script lang="ts">
|
||||||
|
const SIZES = {
|
||||||
|
'small': 'h-6',
|
||||||
|
'medium': 'h-10',
|
||||||
|
'large': 'h-16',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
18
components/base/Button.vue
Normal file
18
components/base/Button.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<button :disabled="disabled" class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||||
|
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50"
|
||||||
|
:class="{'p-1': loading || icon, 'h-[35px] px-[15px]': !loading && !icon}" @click="!loading && emit('click')">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<slot v-else />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { icon = false, loading = false, disabled = false } = defineProps<{
|
||||||
|
icon?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
</script>
|
||||||
47
components/base/Collapsible.vue
Normal file
47
components/base/Collapsible.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<CollapsibleRoot v-model:open="model" :disabled="disabled" :defaultOpen="defaultOpen">
|
||||||
|
<slot name="alwaysVisible"></slot>
|
||||||
|
<div class="flex flex-row justify-center items-center">
|
||||||
|
<span>{{ label }}<slot name="label"></slot></span>
|
||||||
|
<CollapsibleTrigger class="ms-4" asChild>
|
||||||
|
<Button icon :disabled="disabled">
|
||||||
|
<Icon v-if="model" icon="radix-icons:cross-2" class="h-4 w-4" />
|
||||||
|
<Icon v-else icon="radix-icons:row-spacing" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out]">
|
||||||
|
<slot></slot>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</CollapsibleRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { label, disabled = false, defaultOpen = false } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
defaultOpen?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes collapseOpen {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-collapsible-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes collapseClose {
|
||||||
|
from {
|
||||||
|
height: var(--radix-collapsible-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
components/base/Combobox.vue
Normal file
45
components/base/Combobox.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
||||||
|
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||||
|
<ComboboxRoot v-model:model-value="model" v-model:open="open" :multiple="multiple">
|
||||||
|
<ComboboxAnchor :disabled="disabled" class="mx-4 inline-flex min-w-[150px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
||||||
|
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
||||||
|
data-[placeholder]:text-light-50 dark:data-[placeholder]:text-dark-50 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
hover:border-light-50 dark:hover:border-dark-50">
|
||||||
|
<ComboboxTrigger class="flex flex-1 justify-between !cursor-pointer">
|
||||||
|
<span v-if="!multiple">{{ model !== undefined ? options.find(e => e[1] === model)![0] : "" }}</span>
|
||||||
|
<span class="flex gap-2" v-else><span v-if="model !== undefined">{{ options.find(e => e[1] === (model as T[])[0]) !== undefined ? options.find(e => e[1] === (model as T[])[0])![0] : undefined }}</span><span v-if="model !== undefined && (model as T[]).length > 1">{{((model as T[]).length > 1 ? `+${(model as T[]).length - 1}` : "") }}</span></span>
|
||||||
|
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||||
|
</ComboboxTrigger>
|
||||||
|
</ComboboxAnchor>
|
||||||
|
|
||||||
|
<ComboboxPortal :disabled="disabled">
|
||||||
|
<ComboboxContent :position="position" align="start" class="min-w-[150px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-50">
|
||||||
|
<ComboboxViewport>
|
||||||
|
<ComboboxItem v-for="[label, value] of options" :value="value" :disabled="disabled" class="text-base py-2 leading-none text-light-60 dark:text-dark-60 flex items-center px-6 relative Combobox-none data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-light-30 dark:data-[highlighted]:bg-dark-30 data-[highlighted]:text-light-100 dark:data-[highlighted]:text-dark-100">
|
||||||
|
<span class="">{{ label }}</span>
|
||||||
|
<ComboboxItemIndicator class="absolute left-1 w-4 inline-flex items-center justify-center">
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</ComboboxItemIndicator>
|
||||||
|
</ComboboxItem>
|
||||||
|
</ComboboxViewport>
|
||||||
|
</ComboboxContent>
|
||||||
|
</ComboboxPortal>
|
||||||
|
</ComboboxRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends string | number | boolean | Record<string, any>">
|
||||||
|
import { ComboboxInput, ComboboxTrigger, ComboboxViewport, ComboboxContent, ComboboxPortal, ComboboxRoot } from 'radix-vue'
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { disabled = false, position = 'popper', multiple = false } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
position?: 'inline' | 'popper'
|
||||||
|
label?: string
|
||||||
|
multiple?: boolean
|
||||||
|
options: Array<[string, T]>
|
||||||
|
}>();
|
||||||
|
const open = ref(false);
|
||||||
|
const model = defineModel<T | T[]>();
|
||||||
|
</script>
|
||||||
39
components/base/Dialog.vue
Normal file
39
components/base/Dialog.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<DialogRoot v-if="!priority" v-model="model">
|
||||||
|
<DialogTrigger asChild><Button v-if="!!label">{{ label }}</Button><slot name="trigger" /></DialogTrigger>
|
||||||
|
<DialogPortal v-if="!disabled">
|
||||||
|
<DialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<DialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<DialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</DialogTitle>
|
||||||
|
<DialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</DialogDescription>
|
||||||
|
<slot />
|
||||||
|
<DialogClose v-if="iconClose" class="text-light-100 dark:text-dark-100 absolute top-2 right-2 inline-flex h-6 w-6 appearance-none items-center justify-center outline-none text-xl" aria-label="Close">
|
||||||
|
<span aria-hidden>×</span>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
<AlertDialogRoot v-else v-model="model">
|
||||||
|
<AlertDialogTrigger asChild><Button v-if="!!label">{{ label }}</Button><slot name="trigger" /></AlertDialogTrigger>
|
||||||
|
<AlertDialogPortal v-if="!disabled">
|
||||||
|
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||||
|
<AlertDialogContent class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||||
|
<AlertDialogTitle v-if="!!title" class="text-3xl font-light relative -top-2">{{ title }}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription v-if="!!description" class="text-base pb-2">{{ description }}</AlertDialogDescription>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
</AlertDialogRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { label, title, description, priority = false, disabled = false, iconClose = true } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
priority?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
iconClose?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel();
|
||||||
|
</script>
|
||||||
80
components/base/DraggableTree.vue
Normal file
80
components/base/DraggableTree.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<TreeRoot v-bind="forward" v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 overflow-auto max-h-full">
|
||||||
|
<DraggableTreeItem v-for="item in flattenItems" :key="item._id" v-bind="item" class="group flex items-center outline-none relative cursor-pointer max-w-full" @select.prevent @toggle.prevent>
|
||||||
|
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver }">
|
||||||
|
<slot :handleToggle="handleToggle"
|
||||||
|
:handleSelect="handleSelect"
|
||||||
|
:isExpanded="isExpanded"
|
||||||
|
:isSelected="isSelected"
|
||||||
|
:isDragging="isDragging"
|
||||||
|
:isDraggedOver="isDraggedOver"
|
||||||
|
:item="item"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #hint="{ instruction }">
|
||||||
|
<div v-if="instruction">
|
||||||
|
<slot name="hint" :instruction="instruction" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DraggableTreeItem>
|
||||||
|
</TreeRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||||
|
import { useForwardPropsEmits, type FlattenedItem, type TreeRootEmits, type TreeRootProps } from 'radix-vue';
|
||||||
|
import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
|
||||||
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
|
||||||
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||||
|
|
||||||
|
const props = defineProps<TreeRootProps<T>>();
|
||||||
|
const emits = defineEmits<TreeRootEmits<T> & {
|
||||||
|
'updateTree': [instruction: Instruction, itemId: string, targetId: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default: (props: {
|
||||||
|
handleToggle: () => void,
|
||||||
|
handleSelect: () => void,
|
||||||
|
isExpanded: boolean,
|
||||||
|
isSelected: boolean,
|
||||||
|
isDragging: boolean,
|
||||||
|
isDraggedOver: boolean,
|
||||||
|
item: FlattenedItem<T>,
|
||||||
|
}) => any,
|
||||||
|
hint: (props: {
|
||||||
|
instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
|
||||||
|
}) => any,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const forward = useForwardPropsEmits(props, emits);
|
||||||
|
|
||||||
|
watchEffect((onCleanup) => {
|
||||||
|
const dndFunction = combine(
|
||||||
|
monitorForElements({
|
||||||
|
onDrop(args) {
|
||||||
|
const { location, source } = args;
|
||||||
|
|
||||||
|
if (!location.current.dropTargets.length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const itemId = source.data.id as string;
|
||||||
|
const target = location.current.dropTargets[0];
|
||||||
|
const targetId = target.data.id as string;
|
||||||
|
|
||||||
|
const instruction: Instruction | null = extractInstruction(
|
||||||
|
target.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (instruction !== null)
|
||||||
|
{
|
||||||
|
emits('updateTree', instruction, itemId, targetId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
dndFunction();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
140
components/base/DraggableTreeItem.vue
Normal file
140
components/base/DraggableTreeItem.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<TreeItem ref="el" v-bind="forward" v-slot="{ isExpanded, isSelected, isIndeterminate, handleToggle, handleSelect }">
|
||||||
|
<slot
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:is-selected="isSelected"
|
||||||
|
:is-indeterminate="isIndeterminate"
|
||||||
|
:handle-select="handleSelect"
|
||||||
|
:handle-toggle="handleToggle"
|
||||||
|
:isDragging="isDragging"
|
||||||
|
:isDraggedOver="isDraggedOver"
|
||||||
|
/>
|
||||||
|
<div v-if="instruction">
|
||||||
|
<slot name="hint" :instruction="instruction" />
|
||||||
|
</div>
|
||||||
|
</TreeItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||||
|
import { useForwardPropsEmits, type FlattenedItem, type TreeItemEmits, type TreeItemProps } from 'radix-vue';
|
||||||
|
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||||
|
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'
|
||||||
|
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
|
||||||
|
|
||||||
|
const props = defineProps<TreeItemProps<T> & FlattenedItem<T>>();
|
||||||
|
const emits = defineEmits<TreeItemEmits<T>>();
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default: (props: {
|
||||||
|
isExpanded: boolean
|
||||||
|
isSelected: boolean
|
||||||
|
isIndeterminate: boolean | undefined
|
||||||
|
isDragging: boolean
|
||||||
|
isDraggedOver: boolean
|
||||||
|
handleToggle: () => void
|
||||||
|
handleSelect: () => void
|
||||||
|
}) => any,
|
||||||
|
hint: (props: {
|
||||||
|
instruction: Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null
|
||||||
|
}) => any,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const forward = useForwardPropsEmits(props, emits);
|
||||||
|
|
||||||
|
const element = templateRef('el');
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const isDraggedOver = ref(false);
|
||||||
|
const isInitialExpanded = ref(false);
|
||||||
|
const instruction = ref<Extract<Instruction, { type: 'reorder-above' | 'reorder-below' | 'make-child' }> | null>(null);
|
||||||
|
|
||||||
|
const mode = computed(() => {
|
||||||
|
if (props.hasChildren)
|
||||||
|
return 'expanded'
|
||||||
|
if (props.index + 1 === props.parentItem?.children?.length)
|
||||||
|
return 'last-in-group'
|
||||||
|
return 'standard'
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect((onCleanup) => {
|
||||||
|
const currentElement = unrefElement(element) as HTMLElement;
|
||||||
|
|
||||||
|
if (!currentElement)
|
||||||
|
return
|
||||||
|
|
||||||
|
const item = { ...props.value, level: props.level, id: props._id }
|
||||||
|
|
||||||
|
const expandItem = () => {
|
||||||
|
if (!element.value?.isExpanded) {
|
||||||
|
element.value?.handleToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeItem = () => {
|
||||||
|
if (element.value?.isExpanded) {
|
||||||
|
element.value?.handleToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dndFunction = combine(
|
||||||
|
draggable({
|
||||||
|
element: currentElement,
|
||||||
|
getInitialData: () => item,
|
||||||
|
onDragStart: () => {
|
||||||
|
isDragging.value = true
|
||||||
|
isInitialExpanded.value = element.value?.isExpanded ?? false
|
||||||
|
closeItem()
|
||||||
|
},
|
||||||
|
onDrop: () => {
|
||||||
|
isDragging.value = false
|
||||||
|
if (isInitialExpanded.value)
|
||||||
|
expandItem()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
dropTargetForElements({
|
||||||
|
element: currentElement,
|
||||||
|
getData: ({ input, element }) => {
|
||||||
|
const data = { id: item.id }
|
||||||
|
|
||||||
|
return attachInstruction(data, {
|
||||||
|
input,
|
||||||
|
element,
|
||||||
|
indentPerLevel: 16,
|
||||||
|
currentLevel: props.level,
|
||||||
|
mode: mode.value,
|
||||||
|
block: [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
canDrop: ({ source }) => {
|
||||||
|
return source.data.id !== item.id
|
||||||
|
},
|
||||||
|
onDrag: ({ self }) => {
|
||||||
|
instruction.value = extractInstruction(self.data) as typeof instruction.value
|
||||||
|
},
|
||||||
|
onDragEnter: ({ source }) => {
|
||||||
|
if (source.data.id !== item.id) {
|
||||||
|
isDraggedOver.value = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragLeave: () => {
|
||||||
|
isDraggedOver.value = false
|
||||||
|
instruction.value = null
|
||||||
|
},
|
||||||
|
onDrop: ({ location }) => {
|
||||||
|
isDraggedOver.value = false
|
||||||
|
instruction.value = null
|
||||||
|
},
|
||||||
|
getIsSticky: () => true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
monitorForElements({
|
||||||
|
canMonitor: ({ source }) => {
|
||||||
|
return source.data.id !== item.id
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cleanup dnd function
|
||||||
|
onCleanup(() => dndFunction())
|
||||||
|
})
|
||||||
|
</script>
|
||||||
73
components/base/DropdownContentRender.vue
Normal file
73
components/base/DropdownContentRender.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<template v-for="(item, idx) of options">
|
||||||
|
<template v-if="item.type === 'item'">
|
||||||
|
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'!pe-1': item.kbd}" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="absolute left-1.5" />
|
||||||
|
<div class="flex flex-1 justify-between">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="item.type === 'checkbox'">
|
||||||
|
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" v-model:checked="item.checked" @update:checked="item.select" class="cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||||
|
<span class="w-6 flex items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-1 justify-between">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- TODO -->
|
||||||
|
<template v-if="item.type === 'radio'">
|
||||||
|
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuRadioGroup @update:model-value="item.change">
|
||||||
|
<DropdownMenuRadioItem v-for="option in item.items" :disabled="(option as any)?.disabled ?? false" :value="(option as any)?.value ?? option">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Icon icon="radix-icons:dot-filled" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
<span>{{ (option as any)?.label || option }}</span>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="item.type === 'submenu'">
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<Icon icon="radix-icons:chevron-right" class="absolute right-1" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent class="z-50 outline-none bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade border border-light-35 dark:border-dark-35">
|
||||||
|
<DropdownContentRender :options="item.items" />
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="item.type === 'group'">
|
||||||
|
<DropdownMenuLabel class="text-light-70 dark:text-dark-70 text-sm text-center pt-1">{{ item.label }}</DropdownMenuLabel>
|
||||||
|
<DropdownContentRender :options="item.items" />
|
||||||
|
|
||||||
|
<DropdownMenuSeparator v-if="idx !== options.length - 1" />
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownOption } from './DropdownMenu.vue';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { options } = defineProps<{
|
||||||
|
options: DropdownOption[]
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
58
components/base/DropdownMenu.vue
Normal file
58
components/base/DropdownMenu.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<DropdownMenuRoot>
|
||||||
|
<DropdownMenuTrigger :disabled="disabled"><slot /></DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent :align="align" :side="side" class="z-50 outline-none bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade border border-light-35 dark:border-dark-35">
|
||||||
|
<DropdownContentRender :options="options" />
|
||||||
|
|
||||||
|
<DropdownMenuArrow class="fill-light-35 dark:fill-dark-35" />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
export interface DropdownItem {
|
||||||
|
type: 'item';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
select?: () => void;
|
||||||
|
icon?: string;
|
||||||
|
kbd?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownCheckbox {
|
||||||
|
type: 'checkbox';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
checked?: boolean | Ref<boolean>
|
||||||
|
select?: (state: boolean) => void;
|
||||||
|
kbd?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownRadioGroup {
|
||||||
|
type: 'radio';
|
||||||
|
label: string;
|
||||||
|
value?: string | Ref<string>
|
||||||
|
items: (string | {label: string, value?: string, disabled?: boolean})[];
|
||||||
|
change?: (value: string) => void;
|
||||||
|
}
|
||||||
|
export interface DropdownSubmenu {
|
||||||
|
type: 'submenu';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
items: DropdownOption[];
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
export interface DropdownGroup {
|
||||||
|
type: 'group';
|
||||||
|
label?: string;
|
||||||
|
items: DropdownOption[];
|
||||||
|
}
|
||||||
|
export type DropdownOption = DropdownItem | DropdownCheckbox | DropdownRadioGroup | DropdownSubmenu | DropdownGroup;
|
||||||
|
const { options, disabled = false, side, align } = defineProps<{
|
||||||
|
options: DropdownOption[]
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
align?: 'start' | 'center' | 'end'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
36
components/base/HoverCard.vue
Normal file
36
components/base/HoverCard.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<HoverCardRoot :open-delay="delay" @update:open="(...args) => emits('open', ...args)">
|
||||||
|
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
||||||
|
<slot></slot>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardPortal v-if="!disabled">
|
||||||
|
<HoverCardContent :class="$attrs.class" :side="side" :align="align" avoidCollisions :collisionPadding="20" class="max-h-[var(--radix-hover-card-content-available-height)] data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 p-5 data-[state=open]:transition-all text-light-100 dark:text-dark-100" >
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<HoverCardArrow class="fill-light-35 dark:fill-dark-35" />
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
|
</HoverCardRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { delay = 500, disabled = false, side = 'bottom', align = 'center', triggerKey } = defineProps<{
|
||||||
|
delay?: number
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
|
align?: 'start' | 'center' | 'end'
|
||||||
|
triggerKey?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['open']);
|
||||||
|
const canOpen = ref(true);
|
||||||
|
|
||||||
|
if(triggerKey)
|
||||||
|
{
|
||||||
|
const magicKeys = useMagicKeys();
|
||||||
|
const keys = magicKeys[triggerKey];
|
||||||
|
|
||||||
|
watch(keys, (v) => {
|
||||||
|
canOpen.value = v;
|
||||||
|
}, { immediate: true, });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
3
components/base/Kbd.vue
Normal file
3
components/base/Kbd.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<span class="rounded bg-light-35 dark:bg-dark-35 font-mono text-sm px-1 py-0 select-none" style="box-shadow: black 0 2px 0 1px;"><slot /></span>
|
||||||
|
</template>
|
||||||
9
components/base/Loading.vue
Normal file
9
components/base/Loading.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<span :class="{'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}" class="after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { size = 'normal' } = defineProps<{
|
||||||
|
size?: 'small' | 'normal' | 'large'
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
23
components/base/NumberPicker.vue
Normal file
23
components/base/NumberPicker.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="my-2 flex">{{ label }}
|
||||||
|
<NumberFieldRoot :min="min" :max="max" v-model="model" :disabled="disabled" :step="step" class="ms-4 flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||||
|
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
|
||||||
|
<NumberFieldDecrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:minus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldDecrement>
|
||||||
|
<NumberFieldInput class="text-sm tabular-nums w-20 appearance-none bg-transparent px-2 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||||
|
<NumberFieldIncrement class="data-[disabled]:opacity-50 px-1"><Icon icon="radix-icons:plus" :inline="true" class="w-6 text-light-100 dark:text-dark-100 opacity-100" /></NumberFieldIncrement>
|
||||||
|
</NumberFieldRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { min = 0, max = 100, disabled = false, step = 1, label } = defineProps<{
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
disabled?: boolean
|
||||||
|
step?: number
|
||||||
|
label?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>();
|
||||||
|
</script>
|
||||||
20
components/base/PinPicker.vue
Normal file
20
components/base/PinPicker.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="my-2">{{ label }}
|
||||||
|
<PinInputRoot :disabled="disabled" :default-value="model?.split('')" @update:model-value="(v) => model = v.join('')" @complete="() => emit('complete')" class="flex gap-2 items-center mt-1">
|
||||||
|
<PinInputInput :type="hidden ? 'password' : undefined" v-for="(id, index) in amount" :key="id" :index="index" class="border border-light-35 dark:border-dark-35 w-10 h-10 outline-none
|
||||||
|
bg-light-20 dark:bg-dark-20 text-center text-light-100 dark:text-dark-100 placeholder:text-light-60 dark:placeholder:text-dark-60 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
||||||
|
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 caret-light-50 dark:caret-dark-50" />
|
||||||
|
</PinInputRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { hidden = false, amount, label, disabled = false } = defineProps<{
|
||||||
|
hidden?: boolean
|
||||||
|
label?: string
|
||||||
|
amount: number
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
const emit = defineEmits(['complete']);
|
||||||
|
</script>
|
||||||
13
components/base/Progress.vue
Normal file
13
components/base/Progress.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<ProgressRoot :max="max" v-model="model" class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||||
|
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full transition-[width] duration-[660ms] ease-[cubic-bezier(0.65, 0, 0.35, 1)]" :style="`width: ${((model ?? 0) / max) * 100}%`" />
|
||||||
|
</ProgressRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { max = 100, shape = 'normal' } = defineProps<{
|
||||||
|
max?: number
|
||||||
|
shape?: 'thin' | 'normal' | 'large'
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>();
|
||||||
|
</script>
|
||||||
30
components/base/RadioInput.vue
Normal file
30
components/base/RadioInput.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<RadioGroupRoot :disabled="disabled" v-model="model" class="flex flex-col gap-2 p-2">
|
||||||
|
<Label v-for="option in options" class="flex items-center gap-2">
|
||||||
|
<RadioGroupItem :disabled="(option as RadioOption).disabled ?? false"
|
||||||
|
:value="(option as RadioOption).value ?? option"
|
||||||
|
class="border border-light-60 dark:border-dark-35 bg-light-20 dark:bg-dark-25 w-5 h-5 outline-none cursor-default hover:border-light-70 dark:hover:border-dark-40
|
||||||
|
focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20">
|
||||||
|
<RadioGroupIndicator>
|
||||||
|
<Icon icon="radix-icons:check" class="relative w-5 h-5 -top-px -left-px" />
|
||||||
|
</RadioGroupIndicator>
|
||||||
|
</RadioGroupItem>
|
||||||
|
{{ (option as RadioOption).label ?? option }}
|
||||||
|
</Label>
|
||||||
|
</RadioGroupRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
export interface RadioOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const { disabled = false, options } = defineProps<{
|
||||||
|
disabled?: boolean
|
||||||
|
options: string[] | RadioOption[]
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
42
components/base/Select.vue
Normal file
42
components/base/Select.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
||||||
|
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||||
|
<SelectRoot v-model="model" :default-value="defaultValue">
|
||||||
|
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
||||||
|
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
||||||
|
data-[placeholder]:text-light-50 dark:data-[placeholder]:text-dark-50 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
hover:border-light-50 dark:hover:border-dark-50">
|
||||||
|
<SelectValue :placeholder="placeholder" />
|
||||||
|
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectPortal :disabled="disabled">
|
||||||
|
<SelectContent :position="position"
|
||||||
|
class="min-w-[160px] bg-light-20 dark:bg-dark-20 will-change-[opacity,transform] z-50">
|
||||||
|
<SelectScrollUpButton>
|
||||||
|
<Icon icon="radix-icons:chevron-up" />
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
<SelectViewport>
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton>
|
||||||
|
<Icon icon="radix-icons:chevron-down" />
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</SelectRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectContent, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue, SelectViewport } from 'radix-vue'
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { disabled = false, position = 'popper' } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
position?: 'item-aligned' | 'popper'
|
||||||
|
label?: string
|
||||||
|
defaultValue?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
14
components/base/SelectGroup.vue
Normal file
14
components/base/SelectGroup.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<SelectGroup :disabled="disabled" class="">
|
||||||
|
<SelectLabel class="">{{ label }}</SelectLabel>
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectGroup } from 'radix-vue';
|
||||||
|
const { label, disabled = false } = defineProps<{
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
18
components/base/SelectItem.vue
Normal file
18
components/base/SelectItem.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<SelectItem :value="value" :disabled="disabled" class="text-base py-2 leading-none text-light-60 dark:text-dark-60 flex items-center px-6 relative select-none data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-light-30 dark:data-[highlighted]:bg-dark-30 data-[highlighted]:text-light-100 dark:data-[highlighted]:text-dark-100">
|
||||||
|
<SelectItemText class="">{{ label }}</SelectItemText>
|
||||||
|
<SelectItemIndicator class="absolute left-1 w-4 inline-flex items-center justify-center">
|
||||||
|
<Icon icon="radix-icons:check" />
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { SelectItem, SelectItemIndicator, SelectItemText } from 'radix-vue'
|
||||||
|
const { disabled = false, value } = defineProps<{
|
||||||
|
disabled?: boolean
|
||||||
|
value: NonNullable<string>
|
||||||
|
label: string
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
7
components/base/SelectSeparator.vue
Normal file
7
components/base/SelectSeparator.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<SelectSeparator class="" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { SelectSeparator } from 'radix-vue';
|
||||||
|
</script>
|
||||||
25
components/base/SliderInput.vue
Normal file
25
components/base/SliderInput.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="flex justify-center items-center my-2">{{ label }}
|
||||||
|
<SliderRoot class="mx-4 relative flex items-center select-none touch-none w-[160px] h-5"
|
||||||
|
:default-value="model ? [model] : undefined" :v-model="[model]" :disabled="disabled"
|
||||||
|
@update:model-value="(value) => model = value ? value[0] : min" :min="min" :max="max" :step="step">
|
||||||
|
<SliderTrack class="bg-light-30 dark:bg-dark-30 relative h-1 w-full data-[disabled]:bg-light-10 dark:data-[disabled]:bg-dark-10">
|
||||||
|
<SliderRange class="absolute bg-light-40 dark:bg-dark-40 h-full data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30" />
|
||||||
|
</SliderTrack>
|
||||||
|
<SliderThumb
|
||||||
|
class="block w-5 h-5 bg-light-50 dark:bg-dark-50 outline-none focus:shadow-raw transition-[box-shadow] focus:shadow-light-60 dark:focus:shadow-dark-60 border border-light-50 dark:border-dark-50
|
||||||
|
hover:border-light-60 dark:hover:border-dark-60 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20" />
|
||||||
|
</SliderRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { min = 0, max = 100, step = 1, label, disabled = false } = defineProps<{
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
step?: number
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<number>()
|
||||||
|
</script>
|
||||||
27
components/base/Switch.vue
Normal file
27
components/base/Switch.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="flex justify-center items-center my-2">
|
||||||
|
<span class="md:text-base text-sm">{{ label }}</span>
|
||||||
|
<SwitchRoot v-model:checked="model" :disabled="disabled" :default-checked="defaultValue"
|
||||||
|
class="group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||||
|
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||||
|
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative">
|
||||||
|
<SwitchThumb
|
||||||
|
class="block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 data-[state=checked]:translate-x-[26px]
|
||||||
|
data-[disabled]:bg-light-30 dark:data-[disabled]:bg-dark-30 data-[disabled]:border-light-30 dark:data-[disabled]:border-dark-30" />
|
||||||
|
<Icon v-if="onIcon && offIcon" :icon="onIcon" class="group-data-[state=checked]:opacity-100 group-data-[state=unchecked]:opacity-0 absolute top-1 left-1 transition-opacity" />
|
||||||
|
<Icon v-if="onIcon && offIcon" :icon="offIcon" class="group-data-[state=checked]:opacity-0 group-data-[state=unchecked]:opacity-100 absolute top-1 right-1 transition-opacity" />
|
||||||
|
</SwitchRoot>
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
const { label, disabled, onIcon, offIcon } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onIcon?: string
|
||||||
|
offIcon?: string
|
||||||
|
defaultValue?: boolean
|
||||||
|
}>();
|
||||||
|
const model = defineModel<boolean>();
|
||||||
|
</script>
|
||||||
21
components/base/TagsInput.vue
Normal file
21
components/base/TagsInput.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<TagsInputRoot v-model="model" addOnPaste class="flex gap-2 items-center border p-2 w-full flex-wrap border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" >
|
||||||
|
<TagsInputItem v-for="item in model" :key="item" :value="item" class="text-light-100 dark:text-dark-100 flex items-center justify-center gap-2 bg-light-20 dark:bg-dark-20 hover:bg-light-35 dark:hover:bg-dark-35 p-1 border border-light-35 dark:border-dark-35">
|
||||||
|
<TagsInputItemText class="text-sm pl-1" />
|
||||||
|
<TagsInputItemDelete asChild>
|
||||||
|
<Icon icon="radix-icons:cross-2" class="w-4 h-4 cursor-pointer" />
|
||||||
|
</TagsInputItemDelete>
|
||||||
|
</TagsInputItem>
|
||||||
|
|
||||||
|
<TagsInputInput :placeholder="placeholder" class="text-sm focus:outline-none flex-1 rounded text-green9 bg-transparent placeholder:text-mauve9 px-1" />
|
||||||
|
</TagsInputRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
const { placeholder } = defineProps<{
|
||||||
|
placeholder?: string
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string[]>();
|
||||||
|
</script>
|
||||||
25
components/base/TextInput.vue
Normal file
25
components/base/TextInput.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
||||||
|
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||||
|
<input :placeholder="placeholder" :disabled="disabled"
|
||||||
|
class="mx-4 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 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
|
||||||
|
:type="type" v-model="model" :data-disabled="disabled || undefined" v-bind="$attrs" @change="(e) => emits('change', e)" @input="(e) => emits('input', e)">
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { type = 'text', label, disabled = false, placeholder } = defineProps<{
|
||||||
|
type?: 'text' | 'password' | 'email' | 'tel' | 'url'
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
change: [Event]
|
||||||
|
input: [Event]
|
||||||
|
}>();
|
||||||
|
const model = defineModel<string>();
|
||||||
|
</script>
|
||||||
85
components/base/Tooltip.vue
Normal file
85
components/base/Tooltip.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<TooltipRoot :delay-duration="delay" :disabled="disabled">
|
||||||
|
<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 z-50" :class="$attrs.class" :side="side" :align="align" :align-offset="-16" :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
|
||||||
|
disabled?: boolean
|
||||||
|
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||||
|
align?: 'start' | 'center' | 'end'
|
||||||
|
}>();
|
||||||
|
</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>
|
||||||
20
components/base/Tree.vue
Normal file
20
components/base/Tree.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 text-sm" :items="model" :get-key="getKey" :defaultExpanded="flatten(model)">
|
||||||
|
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level / 2 - 0.5}em` }" v-bind="item.bind" class="flex items-center ps-2 outline-none relative cursor-pointer">
|
||||||
|
<slot :isExpanded="isExpanded" :item="item" />
|
||||||
|
</TreeItem>
|
||||||
|
</TreeRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||||
|
const { getKey } = defineProps<{
|
||||||
|
getKey: (val: T) => string
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const model = defineModel<T[]>();
|
||||||
|
|
||||||
|
function flatten(arr: T[]): string[]
|
||||||
|
{
|
||||||
|
return arr.filter(e => e.open).flatMap(e => [getKey(e), ...flatten(e.children ?? [])]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
interface Props
|
|
||||||
{
|
|
||||||
path: {
|
|
||||||
path: string;
|
|
||||||
from: { x: number; y: number };
|
|
||||||
to: { x: number; y: number };
|
|
||||||
side: 'bottom' | 'top' | 'left' | 'right';
|
|
||||||
};
|
|
||||||
color?: string;
|
|
||||||
label?: string;
|
|
||||||
}
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const rotation = {
|
|
||||||
top: "180",
|
|
||||||
bottom: "0",
|
|
||||||
left: "90",
|
|
||||||
right: "270"
|
|
||||||
};
|
|
||||||
|
|
||||||
function hexToRgb(hex: string): string {
|
|
||||||
return `${parseInt(hex.substring(1, 3), 16)}, ${parseInt(hex.substring(3, 5), 16)}, ${parseInt(hex.substring(5, 7), 16)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes: any = { 'is-themed': props.color !== undefined, 'mod-canvas-color-custom': (props?.color?.startsWith('#') ?? false) };
|
|
||||||
|
|
||||||
if (props.color !== undefined) {
|
|
||||||
if (!props.color.startsWith('#'))
|
|
||||||
classes['mod-canvas-color-' + props.color] = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<g :class="classes"
|
|
||||||
:style="{ '--canvas-color': props?.color?.startsWith('#') ? hexToRgb(props.color) : undefined }">
|
|
||||||
<path class="canvas-display-path" :d="props.path.path"></path>
|
|
||||||
</g>
|
|
||||||
<g :class="classes"
|
|
||||||
:style="{ '--canvas-color': props?.color?.startsWith('#') ? hexToRgb(props.color) : undefined, transform: `translate(${props.path.to.x}px, ${props.path.to.y}px) rotate(${rotation[props.path.side]}deg)` }">
|
|
||||||
<polygon class="canvas-path-end" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
|
||||||
</g>
|
|
||||||
</template>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
node: CanvasNode;
|
|
||||||
zoom: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor(color: string): string
|
|
||||||
{
|
|
||||||
if(props.node?.color?.startsWith('#'))
|
|
||||||
return hexToRgb(color);
|
|
||||||
else
|
|
||||||
return getComputedStyle(document.body, null).getPropertyValue('--canvas-color-' + props.node.color);
|
|
||||||
}
|
|
||||||
function hexToRgb(hex: string): string
|
|
||||||
{
|
|
||||||
return `${parseInt(hex.substring(1, 3), 16)},${parseInt(hex.substring(3, 5), 16)},${parseInt(hex.substring(5, 7), 16)}`;
|
|
||||||
}
|
|
||||||
function darken(rgb: string): boolean
|
|
||||||
{
|
|
||||||
const [r, g, b] = rgb.split(',');
|
|
||||||
return (299 * parseInt(r) + 587 * parseInt(g) + 114 * parseInt(b)) / 1e3 >= 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const classes: Record<string, boolean> = { 'canvas-node-group': props.node.type === 'group', 'is-themed': props.node.color !== undefined, 'mod-canvas-color-custom': (props.node?.color?.startsWith('#') ?? false) };
|
|
||||||
const size = Math.max(props.node.width, props.node.height);
|
|
||||||
|
|
||||||
if(props.node.color !== undefined)
|
|
||||||
{
|
|
||||||
if (!props.node.color.startsWith('#'))
|
|
||||||
classes['mod-canvas-color-' + props.node.color] = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="canvas-node" :class="classes"
|
|
||||||
:style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-node-width': `${node.width}px`, '--canvas-node-height': `${node.height}px`, '--canvas-color': node?.color?.startsWith('#') ? hexToRgb(node.color) : undefined}">
|
|
||||||
<div class="canvas-node-container">
|
|
||||||
<template v-if="node.type === 'group' || zoom > Math.min(0.38, 1000 / size)">
|
|
||||||
<div class="canvas-node-content markdown-embed">
|
|
||||||
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
|
|
||||||
<div
|
|
||||||
class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
|
|
||||||
<div class="markdown-preview-sizer markdown-preview-section">
|
|
||||||
<Markdown v-model="node.text" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="canvas-node-placeholder">
|
|
||||||
<div class="canvas-icon-placeholder">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="svg-icon lucide-align-left">
|
|
||||||
<line x1="21" y1="6" x2="3" y2="6"></line>
|
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<ClientOnly><div v-if="node.type === 'group' && node.label !== undefined" class="canvas-group-label"
|
|
||||||
:class="{ 'mod-foreground-dark': darken(getColor(props.node?.color ?? '')), 'mod-foreground-light': !darken(getColor(props.node?.color ?? ''))}">
|
|
||||||
{{ node.label }}</div></ClientOnly>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import("~/assets/canvas.css")
|
|
||||||
|
|
||||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
interface Props
|
|
||||||
{
|
|
||||||
canvas: CanvasContent;
|
|
||||||
}
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
let dragging = false, posX = 0, posY = 0, dispX = ref(0), dispY = ref(0), minZoom = ref(0.3), zoom = ref(1);
|
|
||||||
let centerX = ref(0), centerY = ref(0), canvas = ref<HTMLDivElement>();
|
|
||||||
let minX = ref(+Infinity), minY = ref(+Infinity), maxX = ref(-Infinity), maxY = ref(-Infinity);
|
|
||||||
let bbox = ref<DOMRect>();
|
|
||||||
|
|
||||||
let lastPinchLength = 0;
|
|
||||||
|
|
||||||
let _minX = +Infinity, _minY = +Infinity, _maxX = -Infinity, _maxY = -Infinity;
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
props.canvas.nodes.forEach((e) => {
|
|
||||||
_minX = Math.min(_minX, e.x);
|
|
||||||
_minY = Math.min(_minY, e.y);
|
|
||||||
_maxX = Math.max(_maxX, e.x + e.width);
|
|
||||||
_maxY = Math.max(_maxY, e.y + e.height);
|
|
||||||
});
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
onResize();
|
|
||||||
|
|
||||||
dispX.value = -(_maxX + _minX) / 2;
|
|
||||||
dispY.value = -(_maxY + _minY) / 2;
|
|
||||||
|
|
||||||
zoom.value = minZoom.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', onResize);
|
|
||||||
})
|
|
||||||
|
|
||||||
const onResize = (event?: Event) => {
|
|
||||||
minX.value = _minX = _minX - 32;
|
|
||||||
minY.value = _minY = _minY - 32;
|
|
||||||
maxX.value = _maxX = _maxX + 32;
|
|
||||||
maxY.value = _maxY = _maxY + 32;
|
|
||||||
|
|
||||||
minZoom.value = Math.min((canvas.value?.clientWidth ?? 1920) / (_maxX - _minX), (canvas.value?.clientHeight ?? 1080) / (_maxY - _minY)) * 0.9;
|
|
||||||
zoom.value = clamp(zoom.value, minZoom.value, 3);
|
|
||||||
|
|
||||||
bbox.value = (canvas.value ?? document.getElementById('canvas'))?.getBoundingClientRect();
|
|
||||||
|
|
||||||
centerX.value = (bbox.value?.width ?? 0) / 2;
|
|
||||||
centerY.value = (bbox.value?.height ?? 0) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerDown = (event: PointerEvent) => {
|
|
||||||
if (event.isPrimary === false) return;
|
|
||||||
dragging = true;
|
|
||||||
|
|
||||||
posX = event.clientX;
|
|
||||||
posY = event.clientY;
|
|
||||||
|
|
||||||
document.addEventListener('pointermove', onPointerMove);
|
|
||||||
document.addEventListener('pointerup', onPointerUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
|
||||||
if (event.isPrimary === false || dragging === false) return;
|
|
||||||
dispX.value -= (posX - event.clientX) / zoom.value;
|
|
||||||
dispY.value -= (posY - event.clientY) / zoom.value;
|
|
||||||
|
|
||||||
posX = event.clientX;
|
|
||||||
posY = event.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerUp = (event: PointerEvent) => {
|
|
||||||
if (event.isPrimary === false) return;
|
|
||||||
dragging = false;
|
|
||||||
document.removeEventListener('pointermove', onPointerMove);
|
|
||||||
document.removeEventListener('pointerup', onPointerUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onWheel = (event: WheelEvent) => {
|
|
||||||
zoom.value = clamp(zoom.value + (event.deltaY * -0.001), minZoom.value, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTouchStart = (event: TouchEvent) => {
|
|
||||||
if(event.touches?.length === 2)
|
|
||||||
{
|
|
||||||
dragging = false;
|
|
||||||
lastPinchLength = length(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
|
|
||||||
|
|
||||||
document.addEventListener('touchmove', onTouchMove);
|
|
||||||
document.addEventListener('touchend', onTouchEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTouchEnd = (event: TouchEvent) => {
|
|
||||||
if(event.touches?.length !== 2)
|
|
||||||
dragging = true;
|
|
||||||
|
|
||||||
document.removeEventListener('touchmove', onTouchMove);
|
|
||||||
document.removeEventListener('touchend', onTouchEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTouchMove = (event: TouchEvent) => {
|
|
||||||
if(event.touches?.length === 2)
|
|
||||||
{
|
|
||||||
const l = length(event.touches[0].clientX, event.touches[0].clientY, event.touches[1].clientX, event.touches[1].clientY);
|
|
||||||
zoom.value = clamp(zoom.value + ((lastPinchLength - l) * -0.01), minZoom.value, 3);
|
|
||||||
|
|
||||||
lastPinchLength = l;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = (_: MouseEvent) => {
|
|
||||||
zoom.value = minZoom.value;
|
|
||||||
dispX.value = -(maxX.value + minX.value) / 2;
|
|
||||||
dispY.value = -(maxY.value + minY.value) / 2;
|
|
||||||
}
|
|
||||||
function clamp(x: number, min: number, max: number): number {
|
|
||||||
if (x > max)
|
|
||||||
return max;
|
|
||||||
if (x < min)
|
|
||||||
return min;
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
function length(x1: number, y1: number, x2: number, y2: number): number {
|
|
||||||
return Math.sqrt((x2 - x1)^2 + (y2 - y1)^2);
|
|
||||||
}
|
|
||||||
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, n: number): { x: number, y: number } {
|
|
||||||
switch (side) {
|
|
||||||
case "left":
|
|
||||||
return {
|
|
||||||
x: pos.x - n,
|
|
||||||
y: pos.y
|
|
||||||
};
|
|
||||||
case "right":
|
|
||||||
return {
|
|
||||||
x: pos.x + n,
|
|
||||||
y: pos.y
|
|
||||||
};
|
|
||||||
case "top":
|
|
||||||
return {
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y - n
|
|
||||||
};
|
|
||||||
case "bottom":
|
|
||||||
return {
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y + n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getNode(id: string): CanvasNode | undefined
|
|
||||||
{
|
|
||||||
return props.canvas.nodes.find(e => e.id === id);
|
|
||||||
}
|
|
||||||
function mK(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
|
|
||||||
switch (t) {
|
|
||||||
case "top":
|
|
||||||
return { x: (e.minX + e.maxX) / 2, y: e.minY };
|
|
||||||
case "right":
|
|
||||||
return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
|
|
||||||
case "bottom":
|
|
||||||
return { x: (e.minX + e.maxX) / 2, y: e.maxY };
|
|
||||||
case "left":
|
|
||||||
return { x: e.minX, y: (e.minY + e.maxY) / 2 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function 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 a = mK(getBbox(from), fromSide),
|
|
||||||
l = mK(getBbox(to), toSide);
|
|
||||||
return bezier(a, fromSide, l, toSide);
|
|
||||||
}
|
|
||||||
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
|
||||||
const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
|
|
||||||
return {
|
|
||||||
path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
|
|
||||||
from: from,
|
|
||||||
to: to,
|
|
||||||
side: toSide,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
|
|
||||||
const a = mK(getBbox(from), fromSide), l = mK(getBbox(to), toSide);
|
|
||||||
const r = Math.hypot(a.x - l.x, a.y - l.y), o = clamp(r / 2, 70, 150), b = edgePos(fromSide, a, o), s = edgePos(toSide, l, o);
|
|
||||||
const center = getCenter(a, l, 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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Suspense>
|
|
||||||
<template #default>
|
|
||||||
<div id="canvas" ref="canvas" @pointerdown="onPointerDown" @wheel.passive="onWheel" @touchstart.passive="onTouchStart"
|
|
||||||
@dragstart.prevent="" class="canvas-wrapper node-insert-event mod-zoomed-out"
|
|
||||||
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
|
||||||
<div class="canvas-controls" style="z-index: 999;">
|
|
||||||
<div class="canvas-control-group">
|
|
||||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="canvas-control-item"
|
|
||||||
aria-label="Zoom in" data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" class="svg-icon lucide-plus">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
<path d="M12 5v14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div @click="zoom = 1" class="canvas-control-item" aria-label="Reset zoom"
|
|
||||||
data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" class="svg-icon lucide-rotate-cw">
|
|
||||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
|
||||||
<path d="M21 3v5h-5"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div @click="reset" class="canvas-control-item" aria-label="Zoom to fit"
|
|
||||||
data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" class="svg-icon lucide-maximize">
|
|
||||||
<path d="M8 3H5a2 2 0 0 0-2 2v3"></path>
|
|
||||||
<path d="M21 8V5a2 2 0 0 0-2-2h-3"></path>
|
|
||||||
<path d="M3 16v3a2 2 0 0 0 2 2h3"></path>
|
|
||||||
<path d="M16 21h3a2 2 0 0 0 2-2v-3"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="canvas-control-item"
|
|
||||||
aria-label="Zoom out" data-tooltip-position="left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
||||||
stroke-linejoin="round" class="svg-icon lucide-minus">
|
|
||||||
<path d="M5 12h14"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="canvas"
|
|
||||||
:style="{transform: `translate(${centerX}px, ${centerY}px) scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
|
||||||
<svg class="canvas-edges">
|
|
||||||
<CanvasEdge v-for="edge of props.canvas.edges" :key="edge.id"
|
|
||||||
:path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
|
|
||||||
:color="edge.color" :label="edge.label" />
|
|
||||||
</svg>
|
|
||||||
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
|
||||||
<template v-for="edge of props.canvas.edges">
|
|
||||||
<div :key="edge.id" v-if="edge.label" class="canvas-path-label-wrapper"
|
|
||||||
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
|
|
||||||
<div class="canvas-path-label">{{ edge.label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #fallback>
|
|
||||||
<div class="loading"></div>
|
|
||||||
</template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const { data: comments } = await useFetch(`/api/project/1/comment`, {
|
|
||||||
query: {
|
|
||||||
path: unifySlug(useRoute().params.slug)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</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>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const project = computed(() => parseInt(route.params.projectId as string));
|
|
||||||
|
|
||||||
const { data: navigation, refresh } = await useLazyFetch(() => `/api/project/${project.value}/navigation`, { immediate: false });
|
|
||||||
|
|
||||||
if(!isNaN(project.value))
|
|
||||||
{
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="site-body-left-column">
|
|
||||||
<div class="site-body-left-column-inner">
|
|
||||||
<div class="nav-view-outer">
|
|
||||||
<div class="nav-view">
|
|
||||||
<div class="tree-item">
|
|
||||||
<div class="tree-item-children">
|
|
||||||
<NavigationLink v-if="!!navigation" v-for="link of navigation" :project="project" :link="link" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<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));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="tree-item" v-if="project && !isNaN(project)">
|
|
||||||
<template v-if="hasChildren">
|
|
||||||
<div class="tree-item-self"
|
|
||||||
:class="{ 'is-collapsed': collapsed, 'mod-collapsible is-clickable': hasChildren }"
|
|
||||||
data-path="{{ props.link.title }}" @click="collapsed = hasChildren && !collapsed"
|
|
||||||
:data-type="link.private ? 'privé' : undefined">
|
|
||||||
<div v-if="hasChildren" class="tree-item-icon collapse-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
||||||
class="svg-icon right-triangle">
|
|
||||||
<path d="M3 8L12 17L21 8"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="tree-item-inner">{{ link.title }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!collapsed" class="tree-item-children">
|
|
||||||
<NavigationLink v-if="hasChildren" v-for="l of link.children" :link="l" :project="project" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<NuxtLink v-else class="tree-item-self" :to="{ path: `/explorer/${project}/${link.path}`, force: true }"
|
|
||||||
:active-class="'mod-active'" :data-type="(link.type === 'Canvas' ? 'graph' : (link.private ? 'privé' : undefined))">
|
|
||||||
<div class="tree-item-inner">{{ link.title }}</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Teleport to="#teleports" v-if="display && (!fetched || loaded)">
|
|
||||||
<div class="popover hover-popover" :class="{'is-loaded': fetched}" :style="pos"
|
|
||||||
@mouseenter="debounce(show, 250)" @mouseleave="debounce(() => display = false, 250)">
|
|
||||||
<div v-if="pending" class="loading"></div>
|
|
||||||
<template v-else-if="content !==''">
|
|
||||||
<div v-if="type === 'Markdown'" class=" markdown-embed">
|
|
||||||
<div class="markdown-embed-content">
|
|
||||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
|
||||||
<div class="markdown-preview-sizer markdown-preview-section">
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
<Markdown v-model="content"></Markdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type === 'Canvas'" class="canvas-embed is-loaded">
|
|
||||||
<CanvasRenderer :canvas="JSON.parse(content) " />
|
|
||||||
</div>
|
|
||||||
<div class="not-found-container" v-else>
|
|
||||||
<div class="not-found-image"></div>
|
|
||||||
<div class="not-found-title">Fichier vide</div>
|
|
||||||
<div class="not-found-description">Cette page est vide</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="not-found-container" v-else>
|
|
||||||
<div class="not-found-image"></div>
|
|
||||||
<div class="not-found-title">Impossible d'afficher</div>
|
|
||||||
<div class="not-found-description">Cette page est impossible à traiter</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
<span ref="el" @mouseenter="debounce(show, 250)" @mouseleave="debounce(() => display = false, 250)">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
anchor: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const content = ref(''), title = ref(''), type = ref(''), display = ref(false), pending = ref(false), fetched = ref(false);
|
|
||||||
const el = ref(), pos = ref<Record<string, string>>();
|
|
||||||
const loaded = computed(() => !pending.value && fetched.value && content.value !== '');
|
|
||||||
|
|
||||||
async function fetch()
|
|
||||||
{
|
|
||||||
fetched.value = true;
|
|
||||||
pending.value = true;
|
|
||||||
|
|
||||||
const data = await $fetch(`/api/project/${props.project}/file`, {
|
|
||||||
method: 'get',
|
|
||||||
query: {
|
|
||||||
path: props.path,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
pending.value = false;
|
|
||||||
|
|
||||||
if(data && data[0])
|
|
||||||
{
|
|
||||||
content.value = data[0].content;
|
|
||||||
title.value = data[0].title;
|
|
||||||
type.value = data[0].type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function show()
|
|
||||||
{
|
|
||||||
if(display.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if(!fetched.value)
|
|
||||||
await fetch();
|
|
||||||
|
|
||||||
const rect = (el.value as HTMLDivElement)?.getBoundingClientRect();
|
|
||||||
|
|
||||||
if(!rect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const r: Record<string, string> = {};
|
|
||||||
if (rect.bottom + 450 < window.innerHeight)
|
|
||||||
r.top = (rect.bottom + 4) + "px";
|
|
||||||
else
|
|
||||||
r.bottom = (window.innerHeight - rect.top + 4) + "px";
|
|
||||||
if (rect.right + 550 < window.innerWidth)
|
|
||||||
r.left = rect.left + "px";
|
|
||||||
else
|
|
||||||
r.right = (window.innerWidth - rect.right + 4) + "px";
|
|
||||||
|
|
||||||
pos.value = r;
|
|
||||||
display.value = true;
|
|
||||||
}
|
|
||||||
let debounceFn: () => void, timeout: NodeJS.Timeout;
|
|
||||||
function debounce(fn: () => void, ms: number)
|
|
||||||
{
|
|
||||||
debounceFn = fn;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(debounceFn, ms);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<NuxtLink class="preview-link" v-if="data && data[0] && status !== 'pending'"
|
|
||||||
:to="{ path: `/explorer/${project}/${data[0].path}`, hash: hash }" :class="class">
|
|
||||||
<PreviewContent :project="project" :path="data[0].path" :anchor="hash">
|
|
||||||
<slot v-bind="$attrs"></slot>
|
|
||||||
<ThemeIcon class="link-icon" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
|
||||||
:icon="`link-${data[0].type.toLowerCase()}`" />
|
|
||||||
</PreviewContent>
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink v-else-if="href" :to="href" :class="class">
|
|
||||||
<slot v-bind="$attrs"></slot>
|
|
||||||
<ThemeIcon class="link-icon" v-if="data && data[0] && data[0].type !== 'Markdown'" :height="20" :width="20"
|
|
||||||
:icon="`link-${data[0].type.toLowerCase()}`" />
|
|
||||||
</NuxtLink>
|
|
||||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<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, status } = await useFetch(`/api/project/${project.value}/file`, {
|
|
||||||
method: 'GET',
|
|
||||||
query: {
|
|
||||||
search: `%${pathname}`
|
|
||||||
},
|
|
||||||
transform: (data) => data?.map(e => ({ path: e.path, type: e.type })),
|
|
||||||
key: `file:${project.value}:%${pathname}`,
|
|
||||||
dedupe: 'defer'
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<code><slot /></code>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<em>
|
|
||||||
<slot />
|
|
||||||
</em>
|
|
||||||
</template>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h1 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h2 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h3 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h4 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h5 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h5>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h6 :id="id">
|
|
||||||
<slot />
|
|
||||||
</h6>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<hr>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<li><slot /></li>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ol>
|
|
||||||
<slot />
|
|
||||||
</ol>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<p><slot /></p>
|
|
||||||
</template>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<strong>
|
|
||||||
<slot />
|
|
||||||
</strong>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<table>
|
|
||||||
<slot />
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<tbody>
|
|
||||||
<slot />
|
|
||||||
</tbody>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<td>
|
|
||||||
<slot />
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<th>
|
|
||||||
<slot />
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<thead>
|
|
||||||
<slot />
|
|
||||||
</thead>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<tr>
|
|
||||||
<slot />
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul>
|
|
||||||
<slot />
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<a>
|
|
||||||
<slot></slot>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-if="focused">> </span>
|
|
||||||
<blockquote ref="el" @focusin="focused = true" @focusout="focused = false">
|
|
||||||
<slot />
|
|
||||||
</blockquote>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const focused = ref(false);
|
|
||||||
|
|
||||||
watch(focused, console.log);
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="false"># </span><h1><slot></slot></h1>
|
|
||||||
</template>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="focused">## </span><h2><slot></slot></h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
focused: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="false">### </span><h3><slot></slot></h3>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="false">#### </span><h4><slot></slot></h4>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="false">##### </span><h5><slot></slot></h5>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span v-show="false">###### </span><h6><slot></slot></h6>
|
|
||||||
</template>
|
|
||||||
64
composables/useContent.ts
Normal file
64
composables/useContent.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Content } from '~/shared/content.util';
|
||||||
|
import type { ExploreContent, ContentComposable, TreeItem } from '~/types/content';
|
||||||
|
|
||||||
|
const useContentState = () => useState<ExploreContent[]>('content', () => []);
|
||||||
|
|
||||||
|
export function useContent(): ContentComposable {
|
||||||
|
const contentState = useContentState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: contentState,
|
||||||
|
tree: computed(() => {
|
||||||
|
const arr: TreeItem[] = [];
|
||||||
|
for(const element of contentState.value)
|
||||||
|
{
|
||||||
|
addChild(arr, element);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}),
|
||||||
|
fetch,
|
||||||
|
get,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch(force: boolean = false) {
|
||||||
|
const content = useContentState();
|
||||||
|
if(content.value.length === 0 || force)
|
||||||
|
content.value = await useRequestFetch()('/api/file/overview');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(path: string, force: boolean = false): Promise<ExploreContent | undefined> {
|
||||||
|
const content = useContentState()
|
||||||
|
const value = content.value;
|
||||||
|
const item = value.find(e => e.path === path);
|
||||||
|
|
||||||
|
if(item && !item.content)
|
||||||
|
{
|
||||||
|
item.content = await useRequestFetch()(`/api/file/content/${encodeURIComponent(path)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.value = value;
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChild(arr: TreeItem[], e: ExploreContent): void {
|
||||||
|
const parent = arr.find(f => e.path.startsWith(f.path));
|
||||||
|
|
||||||
|
if(parent)
|
||||||
|
{
|
||||||
|
if(!parent.children)
|
||||||
|
parent.children = [];
|
||||||
|
|
||||||
|
addChild(parent.children, e);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arr.push({ ...e });
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
if(a.order !== b.order)
|
||||||
|
return a.order - b.order;
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
|
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
import * as schema from '../db/schema';
|
||||||
|
|
||||||
let instance: Database | undefined;
|
let instance: BunSQLiteDatabase<typeof schema> & {
|
||||||
|
$client: Database;
|
||||||
|
};
|
||||||
|
export default function useDatabase()
|
||||||
|
{
|
||||||
|
if(!instance)
|
||||||
|
{
|
||||||
|
const database = useRuntimeConfig().database;
|
||||||
|
const sqlite = new Database(database);
|
||||||
|
instance = drizzle({ client: sqlite, schema, /* logger: true */ });
|
||||||
|
|
||||||
export default function useDatabase(): Database {
|
instance.run("PRAGMA journal_mode = WAL;");
|
||||||
if(instance === undefined)
|
instance.run("PRAGMA foreign_keys = true;");
|
||||||
instance = getDatabase();
|
instance.run("PRAGMA optimize=0x10002;");
|
||||||
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabase(): Database
|
|
||||||
{
|
|
||||||
const { dbFile } = useRuntimeConfig();
|
|
||||||
|
|
||||||
const db = new Database(dbFile);
|
|
||||||
|
|
||||||
db.exec("PRAGMA journal_mode = WAL;");
|
|
||||||
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
@@ -4,26 +4,55 @@ import RemarkParse from "remark-parse";
|
|||||||
|
|
||||||
import RemarkRehype from 'remark-rehype';
|
import RemarkRehype from 'remark-rehype';
|
||||||
import RemarkOfm from 'remark-ofm';
|
import RemarkOfm from 'remark-ofm';
|
||||||
import RemarkBreaks from 'remark-breaks'
|
|
||||||
import RemarkGfm from 'remark-gfm';
|
import RemarkGfm from 'remark-gfm';
|
||||||
|
import RemarkBreaks from 'remark-breaks';
|
||||||
import RemarkFrontmatter from 'remark-frontmatter';
|
import RemarkFrontmatter from 'remark-frontmatter';
|
||||||
import RehypeRaw from 'rehype-raw';
|
import StripMarkdown from 'strip-markdown';
|
||||||
|
import RemarkStringify from 'remark-stringify';
|
||||||
|
|
||||||
export default function useMarkdown(): (md: string) => Root
|
interface Parser
|
||||||
{
|
{
|
||||||
let processor: Processor;
|
parse: (md: string) => Promise<Root>;
|
||||||
|
parseSync: (md: string) => Root;
|
||||||
|
text: (md: string) => string;
|
||||||
|
}
|
||||||
|
export default function useMarkdown(): Parser
|
||||||
|
{
|
||||||
|
let processor: Processor, processorSync: Processor;
|
||||||
|
|
||||||
const parse = (markdown: string) => {
|
const parse = (markdown: string) => {
|
||||||
if (!processor)
|
if (!processor)
|
||||||
{
|
{
|
||||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
||||||
processor.use(RemarkRehype, { allowDangerousHtml: true });
|
processor.use(RemarkRehype);
|
||||||
processor.use(RehypeRaw);
|
}
|
||||||
|
|
||||||
|
const processed = processor.run(processor.parse(markdown)) as Promise<Root>;
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSync = (markdown: string) => {
|
||||||
|
if (!processor)
|
||||||
|
{
|
||||||
|
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter]);
|
||||||
|
processor.use(RemarkRehype);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parse;
|
const text = (markdown: string) => {
|
||||||
|
if (!processor)
|
||||||
|
{
|
||||||
|
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter ]);
|
||||||
|
processor.use(StripMarkdown, { remove: [ 'comment', 'tag', 'callout' ] });
|
||||||
|
processor.use(RemarkStringify);
|
||||||
|
}
|
||||||
|
|
||||||
|
const processed = processor.processSync(markdown);
|
||||||
|
return String(processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { parse, parseSync, text };
|
||||||
}
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
194
composables/useShortcuts.ts
Normal file
194
composables/useShortcuts.ts
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { ComputedRef, WatchSource } from 'vue'
|
||||||
|
import { logicAnd, logicNot } from '@vueuse/math'
|
||||||
|
import { useEventListener, useDebounceFn, createSharedComposable, useActiveElement } from '@vueuse/core'
|
||||||
|
|
||||||
|
export interface ShortcutConfig {
|
||||||
|
handler: Function
|
||||||
|
usingInput?: string | boolean
|
||||||
|
whenever?: WatchSource<boolean>[]
|
||||||
|
prevent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsConfig {
|
||||||
|
[key: string]: ShortcutConfig | Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutsOptions {
|
||||||
|
chainDelay?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shortcut {
|
||||||
|
handler: Function
|
||||||
|
condition: ComputedRef<boolean>
|
||||||
|
chained: boolean
|
||||||
|
// KeyboardEvent attributes
|
||||||
|
key: string
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
shiftKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
// code?: string
|
||||||
|
// keyCode?: number
|
||||||
|
prevent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
|
||||||
|
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
|
||||||
|
|
||||||
|
export const useShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
|
||||||
|
const { macOS, usingInput } = _useShortcuts()
|
||||||
|
|
||||||
|
let shortcuts: Shortcut[] = []
|
||||||
|
|
||||||
|
const chainedInputs = ref<string[]>([])
|
||||||
|
const clearChainedInput = () => {
|
||||||
|
chainedInputs.value.splice(0, chainedInputs.value.length)
|
||||||
|
}
|
||||||
|
const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800)
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Input autocomplete triggers a keydown event
|
||||||
|
if (!e.key) { return }
|
||||||
|
|
||||||
|
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
||||||
|
|
||||||
|
let chainedKey
|
||||||
|
chainedInputs.value.push(e.key)
|
||||||
|
// try matching a chained shortcut
|
||||||
|
if (chainedInputs.value.length >= 2) {
|
||||||
|
chainedKey = chainedInputs.value.slice(-2).join('-')
|
||||||
|
|
||||||
|
for (const shortcut of shortcuts.filter(s => s.chained)) {
|
||||||
|
if (shortcut.key !== chainedKey) { continue }
|
||||||
|
|
||||||
|
if (shortcut.condition.value) {
|
||||||
|
e.stopPropagation
|
||||||
|
shortcut.prevent && e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
clearChainedInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try matching a standard shortcut
|
||||||
|
for (const shortcut of shortcuts.filter(s => !s.chained)) {
|
||||||
|
if (e.key.toLowerCase() !== shortcut.key) { continue }
|
||||||
|
if (e.metaKey !== shortcut.metaKey) { continue }
|
||||||
|
if (e.ctrlKey !== shortcut.ctrlKey) { continue }
|
||||||
|
// shift modifier is only checked in combination with alphabetical keys
|
||||||
|
// (shift with non-alphabetical keys would change the key)
|
||||||
|
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) { continue }
|
||||||
|
// alt modifier changes the combined key anyways
|
||||||
|
// if (e.altKey !== shortcut.altKey) { continue }
|
||||||
|
|
||||||
|
if (shortcut.condition.value) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
clearChainedInput()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedClearChainedInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map config to full detailled shortcuts
|
||||||
|
shortcuts = Object.entries(config).map(([key, shortcutConfig]) => {
|
||||||
|
if (!shortcutConfig) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key and modifiers
|
||||||
|
let shortcut: Partial<Shortcut>
|
||||||
|
|
||||||
|
if (key.includes('-') && key !== '-' && !key.match(chainedShortcutRegex)?.length) {
|
||||||
|
console.trace(`[Shortcut] Invalid key: "${key}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.includes('_') && key !== '_' && !key.match(combinedShortcutRegex)?.length) {
|
||||||
|
console.trace(`[Shortcut] Invalid key: "${key}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chained = key.includes('-') && key !== '-'
|
||||||
|
if (chained) {
|
||||||
|
shortcut = {
|
||||||
|
key: key.toLowerCase(),
|
||||||
|
metaKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
altKey: false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keySplit = key.toLowerCase().split('_').map(k => k)
|
||||||
|
shortcut = {
|
||||||
|
key: keySplit.filter(k => !['meta', 'ctrl', 'shift', 'alt'].includes(k)).join('_'),
|
||||||
|
metaKey: keySplit.includes('meta'),
|
||||||
|
ctrlKey: keySplit.includes('ctrl'),
|
||||||
|
shiftKey: keySplit.includes('shift'),
|
||||||
|
altKey: keySplit.includes('alt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shortcut.chained = chained
|
||||||
|
|
||||||
|
// Convert Meta to Ctrl for non-MacOS
|
||||||
|
if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
|
||||||
|
shortcut.metaKey = false
|
||||||
|
shortcut.ctrlKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve handler function
|
||||||
|
if (typeof shortcutConfig === 'function') {
|
||||||
|
shortcut.handler = shortcutConfig
|
||||||
|
} else if (typeof shortcutConfig === 'object') {
|
||||||
|
shortcut = { ...shortcut, handler: shortcutConfig.handler, prevent: shortcutConfig.prevent }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortcut.handler) {
|
||||||
|
console.trace('[Shortcut] Invalid value')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shortcut computed
|
||||||
|
const conditions: ComputedRef<boolean>[] = []
|
||||||
|
if (!(shortcutConfig as ShortcutConfig).usingInput) {
|
||||||
|
conditions.push(logicNot(usingInput))
|
||||||
|
} else if (typeof (shortcutConfig as ShortcutConfig).usingInput === 'string') {
|
||||||
|
conditions.push(computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput))
|
||||||
|
}
|
||||||
|
shortcut.condition = logicAnd(...conditions, ...((shortcutConfig as ShortcutConfig).whenever || []))
|
||||||
|
|
||||||
|
return shortcut as Shortcut
|
||||||
|
}).filter(Boolean) as Shortcut[]
|
||||||
|
|
||||||
|
useEventListener('keydown', onKeyDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _useShortcuts = () => {
|
||||||
|
const macOS = computed(() => process.client && navigator && navigator.userAgent && navigator.userAgent.match(/Macintosh;/))
|
||||||
|
|
||||||
|
const metaSymbol = ref(' ')
|
||||||
|
|
||||||
|
const activeElement = useActiveElement()
|
||||||
|
const usingInput = computed(() => {
|
||||||
|
const usingInput = !!(activeElement.value?.tagName === 'INPUT' || activeElement.value?.tagName === 'TEXTAREA' || activeElement.value?.contentEditable === 'true')
|
||||||
|
|
||||||
|
if (usingInput) {
|
||||||
|
return ((activeElement.value as any)?.name as string) || true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
tryOnMounted(() => {
|
||||||
|
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
macOS,
|
||||||
|
metaSymbol,
|
||||||
|
activeElement,
|
||||||
|
usingInput
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable to get back the user session and utils around it.
|
* Composable to get back the user session and utils around it.
|
||||||
* @see https://github.com/atinux/nuxt-auth-utils
|
|
||||||
*/
|
*/
|
||||||
export function useUserSession(): UserSessionComposable {
|
export function useUserSession(): UserSessionComposable {
|
||||||
const sessionState = useSessionState()
|
const sessionState = useSessionState()
|
||||||
@@ -13,28 +12,27 @@ export function useUserSession(): UserSessionComposable {
|
|||||||
return {
|
return {
|
||||||
ready: computed(() => authReadyState.value),
|
ready: computed(() => authReadyState.value),
|
||||||
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
||||||
user: computed(() => sessionState.value.user || null),
|
user: computed(() => sessionState.value.user ?? null),
|
||||||
session: sessionState,
|
session: sessionState,
|
||||||
fetch,
|
fetch,
|
||||||
clear,
|
clear,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch(): Promise<boolean> {
|
||||||
const authReadyState = useAuthReadyState()
|
const authReadyState = useAuthReadyState()
|
||||||
useSessionState().value = await useRequestFetch()('/api/auth/session', {
|
const sessionState = useSessionState()
|
||||||
headers: {
|
const loggedIn = Boolean(sessionState.value.user)
|
||||||
Accept: 'text/json',
|
sessionState.value = await useRequestFetch()('/api/auth/session').catch(() => ({}))
|
||||||
},
|
if (!authReadyState.value)
|
||||||
retry: false,
|
{
|
||||||
}).catch(() => ({}))
|
|
||||||
if (!authReadyState.value) {
|
|
||||||
authReadyState.value = true
|
authReadyState.value = true
|
||||||
}
|
}
|
||||||
|
return loggedIn !== Boolean(sessionState.value.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clear() {
|
async function clear() {
|
||||||
await $fetch('/api/auth/session', { method: 'DELETE' })
|
await useRequestFetch()('/api/auth/session', { method: 'DELETE' })
|
||||||
useSessionState().value = {}
|
useSessionState().value = {}
|
||||||
useRouter().go(0);
|
useRouter().go(0)
|
||||||
}
|
}
|
||||||
BIN
db.sqlite-shm
Normal file
BIN
db.sqlite-shm
Normal file
Binary file not shown.
BIN
db.sqlite-wal
Normal file
BIN
db.sqlite-wal
Normal file
Binary file not shown.
129
db/schema.ts
Normal file
129
db/schema.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { relations } from 'drizzle-orm';
|
||||||
|
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
|
export const usersTable = table("users", {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
username: text().notNull().unique(),
|
||||||
|
email: text().notNull().unique(),
|
||||||
|
hash: text().notNull().unique(),
|
||||||
|
state: int().notNull().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersDataTable = table("users_data", {
|
||||||
|
id: int().primaryKey().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
signin: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
lastTimestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userSessionsTable = table("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) => [primaryKey({ columns: [table.id, table.user_id] })]);
|
||||||
|
|
||||||
|
export const userPermissionsTable = table("user_permissions", {
|
||||||
|
id: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
permission: text().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.id, table.permission] })]);
|
||||||
|
|
||||||
|
export const projectFilesTable = table("project_files", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
path: text().notNull().unique(),
|
||||||
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
title: text().notNull(),
|
||||||
|
type: text({ enum: ['file', 'folder', 'markdown', 'canvas', 'map'] }).notNull(),
|
||||||
|
navigable: int({ mode: 'boolean' }).notNull().default(true),
|
||||||
|
private: int({ mode: 'boolean' }).notNull().default(false),
|
||||||
|
order: int().notNull(),
|
||||||
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const projectContentTable = table("project_content", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
content: blob({ mode: 'buffer' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const emailValidationTable = table("email_validation", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const characterTable = table("character", {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
people: text().notNull(),
|
||||||
|
level: int().notNull().default(1),
|
||||||
|
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"items": [],"exhaustion": 0,"sickness": [],"poisons": []}'),
|
||||||
|
aspect: int(),
|
||||||
|
public_notes: text(),
|
||||||
|
private_notes: text(),
|
||||||
|
|
||||||
|
visibility: text({ enum: ['private', 'public'] }).notNull().default('private'),
|
||||||
|
thumbnail: blob(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const characterTrainingTable = table("character_training", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
stat: text({ enum: ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] }).notNull(),
|
||||||
|
level: int().notNull(),
|
||||||
|
choice: int().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.stat, table.level] })]);
|
||||||
|
|
||||||
|
export const characterLevelingTable = table("character_leveling", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
level: int().notNull(),
|
||||||
|
choice: int().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.level] })]);
|
||||||
|
|
||||||
|
export const characterAbilitiesTable = table("character_abilities", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
ability: text({ enum: ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] }).notNull(),
|
||||||
|
value: int().notNull().default(0),
|
||||||
|
max: int().notNull().default(0),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.ability] })]);
|
||||||
|
|
||||||
|
export const characterChoicesTable = table("character_choices", {
|
||||||
|
character: int().notNull().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||||
|
id: text().notNull(),
|
||||||
|
choice: int().notNull(),
|
||||||
|
}, (table) => [primaryKey({ columns: [table.character, table.id, table.choice] })]);
|
||||||
|
|
||||||
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
|
session: many(userSessionsTable),
|
||||||
|
permission: many(userPermissionsTable),
|
||||||
|
files: many(projectFilesTable),
|
||||||
|
}));
|
||||||
|
export const usersDataRelation = relations(usersDataTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [usersDataTable.id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const userSessionsRelation = relations(userSessionsTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [userSessionsTable.user_id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const userPermissionsRelation = relations(userPermissionsTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [userPermissionsTable.id], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const projectFilesRelation = relations(projectFilesTable, ({ one }) => ({
|
||||||
|
users: one(usersTable, { fields: [projectFilesTable.owner], references: [usersTable.id], }),
|
||||||
|
}));
|
||||||
|
export const characterRelation = relations(characterTable, ({ one, many }) => ({
|
||||||
|
user: one(usersTable, { fields: [characterTable.owner], references: [usersTable.id], }),
|
||||||
|
training: many(characterTrainingTable),
|
||||||
|
levels: many(characterLevelingTable),
|
||||||
|
abilities: many(characterAbilitiesTable),
|
||||||
|
choices: many(characterChoicesTable)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const characterTrainingRelation = relations(characterTrainingTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterTrainingTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterLevelingRelation = relations(characterLevelingTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterLevelingTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterAbilitiesRelation = relations(characterAbilitiesTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterAbilitiesTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
|
export const characterChoicesRelation = relations(characterChoicesTable, ({ one }) => ({
|
||||||
|
character: one(characterTable, { fields: [characterChoicesTable.character], references: [characterTable.id] })
|
||||||
|
}));
|
||||||
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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!,
|
||||||
|
},
|
||||||
|
});
|
||||||
36
drizzle/0000_needy_rictor.sql
Normal file
36
drizzle/0000_needy_rictor.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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,
|
||||||
|
`signin` integer 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 NOT NULL
|
||||||
|
);
|
||||||
|
--> 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`);
|
||||||
6
drizzle/0001_sticky_jack_flag.sql
Normal file
6
drizzle/0001_sticky_jack_flag.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE `user_permissions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`permissions` text NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `permissions`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
12
drizzle/0002_messy_solo.sql
Normal file
12
drizzle/0002_messy_solo.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_user_permissions` (
|
||||||
|
`id` integer NOT NULL,
|
||||||
|
`permission` text NOT NULL,
|
||||||
|
PRIMARY KEY(`id`, `permission`),
|
||||||
|
FOREIGN KEY (`id`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_user_permissions`("id", "permission") SELECT "id", "permission" FROM `user_permissions`;--> statement-breakpoint
|
||||||
|
DROP TABLE `user_permissions`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_user_permissions` RENAME TO `user_permissions`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
2
drizzle/0003_cultured_skaar.sql
Normal file
2
drizzle/0003_cultured_skaar.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `explorer_content` ADD `order` integer;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `order` ON `explorer_content` (`order`);
|
||||||
21
drizzle/0004_ancient_thunderball.sql
Normal file
21
drizzle/0004_ancient_thunderball.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_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 NOT NULL,
|
||||||
|
`private` integer DEFAULT false NOT NULL,
|
||||||
|
`order` integer NOT NULL,
|
||||||
|
`visit` integer DEFAULT 0 NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`owner`) REFERENCES `users`(`id`) ON UPDATE cascade ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_explorer_content`("path", "owner", "title", "type", "content", "navigable", "private", "order", "visit", "timestamp") SELECT "path", "owner", "title", "type", "content", "navigable", "private", "order", "visit", "timestamp" FROM `explorer_content`;--> statement-breakpoint
|
||||||
|
DROP TABLE `explorer_content`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_explorer_content` RENAME TO `explorer_content`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
ALTER TABLE `users_data` ADD `lastTimestamp` integer NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `users_data` ADD `logCount` integer DEFAULT 0 NOT NULL;
|
||||||
4
drizzle/0005_panoramic_slayback.sql
Normal file
4
drizzle/0005_panoramic_slayback.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE `email_validation` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL
|
||||||
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user