You've already forked obsidian-visualiser
Compare commits
33 Commits
6f5566326e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e551159fc | |||
| 8cbc25a601 | |||
| a5317d6156 | |||
| bc1839c5e3 | |||
| f9e0473b2a | |||
|
|
3bafc14255 | ||
|
|
974989abd3 | ||
|
|
9face0ac3b | ||
|
|
898d95793a | ||
|
|
8335871883 | ||
| 3081c05b55 | |||
|
|
a412116b9c | ||
| e9a892076d | |||
|
|
777443471c | ||
|
|
1a71637ebb | ||
|
|
ce3dbb0d6e | ||
|
|
796b335b2e | ||
|
|
f761e44569 | ||
|
|
0eaffcaa04 | ||
|
|
7021264c11 | ||
| 04534b2530 | |||
| 32b6cf4af7 | |||
|
|
e9ffdd58a5 | ||
|
|
78a101b79d | ||
|
|
49691feeee | ||
| 94645f9dbf | |||
| 888adc4743 | |||
| 4862181d61 | |||
|
|
323cb0ba7f | ||
|
|
4cd478b47a | ||
|
|
1b0b9ca7f4 | ||
| 97578132bb | |||
| cbe4e1d068 |
119
app/app.vue
119
app/app.vue
@@ -1,25 +1,26 @@
|
||||
<template>
|
||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||
<NuxtRouteAnnouncer/>
|
||||
<TooltipProvider>
|
||||
<NuxtLayout>
|
||||
<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">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</TooltipProvider>
|
||||
<NuxtLoadingIndicator :throttle="50"/>
|
||||
<NuxtLayout>
|
||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 md:px-6 py-3 px-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative @container/main box-border" id="mainContainer">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Content } from '#shared/content.util';
|
||||
import * as Floating from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Content } from '#shared/content';
|
||||
import * as Floating from '#shared/floating';
|
||||
import { Toaster } from '#shared/components';
|
||||
import { init } from '#shared/i18n';
|
||||
|
||||
onBeforeMount(() => {
|
||||
Content.init();
|
||||
Floating.init();
|
||||
Toaster.init();
|
||||
init()
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure) return;
|
||||
@@ -35,6 +36,7 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "./assets/css/main.css";
|
||||
iconify-icon
|
||||
{
|
||||
display: inline-block;
|
||||
@@ -45,71 +47,76 @@ iconify-icon
|
||||
.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;
|
||||
@apply bg-light-red/50;
|
||||
@apply dark:bg-dark-red/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;
|
||||
@apply bg-light-green/50;
|
||||
@apply dark:bg-dark-green/50;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-light-40;
|
||||
@apply dark:bg-dark-40;
|
||||
@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;
|
||||
}
|
||||
.no-scroll::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
display: none;
|
||||
}
|
||||
.no-scroll::-webkit-scrollbar-thumb {
|
||||
@apply bg-transparent;
|
||||
display: none;
|
||||
}
|
||||
.callout[data-type="abstract"],
|
||||
.callout[data-type="summary"],
|
||||
.callout[data-type="tldr"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply bg-light-cyan/25;
|
||||
@apply dark:bg-dark-cyan/25;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
.callout[data-type="info"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply bg-light-blue/25;
|
||||
@apply dark:bg-dark-blue/25;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="todo"]
|
||||
{
|
||||
@apply bg-light-blue;
|
||||
@apply dark:bg-dark-blue;
|
||||
@apply bg-light-blue/25;
|
||||
@apply dark:bg-dark-blue/25;
|
||||
@apply text-light-blue;
|
||||
@apply dark:text-dark-blue;
|
||||
}
|
||||
.callout[data-type="important"]
|
||||
{
|
||||
@apply bg-light-cyan;
|
||||
@apply dark:bg-dark-cyan;
|
||||
@apply bg-light-cyan/25;
|
||||
@apply dark:bg-dark-cyan/25;
|
||||
@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 bg-light-cyan/25;
|
||||
@apply dark:bg-dark-cyan/25;
|
||||
@apply text-light-cyan;
|
||||
@apply dark:text-dark-cyan;
|
||||
}
|
||||
@@ -117,8 +124,8 @@ iconify-icon
|
||||
.callout[data-type="check"],
|
||||
.callout[data-type="done"]
|
||||
{
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply bg-light-green/25;
|
||||
@apply dark:bg-dark-green/25;
|
||||
@apply text-light-green;
|
||||
@apply dark:text-dark-green;
|
||||
}
|
||||
@@ -126,8 +133,8 @@ iconify-icon
|
||||
.callout[data-type="help"],
|
||||
.callout[data-type="faq"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply bg-light-orange/25;
|
||||
@apply dark:bg-dark-orange/25;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
@@ -135,8 +142,8 @@ iconify-icon
|
||||
.callout[data-type="caution"],
|
||||
.callout[data-type="attention"]
|
||||
{
|
||||
@apply bg-light-orange;
|
||||
@apply dark:bg-dark-orange;
|
||||
@apply bg-light-orange/25;
|
||||
@apply dark:bg-dark-orange/25;
|
||||
@apply text-light-orange;
|
||||
@apply dark:text-dark-orange;
|
||||
}
|
||||
@@ -144,30 +151,30 @@ iconify-icon
|
||||
.callout[data-type="fail"],
|
||||
.callout[data-type="missing"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply bg-light-red/25;
|
||||
@apply dark:bg-dark-red/25;
|
||||
@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 bg-light-red/25;
|
||||
@apply dark:bg-dark-red/25;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="bug"]
|
||||
{
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply bg-light-red/25;
|
||||
@apply dark:bg-dark-red/25;
|
||||
@apply text-light-red;
|
||||
@apply dark:text-dark-red;
|
||||
}
|
||||
.callout[data-type="example"]
|
||||
{
|
||||
@apply bg-light-purple;
|
||||
@apply dark:bg-dark-purple;
|
||||
@apply bg-light-purple/25;
|
||||
@apply dark:bg-dark-purple/25;
|
||||
@apply text-light-purple;
|
||||
@apply dark:text-dark-purple;
|
||||
}
|
||||
@@ -183,6 +190,10 @@ iconify-icon
|
||||
|
||||
@apply text-light-100 dark:text-dark-100;
|
||||
}
|
||||
.cm-focused
|
||||
{
|
||||
@apply outline-hidden;
|
||||
}
|
||||
.cm-editor .cm-content
|
||||
{
|
||||
@apply caret-light-100 dark:caret-dark-100;
|
||||
@@ -195,10 +206,10 @@ iconify-icon
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
@apply max-w-[400px];
|
||||
@apply !bg-light-20;
|
||||
@apply dark:!bg-dark-20;
|
||||
@apply !border-light-40;
|
||||
@apply dark:!border-dark-40;
|
||||
@apply bg-light-20;
|
||||
@apply dark:bg-dark-20;
|
||||
@apply border-light-40;
|
||||
@apply dark:border-dark-40;
|
||||
}
|
||||
|
||||
/* .cm-tooltip-autocomplete > ul {
|
||||
@@ -208,18 +219,18 @@ iconify-icon
|
||||
.cm-tooltip-autocomplete > ul > li {
|
||||
@apply flex;
|
||||
@apply flex-col;
|
||||
@apply !py-1;
|
||||
@apply py-1!;
|
||||
@apply hover:bg-light-30;
|
||||
@apply dark:hover:bg-dark-30;
|
||||
@apply hover:dark:bg-dark-30;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete > ul > li[aria-selected] {
|
||||
@apply !bg-light-35;
|
||||
@apply dark:!bg-dark-35;
|
||||
@apply bg-light-35!;
|
||||
@apply dark:bg-dark-35!;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply !hidden;
|
||||
@apply hidden!;
|
||||
}
|
||||
|
||||
.cm-completionLabel {
|
||||
@@ -233,7 +244,7 @@ iconify-icon
|
||||
|
||||
.cm-completionMatchedText {
|
||||
@apply font-bold;
|
||||
@apply !no-underline;
|
||||
@apply no-underline!;
|
||||
}
|
||||
|
||||
.cm-completionDetail {
|
||||
|
||||
92
app/assets/css/main.css
Normal file
92
app/assets/css/main.css
Normal file
@@ -0,0 +1,92 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@source "../../../shared";
|
||||
|
||||
@theme {
|
||||
--color-transparent: transparent;
|
||||
--color-current: currentColor;
|
||||
|
||||
--color-light-red: #e93147;
|
||||
--color-light-orange: #FF9800;
|
||||
--color-light-yellow: #FFEB3B;
|
||||
--color-light-green: #388E3C;
|
||||
--color-light-indigo: #7986CB;
|
||||
--color-light-cyan: #00bfbc;
|
||||
--color-light-lime: #8BC34A;
|
||||
--color-light-blue: #086ddd;
|
||||
--color-light-purple: #AB47BC;
|
||||
--color-light-pink: #d53984;
|
||||
--color-light-0: #ffffff;
|
||||
--color-light-5: #fcfcfc;
|
||||
--color-light-10: #fafafa;
|
||||
--color-light-20: #f7f7f7;
|
||||
--color-light-25: #e4e4e4;
|
||||
--color-light-30: #dfdfdf;
|
||||
--color-light-35: #d2d2d2;
|
||||
--color-light-40: #bdbdbd;
|
||||
--color-light-50: #ababab;
|
||||
--color-light-60: #707070;
|
||||
--color-light-70: #5c5c5c;
|
||||
--color-light-100: #202020;
|
||||
|
||||
--color-dark-red: #fb464c;
|
||||
--color-dark-redBack: #5A292B;
|
||||
--color-dark-orange: #e9973f;
|
||||
--color-dark-yellow: #e0de71;
|
||||
--color-dark-green: #44cf6e;
|
||||
--color-dark-greenBack: #284E34;
|
||||
--color-dark-cyan: #53dfdd;
|
||||
--color-dark-blue: #027aff;
|
||||
--color-dark-purple: #a882ff;
|
||||
--color-dark-pink: #fa99cd;
|
||||
--color-dark-0: #1e1e1e;
|
||||
--color-dark-5: #212121;
|
||||
--color-dark-10: #242424;
|
||||
--color-dark-20: #262626;
|
||||
--color-dark-25: #2a2a2a;
|
||||
--color-dark-30: #363636;
|
||||
--color-dark-35: #3f3f3f;
|
||||
--color-dark-40: #555555;
|
||||
--color-dark-50: #666666;
|
||||
--color-dark-60: #999999;
|
||||
--color-dark-70: #b3b3b3;
|
||||
--color-dark-100: #dadada;
|
||||
|
||||
--color-accent-purple: #43A047;
|
||||
--color-accent-blue: #26C6DA;
|
||||
|
||||
--shadow-raw: 0 0 0 2px var(--tw-shadow-color);
|
||||
|
||||
--animate-slideDownAndFade: slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slideLeftAndFade: slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slideUpAndFade: slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-slideRightAndFade: slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--animate-contentShow: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes slideDownAndFade {
|
||||
from { opacity: 0; transform: translateY(-2px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideLeftAndFade {
|
||||
from { opacity: 0; transform: translateX(2px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUpAndFade {
|
||||
from { opacity: 0; transform: translateY(2px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideRightAndFade {
|
||||
from { opacity: 0; transform: translateX(-2px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes contentShow {
|
||||
from { opacity: 0; transform: translate(-50%, -48%) scale(0.96); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import render, { type MDProperties } from '#shared/markdown.util'
|
||||
import render, { type MDProperties } from '~~/shared/markdown'
|
||||
const { content, filter, properties } = defineProps<{
|
||||
content?: string,
|
||||
filter?: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<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
|
||||
<button :disabled="disabled" class="text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 hover:dark:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-hidden
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 hover:dark:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark: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" />
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<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">
|
||||
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-hidden 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 focus:dark:shadow-dark-40
|
||||
hover:border-light-50 hover:dark: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>
|
||||
@@ -16,7 +16,7 @@
|
||||
<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">
|
||||
<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-hidden 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" />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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">
|
||||
<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-hidden text-xl" aria-label="Close">
|
||||
<span aria-hidden>×</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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">
|
||||
<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-hidden 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>
|
||||
@@ -11,7 +11,7 @@
|
||||
</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">
|
||||
<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-hidden 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" />
|
||||
@@ -41,13 +41,13 @@
|
||||
|
||||
<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">
|
||||
<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-hidden 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">
|
||||
<DropdownMenuSubContent class="z-50 outline-hidden 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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<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">
|
||||
<DropdownMenuContent :align="align" :side="side" class="z-50 outline-hidden 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" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<HoverCardRoot :open-delay="delay" @update:open="(...args) => emits('open', ...args)">
|
||||
<HoverCardTrigger class="inline-block cursor-help outline-none">
|
||||
<HoverCardTrigger class="inline-block cursor-help outline-hidden">
|
||||
<slot></slot>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal v-if="!disabled">
|
||||
|
||||
@@ -1,3 +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>
|
||||
<span class="rounded-sm 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>
|
||||
@@ -1,9 +1,9 @@
|
||||
<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">
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 hover:border-light-50 hover:dark: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" />
|
||||
<NumberFieldInput class="text-sm tabular-nums w-20 appearance-none bg-transparent px-2 py-1 outline-hidden 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>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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
|
||||
<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-hidden
|
||||
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" />
|
||||
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40 caret-light-50 dark:caret-dark-50" />
|
||||
</PinInputRoot>
|
||||
</Label>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<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">
|
||||
class="border border-light-60 dark:border-dark-35 bg-light-20 dark:bg-dark-25 w-5 h-5 outline-hidden cursor-default hover:border-light-70 hover:dark:border-dark-40
|
||||
focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark: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>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<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">
|
||||
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-hidden 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 focus:dark:shadow-dark-40
|
||||
hover:border-light-50 hover:dark:border-dark-50">
|
||||
<SelectValue :placeholder="placeholder" />
|
||||
<Icon icon="radix-icons:caret-down" class="h-4 w-4" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<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-hidden 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" />
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<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" />
|
||||
class="block w-5 h-5 bg-light-50 dark:bg-dark-50 outline-hidden focus:shadow-raw transition-[box-shadow] focus:shadow-light-60 focus:dark:shadow-dark-60 border border-light-50 dark:border-dark-50
|
||||
hover:border-light-60 hover:dark: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>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<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
|
||||
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-hidden
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 hover:dark:border-dark-50 focus:shadow-raw focus:shadow-light-40 focus:dark: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]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<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">
|
||||
<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 hover:dark: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" />
|
||||
<TagsInputInput :placeholder="placeholder" class="text-sm focus:outline-hidden flex-1 rounded-sm text-green9 bg-transparent placeholder:text-mauve9 px-1" />
|
||||
</TagsInputRoot>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<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"
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-hidden px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 focus:dark:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark: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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<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">
|
||||
<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-hidden relative cursor-pointer">
|
||||
<slot :isExpanded="isExpanded" :item="item" />
|
||||
</TreeItem>
|
||||
</TreeRoot>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import * as schema from '../db/schema';
|
||||
import { eq, or, sql } from "drizzle-orm";
|
||||
|
||||
let instance: BunSQLiteDatabase<typeof schema> & {
|
||||
$client: Database;
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Parser
|
||||
}
|
||||
export default function useMarkdown(): Parser
|
||||
{
|
||||
let processor: Processor, processorSync: Processor;
|
||||
let processor: Processor, processorText: Processor;
|
||||
|
||||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
@@ -43,14 +43,14 @@ export default function useMarkdown(): Parser
|
||||
}
|
||||
|
||||
const text = (markdown: string) => {
|
||||
if (!processor)
|
||||
if (!processorText)
|
||||
{
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter ]);
|
||||
processor.use(StripMarkdown, { remove: [ 'comment', 'tag', 'callout' ] });
|
||||
processor.use(RemarkStringify);
|
||||
processorText = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkBreaks, RemarkFrontmatter ]);
|
||||
processorText.use(StripMarkdown, { remove: [ 'comment', 'tag', 'callout' ] });
|
||||
processorText.use(RemarkStringify);
|
||||
}
|
||||
|
||||
const processed = processor.processSync(markdown);
|
||||
const processed = processorText.processSync(markdown);
|
||||
return String(processed);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { relations, sql } from 'drizzle-orm';
|
||||
import { int, text, sqliteTable as table, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
import type { ItemState } from '~/types/character';
|
||||
|
||||
export const usersTable = table("users", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
@@ -87,8 +88,8 @@ export const campaignTable = table("campaign", {
|
||||
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
link: text().notNull(),
|
||||
status: text({ enum: ['PREPARING', 'PLAYING', 'ARCHIVED'] }).default('PREPARING'),
|
||||
settings: text({ mode: 'json' }).default('{}'),
|
||||
inventory: text({ mode: 'json' }).default('[]'),
|
||||
settings: text({ mode: 'json' }).default({}).$type<{}>(),
|
||||
items: text({ mode: 'json' }).default([]).$type<ItemState[]>(),
|
||||
money: int().default(0),
|
||||
public_notes: text().default(''),
|
||||
dm_notes: text().default(''),
|
||||
@@ -101,13 +102,6 @@ export const campaignCharactersTable = table("campaign_characters", {
|
||||
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
character: int().references(() => characterTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
}, (table) => [primaryKey({ columns: [table.id, table.character] })]);
|
||||
export const campaignLogsTable = table("campaign_logs", {
|
||||
id: int().references(() => campaignTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
target: int(),
|
||||
timestamp: int({ mode: 'timestamp_ms' }).notNull(),
|
||||
type: text({ enum: ['ITEM', 'CHARACTER', 'PLACE', 'EVENT', 'FIGHT', 'TEXT'] }),
|
||||
details: text().notNull(),
|
||||
}, (table) => [primaryKey({ columns: [table.id, table.target, table.timestamp] })]);
|
||||
|
||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||
@@ -153,7 +147,6 @@ export const characterChoicesRelation = relations(characterChoicesTable, ({ one
|
||||
export const campaignRelation = relations(campaignTable, ({ one, many }) => ({
|
||||
members: many(campaignMembersTable),
|
||||
characters: many(campaignCharactersTable),
|
||||
logs: many(campaignLogsTable),
|
||||
owner: one(usersTable, { fields: [campaignTable.owner], references: [usersTable.id], }),
|
||||
}));
|
||||
export const campaignMembersRelation = relations(campaignMembersTable, ({ one }) => ({
|
||||
@@ -163,7 +156,4 @@ export const campaignMembersRelation = relations(campaignMembersTable, ({ one })
|
||||
export const campaignCharacterRelation = relations(campaignCharactersTable, ({ one }) => ({
|
||||
campaign: one(campaignTable, { fields: [campaignCharactersTable.id], references: [campaignTable.id], }),
|
||||
character: one(characterTable, { fields: [campaignCharactersTable.character], references: [characterTable.id], }),
|
||||
}));
|
||||
export const campaignLogsRelation = relations(campaignLogsTable, ({ one }) => ({
|
||||
campaign: one(campaignTable, { fields: [campaignLogsTable.id], references: [campaignTable.id], }),
|
||||
}));
|
||||
@@ -9,10 +9,10 @@
|
||||
<div class="text-3xl">Une erreur est survenue.</div>
|
||||
</div>
|
||||
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||
<button class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
<button class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 p-2" @click="handleError">Revenir en lieu sûr</button>
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 p-2" @click="handleError">Revenir en lieu sûr</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
<template>
|
||||
<div class="flex flex-row w-full max-w-full h-full max-h-full" style="--sidebar-width: 300px">
|
||||
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
|
||||
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
|
||||
</NuxtLink>
|
||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent"></div>
|
||||
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
|
||||
Copyright Peaceultime - 2025
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
|
||||
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<NavigationMenuRoot class="relative">
|
||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<NuxtLink :href="{ name: 'campaign' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||
<div class="flex flex-row w-full h-full group/sidebar" :data-active="open || undefined">
|
||||
<div v-if="open" class="fixed inset-0 bg-black/30 z-[5] xl:hidden" @click="open = false" />
|
||||
<div class="bg-light-0 dark:bg-dark-0 z-10 transition-[width] w-0 group-data-[active]/sidebar:w-full group-data-[active]/sidebar:md:w-64 overflow-hidden border-r border-light-30 dark:border-dark-30 flex flex-col gap-2 absolute xl:fixed top-0 bottom-0 left-0" @click.capture="onSidebarClick">
|
||||
<span class="w-full h-14"></span>
|
||||
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="treeParent"></div>
|
||||
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
|
||||
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
|
||||
Copyright Peaceultime - 2025
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 h-full w-full">
|
||||
<div class="flex flex-row w-full border-b border-light-30 dark:border-dark-30">
|
||||
<div class="z-40 flex flex-row gap-4 items-center justify-between px-4 w-64">
|
||||
<div class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 p-1" @click="() => open = !open"><Icon :icon="open ? 'radix-icons:pin-left' : 'radix-icons:pin-right'" height="12" width="12"/></div>
|
||||
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
|
||||
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
|
||||
<Avatar src="/logo.light.svg" class="block dark:hidden" />
|
||||
<span class="hidden md:inline text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
|
||||
</NuxtLink>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<template v-if="!loggedIn">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</template>
|
||||
<div class="flex flex-1 flex-row justify-between px-8">
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<NavigationMenuRoot class="relative">
|
||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 hover:dark:bg-dark-30 px-4 py-2 select-none" active-class="text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 hover:dark:bg-dark-30 px-4 py-2 select-none" active-class="text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<NuxtLink :href="{ name: 'campaign' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<template v-if="!loggedIn">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 hover:dark:text-dark-70" :to="{ name: 'user-login' }">Se connecter</NuxtLink>
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 hover:dark:text-dark-70 max-md:hidden" :to="{ name: 'user-register' }">Créer un compte</NuxtLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 hover:dark:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="flex flex-1 z-0 overflow-auto group-data-[active]/sidebar:xl:ms-64 transition-[margin]"><slot></slot></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,20 +63,27 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { TreeDOM } from '#shared/tree';
|
||||
import { Content, iconByType } from '#shared/content.util';
|
||||
import { dom, icon } from '#shared/dom.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { tooltip } from '#shared/floating.util';
|
||||
import { link, loading } from '#shared/components.util';
|
||||
import { Content, iconByType } from '~~/shared/content';
|
||||
import { dom, icon } from '~~/shared/dom';
|
||||
import { unifySlug } from '~~/shared/general';
|
||||
import { tooltip } from '~~/shared/floating';
|
||||
import { link, loading } from '~~/shared/components';
|
||||
import { breakpoint } from '~~/shared/breakpoint';
|
||||
|
||||
const open = ref(false);
|
||||
const open = useLocalStorage('sidebar', true, { writeDefaults: true });
|
||||
let tree: TreeDOM | undefined;
|
||||
const { loggedIn, user } = useUserSession();
|
||||
|
||||
function onSidebarClick(e: MouseEvent)
|
||||
{
|
||||
const link = (e.target as HTMLElement).closest('a');
|
||||
if (link && breakpoint.viewport !== 'xl' && breakpoint.viewport !== '2xl')
|
||||
nextTick(() => { open.value = false; });
|
||||
}
|
||||
|
||||
const route = useRouter().currentRoute;
|
||||
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);
|
||||
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure)
|
||||
return;
|
||||
@@ -73,10 +91,6 @@ const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
open.value = false;
|
||||
});
|
||||
|
||||
const treeParent = useTemplateRef('treeParent');
|
||||
onMounted(() => {
|
||||
if(treeParent.value)
|
||||
@@ -84,13 +98,13 @@ onMounted(() => {
|
||||
treeParent.value.replaceChildren(loading('normal'));
|
||||
Content.ready.then(() => {
|
||||
tree = new TreeDOM((item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-hidden relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
])]);
|
||||
}, (item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-hidden relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link([
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
|
||||
@@ -98,7 +112,7 @@ onMounted(() => {
|
||||
}, (item) => item.navigable);
|
||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree?.toggle(tree.tree.search('path', e)[0], true));
|
||||
|
||||
treeParent.value!.replaceChildren(tree.container);
|
||||
treeParent.value?.replaceChildren(tree.container);
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<div class="w-full md:w-auto h-full border-e border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 md:p-8 xl:p-16 flex justify-center items-center">
|
||||
<div class="w-full md:w-[48rem] h-full border-e border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 md:p-8 xl:p-16 flex justify-center items-center">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="hidden md:block flex-auto h-full"></div>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const { loggedIn, fetch, user } = useUserSession();
|
||||
const meta = to.meta;
|
||||
|
||||
await fetch()
|
||||
await fetch();
|
||||
|
||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||
if(meta.requiresAuth && !loggedIn.value)
|
||||
{
|
||||
return navigateTo(meta.guestsGoesTo);
|
||||
}
|
||||
else if(meta.requireAuth && !loggedIn.value)
|
||||
{
|
||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||
return navigateTo({ name: 'user-login', query: { t: encodeURIComponent(to.path) } });
|
||||
}
|
||||
else if(!!meta.usersGoesTo && loggedIn.value)
|
||||
{
|
||||
@@ -25,13 +21,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
else if(!!meta.rights)
|
||||
{
|
||||
if(!user.value)
|
||||
{
|
||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||
}
|
||||
else if(!hasPermissions(user.value.permissions, meta.rights))
|
||||
{
|
||||
return abortNavigation({ statusCode: 401, message: 'Unauthorized', });
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -31,10 +31,10 @@
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format } from '#shared/general.util';
|
||||
import { iconByType } from '#shared/content.util';
|
||||
import { format } from '~~/shared/general';
|
||||
import { iconByType } from '~~/shared/content';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
interface File
|
||||
{
|
||||
@@ -199,7 +199,7 @@ async function logout(user: User)
|
||||
</DialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<DialogClose asChild><Button>Non</Button></DialogClose>
|
||||
<DialogClose asChild><Button @click="() => logout(user)" class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Oui</Button></DialogClose>
|
||||
<DialogClose asChild><Button @click="() => logout(user)" class="border-light-green dark:border-dark-green hover:border-light-green hover:dark:border-dark-green hover:bg-light-greenBack hover:dark:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green focus:dark:shadow-dark-green">Oui</Button></DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
@@ -216,7 +216,7 @@ async function logout(user: User)
|
||||
<AlertDialogDescription><TagsInput v-model="permissionCopy" /></AlertDialogDescription>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => editPermissions(user)" class="border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green hover:bg-light-greenBack dark:hover:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green dark:focus:shadow-dark-green">Modifier</Button></AlertDialogAction>
|
||||
<AlertDialogAction asChild><Button @click="() => editPermissions(user)" class="border-light-green dark:border-dark-green hover:border-light-green hover:dark:border-dark-green hover:bg-light-greenBack hover:dark:bg-dark-greenBack text-light-green dark:text-dark-green focus:shadow-light-green focus:dark:shadow-dark-green">Modifier</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
|
||||
@@ -15,7 +15,8 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod/v4';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
import { Content } from '~~/shared/content';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin'],
|
||||
@@ -51,6 +52,9 @@ async function fetch()
|
||||
error.value = null;
|
||||
success.value = true;
|
||||
|
||||
if(job.value === 'pull')
|
||||
await Content.pull(true);
|
||||
|
||||
Toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
|
||||
}
|
||||
catch(e)
|
||||
@@ -70,7 +74,7 @@ async function fetch()
|
||||
</Head>
|
||||
<div class="flex flex-col justify-start items-center p-4">
|
||||
<div class="flex flex-row justify-between items-center gap-8">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h2 class="text-center flex-1 text-2xl font-bold">Administration</h2>
|
||||
</div>
|
||||
<div class="flex flex-row w-full gap-8">
|
||||
@@ -83,7 +87,7 @@ async function fetch()
|
||||
</div>
|
||||
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
|
||||
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
|
||||
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
|
||||
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-hidden m-2 px-2"></textarea>
|
||||
</div>
|
||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||
<span>Executer</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { CampaignSheet } from '#shared/campaign.util';
|
||||
import { unifySlug } from '~~/shared/general';
|
||||
import { CampaignSheet } from '~~/shared/campaign';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const id = unifySlug(useRoute().params.id ?? '');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { user, loggedIn } = useUserSession();
|
||||
@@ -39,7 +39,7 @@ function create()
|
||||
useRequestFetch()('/api/campaign', {
|
||||
method: 'POST',
|
||||
body: { name: 'Margooning', public_notes: '', dm_notes: '', settings: {} },
|
||||
}).then(() => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
|
||||
}).then((result) => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -65,7 +65,7 @@ function create()
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100/10 dark:bg-dark-100/10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
||||
@@ -80,27 +80,13 @@ function create()
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Vous vous appretez à {{ user.id !== campaign.owner.id ? 'quitter' : 'supprimer' }} "{{ campaign.name }}". Etes vous sûr ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
<AlertDialogAction asChild><Button @click="() => user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red hover:dark:border-dark-red hover:bg-light-redBack hover:dark:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red focus:dark:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
|
||||
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
|
||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="archives && archives.length > 0" class="flex flex-row w-full gap-8 justify-center items-center"><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span><span class="text-lg font-semibold">Archives</span><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span></div>
|
||||
<div v-if="archives && archives.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||
@@ -114,7 +100,7 @@ function create()
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100/10 dark:bg-dark-100/10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
||||
@@ -129,39 +115,25 @@ function create()
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Vous vous appretez à {{ user.id !== campaign.owner.id ? 'quitter' : 'supprimer' }} "{{ campaign.name }}". Etes vous sûr ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
<AlertDialogAction asChild><Button @click="() => user?.id !== campaign.owner.id ? leaveCampaign(campaign.id) : removeCampaign(campaign.id)" class="border-light-red dark:border-dark-red hover:border-light-red hover:dark:border-dark-red hover:bg-light-redBack hover:dark:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red focus:dark:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
|
||||
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
|
||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||
<span class="text-lg font-bold">Vous n'avez pas encore rejoint de campagne</span>
|
||||
<div class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
<div class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" @click="create">Créer ma campagne</div>
|
||||
<!-- <NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 py-2 px-4" @click="create">Créer ma campagne</div>
|
||||
<!-- <NuxtLink class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'campaign-id-edit', params: { id: 'new' } }">Créer ma campagne</NuxtLink> -->
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 py-2 px-4" :to="{ name: 'campaign-id-edit', params: { id: 'new' } }">Créer ma campagne</NuxtLink> -->
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { CharacterBuilder } from '#shared/character.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { CharacterBuilder } from '~~/shared/character';
|
||||
import { unifySlug } from '~~/shared/general';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
validState: true,
|
||||
});
|
||||
const id = unifySlug(useRouter().currentRoute.value.params.id ?? "new");
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { unifySlug } from '~~/shared/general';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
import { CharacterSheet } from '#shared/character.util';
|
||||
import { CharacterSheet } from '~~/shared/character';
|
||||
|
||||
/*
|
||||
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
})
|
||||
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
const { user } = useUserSession();
|
||||
|
||||
async function deleteCharacter(id: number)
|
||||
{
|
||||
status.value = "pending";
|
||||
@@ -49,11 +51,11 @@ async function duplicateCharacter(id: number)
|
||||
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100/10 dark:bg-dark-100/10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
||||
<NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
|
||||
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
|
||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||
<NuxtLink @click="duplicateCharacter(character.id)" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Dupliquer</NuxtLink>
|
||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||
@@ -68,7 +70,7 @@ async function duplicateCharacter(id: number)
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red hover:dark:border-dark-red hover:bg-light-redBack hover:dark:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red focus:dark:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
@@ -78,14 +80,15 @@ async function duplicateCharacter(id: number)
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||
<span class="text-lg font-bold">Vous n'avez pas encore de personnage</span>
|
||||
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
<NuxtLink v-if="user && user.state === 1" class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||
<div v-else>Veuillez validez votre adresse mail pour pouvoir créer des personnages.</div>
|
||||
<NuxtLink class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-list' }">Qu'ont fait les autres ?</NuxtLink>
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 py-2 px-4" :to="{ name: 'character-list' }">Qu'ont fait les autres ?</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
|
||||
@@ -3,6 +3,8 @@ import characterConfig from '#shared/character-config.json';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
const { user } = useUserSession();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,18 +29,21 @@ const config = characterConfig as CharacterConfig;
|
||||
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100/10 dark:bg-dark-100/10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||
<span class="text-lg font-bold">Il n'existe pas encore de personnage public</span>
|
||||
Soyez le premier à partager vos créations !
|
||||
<NuxtLink class="inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||
<template v-if="user && user.state === 1">
|
||||
Soyez le premier à partager vos créations !
|
||||
<NuxtLink class="inline-flex justify-center items-center outline-hidden leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 hover:dark:bg-dark-25 hover:border-light-50 hover:dark:border-dark-50
|
||||
focus:bg-light-30 focus:dark:bg-dark-30 focus:border-light-50 focus:dark:border-dark-50 focus:shadow-raw focus:shadow-light-50 focus:dark:shadow-dark-50 py-2 px-4" :to="{ name: 'character-id-edit', params: { id: 'new' } }">Nouveau personnage</NuxtLink>
|
||||
</template>
|
||||
<div v-else>Veuillez valider votre adresse mail pour pouvoir créer des personnages.</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { HomebrewBuilder } from '#shared/feature.util';
|
||||
import { HomebrewBuilder } from '#shared/feature';
|
||||
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Content } from '#shared/content.util';
|
||||
import { unifySlug } from '#shared/general.util';
|
||||
import { Content } from '~~/shared/content';
|
||||
import { unifySlug } from '~~/shared/general';
|
||||
|
||||
const element = useTemplateRef('element'), overview = ref();
|
||||
const route = useRouter().currentRoute;
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 hover:dark:bg-dark-30 px-4 py-2 select-none" active-class="text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 hover:dark:bg-dark-30 px-4 py-2 select-none" active-class="text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
@@ -35,10 +35,10 @@
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-row gap-16 items-center">
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 hover:dark:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
|
||||
@@ -47,11 +47,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Content, Editor } from '#shared/content.util';
|
||||
import { button, loading } from '#shared/components.util';
|
||||
import { dom, icon } from '#shared/dom.util';
|
||||
import { modal, tooltip } from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Content, Editor } from '~~/shared/content';
|
||||
import { button, loading } from '~~/shared/components';
|
||||
import { dom, icon } from '~~/shared/dom';
|
||||
import { modal, tooltip } from '~~/shared/floating';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
definePageMeta({
|
||||
@@ -92,19 +92,26 @@ onMounted(async () => {
|
||||
tree.value.appendChild(load);
|
||||
|
||||
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
|
||||
tooltip(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), 'Actualiser', 'top'),
|
||||
tooltip(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), 'Enregistrer', 'top'),
|
||||
tooltip(button(icon('ph:cloud-arrow-down', { height: 16, width: 16 }), pull, 'p-1'), 'Actualiser', 'top'),
|
||||
tooltip(button(icon('ph:cloud-arrow-up', { height: 16, width: 16 }), push, 'p-1'), 'Enregistrer', 'top'),
|
||||
tooltip(button(icon('radix-icons:reset', { height: 16, width: 16 }), () => editor?.undo(), 'p-1'), 'Annuler', 'top'),
|
||||
tooltip(button(icon('radix-icons:reset', { height: 16, width: 16, hFlip: true }), () => editor?.redo(), 'p-1'), 'Rétablir', 'top'),
|
||||
])
|
||||
|
||||
tree.value.insertBefore(content, load);
|
||||
|
||||
editor = new Editor();
|
||||
|
||||
Content.ready.then(() => tree.value!.replaceChild(editor.tree.container, load));
|
||||
Content.ready.then(() => tree.value?.replaceChild(editor.tree.container, load));
|
||||
container.value.appendChild(editor.container);
|
||||
}
|
||||
});
|
||||
|
||||
useShortcuts({
|
||||
"Meta-Z": () => editor?.undo(),
|
||||
"Meta-Y": () => editor?.redo(),
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor?.unmount();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-center flex-1 text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-center flex-1 text-xl font-bold">Modification de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
@@ -26,11 +26,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { user } = useUserSession();
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Connexion</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
|
||||
<TextInput type="password" label="Mot de passe" name="password" autocomplete="current-password" v-model="state.password"/>
|
||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
|
||||
@@ -21,7 +21,7 @@
|
||||
import type { ZodError } from 'zod/v4';
|
||||
import { schema, type Login } from '~/schemas/login';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
@@ -63,7 +63,10 @@ async function submit()
|
||||
{
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-profile' });
|
||||
|
||||
const router = useRouter();
|
||||
const target = router.currentRoute.value.query?.t as string | undefined;
|
||||
router.push(target ? decodeURIComponent(target) : { name: 'user-profile' });
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
requiresAuth: true,
|
||||
})
|
||||
const { user, clear } = useUserSession();
|
||||
const loading = ref<boolean>(false);
|
||||
@@ -57,7 +57,7 @@ async function deleteUser()
|
||||
<NuxtLink :to="{ name: 'user-changing-password' }" class="flex flex-1"><Button>Modifier mon mot de passe</Button></NuxtLink>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger asChild><Button :loading="loading"
|
||||
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer
|
||||
class="border-light-red dark:border-dark-red hover:border-light-red hover:dark:border-dark-red hover:bg-light-redBack hover:dark:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red focus:dark:shadow-dark-red">Supprimer
|
||||
mon compte</Button></AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
@@ -72,7 +72,7 @@ async function deleteUser()
|
||||
</AlertDialogDescription>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteUser()" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button></AlertDialogAction>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteUser()" class="border-light-red dark:border-dark-red hover:border-light-red hover:dark:border-dark-red hover:bg-light-redBack hover:dark:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red focus:dark:shadow-dark-red">Supprimer</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<span class="border border-transparent hover:border-light-35 hover:dark:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Inscription</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
|
||||
@@ -20,7 +20,7 @@
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||
</div>
|
||||
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
||||
<Label class="pb-2 col-span-2 md:col-span-1 flex flex-row gap-2 items-center"><CheckboxRoot v-model:checked="agreeOnRules" class="border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 w-5 h-5" ><CheckboxIndicator ><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span>J'ai lu et j'accepte les <NuxtLink class="text-accent-blue cursor-pointer" :to="{ name: 'usage' }" target="_blank">conditions d'utilisation</NuxtLink></span></Label>
|
||||
<Label class="pb-2 col-span-2 md:col-span-1 flex flex-row gap-2 items-center"><CheckboxRoot v-model:checked="agreeOnRules" class="border border-light-35 dark:border-dark-35 hover:border-light-50 hover:dark:border-dark-50 w-5 h-5" ><CheckboxIndicator ><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span>J'ai lu et j'accepte les <NuxtLink class="text-accent-blue cursor-pointer" :to="{ name: 'usage' }" target="_blank">conditions d'utilisation</NuxtLink></span></Label>
|
||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
|
||||
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
|
||||
</form>
|
||||
@@ -31,7 +31,7 @@
|
||||
import { ZodError } from 'zod/v4';
|
||||
import { schema, type Registration } from '~/schemas/registration';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import { Toaster } from '~~/shared/components';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.directive('autofocus', {
|
||||
mounted(el, binding) {
|
||||
el.focus();
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const schema = z.object({
|
||||
usernameOrEmail: z.string({ required_error: "Nom d'utilisateur ou email obligatoire" }),
|
||||
password: z.string({ required_error: "Mot de passe obligatoire" }),
|
||||
usernameOrEmail: z.string({ error: "Nom d'utilisateur ou email obligatoire" }),
|
||||
password: z.string({ error: "Mot de passe obligatoire" }),
|
||||
});
|
||||
|
||||
export type Login = z.infer<typeof schema>;
|
||||
1
app/types/auth.d.ts
vendored
1
app/types/auth.d.ts
vendored
@@ -7,7 +7,6 @@ declare module 'vue-router'
|
||||
interface RouteMeta
|
||||
{
|
||||
requiresAuth?: boolean;
|
||||
guestsGoesTo?: string;
|
||||
usersGoesTo?: string;
|
||||
rights?: string[];
|
||||
validState?: boolean;
|
||||
|
||||
11
app/types/campaign.d.ts
vendored
11
app/types/campaign.d.ts
vendored
@@ -4,7 +4,7 @@ import type { Serialize } from 'nitropack';
|
||||
|
||||
export type CampaignVariables = {
|
||||
money: number;
|
||||
inventory: ItemState[];
|
||||
items: ItemState[];
|
||||
};
|
||||
export type Campaign = {
|
||||
id: number;
|
||||
@@ -16,11 +16,4 @@ export type Campaign = {
|
||||
characters: Array<Partial<{ character: { id: number, name: string, owner: number } }>>;
|
||||
public_notes: string;
|
||||
dm_notes: string;
|
||||
logs: CampaignLog[];
|
||||
} & CampaignVariables;
|
||||
export type CampaignLog = {
|
||||
target: number;
|
||||
timestamp: Serialize<Date>;
|
||||
type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'FIGHT' | 'TEXT';
|
||||
details: string;
|
||||
};
|
||||
} & CampaignVariables;
|
||||
144
app/types/character.d.ts
vendored
144
app/types/character.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES } from "#shared/character.util";
|
||||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES, CRAFTING_TYPES, PropertySum, ITEM_BUFFER_KEYS } from "#shared/character";
|
||||
import type { Localized } from "../types/general";
|
||||
|
||||
export type MainStat = typeof MAIN_STATS[number];
|
||||
@@ -12,8 +12,10 @@ export type Alignment = typeof ALIGNMENTS[number];
|
||||
export type Resistance = typeof RESISTANCES[number];
|
||||
export type DamageType = typeof DAMAGE_TYPES[number];
|
||||
export type WeaponType = typeof WEAPON_TYPES[number];
|
||||
export type CraftingType = typeof CRAFTING_TYPES[number];
|
||||
|
||||
export type FeatureID = string;
|
||||
export type FeatureEffectID = string;
|
||||
export type i18nID = string;
|
||||
|
||||
export type RecursiveKeyOf<TObj extends object> = {
|
||||
@@ -55,15 +57,37 @@ export type CharacterVariables = {
|
||||
spells: string[]; //Spell ID
|
||||
items: ItemState[];
|
||||
|
||||
money: number;
|
||||
components: { money: number, natural: number, mineral: number, processed: number, magical: number };
|
||||
transformed: boolean;
|
||||
craft?: { item: string, progress: number };
|
||||
};
|
||||
export type TreeLeaf = {
|
||||
id: FeatureID;
|
||||
to?: FeatureID | Array<FeatureID> | Record<string, FeatureID>;
|
||||
flags?: number; //Flags from TreeFlag
|
||||
};
|
||||
export type TreeStructure = {
|
||||
name: string;
|
||||
start: FeatureID | Array<FeatureID> | Record<string, FeatureID>;
|
||||
nodes: Record<FeatureID, TreeLeaf>;
|
||||
};
|
||||
type CommonState = {
|
||||
capacity?: number;
|
||||
powercost?: number;
|
||||
analysed?: boolean;
|
||||
};
|
||||
type ArmorState = { loss: number, health?: number, absorb: { flat?: number, percent?: number } };
|
||||
type WeaponState = { attack?: number | string, hit?: number };
|
||||
type WondrousState = { };
|
||||
type MundaneState = { };
|
||||
type ItemState = {
|
||||
id: string;
|
||||
amount: number;
|
||||
enchantments?: string[];
|
||||
improvements?: string[];
|
||||
charges?: number;
|
||||
equipped?: boolean;
|
||||
state?: any;
|
||||
state?: (ArmorState | WeaponState | WondrousState | MundaneState) & CommonState;
|
||||
buffer?: Record<string, PropertySum>
|
||||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: Record<string, RaceConfig>;
|
||||
@@ -71,36 +95,50 @@ export type CharacterConfig = {
|
||||
spells: Record<string, SpellConfig>;
|
||||
aspects: Record<string, AspectConfig>;
|
||||
features: Record<FeatureID, Feature>;
|
||||
enchantments: Record<string, EnchantementConfig>; //TODO
|
||||
improvements: Record<string, ImprovementConfig>;
|
||||
items: Record<string, ItemConfig>;
|
||||
sickness: Record<string, { id: string, name: string, description: string, effect: FeatureID[] }>;
|
||||
action: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||
reaction: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||
freeaction: Record<string, { id: string, name: string, description: string }>;
|
||||
passive: Record<string, { id: string, name: string, description: string }>;
|
||||
action: Record<string, { id: string, name: string, description: string, cost?: number, variants?: string[], parent?: string }>;
|
||||
reaction: Record<string, { id: string, name: string, description: string, cost?: number, variants?: string[], parent?: string }>;
|
||||
freeaction: Record<string, { id: string, name: string, description: string, variants?: string[], parent?: string }>;
|
||||
passive: Record<string, { id: string, name: string, description: string, variants?: string[], parent?: string }>;
|
||||
texts: Record<i18nID, Localized>;
|
||||
trees: Record<string, TreeStructure>;
|
||||
|
||||
//Each of these groups extend an existing feature as they all use the same properties
|
||||
sickness: Record<FeatureID, { name: string, stage: number }>; //TODO
|
||||
poison: Record<FeatureID, { name: string, difficulty: number, efficienty: number, solubility: number }>; //TODO
|
||||
dedication: Record<FeatureID, { name: string, requirement: Array<{ stat: MainStat, amount: number }> }>; //TODO
|
||||
};
|
||||
export type EnchantementConfig = {
|
||||
export type ImprovementConfig = {
|
||||
id: string;
|
||||
name: string; //TODO -> TextID
|
||||
description: i18nID;
|
||||
rarity: 'common' | 'uncommon' | 'rare' | 'veryrare' | 'legendary';
|
||||
effect: Array<FeatureEquipment | FeatureValue | FeatureList>;
|
||||
power: number;
|
||||
craft: { difficulty?: number, ability?: CraftingType };
|
||||
restrictions?: Partial<Record<'armor' | 'mundane' | 'wondrous' | 'weapon' | `armor/${ArmorConfig['type']}` | `weapon/${WeaponConfig['type'][number]}` | string, boolean>>; // Need to respect *any* of the restriction, not every restrictions.
|
||||
cursed: boolean;
|
||||
}
|
||||
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
|
||||
type CommonItemConfig = {
|
||||
id: string;
|
||||
name: string; //TODO -> TextID
|
||||
flavoring?: i18nID;
|
||||
description: i18nID;
|
||||
rarity: 'common' | 'uncommon' | 'rare' | 'legendary';
|
||||
rarity: 'common' | 'uncommon' | 'rare' | 'veryrare' | 'legendary';
|
||||
weight?: number; //Optionnal but highly recommended
|
||||
price?: number; //Optionnal but highly recommended
|
||||
capacity?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...)
|
||||
capacity?: number; //Optionnal as most mundane items should not receive improvements (potions, herbal heals, etc...)
|
||||
powercost?: number; //Optionnal
|
||||
charge?: number //Max amount of charges
|
||||
enchantments?: string[]; //Enchantment ID
|
||||
effects?: Array<FeatureValue | FeatureEquipment | FeatureList>;
|
||||
improvements?: string[]; //Improvement ID
|
||||
effects?: Array<FeatureValue | FeatureState | FeatureEquipment | FeatureList>;
|
||||
equippable: boolean;
|
||||
consummable: boolean;
|
||||
}
|
||||
craft: { mineral: number, natural: number, processed: number, magical: number, difficulty?: number, ability?: CraftingType };
|
||||
variants?: string[]; //ID array
|
||||
};
|
||||
type ArmorConfig = {
|
||||
category: 'armor';
|
||||
health: number;
|
||||
@@ -127,7 +165,7 @@ export type SpellConfig = {
|
||||
rank: 1 | 2 | 3 | 4;
|
||||
type: SpellType;
|
||||
cost: number;
|
||||
speed: "action" | "reaction" | number;
|
||||
speed: "action" | "reaction" | "channeling" | number;
|
||||
elements: Array<SpellElement>;
|
||||
description: string; //TODO -> TextID
|
||||
concentration: boolean;
|
||||
@@ -155,37 +193,50 @@ export type AspectConfig = {
|
||||
};
|
||||
|
||||
export type FeatureValue = {
|
||||
id: FeatureID;
|
||||
id: FeatureEffectID;
|
||||
category: "value";
|
||||
operation: "add" | "set" | "min";
|
||||
property: RecursiveKeyOf<CompiledCharacter> | 'spec' | 'ability' | 'training';
|
||||
value: number | `modifier/${MainStat}` | false;
|
||||
}
|
||||
export type FeatureState = {
|
||||
id: FeatureEffectID;
|
||||
category: "state";
|
||||
property: RecursiveKeyOf<CompiledCharacter>;
|
||||
value: string;
|
||||
}
|
||||
export type FeatureEquipment = {
|
||||
id: FeatureID;
|
||||
id: FeatureEffectID;
|
||||
category: "value";
|
||||
operation: "add" | "set" | "min";
|
||||
property: 'weapon/damage/value' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent';
|
||||
property: `item/${RecursiveKeyOf<ArmorState & WeaponState & WondrousState & MundaneState & CommonState>}`;
|
||||
value: number | `modifier/${MainStat}` | false;
|
||||
}
|
||||
};
|
||||
export type FeatureList = {
|
||||
id: FeatureID;
|
||||
id: FeatureEffectID;
|
||||
category: "list";
|
||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive" | "mastery" | "poison" | "dedication";
|
||||
action: "add" | "remove";
|
||||
item: string;
|
||||
};
|
||||
export type FeatureTree = {
|
||||
id: FeatureEffectID;
|
||||
category: "tree";
|
||||
tree: string;
|
||||
option?: string;
|
||||
priority?: number;
|
||||
};
|
||||
export type FeatureChoice = {
|
||||
id: FeatureID;
|
||||
id: FeatureEffectID;
|
||||
category: "choice";
|
||||
text: string; //TODO -> TextID
|
||||
settings?: { //If undefined, amount is 1 by default
|
||||
amount: number;
|
||||
exclusive: boolean; //Disallow to pick the same option twice
|
||||
};
|
||||
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList> }>; //TODO -> TextID
|
||||
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList | FeatureTree> }>; //TODO -> TextID
|
||||
};
|
||||
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
||||
export type FeatureItem = FeatureValue | FeatureState | FeatureList | FeatureChoice | FeatureTree;
|
||||
export type Feature = {
|
||||
id: FeatureID;
|
||||
description: string; //TODO -> TextID
|
||||
@@ -203,16 +254,22 @@ export type CompiledCharacter = {
|
||||
spellslots: number; //Max
|
||||
artslots: number; //Max
|
||||
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
|
||||
aspect: string; //ID
|
||||
aspect: {
|
||||
id: string,
|
||||
amount: number;
|
||||
duration: number;
|
||||
shift_bonus: number;
|
||||
tier: 0 | 1 | 2;
|
||||
|
||||
bonus?: Partial<CompiledCharacter['bonus']>;
|
||||
};
|
||||
mastery: Array<`weapon/${WeaponType}` | `armor/${'light' | 'medium' | 'heavy'}`>;
|
||||
speed: number | false;
|
||||
capacity: number | false;
|
||||
initiative: number;
|
||||
exhaust: number;
|
||||
itempower: number;
|
||||
|
||||
action: number;
|
||||
reaction: number;
|
||||
|
||||
variables: CharacterVariables,
|
||||
|
||||
defense: {
|
||||
@@ -224,28 +281,25 @@ export type CompiledCharacter = {
|
||||
passivedodge: number;
|
||||
};
|
||||
|
||||
mastery: {
|
||||
strength: number;
|
||||
dexterity: number;
|
||||
shield: number;
|
||||
armor: number;
|
||||
multiattack: number;
|
||||
magicpower: number;
|
||||
magicspeed: number;
|
||||
magicelement: number;
|
||||
magicinstinct: number;
|
||||
};
|
||||
|
||||
bonus: {
|
||||
defense: Partial<Record<MainStat, number>>;
|
||||
defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
spells: {
|
||||
type: Partial<Record<SpellType | 'arts', number>>;
|
||||
rank: Partial<Record<1 | 2 | 3 | 4, number>>;
|
||||
elements: Partial<Record<SpellElement, number>>;
|
||||
};
|
||||
weapon: Partial<Record<WeaponType, number>>;
|
||||
resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque
|
||||
damage: Partial<DamageType, 'resistance' | 'immunity' | 'vulnerability'>;
|
||||
}; //Any special bonus goes here
|
||||
resistance: Record<string, number>;
|
||||
|
||||
craft: { level: number, bonus: number };
|
||||
|
||||
modifier: Record<MainStat, number>;
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
level: number;
|
||||
lists: { [K in FeatureList['list']]?: string[] }; //string => ListItem ID
|
||||
lists: { [K in Exclude<FeatureList['list'], 'mastery'>]: string[] }; //string => ListItem ID
|
||||
|
||||
notes: { public: string, private: string };
|
||||
};
|
||||
1
app/types/general.d.ts
vendored
1
app/types/general.d.ts
vendored
@@ -17,5 +17,4 @@ type CanvasPreferences = {
|
||||
export type Localized = {
|
||||
fr_FR?: string;
|
||||
en_US?: string;
|
||||
default: string;
|
||||
}
|
||||
2
drizzle/0026_absurd_firelord.sql
Normal file
2
drizzle/0026_absurd_firelord.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `campaign` RENAME COLUMN "inventory" TO "items";--> statement-breakpoint
|
||||
DROP TABLE `campaign_logs`;
|
||||
929
drizzle/meta/0026_snapshot.json
Normal file
929
drizzle/meta/0026_snapshot.json
Normal file
@@ -0,0 +1,929 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "0a61f9fe-e6b1-4ac4-9d58-206dbbcf9cda",
|
||||
"prevId": "fdee27cd-0188-4e54-bc2c-a96a375e83a1",
|
||||
"tables": {
|
||||
"campaign_characters": {
|
||||
"name": "campaign_characters",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"character": {
|
||||
"name": "character",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"campaign_characters_id_campaign_id_fk": {
|
||||
"name": "campaign_characters_id_campaign_id_fk",
|
||||
"tableFrom": "campaign_characters",
|
||||
"tableTo": "campaign",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_characters_character_character_id_fk": {
|
||||
"name": "campaign_characters_character_character_id_fk",
|
||||
"tableFrom": "campaign_characters",
|
||||
"tableTo": "character",
|
||||
"columnsFrom": [
|
||||
"character"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"campaign_characters_id_character_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"character"
|
||||
],
|
||||
"name": "campaign_characters_id_character_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"campaign_members": {
|
||||
"name": "campaign_members",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"campaign_members_id_campaign_id_fk": {
|
||||
"name": "campaign_members_id_campaign_id_fk",
|
||||
"tableFrom": "campaign_members",
|
||||
"tableTo": "campaign",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
},
|
||||
"campaign_members_user_users_id_fk": {
|
||||
"name": "campaign_members_user_users_id_fk",
|
||||
"tableFrom": "campaign_members",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"campaign_members_id_user_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"user"
|
||||
],
|
||||
"name": "campaign_members_id_user_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"campaign": {
|
||||
"name": "campaign",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"link": {
|
||||
"name": "link",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'PREPARING'"
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"items": {
|
||||
"name": "items",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"money": {
|
||||
"name": "money",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"public_notes": {
|
||||
"name": "public_notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"dm_notes": {
|
||||
"name": "dm_notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"campaign_owner_users_id_fk": {
|
||||
"name": "campaign_owner_users_id_fk",
|
||||
"tableFrom": "campaign",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"character_abilities": {
|
||||
"name": "character_abilities",
|
||||
"columns": {
|
||||
"character": {
|
||||
"name": "character",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ability": {
|
||||
"name": "ability",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"max": {
|
||||
"name": "max",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"character_abilities_character_character_id_fk": {
|
||||
"name": "character_abilities_character_character_id_fk",
|
||||
"tableFrom": "character_abilities",
|
||||
"tableTo": "character",
|
||||
"columnsFrom": [
|
||||
"character"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"character_abilities_character_ability_pk": {
|
||||
"columns": [
|
||||
"character",
|
||||
"ability"
|
||||
],
|
||||
"name": "character_abilities_character_ability_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"character_choices": {
|
||||
"name": "character_choices",
|
||||
"columns": {
|
||||
"character": {
|
||||
"name": "character",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"choice": {
|
||||
"name": "choice",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"character_choices_character_character_id_fk": {
|
||||
"name": "character_choices_character_character_id_fk",
|
||||
"tableFrom": "character_choices",
|
||||
"tableTo": "character",
|
||||
"columnsFrom": [
|
||||
"character"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"character_choices_character_id_choice_pk": {
|
||||
"columns": [
|
||||
"character",
|
||||
"id",
|
||||
"choice"
|
||||
],
|
||||
"name": "character_choices_character_id_choice_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"character_leveling": {
|
||||
"name": "character_leveling",
|
||||
"columns": {
|
||||
"character": {
|
||||
"name": "character",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"choice": {
|
||||
"name": "choice",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"character_leveling_character_character_id_fk": {
|
||||
"name": "character_leveling_character_character_id_fk",
|
||||
"tableFrom": "character_leveling",
|
||||
"tableTo": "character",
|
||||
"columnsFrom": [
|
||||
"character"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"character_leveling_character_level_pk": {
|
||||
"columns": [
|
||||
"character",
|
||||
"level"
|
||||
],
|
||||
"name": "character_leveling_character_level_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"character": {
|
||||
"name": "character",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"people": {
|
||||
"name": "people",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"variables": {
|
||||
"name": "variables",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'{\"health\": 0,\"mana\": 0,\"spells\": [],\"items\": [],\"exhaustion\": 0,\"sickness\": [],\"poisons\": []}'"
|
||||
},
|
||||
"aspect": {
|
||||
"name": "aspect",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"public_notes": {
|
||||
"name": "public_notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"private_notes": {
|
||||
"name": "private_notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"visibility": {
|
||||
"name": "visibility",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'private'"
|
||||
},
|
||||
"thumbnail": {
|
||||
"name": "thumbnail",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"character_owner_users_id_fk": {
|
||||
"name": "character_owner_users_id_fk",
|
||||
"tableFrom": "character",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"character_training": {
|
||||
"name": "character_training",
|
||||
"columns": {
|
||||
"character": {
|
||||
"name": "character",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"stat": {
|
||||
"name": "stat",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"choice": {
|
||||
"name": "choice",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"character_training_character_character_id_fk": {
|
||||
"name": "character_training_character_character_id_fk",
|
||||
"tableFrom": "character_training",
|
||||
"tableTo": "character",
|
||||
"columnsFrom": [
|
||||
"character"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"character_training_character_stat_level_pk": {
|
||||
"columns": [
|
||||
"character",
|
||||
"stat",
|
||||
"level"
|
||||
],
|
||||
"name": "character_training_character_stat_level_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"email_validation": {
|
||||
"name": "email_validation",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"project_content": {
|
||||
"name": "project_content",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"project_files": {
|
||||
"name": "project_files",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"owner": {
|
||||
"name": "owner",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"navigable": {
|
||||
"name": "navigable",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"private": {
|
||||
"name": "private",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"project_files_path_unique": {
|
||||
"name": "project_files_path_unique",
|
||||
"columns": [
|
||||
"path"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"project_files_owner_users_id_fk": {
|
||||
"name": "project_files_owner_users_id_fk",
|
||||
"tableFrom": "project_files",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"owner"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_permissions": {
|
||||
"name": "user_permissions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permission": {
|
||||
"name": "permission",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_permissions_id_users_id_fk": {
|
||||
"name": "user_permissions_id_users_id_fk",
|
||||
"tableFrom": "user_permissions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_permissions_id_permission_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"permission"
|
||||
],
|
||||
"name": "user_permissions_id_permission_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_sessions": {
|
||||
"name": "user_sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_sessions_user_id_users_id_fk": {
|
||||
"name": "user_sessions_user_id_users_id_fk",
|
||||
"tableFrom": "user_sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_sessions_id_user_id_pk": {
|
||||
"columns": [
|
||||
"id",
|
||||
"user_id"
|
||||
],
|
||||
"name": "user_sessions_id_user_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_data": {
|
||||
"name": "users_data",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"signin": {
|
||||
"name": "signin",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lastTimestamp": {
|
||||
"name": "lastTimestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"users_data_id_users_id_fk": {
|
||||
"name": "users_data_id_users_id_fk",
|
||||
"tableFrom": "users_data",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hash": {
|
||||
"name": "hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_hash_unique": {
|
||||
"name": "users_hash_unique",
|
||||
"columns": [
|
||||
"hash"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {
|
||||
"\"campaign\".\"inventory\"": "\"campaign\".\"items\""
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +183,13 @@
|
||||
"when": 1764763792974,
|
||||
"tag": "0025_majestic_grim_reaper",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 26,
|
||||
"version": "6",
|
||||
"when": 1766864228037,
|
||||
"tag": "0026_absurd_firelord",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
198
nuxt.config.ts
198
nuxt.config.ts
@@ -1,6 +1,7 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
@@ -8,113 +9,11 @@ export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxtjs/color-mode',
|
||||
'nuxt-security',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@vueuse/nuxt',
|
||||
'radix-vue/nuxt',
|
||||
'@nuxtjs/sitemap',
|
||||
],
|
||||
tailwindcss: {
|
||||
viewer: false,
|
||||
config: {
|
||||
content: {
|
||||
files: [
|
||||
"./shared/**/*.{vue,js,jsx,mjs,ts,tsx}"
|
||||
]
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
boxShadow: {
|
||||
raw: '0 0 0 2px var(--tw-shadow-color)'
|
||||
},
|
||||
keyframes: {
|
||||
slideDownAndFade: {
|
||||
from: { opacity: '0', transform: 'translateY(-2px)' },
|
||||
to: { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
slideLeftAndFade: {
|
||||
from: { opacity: '0', transform: 'translateX(2px)' },
|
||||
to: { opacity: '1', transform: 'translateX(0)' },
|
||||
},
|
||||
slideUpAndFade: {
|
||||
from: { opacity: '0', transform: 'translateY(2px)' },
|
||||
to: { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
slideRightAndFade: {
|
||||
from: { opacity: '0', transform: 'translateX(-2px)' },
|
||||
to: { opacity: '1', transform: 'translateX(0)' },
|
||||
},
|
||||
contentShow: {
|
||||
from: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.96)' },
|
||||
to: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
contentShow: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
light: {
|
||||
red: '#e93147',
|
||||
orange: '#FF9800',
|
||||
yellow: '#FFEB3B',
|
||||
green: '#388E3C',
|
||||
indigo: '#7986CB',
|
||||
cyan: '#00bfbc',
|
||||
lime: '#8BC34A',
|
||||
blue: '#086ddd',
|
||||
purple: '#AB47BC',
|
||||
pink: '#d53984',
|
||||
0: "#ffffff",
|
||||
5: "#fcfcfc",
|
||||
10: "#fafafa",
|
||||
20: "#f7f7f7",
|
||||
25: "#e4e4e4",
|
||||
30: "#dfdfdf",
|
||||
35: "#d2d2d2",
|
||||
40: "#bdbdbd",
|
||||
50: "#ababab",
|
||||
60: "#707070",
|
||||
70: "#5c5c5c",
|
||||
100: "#202020",
|
||||
},
|
||||
dark: {
|
||||
red: '#fb464c',
|
||||
redBack: '#5A292B',
|
||||
orange: '#e9973f',
|
||||
yellow: '#e0de71',
|
||||
green: '#44cf6e',
|
||||
greenBack: '#284E34',
|
||||
cyan: '#53dfdd',
|
||||
blue: '#027aff',
|
||||
purple: '#a882ff',
|
||||
pink: '#fa99cd',
|
||||
0: '#1e1e1e',
|
||||
5: '#212121',
|
||||
10: '#242424',
|
||||
20: '#262626',
|
||||
25: '#2a2a2a',
|
||||
30: '#363636',
|
||||
35: '#3f3f3f',
|
||||
40: '#555555',
|
||||
50: '#666666',
|
||||
60: '#999999',
|
||||
70: '#b3b3b3',
|
||||
100: '#dadada',
|
||||
},
|
||||
accent: {
|
||||
purple: '#43A047',
|
||||
blue: '#26C6DA',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
css: ['~/assets/css/main.css'],
|
||||
app: {
|
||||
pageTransition: false,
|
||||
layoutTransition: false
|
||||
@@ -151,8 +50,46 @@ export default defineNuxtConfig({
|
||||
passwd: '',
|
||||
}
|
||||
},
|
||||
routeRules: {
|
||||
'/api/auth/session': {
|
||||
security: {
|
||||
rateLimiter: {
|
||||
headers: true,
|
||||
interval: 100,
|
||||
tokensPerInterval: 3
|
||||
},
|
||||
}
|
||||
},
|
||||
'/api/auth/login': {
|
||||
security: {
|
||||
rateLimiter: {
|
||||
headers: true,
|
||||
interval: 1000,
|
||||
tokensPerInterval: 1
|
||||
},
|
||||
}
|
||||
},
|
||||
'/api/auth/register': {
|
||||
security: {
|
||||
rateLimiter: {
|
||||
headers: true,
|
||||
interval: 1000,
|
||||
tokensPerInterval: 1
|
||||
},
|
||||
}
|
||||
},
|
||||
'/api/file/**': {
|
||||
security: {
|
||||
rateLimiter: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
security: {
|
||||
rateLimiter: false,
|
||||
rateLimiter: {
|
||||
headers: true,
|
||||
interval: 1000,
|
||||
tokensPerInterval: 10
|
||||
},
|
||||
headers: {
|
||||
contentSecurityPolicy: {
|
||||
"img-src": "'self' data: blob:",
|
||||
@@ -164,7 +101,7 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
sitemap: {
|
||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password', '/character/manage', '/campaign/create'],
|
||||
exclude: ['/admin/**', '/explore/edit', '/user/**', '/character/**', '/campaign/**'],
|
||||
sources: ['/api/__sitemap__/urls']
|
||||
},
|
||||
experimental: {
|
||||
@@ -187,15 +124,62 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
hmr: {
|
||||
protocol: 'wss',
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
clientPort: 3000,
|
||||
}
|
||||
}
|
||||
path: '/ws'
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'@atlaskit/pragmatic-drag-and-drop-auto-scroll/element',
|
||||
'@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item',
|
||||
'@atlaskit/pragmatic-drag-and-drop/combine',
|
||||
'@atlaskit/pragmatic-drag-and-drop/element/adapter',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/lang-markdown',
|
||||
'@codemirror/language',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/view',
|
||||
'@floating-ui/dom',
|
||||
'@iconify/vue',
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'hast-util-heading',
|
||||
'hast-util-heading-rank',
|
||||
'hast-util-select',
|
||||
'iconify-icon',
|
||||
'lodash.capitalize',
|
||||
'mdast-util-find-and-replace',
|
||||
'mdast-util-to-string',
|
||||
'remark-breaks',
|
||||
'remark-frontmatter',
|
||||
'remark-gfm',
|
||||
'remark-parse',
|
||||
'remark-rehype',
|
||||
'remark-stringify',
|
||||
'strip-markdown',
|
||||
'unified',
|
||||
'unist-util-visit',
|
||||
'zod',
|
||||
'zod/v4',
|
||||
]
|
||||
},
|
||||
},
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
||||
}
|
||||
},
|
||||
devtools: {
|
||||
enabled: false,
|
||||
}
|
||||
})
|
||||
48
package.json
48
package.json
@@ -9,31 +9,32 @@
|
||||
"migrate": "bun migrate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.2",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.1.5",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@floating-ui/dom": "^1.7.4",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@floating-ui/dom": "^1.7.6",
|
||||
"@iconify/vue": "^5.0.1",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@markdoc/markdoc": "^0.5.4",
|
||||
"@markdoc/markdoc": "^0.5.7",
|
||||
"@nuxtjs/color-mode": "^4.0.0",
|
||||
"@nuxtjs/sitemap": "^7.4.7",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@nuxtjs/sitemap": "^8.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vueuse/gesture": "^2.0.0",
|
||||
"@vueuse/math": "^14.0.0",
|
||||
"@vueuse/nuxt": "^14.0.0",
|
||||
"@vueuse/math": "^14.3.0",
|
||||
"@vueuse/nuxt": "^14.3.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"hast": "^1.0.0",
|
||||
"hast-util-heading": "^3.0.0",
|
||||
"hast-util-heading-rank": "^3.0.0",
|
||||
"hast-util-select": "^6.0.4",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"mdast-util-find-and-replace": "^3.0.2",
|
||||
"nodemailer": "^7.0.10",
|
||||
"nuxt": "^4.2.1",
|
||||
"nuxt-security": "^2.4.0",
|
||||
"nodemailer": "^8.0.10",
|
||||
"nuxt": "^4.4.8",
|
||||
"nuxt-security": "^2.6.0",
|
||||
"radix-vue": "^1.9.17",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
@@ -42,23 +43,22 @@
|
||||
"remark-ofm": "link:remark-ofm",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"rollup-plugin-vue": "^6.0.0",
|
||||
"strip-markdown": "^6.0.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3",
|
||||
"zod": "^4.1.12"
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vue": "^3.5.35",
|
||||
"vue-router": "^5.1.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.2",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/nodemailer": "^7.0.3",
|
||||
"@types/nodemailer": "^8.0.0",
|
||||
"@types/unist": "^3.0.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"bun-types": "^1.3.2",
|
||||
"drizzle-kit": "^0.31.6",
|
||||
"bun-types": "^1.3.14",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"rehype-stringify": "^10.0.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
|
||||
declare module 'nitropack'
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { projectFilesTable } from '~/db/schema';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { hasPermissions } from '~~/shared/auth';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { hasPermissions } from '~~/shared/auth';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const session = await getUserSession(e);
|
||||
|
||||
@@ -74,7 +74,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
|
||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date() } }) as UserSessionRequired);
|
||||
|
||||
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||
const emailId = Bun.hash('register' + id.id + hash, Date.now()).toString(10);
|
||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||
|
||||
await runTask('validation', {
|
||||
@@ -90,7 +90,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||
template: 'registration',
|
||||
data: {
|
||||
id: emailId, timestamp,
|
||||
userId: id,
|
||||
userId: id.id,
|
||||
username: body.data.username,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineEventHandler(async (e) => {
|
||||
|
||||
if(result && result.id)
|
||||
{
|
||||
const id = hash('reset' + result.id + result.hash, Date.now());
|
||||
const id = hash('reset' + result.id + result.hash, Date.now()).toString(10);
|
||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||
await runTask('validation', {
|
||||
payload: {
|
||||
|
||||
@@ -2,15 +2,15 @@ import { eq } from 'drizzle-orm';
|
||||
import { z } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { campaignTable } from '~/db/schema';
|
||||
import { CampaignValidation } from '#shared/campaign.util';
|
||||
import { cryptURI } from '#shared/general.util';
|
||||
import { CampaignValidation } from '~~/shared/campaign';
|
||||
import { cryptURI } from '~~/shared/general';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const body = await readValidatedBody(e, CampaignValidation.extend({ id: z.unknown(), }).safeParse);
|
||||
if(!body.success)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
return body.error.message;
|
||||
throw body.error.message;
|
||||
}
|
||||
|
||||
const session = await getUserSession(e);
|
||||
@@ -39,7 +39,7 @@ export default defineEventHandler(async (e) => {
|
||||
});
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return id;
|
||||
return { id, link: cryptURI('campaign', id) };
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ export default defineEventHandler(async (e) => {
|
||||
members: { with: { member: { columns: { username: true, id: true } } }, columns: { id: false, user: false } },
|
||||
characters: { with: { character: { columns: { id: true, name: true, owner: true } } }, columns: { character: false } },
|
||||
owner: { columns: { username: true, id: true } },
|
||||
logs: { columns: { details: true, target: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp },
|
||||
},
|
||||
where: ({ id: _id }) => eq(_id, parseInt(id, 10)),
|
||||
}).sync();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { campaignTable } from '~/db/schema';
|
||||
import { CampaignValidation } from '#shared/campaign.util';
|
||||
import { CampaignValidation } from '~~/shared/campaign';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const params = getRouterParam(e, "id");
|
||||
@@ -12,7 +12,7 @@ export default defineEventHandler(async (e) => {
|
||||
}
|
||||
const id = parseInt(params, 10);
|
||||
|
||||
const body = await readValidatedBody(e, CampaignValidation.safeParse);
|
||||
const body = await readValidatedBody(e, CampaignValidation.partial().safeParse);
|
||||
if(!body.success)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { eq, SQL, type Operators } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { group } from '#shared/general.util';
|
||||
import { hasPermissions } from '~~/shared/auth';
|
||||
import { group } from '~~/shared/general';
|
||||
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod/v4';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation } from '#shared/character.util';
|
||||
import { CharacterValidation } from '~~/shared/character';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { campaignCharactersTable, campaignMembersTable, campaignTable, characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterTable, characterTrainingTable, usersTable } from '~/db/schema';
|
||||
import { group } from '#shared/general.util';
|
||||
import { group } from '~~/shared/general';
|
||||
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||
import { and, eq, exists, getTableColumns, isNotNull, or, sql } from 'drizzle-orm';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation } from '#shared/character.util';
|
||||
import { CharacterValidation } from '~~/shared/character';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
@@ -62,6 +62,9 @@ export default defineEventHandler(async (e) => {
|
||||
|
||||
const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1], max: 0 }));
|
||||
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||
|
||||
const choices = Object.entries(body.data.choices).flatMap(e => e[1].map(_e => ({ character: id, id: e[0], choice: _e })));
|
||||
if(choices.length > 0) tx.insert(characterChoicesTable).values(choices).run();
|
||||
});
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable } from '~/db/schema';
|
||||
import { CharacterNotesValidation } from '#shared/character.util';
|
||||
import { CharacterNotesValidation } from '~~/shared/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterTable } from '~/db/schema';
|
||||
import { CharacterVariablesValidation } from '#shared/character.util';
|
||||
import type { CharacterVariables } from '~/types/character';
|
||||
import { CharacterVariablesValidation } from '~~/shared/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from '#shared/auth.util';
|
||||
import { hasPermissions } from '~~/shared/auth';
|
||||
import { projectContentTable, projectFilesTable } from '~/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
@@ -14,7 +14,7 @@ export default defineEventHandler(async (e) => {
|
||||
try
|
||||
{
|
||||
const id = getRouterParam(e, "id") ?? '';
|
||||
const body = await readRawBody(e);
|
||||
const body = Buffer.from(await readRawBody(e) ?? '');
|
||||
|
||||
if(!id)
|
||||
{
|
||||
@@ -32,7 +32,7 @@ export default defineEventHandler(async (e) => {
|
||||
}
|
||||
|
||||
db.insert(projectContentTable).values({ id: id, content: body }).onConflictDoUpdate({ set: { content: body }, target: projectContentTable.id }).run();
|
||||
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run()
|
||||
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run();
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { hasPermissions } from "#shared/auth";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { projectFilesTable } from "~/db/schema";
|
||||
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
||||
import { Project } from "~/schemas/project";
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
@@ -19,23 +19,24 @@ export default defineEventHandler(async (e) => {
|
||||
throw body.error;
|
||||
}
|
||||
|
||||
const db = useDatabase(), items = body.data, blocked: string[] = [];
|
||||
const db = useDatabase(), items = body.data, requested: string[] = [];
|
||||
|
||||
db.transaction((tx) => {
|
||||
const data = tx.select({ id: projectFilesTable.id, timestamp: projectFilesTable.timestamp }).from(projectFilesTable).all();
|
||||
const deletion = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
|
||||
const deleteOverview = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
|
||||
const deleteContent = tx.delete(projectContentTable).where(eq(projectContentTable.id, sql.placeholder('id'))).prepare();
|
||||
|
||||
for(let i = 0; i < items.length; i++)
|
||||
{
|
||||
const item = items[i];
|
||||
const submitted = items[i]!;
|
||||
|
||||
const index = data.findIndex(e => e.id === item.id);
|
||||
const index = data.findIndex(e => e.id === submitted.id);
|
||||
|
||||
if(index !== -1)
|
||||
{
|
||||
if(data[index].timestamp > new Date(item.timestamp))
|
||||
if(data[index]!.timestamp < new Date(submitted.timestamp))
|
||||
{
|
||||
blocked.push(item.id);
|
||||
requested.push(submitted.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -43,34 +44,36 @@ export default defineEventHandler(async (e) => {
|
||||
}
|
||||
|
||||
tx.insert(projectFilesTable).values({
|
||||
id: item.id,
|
||||
path: item.path,
|
||||
id: submitted.id,
|
||||
path: submitted.path,
|
||||
owner: user.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
title: submitted.title,
|
||||
type: submitted.type,
|
||||
navigable: submitted.navigable,
|
||||
private: submitted.private,
|
||||
order: submitted.order,
|
||||
}).onConflictDoUpdate({
|
||||
set: {
|
||||
id: item.id,
|
||||
path: item.path,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
navigable: item.navigable,
|
||||
private: item.private,
|
||||
order: item.order,
|
||||
id: submitted.id,
|
||||
path: submitted.path,
|
||||
title: submitted.title,
|
||||
type: submitted.type,
|
||||
navigable: submitted.navigable,
|
||||
private: submitted.private,
|
||||
order: submitted.order,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
target: projectFilesTable.id,
|
||||
}).run();
|
||||
}
|
||||
|
||||
//Delete the remaining data has they have not been found in the new overview
|
||||
for(let i = 0; i < data.length; i++)
|
||||
{
|
||||
deletion.run({ id: data[i].id });
|
||||
deleteOverview.run({ id: data[i]!.id });
|
||||
deleteContent.run({ id: data[i]!.id });
|
||||
}
|
||||
});
|
||||
|
||||
return blocked;
|
||||
return requested;
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
export default defineEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session.user)
|
||||
{
|
||||
setResponseStatus(e, 401);
|
||||
return;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return {};
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
export default defineEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
|
||||
if(!id)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getUserSession(e);
|
||||
|
||||
if(!session.user)
|
||||
{
|
||||
setResponseStatus(e, 401);
|
||||
return;
|
||||
}
|
||||
|
||||
setResponseStatus(e, 200);
|
||||
return;
|
||||
});
|
||||
@@ -47,7 +47,7 @@ export default defineEventHandler(async (e) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailId = hash('register' + data.id + data.hash, Date.now());
|
||||
const emailId = hash('register' + data.id + data.hash, Date.now()).toString(10);
|
||||
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||
|
||||
await runTask('validation', {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { dom, type VirtualNode } from "#shared/dom.virtual.util";
|
||||
import { dom, type VirtualNode } from "#shared/dom.virtual";
|
||||
|
||||
export default function(content: VirtualNode[])
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { dom, text } from "#shared/dom.virtual.util";
|
||||
import { dom, text } from "#shared/dom.virtual";
|
||||
|
||||
export default function(data: any)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dom, text } from "#shared/dom.virtual.util";
|
||||
import { dom, text } from "#shared/dom.virtual";
|
||||
import { format } from "#shared/general";
|
||||
|
||||
export default function(data: any)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import useDatabase from "~/composables/useDatabase";
|
||||
import { campaignMembersTable, campaignTable } from "~/db/schema";
|
||||
import { decryptURI } from "#shared/general.util";
|
||||
import { decryptURI } from "#shared/general";
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const link = getRouterParam(e, "link");
|
||||
|
||||
@@ -16,6 +16,8 @@ export default defineEventHandler(async (e) => {
|
||||
if(!query.success)
|
||||
throw query.error;
|
||||
|
||||
const date = new Date();
|
||||
|
||||
if(Bun.hash('1' + query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||
{
|
||||
return createError({
|
||||
@@ -23,7 +25,7 @@ export default defineEventHandler(async (e) => {
|
||||
message: 'Lien incorrect',
|
||||
});
|
||||
}
|
||||
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||
if(date.getTime() > query.data.t + (60 * 60 * 1000))
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
@@ -34,7 +36,7 @@ export default defineEventHandler(async (e) => {
|
||||
const db = useDatabase();
|
||||
const validate = db.select(getTableColumns(emailValidationTable)).from(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).get();
|
||||
|
||||
if(!validate || validate.timestamp <= new Date())
|
||||
if(!validate || validate.timestamp <= date)
|
||||
{
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
@@ -42,7 +44,7 @@ export default defineEventHandler(async (e) => {
|
||||
});
|
||||
}
|
||||
|
||||
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, new Date())).run();
|
||||
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, date)).run();
|
||||
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||
|
||||
if(result === undefined)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SocketMessage } from "#shared/websocket.util";
|
||||
import type { User } from "~/types/auth";
|
||||
import type { SocketMessage } from "#shared/websocket";
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
message(peer, message) {
|
||||
@@ -31,14 +30,11 @@ export default defineWebSocketHandler({
|
||||
|
||||
const topic = `campaigns/${id}`;
|
||||
peer.subscribe(topic);
|
||||
peer.publish(topic, { type: 'status', data: [{ user: (peer.context.user as User).id, status: true }] });
|
||||
peer.send({ type: 'status', data: peer.peers.values().filter(e => e.topics.has(topic)).map(e => ({ user: (e.context.user as User).id, status: true })).toArray() })
|
||||
},
|
||||
close(peer, details) {
|
||||
const id = new URL(peer.request.url).pathname.split('/').slice(-1)[0];
|
||||
if(!id) return peer.close();
|
||||
|
||||
peer.publish(`campaigns/${id}`, { type: 'status', data: [{ user: (peer.context.user as User).id, status: false }] });
|
||||
peer.unsubscribe(`campaigns/${id}`);
|
||||
}
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import useDatabase from "~/composables/useDatabase";
|
||||
import { extname, basename } from 'node:path';
|
||||
import type { CanvasColor, CanvasContent } from "~/types/canvas";
|
||||
import type { FileType, ProjectContent } from "#shared/content.util";
|
||||
import { getID, parsePath } from "#shared/general.util";
|
||||
import type { FileType, ProjectContent } from "#shared/content";
|
||||
import { getID, parsePath } from "#shared/general";
|
||||
import { projectContentTable, projectFilesTable } from "~/db/schema";
|
||||
|
||||
const typeMapping: Record<string, FileType> = {
|
||||
@@ -71,10 +71,10 @@ export default defineTask({
|
||||
|
||||
const db = useDatabase();
|
||||
db.transaction(tx => {
|
||||
db.delete(projectFilesTable).run();
|
||||
db.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
|
||||
db.delete(projectContentTable).run();
|
||||
db.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
|
||||
tx.delete(projectFilesTable).run();
|
||||
tx.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
|
||||
tx.delete(projectContentTable).run();
|
||||
tx.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
|
||||
});
|
||||
|
||||
return { result: true };
|
||||
|
||||
@@ -14,18 +14,18 @@ export default defineTask({
|
||||
name: 'validation',
|
||||
description: 'Add email ID to DB',
|
||||
},
|
||||
run(e) {
|
||||
run({ payload, context }) {
|
||||
try {
|
||||
if(e.payload.type !== 'validation')
|
||||
if(payload.type !== 'validation')
|
||||
{
|
||||
throw new Error(`Données inconnues`);
|
||||
}
|
||||
|
||||
const payload = e.payload as ValidationPayload;
|
||||
const _payload = payload as ValidationPayload;
|
||||
const db = useDatabase();
|
||||
|
||||
db.delete(emailValidationTable).where(lt(emailValidationTable.timestamp, new Date())).run();
|
||||
db.insert(emailValidationTable).values({ id: payload.id, timestamp: new Date(payload.timestamp) }).run();
|
||||
db.insert(emailValidationTable).values({ id: _payload.id, timestamp: new Date(_payload.timestamp) }).run();
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { defu } from 'defu'
|
||||
import { createHooks } from 'hookable'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth'
|
||||
import type { CompatEvent } from '~~/shared/websocket.util'
|
||||
import type { CompatEvent } from '~~/shared/websocket'
|
||||
|
||||
export interface SessionHooks {
|
||||
/**
|
||||
|
||||
34
shared/breakpoint.ts
Normal file
34
shared/breakpoint.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { reactive } from '#shared/reactive';
|
||||
|
||||
export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
|
||||
const BREAKPOINT_WIDTHS: [Breakpoint, number][] = [
|
||||
['sm', 640],
|
||||
['md', 768],
|
||||
['lg', 1024],
|
||||
['xl', 1280],
|
||||
['2xl', 1536],
|
||||
];
|
||||
|
||||
export const breakpoint = reactive({ container: 'lg' as Breakpoint, viewport: 'lg' as Breakpoint });
|
||||
|
||||
function updateBreakpoint()
|
||||
{
|
||||
for (let i = BREAKPOINT_WIDTHS.length - 1; i >= 0; i--)
|
||||
{
|
||||
if (window.innerWidth >= BREAKPOINT_WIDTHS[i]![1])
|
||||
{
|
||||
const bp = BREAKPOINT_WIDTHS[i]![0];
|
||||
breakpoint.viewport = bp;
|
||||
breakpoint.container = bp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined')
|
||||
{
|
||||
updateBreakpoint();
|
||||
|
||||
BREAKPOINT_WIDTHS.forEach(e => window.matchMedia(`(min-width: ${e[1]}px)`).addEventListener('change', updateBreakpoint));
|
||||
}
|
||||
314
shared/campaign.ts
Normal file
314
shared/campaign.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { User } from "~/types/auth";
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import type { Campaign } from "~/types/campaign";
|
||||
import { div, dom, icon, span, text } from "#shared/dom";
|
||||
import { button, foldable, loading, numberpicker, tabgroup, Toaster } from "#shared/components";
|
||||
import { CharacterCompiler, colorByRarity, stateFactory, subnameFactory } from "#shared/character";
|
||||
import { modal, tooltip } from "#shared/floating";
|
||||
import markdown from "#shared/markdown";
|
||||
import { preview } from "#shared/proses";
|
||||
import { Socket } from "#shared/websocket";
|
||||
import { reactive } from "#shared/reactive";
|
||||
import type { Character, CharacterConfig } from "~/types/character";
|
||||
import { MarkdownEditor } from "./editor";
|
||||
import { getText } from "./i18n";
|
||||
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
export const CampaignValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string().nonempty(),
|
||||
public_notes: z.string(),
|
||||
dm_notes: z.string(),
|
||||
settings: z.object(),
|
||||
});
|
||||
|
||||
class CharacterPrinter
|
||||
{
|
||||
compiler?: CharacterCompiler;
|
||||
container: HTMLElement;
|
||||
name: string;
|
||||
id: number;
|
||||
constructor(character: number, name: string)
|
||||
{
|
||||
this.id = character;
|
||||
this.name = name;
|
||||
this.container = div('flex flex-col gap-2 px-1', [ div('flex flex-row justify-between items-center', [ span('text-bold text-xl', name), loading('small') ]) ]);
|
||||
useRequestFetch()(`/api/character/${character}`).then((character) => {
|
||||
if(character)
|
||||
{
|
||||
this.compiler = new CharacterCompiler(character);
|
||||
const compiled = this.compiler.compiled;
|
||||
const armor = this.compiler.armor;
|
||||
|
||||
this.container.replaceChildren(div('flex flex-row justify-between items-center', [
|
||||
span('text-bold text-xl', compiled.name),
|
||||
div('flex flex-row gap-2 items-baseline', [ span('text-sm font-bold', 'PV'), span('text-lg', `${(compiled.health ?? 0) - (compiled.variables.health ?? 0)}/${compiled.health ?? 0}`) ]),
|
||||
]), div('flex flex-row justify-between items-center', [
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Armure'), span('text-lg', armor === undefined ? `-` : `${armor.current}/${armor.max}`) ]),
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Mana'), span('text-lg', `${(compiled.mana ?? 0) - (compiled.variables.mana ?? 0)}/${compiled.mana ?? 0}`) ]),
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Fatigue'), span('text-lg', `${compiled.variables.exhaustion ?? 0}`) ]),
|
||||
]));
|
||||
}
|
||||
else throw new Error();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.container.replaceChildren(span('text-sm italic text-light-red dark:text-dark-red', 'Données indisponible'));
|
||||
})
|
||||
}
|
||||
}
|
||||
export class CampaignSheet
|
||||
{
|
||||
private user: ComputedRef<User | null>;
|
||||
private campaign?: Campaign;
|
||||
private characters!: Array<CharacterPrinter>;
|
||||
|
||||
container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6');
|
||||
ws?: Socket;
|
||||
|
||||
constructor(id: string, user: ComputedRef<User | null>)
|
||||
{
|
||||
this.user = user;
|
||||
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
||||
this.container.replaceChildren(load);
|
||||
useRequestFetch()(`/api/campaign/${id}`).then((campaign) => {
|
||||
if(campaign)
|
||||
{
|
||||
this.campaign = reactive(campaign);
|
||||
this.characters = reactive(campaign.characters.map(e => new CharacterPrinter(e.character!.id, e.character!.name)));
|
||||
/* this.ws = new Socket(`/ws/campaign/${id}`, true);
|
||||
|
||||
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('character', (character) => {
|
||||
if(character.action === 'ADD')
|
||||
{
|
||||
this.characters.push(new CharacterPrinter(character.id, character.name));
|
||||
}
|
||||
else if(character.action === 'REMOVE')
|
||||
{
|
||||
const idx = this.characters.findIndex(e => e.compiler?.character.id !== character.id);
|
||||
|
||||
idx !== -1 && this.characters.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('player', (player) => {
|
||||
if(player.action === 'ADD')
|
||||
{
|
||||
this.campaign?.members.push({ member: { id: player.id, username: player.name } });
|
||||
}
|
||||
else if(player.action === 'REMOVE')
|
||||
{
|
||||
const idx = this.campaign?.members.findIndex(e => e.member.id !== player.id);
|
||||
|
||||
idx && idx !== -1 && this.characters.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
this.ws.handleMessage<void>('hardsync', () => {
|
||||
useRequestFetch()(`/api/campaign/${id}`).then((campaign) => {
|
||||
this.campaign = reactive(campaign);
|
||||
this.characters = reactive(campaign.characters.map(e => new CharacterPrinter(e.character!.id, e.character!.name)));
|
||||
});
|
||||
}); */
|
||||
|
||||
document.title = `d[any] - Campagne ${campaign.name}`;
|
||||
this.render();
|
||||
|
||||
load.remove();
|
||||
}
|
||||
else throw new Error();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
|
||||
span('text-2xl font-bold tracking-wider', 'Campagne introuvable'),
|
||||
span(undefined, 'Cette campagne n\'existe pas ou est privé.'),
|
||||
div('flex flex-row gap-4 justify-center items-center', [
|
||||
button(text('Mes campagnes'), () => useRouter().push({ name: 'campaign' }), 'px-2 py-1'),
|
||||
button(text('Créer une campagne'), () => useRouter().push({ name: 'campaign-create' }), 'px-2 py-1')
|
||||
])
|
||||
]));
|
||||
});
|
||||
}
|
||||
save()
|
||||
{
|
||||
if(!this.campaign)
|
||||
return;
|
||||
|
||||
return useRequestFetch()(`/api/campaign/${this.campaign.id}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: this.campaign.name,
|
||||
status: this.campaign.status,
|
||||
public_notes: this.campaign.public_notes,
|
||||
dm_notes: this.campaign.dm_notes,
|
||||
},
|
||||
}).then(() => {}).catch(() => {
|
||||
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
|
||||
});
|
||||
}
|
||||
saveVariables()
|
||||
{
|
||||
|
||||
}
|
||||
private render()
|
||||
{
|
||||
const campaign = this.campaign;
|
||||
|
||||
if(!campaign)
|
||||
return;
|
||||
|
||||
const charPicker = this.characterPicker();
|
||||
|
||||
this.container.replaceChildren(div('grid grid-cols-3 gap-2', [
|
||||
div('flex flex-row gap-2 items-center py-2', [
|
||||
div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), campaign.owner.username, 'bottom') ]),
|
||||
div('border-l h-full w-0 border-light-40 dark:border-dark-40'),
|
||||
div('flex flex-row gap-1', { list: campaign.members, render: (member, _c) => _c ?? div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), member.member.username, 'bottom') ]) }),
|
||||
]),
|
||||
div('flex flex-1 flex-col items-center justify-center gap-2', [
|
||||
span('text-2xl font-serif font-bold italic', campaign.name),
|
||||
span('italic text-light-70 dark:text-dark-70 text-sm', campaign.status === 'PREPARING' ? 'En préparation' : campaign.status === 'PLAYING' ? 'En jeu' : 'Archivé'),
|
||||
]),
|
||||
div('flex flex-1 flex-col items-center justify-center', [
|
||||
div('border border-light-35 dark:border-dark-35 p-1 flex flex-row items-center gap-2', [
|
||||
dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`d-any.com/campaign/join/${ encodeURIComponent(campaign.link) }`) ]),
|
||||
button(icon(() => 'radix-icons:clipboard', { width: 16, height: 16 }), () => {}, 'p-1'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
div('flex flex-row gap-4 flex-1 h-0', [
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]),
|
||||
div('flex flex-col gap-2', { list: this.characters, render: (e, _c) => _c ?? e.container }),
|
||||
div('px-8 py-4 w-full flex', [
|
||||
button([
|
||||
icon('radix-icons:plus-circled', { width: 24, height: 24 }),
|
||||
span('text-sm', 'Ajouter un personnage'),
|
||||
], () => charPicker.show(), 'flex flex-col flex-1 gap-2 p-4 items-center justify-center text-light-60 dark:text-dark-60'),
|
||||
])
|
||||
]),
|
||||
div('flex h-full border-l border-light-40 dark:border-dark-40'),
|
||||
div('flex flex-col', [
|
||||
tabgroup([
|
||||
{ id: 'campaign', title: [ text('Campagne') ], content: () => {
|
||||
const editor = new MarkdownEditor();
|
||||
editor.content = campaign.public_notes;
|
||||
editor.onChange = (v) => campaign.public_notes = v;
|
||||
|
||||
return [
|
||||
this.user.value && this.user.value.id === campaign.owner.id ? div('flex flex-col gap-4 p-1', [ div('flex flex-row justify-between items-center', [ span('text-xl font-bold', 'Notes destinées aux joueurs'), div('flex flex-row gap-2', [ tooltip(button(icon('radix-icons:paper-plane', { width: 16, height: 16 }), () => this.save(), 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]) ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [editor.dom])]) : markdown(campaign.public_notes, '', { tags: { a: preview }, class: 'px-2' }),
|
||||
];
|
||||
} },
|
||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.items() },
|
||||
{ id: 'settings', title: [ text('Paramètres') ], content: () => [
|
||||
|
||||
] },
|
||||
{ id: 'ressources', title: [ text('Ressources') ], content: () => [
|
||||
|
||||
] }
|
||||
], { focused: 'campaign', class: { container: 'max-w-[900px] w-[900px] h-full', content: 'overflow-auto p-2', tabbar: 'gap-4 border-b border-light-30 dark:border-dark-30' } }),
|
||||
])
|
||||
]))
|
||||
}
|
||||
characterPicker()
|
||||
{
|
||||
const current = reactive({
|
||||
characters: [] as Character[],
|
||||
loading: true,
|
||||
});
|
||||
const _modal = modal([
|
||||
div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [
|
||||
span('text-xl font-bold', 'Mes personnages'),
|
||||
div('grid grid-cols-3 gap-2', { list: () => current.characters, render: (e, _c) => _c ?? div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
|
||||
span('font-bold', e.name),
|
||||
span('', `Niveau ${e.level}`),
|
||||
button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(_modal.close)),
|
||||
]), fallback: () => div('flex justify-center items-center col-span-3', [current.loading ? loading('large') : span('text-light-60 dark:text-dark-60 text-sm italic', 'Vous n\'avez pas de personnage disponible')]) }),
|
||||
]),
|
||||
], { closeWhenOutside: true, priority: true, class: { container: 'max-w-[560px]' }, open: false });
|
||||
|
||||
return { show: () => {
|
||||
current.loading = true;
|
||||
|
||||
useRequestFetch()(`/api/character`).then((list) => {
|
||||
current.characters = list?.filter(e => !this.characters.find(_e => _e.compiler?.character.id === e.id)) ?? [];
|
||||
}).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(() => {
|
||||
current.loading = false;
|
||||
});
|
||||
|
||||
_modal.open();
|
||||
}, hide: _modal.close }
|
||||
}
|
||||
items()
|
||||
{
|
||||
if(!this.campaign)
|
||||
return [];
|
||||
|
||||
const items = this.campaign.items;
|
||||
|
||||
/* const money = {
|
||||
readonly: dom('div', { listeners: { click: () => { money.readonly.replaceWith(money.edit); money.edit.focus(); } }, class: 'cursor-pointer border border-transparent hover:border-light-40 hover:dark:border-dark-40 px-2 py-px flex flex-row gap-1 items-center' }, [ span('text-lg font-bold', () => this.campaign!.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]),
|
||||
edit: numberpicker({ defaultValue: this.campaign.money, change: v => { this.campaign!.money = v; this.saveVariables(); money.edit.replaceWith(money.readonly); }, blur: v => { this.campaign!.money = v; this.saveVariables(); money.edit.replaceWith(money.readonly); }, min: 0, class: 'w-24' }),
|
||||
}; */
|
||||
|
||||
return [
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row justify-between items-center', [
|
||||
div('flex flex-row justify-end items-center gap-8', [
|
||||
div('flex flex-row gap-1 items-center', [ span('italic text-sm', 'Argent'), /* this.user.value && this.user.value.id === this.campaign.owner.id ? money.readonly : div('cursor-pointer px-2 py-px flex flex-row gap-1 items-center', [ span('text-lg font-bold', () => this.campaign!.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]) */text("TODO") ]),
|
||||
])
|
||||
]),
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.campaign.items, render: (e, _c) => {
|
||||
if(_c) return _c;
|
||||
|
||||
const item = config.items[e.id];
|
||||
|
||||
if(!item) return;
|
||||
|
||||
const itempower = () => (item.powercost ?? 0) + (e.improvements?.reduce((_p, _v) => (config.improvements[_v]?.power ?? 0) + _p, 0) ?? 0);
|
||||
|
||||
const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]);
|
||||
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
|
||||
return foldable(() => [
|
||||
markdown(getText(item.description)),
|
||||
div('flex flex-row justify-center gap-1', [
|
||||
button(text('Offrir'), () => {
|
||||
|
||||
}, 'px-2 text-sm h-5 box-content'),
|
||||
button(icon('radix-icons:minus', { width: 12, height: 12 }), () => {
|
||||
const idx = items.findIndex(_e => _e === e);
|
||||
if(idx === -1) return;
|
||||
|
||||
items[idx]!.amount--;
|
||||
if(items[idx]!.amount <= 0) items.splice(idx, 1);
|
||||
|
||||
this.saveVariables();
|
||||
}, 'p-1'),
|
||||
button(icon('radix-icons:plus', { width: 12, height: 12 }), () => {
|
||||
const idx = items.findIndex(_e => _e === e);
|
||||
if(idx === -1) return;
|
||||
|
||||
if(item.equippable) items.push(stateFactory(item));
|
||||
else if(items.find(_e => _e === e)) items.find(_e => _e === e)!.amount++;
|
||||
else items.push(stateFactory(item));
|
||||
|
||||
this.saveVariables();
|
||||
}, 'p-1')
|
||||
]) ], [div('flex flex-row justify-between', [
|
||||
div('flex flex-row items-center gap-4', [div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),]),
|
||||
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
||||
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('radix-icons:cross-2', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => e.amount ?? '-') ]),
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'text-red': !!item.capacity && itempower() > item.capacity }), () => item.capacity ? `${itempower()}/${item.capacity ?? 0}` : '-') ]),
|
||||
e.amount > 1 && !!item.weight ? tooltip(weight, `Poids unitaire: ${item.weight}`, 'bottom') : weight,
|
||||
div('flex flex-row min-w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.charge ? `${item.charge}` : '-') ]),
|
||||
]),
|
||||
])], { open: false, class: { icon: 'px-2', container: 'p-1 gap-2', content: 'px-4 pb-1 flex flex-col' } })
|
||||
}})
|
||||
])
|
||||
];
|
||||
}
|
||||
settings()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import type { User } from "~/types/auth";
|
||||
import type { Campaign, CampaignLog } from "~/types/campaign";
|
||||
import { div, dom, icon, span, svg, text } from "#shared/dom.util";
|
||||
import { button, loading, tabgroup, Toaster } from "#shared/components.util";
|
||||
import { CharacterCompiler } from "#shared/character.util";
|
||||
import { modal, tooltip } from "#shared/floating.util";
|
||||
import markdown from "#shared/markdown.util";
|
||||
import { preview } from "#shared/proses";
|
||||
import { format } from "#shared/general.util";
|
||||
import { Socket } from "#shared/websocket.util";
|
||||
|
||||
export const CampaignValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string().nonempty(),
|
||||
public_notes: z.string(),
|
||||
dm_notes: z.string(),
|
||||
settings: z.object(),
|
||||
});
|
||||
|
||||
class CharacterPrinter
|
||||
{
|
||||
compiler?: CharacterCompiler;
|
||||
container: HTMLElement = div('flex flex-col gap-2 px-1');
|
||||
constructor(character: number, name: string)
|
||||
{
|
||||
this.container.replaceChildren(div('flex flex-row justify-between items-center', [ span('text-bold text-xl', name), loading('small')]));
|
||||
useRequestFetch()(`/api/character/${character}`).then((character) => {
|
||||
if(character)
|
||||
{
|
||||
this.compiler = new CharacterCompiler(character);
|
||||
const compiled = this.compiler.compiled;
|
||||
const armor = this.compiler.armor;
|
||||
|
||||
this.container.replaceChildren(div('flex flex-row justify-between items-center', [
|
||||
span('text-bold text-xl', compiled.name),
|
||||
div('flex flex-row gap-2 items-baseline', [ span('text-sm font-bold', 'PV'), span('text-lg', `${(compiled.health ?? 0) - (compiled.variables.health ?? 0)}/${compiled.health ?? 0}`) ]),
|
||||
]), div('flex flex-row justify-between items-center', [
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Armure'), span('text-lg', armor === undefined ? `-` : `${armor.current}/${armor.max}`) ]),
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Mana'), span('text-lg', `${(compiled.mana ?? 0) - (compiled.variables.mana ?? 0)}/${compiled.mana ?? 0}`) ]),
|
||||
div('flex flex-col gap-px items-center justify-center', [ span('text-sm font-bold', 'Fatigue'), span('text-lg', `${compiled.variables.exhaustion ?? 0}`) ]),
|
||||
]));
|
||||
}
|
||||
else throw new Error();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.container.replaceChildren(span('text-sm italic text-light-red dark:text-dark-red', 'Données indisponible'));
|
||||
})
|
||||
}
|
||||
}
|
||||
type PlayerState = {
|
||||
statusDOM: HTMLElement;
|
||||
statusTooltip: Text;
|
||||
dom: HTMLElement;
|
||||
user: { id: number, username: string };
|
||||
};
|
||||
const logType: Record<CampaignLog['type'], string> = {
|
||||
CHARACTER: ' a rencontré ',
|
||||
FIGHT: ' a affronté ',
|
||||
ITEM: ' a obtenu ',
|
||||
PLACE: ' est arrivé ',
|
||||
TEXT: ' ',
|
||||
}
|
||||
const activity = {
|
||||
online: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-green dark:bg-dark-green border-light-green dark:border-dark-green', text: 'En ligne' },
|
||||
afk: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content bg-light-yellow dark:bg-dark-yellow border-light-yellow dark:border-dark-yellow', text: 'Inactif' },
|
||||
offline: { class: 'absolute -bottom-1 -right-1 rounded-full w-3 h-3 block border-2 box-content border-dashed border-light-50 dark:border-dark-50 bg-light-0 dark:bg-dark-0', text: 'Hors ligne' },
|
||||
}
|
||||
function defaultPlayerState(user: { id: number, username: string }): PlayerState
|
||||
{
|
||||
const statusTooltip = text(activity.offline.text), statusDOM = span(activity.offline.class);
|
||||
return {
|
||||
statusDOM,
|
||||
statusTooltip,
|
||||
dom: div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), user.username, 'bottom'), tooltip(statusDOM, statusTooltip, 'bottom') ]),
|
||||
user
|
||||
}
|
||||
}
|
||||
export class CampaignSheet
|
||||
{
|
||||
private user: ComputedRef<User | null>;
|
||||
private campaign?: Campaign;
|
||||
|
||||
container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6');
|
||||
|
||||
private dm!: PlayerState;
|
||||
private players!: Array<PlayerState>;
|
||||
private characters!: Array<CharacterPrinter>;
|
||||
private characterList!: HTMLElement;
|
||||
|
||||
private tab: string = 'campaign';
|
||||
|
||||
ws?: Socket;
|
||||
|
||||
constructor(id: string, user: ComputedRef<User | null>)
|
||||
{
|
||||
this.user = user;
|
||||
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
||||
this.container.replaceChildren(load);
|
||||
useRequestFetch()(`/api/campaign/${id}`).then((campaign) => {
|
||||
if(campaign)
|
||||
{
|
||||
this.campaign = campaign;
|
||||
this.dm = defaultPlayerState(campaign.owner);
|
||||
this.players = campaign.members.map(e => defaultPlayerState(e.member));
|
||||
this.characters = campaign.characters.map(e => new CharacterPrinter(e.character!.id, e.character!.name));
|
||||
this.ws = new Socket(`/ws/campaign/${id}`, true);
|
||||
this.ws.handleMessage<{ user: number, status: boolean }[]>('status', (users) => {
|
||||
users.forEach(user => {
|
||||
if(this.dm.user.id === user.user)
|
||||
{
|
||||
this.dm.statusTooltip.textContent = activity[user.status ? 'online' : 'offline'].text;
|
||||
this.dm.statusDOM.className = activity[user.status ? 'online' : 'offline'].class;
|
||||
}
|
||||
else
|
||||
{
|
||||
const player = this.players.find(e => e.user.id === user.user)
|
||||
|
||||
if(player)
|
||||
{
|
||||
player.statusTooltip.textContent = activity[user.status ? 'online' : 'offline'].text;
|
||||
player.statusDOM.className = activity[user.status ? 'online' : 'offline'].class;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('character', (character) => {
|
||||
if(character.action === 'ADD')
|
||||
{
|
||||
const printer = new CharacterPrinter(character.id, character.name);
|
||||
this.characters.push(printer);
|
||||
this.characterList.appendChild(printer.container);
|
||||
}
|
||||
else if(character.action === 'REMOVE')
|
||||
{
|
||||
const idx = this.characters.findIndex(e => e.compiler?.character.id !== character.id);
|
||||
|
||||
if(idx !== -1)
|
||||
{
|
||||
this.characters[idx]!.container.remove();
|
||||
this.characters.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.ws.handleMessage<{ id: number, name: string, action: 'ADD' | 'REMOVE' }>('player', () => {
|
||||
this.render();
|
||||
});
|
||||
this.ws.handleMessage<void>('hardsync', () => {
|
||||
this.render();
|
||||
});
|
||||
|
||||
document.title = `d[any] - Campagne ${campaign.name}`;
|
||||
this.render();
|
||||
|
||||
load.remove();
|
||||
}
|
||||
else throw new Error();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
|
||||
span('text-2xl font-bold tracking-wider', 'Campagne introuvable'),
|
||||
span(undefined, 'Cette campagne n\'existe pas ou est privé.'),
|
||||
div('flex flex-row gap-4 justify-center items-center', [
|
||||
button(text('Mes campagnes'), () => useRouter().push({ name: 'campaign' }), 'px-2 py-1'),
|
||||
button(text('Créer une campagne'), () => useRouter().push({ name: 'campaign-create' }), 'px-2 py-1')
|
||||
])
|
||||
]));
|
||||
});
|
||||
}
|
||||
private logText(log: CampaignLog)
|
||||
{
|
||||
return `${log.target === 0 ? 'Le groupe' : this.players.find(e => e.user.id === log.target)?.user.username ?? 'Un personange'}${logType[log.type]}${log.details}`;
|
||||
}
|
||||
private render()
|
||||
{
|
||||
const campaign = this.campaign;
|
||||
|
||||
if(!campaign)
|
||||
return;
|
||||
|
||||
this.characterList = div('flex flex-col gap-2', this.characters.map(e => e.container));
|
||||
this.container.replaceChildren(div('grid grid-cols-3 gap-2', [
|
||||
div('flex flex-row gap-2 items-center py-2', [
|
||||
this.dm.dom,
|
||||
div('border-l h-full w-0 border-light-40 dark:border-dark-40'),
|
||||
div('flex flex-row gap-1', this.players.map(e => e.dom)),
|
||||
]),
|
||||
div('flex flex-1 flex-col items-center justify-center gap-2', [
|
||||
span('text-2xl font-serif font-bold italic', campaign.name),
|
||||
span('italic text-light-70 dark:text-dark-70 text-sm', campaign.status === 'PREPARING' ? 'En préparation' : campaign.status === 'PLAYING' ? 'En jeu' : 'Archivé'),
|
||||
]),
|
||||
div('flex flex-1 flex-col items-center justify-center', [
|
||||
div('border border-light-35 dark:border-dark-35 p-1 flex flex-row items-center gap-2', [
|
||||
dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`d-any.com/campaign/join/${ encodeURIComponent(campaign.link) }`) ]),
|
||||
button(icon('radix-icons:clipboard', { width: 16, height: 16 }), () => {}, 'p-1'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
div('flex flex-row gap-4 flex-1 h-0', [
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]),
|
||||
this.characterList,
|
||||
div('px-8 py-4 w-full flex', [
|
||||
button([
|
||||
icon('radix-icons:plus-circled', { width: 24, height: 24 }),
|
||||
span('text-sm', 'Ajouter un personnage'),
|
||||
], () => {
|
||||
const load = loading('normal');
|
||||
let characters: HTMLElement[] = [];
|
||||
const close = modal([
|
||||
div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [
|
||||
span('text-xl font-bold', 'Mes personnages'),
|
||||
load,
|
||||
]),
|
||||
], { closeWhenOutside: true, priority: true, class: { container: 'max-w-[560px]' } }).close;
|
||||
useRequestFetch()(`/api/character`).then((list) => {
|
||||
characters = list?.map(e => div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
|
||||
span('font-bold', e.name),
|
||||
span('', `Niveau ${e.level}`),
|
||||
button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(close)),
|
||||
])) ?? [];
|
||||
}).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(() => {
|
||||
load.replaceWith(div('grid grid-cols-3 gap-2', characters.length > 0 ? characters : [span('text-light-60 dark:text-dark-60 text-sm italic', 'Vous n\'avez pas de personnage disponible')]));
|
||||
});
|
||||
}, 'flex flex-col flex-1 gap-2 p-4 items-center justify-center text-light-60 dark:text-dark-60'),
|
||||
])
|
||||
]),
|
||||
div('flex h-full border-l border-light-40 dark:border-dark-40'),
|
||||
div('flex flex-col', [
|
||||
tabgroup([
|
||||
{ id: 'campaign', title: [ text('Campagne') ], content: () => [
|
||||
markdown(campaign.public_notes, '', { tags: { a: preview }, class: 'px-2' }),
|
||||
] },
|
||||
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
||||
|
||||
] },
|
||||
{ id: 'logs', title: [ text('Logs') ], content: () => {
|
||||
let lastDate: Date = new Date(0);
|
||||
const logs = campaign.logs.flatMap(e => {
|
||||
const date = new Date(e.timestamp), arr = [];
|
||||
if(Math.floor(lastDate.getTime() / 86400000) < Math.floor(date.getTime() / 86400000))
|
||||
{
|
||||
lastDate = date;
|
||||
arr.push(div('flex flex-row gap-2 items-center relative -left-2 mx-px', [
|
||||
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:bg-dark-40 border-light-0 dark:border-dark-0'),
|
||||
div('flex flex-row gap-2 items-center flex-1', [
|
||||
div('flex-1 border-t border-light-40 dark:border-dark-40 border-dashed'),
|
||||
span('text-light-70 dark:text-dark-70 text-sm italic tracking-tight', format(date, 'dd MMMM yyyy')),
|
||||
div('flex-1 border-t border-light-40 dark:border-dark-40 border-dashed'),
|
||||
])
|
||||
]))
|
||||
}
|
||||
arr.push(div('flex flex-row gap-2 items-center relative -left-2 mx-px group', [
|
||||
div('w-3 h-3 border-2 rounded-full bg-light-40 dark:bg-dark-40 border-light-0 dark:border-dark-0'),
|
||||
div('flex flex-row items-center', [ svg('svg', { class: 'fill-light-40 dark:fill-dark-40', attributes: { width: "8", height: "12", viewBox: "0 0 6 9" } }, [svg('path', { attributes: { d: "M0 4.5L6 0L6 9L0 4.5Z" } })]), span('px-4 py-2 bg-light-25 dark:bg-dark-25 border border-light-40 dark:border-dark-40', this.logText(e)) ]),
|
||||
span('italic text-xs tracking-tight text-light-70 dark:text-dark-70 font-mono invisible group-hover:visible', format(new Date(e.timestamp), 'HH:mm:ss')),
|
||||
]));
|
||||
return arr;
|
||||
});
|
||||
return [
|
||||
campaign.logs.length > 0 ? div('flex flex-row ps-12 py-4', [
|
||||
div('border-l-2 border-light-40 dark:border-dark-40 relative before:absolute before:block before:border-[6px] before:border-b-[12px] before:-left-px before:-translate-x-1/2 before:border-transparent before:border-b-light-40 dark:before:border-b-dark-40 before:-top-3'),
|
||||
div('flex flex-col-reverse gap-8 py-4', logs),
|
||||
]) : div('flex py-4 px-16', [ span('italic text-light-70 dark:text-darl-70', 'Aucune entrée pour le moment') ]),
|
||||
]
|
||||
} },
|
||||
{ id: 'settings', title: [ text('Paramètres') ], content: () => [
|
||||
|
||||
] },
|
||||
{ id: 'ressources', title: [ text('Ressources') ], content: () => [
|
||||
|
||||
] }
|
||||
], { focused: 'campaign', class: { container: 'max-w-[900px] w-[900px] h-full', content: 'overflow-auto', tabbar: 'gap-4' } }),
|
||||
])
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
|
||||
import { clamp, lerp } from "#shared/general.util";
|
||||
import { dom, icon, svg } from "#shared/dom.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { tooltip } from "#shared/floating.util";
|
||||
import { History } from "#shared/history.util";
|
||||
import { clamp, lerp } from "#shared/general";
|
||||
import { dom, icon, svg } from "#shared/dom";
|
||||
import render from "#shared/markdown";
|
||||
import { tooltip } from "#shared/floating";
|
||||
import { History } from "#shared/history";
|
||||
import { preview } from "#shared/proses";
|
||||
import { SpatialGrid } from "#shared/physics.util";
|
||||
import { SpatialGrid } from "#shared/physics";
|
||||
import type { CanvasPreferences } from "~/types/general";
|
||||
|
||||
/*
|
||||
@@ -200,15 +200,13 @@ export class Node extends EventTarget
|
||||
{
|
||||
properties: CanvasNode;
|
||||
|
||||
nodeDom!: HTMLDivElement;
|
||||
nodeDom?: HTMLElement;
|
||||
|
||||
constructor(properties: CanvasNode)
|
||||
{
|
||||
super();
|
||||
|
||||
this.properties = properties;
|
||||
|
||||
this.getDOM()
|
||||
}
|
||||
|
||||
protected getDOM()
|
||||
@@ -216,8 +214,8 @@ export class Node extends EventTarget
|
||||
const style = this.style;
|
||||
|
||||
this.nodeDom = dom('div', { class: ['absolute', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
|
||||
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4', style.border] }, [
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg] }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text)]) : undefined])
|
||||
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full', style.border] }, [
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex overflow-auto', style.bg] }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text)]) : undefined])
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -230,12 +228,19 @@ export class Node extends EventTarget
|
||||
}
|
||||
}
|
||||
|
||||
get dom()
|
||||
{
|
||||
if(this.nodeDom === undefined)
|
||||
this.getDOM();
|
||||
|
||||
return this.nodeDom;
|
||||
}
|
||||
get style()
|
||||
{
|
||||
return this.properties.color ? this.properties.color?.class ?
|
||||
{ bg: `bg-light-${this.properties.color?.class} dark:bg-dark-${this.properties.color?.class}`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}` } :
|
||||
{ bg: `bg-light-${this.properties.color?.class}/10 dark:bg-dark-${this.properties.color?.class}/10`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}` } :
|
||||
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]` } :
|
||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
|
||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40/10 dark:bg-dark-40/10` }
|
||||
}
|
||||
}
|
||||
export class NodeEditable extends Node
|
||||
@@ -253,7 +258,7 @@ export class NodeEditable extends Node
|
||||
|
||||
this.nodeDom = dom('div', { class: ['absolute group', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
|
||||
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full group-hover:outline-4', style.border, style.outline] }, [
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: preview } })]) : undefined])
|
||||
dom('div', { class: ['w-full h-full py-2 px-4 flex overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: preview } })]) : undefined])
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -270,7 +275,7 @@ export class NodeEditable extends Node
|
||||
if(!this.dirty)
|
||||
return;
|
||||
|
||||
Object.assign(this.nodeDom.style, {
|
||||
Object.assign(this.nodeDom!.style, {
|
||||
transform: `translate(${this.properties.x}px, ${this.properties.y}px)`,
|
||||
width: `${this.properties.width}px`,
|
||||
height: `${this.properties.height}px`,
|
||||
@@ -282,9 +287,9 @@ export class NodeEditable extends Node
|
||||
override get style()
|
||||
{
|
||||
return this.properties.color ? this.properties.color?.class ?
|
||||
{ bg: `bg-light-${this.properties.color?.class} dark:bg-dark-${this.properties.color?.class}`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}`, outline: `outline-light-${this.properties.color?.class} dark:outline-dark-${this.properties.color?.class}` } :
|
||||
{ bg: `bg-light-${this.properties.color?.class}/10 dark:bg-dark-${this.properties.color?.class}/10`, border: `border-light-${this.properties.color?.class} dark:border-dark-${this.properties.color?.class}`, outline: `outline-light-${this.properties.color?.class} dark:outline-dark-${this.properties.color?.class}` } :
|
||||
{ bg: `bg-colored`, border: `border-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
|
||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
|
||||
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40/10 dark:bg-dark-40/10`, outline: `outline-light-40 dark:outline-dark-40` }
|
||||
}
|
||||
|
||||
get x()
|
||||
@@ -329,7 +334,7 @@ export class Edge extends EventTarget
|
||||
{
|
||||
properties: CanvasEdge;
|
||||
|
||||
edgeDom!: HTMLDivElement;
|
||||
edgeDom?: HTMLElement;
|
||||
protected from: Node;
|
||||
protected to: Node;
|
||||
protected path: Path;
|
||||
@@ -344,8 +349,6 @@ export class Edge extends EventTarget
|
||||
this.to = to;
|
||||
this.path = getPath(this.from.properties, properties.fromSide, this.to.properties, properties.toSide)!;
|
||||
this.labelPos = labelCenter(this.from.properties, properties.fromSide, this.to.properties, properties.toSide);
|
||||
|
||||
this.getDOM();
|
||||
}
|
||||
|
||||
protected getDOM()
|
||||
@@ -364,6 +367,13 @@ export class Edge extends EventTarget
|
||||
]);
|
||||
}
|
||||
|
||||
get dom()
|
||||
{
|
||||
if(this.edgeDom === undefined)
|
||||
this.getDOM();
|
||||
|
||||
return this.edgeDom;
|
||||
}
|
||||
get style()
|
||||
{
|
||||
return this.properties.color ? this.properties.color?.class ?
|
||||
@@ -377,8 +387,8 @@ export class EdgeEditable extends Edge
|
||||
private focusing: boolean = false;
|
||||
private editing: boolean = false;
|
||||
|
||||
private pathDom!: SVGPathElement;
|
||||
private inputDom!: HTMLDivElement;
|
||||
private pathDom?: SVGPathElement;
|
||||
private inputDom?: HTMLElement;
|
||||
constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable)
|
||||
{
|
||||
super(properties, from, to);
|
||||
@@ -406,7 +416,7 @@ export class EdgeEditable extends Edge
|
||||
update()
|
||||
{
|
||||
this.path = getPath(this.from.properties, this.properties.fromSide, this.to.properties, this.properties.toSide)!;
|
||||
this.pathDom.setAttribute('d', this.path.path);
|
||||
this.pathDom!.setAttribute('d', this.path.path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,8 +441,8 @@ export class Canvas
|
||||
protected tweener: Tweener = new Tweener();
|
||||
private debouncedTimeout: Timer = setTimeout(() => {}, 0);
|
||||
|
||||
protected transform!: HTMLDivElement;
|
||||
container!: HTMLDivElement;
|
||||
protected transform!: HTMLElement;
|
||||
container!: HTMLElement;
|
||||
|
||||
protected firstX = 0;
|
||||
protected firstY = 0;
|
||||
@@ -469,16 +479,16 @@ export class Canvas
|
||||
//const { loggedIn, user } = useUserSession();
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, this.nodes.map(e => e.nodeDom)), dom('div', {}, this.edges.map(e => e.edgeDom)),
|
||||
dom('div', {}, this.nodes.map(e => e.dom)), dom('div', {}, this.edges.map(e => e.dom)),
|
||||
])
|
||||
]);
|
||||
|
||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
|
||||
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
|
||||
]),
|
||||
]), this.transform,
|
||||
]);
|
||||
@@ -688,14 +698,14 @@ export class CanvasEditor extends Canvas
|
||||
private focused: NodeEditable | EdgeEditable | undefined;
|
||||
private selection: Set<NodeEditable> = new Set();
|
||||
|
||||
private dragging: boolean = false;
|
||||
private dragger: HTMLElement = dom('div', { class: 'border border-accent-blue absolute shadow-accent-blue pointer-events-none', style: { 'box-shadow': '0 0 2px var(--tw-shadow-color)' } });
|
||||
private dragging = false;
|
||||
private dragger = dom('div', { class: 'border border-accent-blue absolute shadow-accent-blue pointer-events-none', style: { 'box-shadow': '0 0 2px var(--tw-shadow-color)' } });
|
||||
|
||||
private pattern: SVGElement = svg('svg', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none' }, [
|
||||
private pattern = svg('svg', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none' }, [
|
||||
svg('pattern', { attributes: { id: 'canvasPattern', patternUnits: 'userSpaceOnUse' } }, [ svg('circle', { class: 'fill-light-35 dark:fill-dark-35', attributes: { cx: '0.75', cy: '0.75', r: '0.75' } }) ]),
|
||||
svg('rect', { attributes: { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#canvasPattern)' } })
|
||||
]);
|
||||
private nodeHelper: HTMLElement = dom('div', { class: 'cursor-move absolute z-40', listeners: { mousedown: e => this.moveSelection(e) }, style: { width: '0px', height: '0px' } }, [
|
||||
private nodeHelper = dom('div', { class: 'cursor-move absolute z-40', listeners: { mousedown: e => this.moveSelection(e) }, style: { width: '0px', height: '0px' } }, [
|
||||
dom('span', { class: 'cursor-n-resize absolute -top-3 -right-3 -left-3 h-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 1, 0, -1) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 top-1 left-1/2 -translate-x-2', listeners: { mousedown: e => this.dragNewEdge(e, 'top') } }) ]),
|
||||
dom('span', { class: 'cursor-s-resize absolute -bottom-3 -right-3 -left-3 h-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 0, 1) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 bottom-1 left-1/2 -translate-x-2', listeners: { mousedown: e => this.dragNewEdge(e, 'bottom') } }) ]),
|
||||
dom('span', { class: 'cursor-e-resize absolute -top-3 -bottom-3 -right-3 w-6 group', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 1, 0) } }, [ dom('span', { class: 'hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-4 h-4 right-1 top-1/2 -translate-y-2', listeners: { mousedown: e => this.dragNewEdge(e, 'right') } }) ]),
|
||||
@@ -705,8 +715,8 @@ export class CanvasEditor extends Canvas
|
||||
dom('span', { class: 'cursor-se-resize absolute -bottom-4 -right-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 0, 0, 1, 1) } }),
|
||||
dom('span', { class: 'cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8', listeners: { mousedown: e => this.resizeSelection(e, 1, 0, -1, 1) } }),
|
||||
]);
|
||||
private edgeHelper: HTMLElement = dom('div', { class: 'absolute', listeners: { } });
|
||||
private boxHelper: HTMLElement = dom('div', { class: '-m-2 border border-accent-purple absolute z-10 p-2 box-content', listeners: { mouseenter: () => this.focusSelection() } });
|
||||
private edgeHelper = dom('div', { class: 'absolute', listeners: { } });
|
||||
private boxHelper = dom('div', { class: '-m-2 border border-accent-purple absolute z-10 p-2 box-content', listeners: { mouseenter: () => this.focusSelection() } });
|
||||
|
||||
protected override nodes: NodeEditable[] = [];
|
||||
protected override edges: EdgeEditable[] = [];
|
||||
@@ -763,24 +773,24 @@ export class CanvasEditor extends Canvas
|
||||
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, [...this.nodes.map(e => e.nodeDom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]),
|
||||
dom('div', {}, [...this.nodes.map(e => e.dom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.dom)]),
|
||||
]), this.edgeHelper,
|
||||
]);
|
||||
|
||||
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none', listeners: { mousedown: () => { this.selection.clear(); this.updateSelection() } } }, [
|
||||
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.reset() } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.reset() } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
|
||||
]),
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), 'Annuler (Ctrl+Z)', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), 'Rétablir (Ctrl+Y)', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), 'Annuler (Ctrl+Z)', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), 'Rétablir (Ctrl+Y)', 'right'),
|
||||
]),
|
||||
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:gear')]), 'Préférences', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:question-mark-circled')]), 'Aide', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:gear')]), 'Préférences', 'right'),
|
||||
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 hover:dark:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:question-mark-circled')]), 'Aide', 'right'),
|
||||
]),
|
||||
]), this.pattern, this.transform
|
||||
]);
|
||||
File diff suppressed because one or more lines are too long
2476
shared/character.ts
Normal file
2476
shared/character.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1077
shared/components.ts
Normal file
1077
shared/components.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,782 +0,0 @@
|
||||
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
|
||||
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
|
||||
import { clamp } from "./general.util";
|
||||
import { Tree } from "./tree";
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
|
||||
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
|
||||
{
|
||||
const router = useRouter();
|
||||
const nav = link ? router.resolve(link) : undefined;
|
||||
return dom('a', { ...properties, class: [properties?.class, properties?.active && router.currentRoute.value.fullPath === nav?.fullPath ? properties.active : undefined], attributes: { href: nav?.href, 'data-active': properties?.active ? mergeClasses(properties?.active) : undefined }, listeners: link ? {
|
||||
click: function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
router.push(link);
|
||||
}
|
||||
} : undefined }, children);
|
||||
}
|
||||
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
|
||||
{
|
||||
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'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'}] })
|
||||
}
|
||||
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>)
|
||||
{
|
||||
let state = { current: loading(size) };
|
||||
|
||||
fn.then((element) => {
|
||||
state.current.replaceWith(element);
|
||||
state.current = element;
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
state.current.remove();
|
||||
})
|
||||
|
||||
return state;
|
||||
}
|
||||
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: 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
|
||||
*/
|
||||
const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
|
||||
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
|
||||
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
|
||||
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
|
||||
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, Array.isArray(content) ? content : [content]);
|
||||
let disabled = false;
|
||||
Object.defineProperty(btn, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
btn.toggleAttribute('disabled', disabled);
|
||||
}
|
||||
})
|
||||
return btn;
|
||||
}
|
||||
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
|
||||
{
|
||||
let currentValue = settings?.value;
|
||||
const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none
|
||||
border border-light-40 dark:border-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[selected]:z-10 data-[selected]:border-light-50 dark:data-[selected]:border-dark-50
|
||||
data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40`,
|
||||
settings?.class?.option], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() {
|
||||
if(currentValue !== e.value)
|
||||
{
|
||||
elements.forEach(e => e.toggleAttribute('data-selected', false));
|
||||
this.toggleAttribute('data-selected', true);
|
||||
|
||||
if(!settings?.onChange || settings?.onChange(e.value) !== false)
|
||||
{
|
||||
currentValue = e.value;
|
||||
}
|
||||
}
|
||||
}}}))
|
||||
return div(['flex flex-row', settings?.class?.container], elements);
|
||||
}
|
||||
export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void
|
||||
{
|
||||
let close: () => void;
|
||||
const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 dark:hover:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } })));
|
||||
return function(this: HTMLElement, target?: HTMLElement) {
|
||||
close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close;
|
||||
}
|
||||
}
|
||||
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
|
||||
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
|
||||
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
let context: { close: Function };
|
||||
let focused: number | undefined;
|
||||
|
||||
options = options.filter(e => !!e);
|
||||
|
||||
const focus = (i?: number) => {
|
||||
focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
|
||||
i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? '');
|
||||
const optionElements = options.map((e, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
return dom('div', { listeners: { click: (_e) => {
|
||||
textValue.textContent = e.text;
|
||||
settings?.change && settings?.change(e.value);
|
||||
context && context.close && !_e.ctrlKey && context.close();
|
||||
}, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const handleKeys = (e: KeyboardEvent) => {
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(0);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(optionElements.length - 1);
|
||||
return;
|
||||
case 'enter':
|
||||
focused && optionElements[focused]?.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeys);
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
|
||||
} }, class: ['mx-4 inline-flex 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 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(select, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
select.toggleAttribute('data-disabled', disabled);
|
||||
},
|
||||
})
|
||||
return select;
|
||||
}
|
||||
export function multiselect<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
let context: { close: Function };
|
||||
let focused: number | undefined;
|
||||
let selection: T[] = settings?.defaultValue ?? [];
|
||||
|
||||
options = options.filter(e => !!e);
|
||||
|
||||
const focus = (i?: number) => {
|
||||
focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
|
||||
i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const textValue = text(selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '');
|
||||
const optionElements = options.map((e, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
const element = dom('div', { listeners: { click: (_e) => {
|
||||
selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value];
|
||||
textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '';
|
||||
element.toggleAttribute('data-selected', selection.includes(e.value));
|
||||
settings?.change && settings?.change(selection);
|
||||
context && context.close && !_e.ctrlKey && context.close();
|
||||
}, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]);
|
||||
return element;
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const handleKeys = (e: KeyboardEvent) => {
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(0);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(optionElements.length - 1);
|
||||
return;
|
||||
case 'enter':
|
||||
focused && optionElements[focused]?.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeys);
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
|
||||
} }, class: ['mx-4 inline-flex 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 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(select, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
select.toggleAttribute('data-disabled', disabled);
|
||||
},
|
||||
})
|
||||
return select;
|
||||
}
|
||||
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' }): HTMLElement
|
||||
{
|
||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||
let selected = true, tree: StoredOption<T>[] = [];
|
||||
let focused: number | undefined;
|
||||
let currentOptions: StoredOption<T>[] = [];
|
||||
|
||||
const focus = (value?: T | Option<T>[]) => {
|
||||
focused !== undefined && currentOptions[focused]?.dom.toggleAttribute('data-focused', false);
|
||||
if(value !== undefined)
|
||||
{
|
||||
const i = currentOptions.findIndex(e => e.item?.value === value);
|
||||
if(i !== -1)
|
||||
{
|
||||
currentOptions[i]?.dom.toggleAttribute('data-focused', true);
|
||||
currentOptions[i]?.dom.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
focused = undefined;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
focused = undefined;
|
||||
}
|
||||
}
|
||||
const show = () => {
|
||||
if(disabled || (context && context.container.parentElement))
|
||||
return;
|
||||
|
||||
const box = container.getBoundingClientRect();
|
||||
focus();
|
||||
context = followermenu(container, currentOptions.length > 0 ? currentOptions.map(e => e.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": settings?.fill === 'cover' && `${box.width}px`, "max-width": settings?.fill === 'contain' && `${box.width}px` }, blur: hide });
|
||||
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
||||
};
|
||||
const hide = () => {
|
||||
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
||||
tree = [];
|
||||
|
||||
context && context.container.parentElement && context.close();
|
||||
};
|
||||
const progress = (option: StoredOption<T>) => {
|
||||
if(!context || !context.container.parentElement || option.container === undefined)
|
||||
return;
|
||||
|
||||
context.container.replaceChildren(option.container);
|
||||
tree.push(option);
|
||||
currentOptions = option.children!;
|
||||
focus();
|
||||
};
|
||||
const back = () => {
|
||||
tree.pop();
|
||||
const last = tree.slice(-1)[0];
|
||||
currentOptions = last?.children ?? optionElements;
|
||||
|
||||
last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
|
||||
};
|
||||
const render = (option: Option<T>): StoredOption<T> | undefined => {
|
||||
if(option === undefined)
|
||||
return;
|
||||
|
||||
if(Array.isArray(option.value))
|
||||
{
|
||||
const children = option.value.map(render).filter(e => !!e);
|
||||
|
||||
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
|
||||
return stored;
|
||||
}
|
||||
else
|
||||
{
|
||||
return { item: option, dom: dom('div', { listeners: { click: (_e) => {
|
||||
select.value = option.text;
|
||||
settings?.change && settings?.change(option.value as T);
|
||||
selected = true;
|
||||
!_e.ctrlKey && hide();
|
||||
}, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
|
||||
}
|
||||
}
|
||||
const filter = (value: string, option?: StoredOption<T>): StoredOption<T>[] => {
|
||||
if(option && option.children !== undefined)
|
||||
{
|
||||
return option.children.flatMap(e => filter(value, e));
|
||||
}
|
||||
else if(option && option.item)
|
||||
{
|
||||
return option.item.text.toLowerCase().normalize().includes(value) ? [ option ] : [];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const optionElements = currentOptions = options.map(render).filter(e => !!e);
|
||||
const select = dom('input', { listeners: { focus: show, input: () => {
|
||||
focus();
|
||||
currentOptions = context && select.value ? optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e)) : optionElements.filter(e => !!e);
|
||||
context && context.container.replaceChildren(...currentOptions.map(e => e.dom));
|
||||
selected = false;
|
||||
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
||||
}, keydown: (e) => {
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(currentOptions[clamp((focused ?? -1) + 1, 0, currentOptions.length - 1)]?.item?.value);
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(currentOptions[clamp((focused ?? 1) - 1, 0, currentOptions.length - 1)]?.item?.value);
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(currentOptions[0]?.item?.value);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(currentOptions[currentOptions.length - 1]?.item?.value);
|
||||
return;
|
||||
case 'enter':
|
||||
focused !== undefined ? currentOptions[focused]?.dom.click() : currentOptions[0]?.dom.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
||||
settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });
|
||||
|
||||
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(container, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
container.toggleAttribute('data-disabled', disabled);
|
||||
select.toggleAttribute('disabled', disabled);
|
||||
},
|
||||
})
|
||||
return container;
|
||||
}
|
||||
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void | boolean, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean, placeholder?: string }): HTMLInputElement
|
||||
{
|
||||
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, 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`, settings?.class], listeners: {
|
||||
input: (e) => { if(settings?.input && settings.input(input.value) === false) input.value = value; else value = input.value; },
|
||||
change: () => settings?.change && settings.change(input.value),
|
||||
focus: settings?.focus,
|
||||
blur: settings?.blur,
|
||||
}})
|
||||
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
||||
let value = input.value;
|
||||
|
||||
return input;
|
||||
}
|
||||
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: () => void, blur?: () => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
|
||||
{
|
||||
let storedValue = settings?.defaultValue ?? 0;
|
||||
const validateAndChange = (value: number) => {
|
||||
if(isNaN(value))
|
||||
field.value = '';
|
||||
else
|
||||
{
|
||||
value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
|
||||
field.value = value.toString(10);
|
||||
if(storedValue !== value)
|
||||
{
|
||||
storedValue = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 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 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-30 dark:disabled:border-dark-30`, settings?.class], listeners: {
|
||||
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
if(field.disabled)
|
||||
return;
|
||||
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
validateAndChange(storedValue + (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
validateAndChange(storedValue - (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageUp":
|
||||
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageDown":
|
||||
settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
change: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.change && settings.change(storedValue),
|
||||
focus: () => settings?.focus && settings.focus(),
|
||||
blur: () => settings?.blur && settings.blur(),
|
||||
}});
|
||||
if(settings?.defaultValue !== undefined) field.value = storedValue.toString(10);
|
||||
|
||||
return field;
|
||||
}
|
||||
// Open by default
|
||||
export function foldable(content: NodeChildren | (() => NodeChildren), title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
|
||||
{
|
||||
let _content: NodeChildren;
|
||||
const display = (state: boolean) => {
|
||||
if(state && !_content)
|
||||
{
|
||||
_content = typeof content === 'function' ? content() : content;
|
||||
_content && contentContainer.replaceChildren(..._content.filter(e => !!e));
|
||||
}
|
||||
}
|
||||
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
|
||||
const fold = div(['group flex w-full flex-col', settings?.class?.container], [
|
||||
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
|
||||
contentContainer
|
||||
]);
|
||||
display(settings?.open ?? true);
|
||||
fold.toggleAttribute('data-active', settings?.open ?? true);
|
||||
return fold;
|
||||
}
|
||||
type TableRow = Record<string, (() => HTMLElement) | HTMLElement | string>;
|
||||
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
|
||||
{
|
||||
const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
|
||||
return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
|
||||
}
|
||||
export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } })
|
||||
{
|
||||
let state = settings?.defaultValue ?? false;
|
||||
const element = dom("div", { 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 py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
|
||||
click: function(e: Event) {
|
||||
if(this.hasAttribute('data-disabled'))
|
||||
return;
|
||||
|
||||
state = !state;
|
||||
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
||||
settings?.change && settings.change(state);
|
||||
}
|
||||
}
|
||||
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
||||
return element;
|
||||
}
|
||||
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } })
|
||||
{
|
||||
let state = settings?.defaultValue ?? false;
|
||||
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
|
||||
cursor-pointer hover:bg-light-30 dark:hover:bg-dark-30 hover:border-light-60 dark:hover:border-dark-60
|
||||
data-[disabled]:cursor-default data-[disabled]:border-dashed data-[disabled]:border-light-40 dark:data-[disabled]:border-dark-40 data-[disabled]:bg-0 dark:data-[disabled]:bg-0 hover:data-[disabled]:bg-0 dark:hover:data-[disabled]:bg-0`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked", "data-disabled": settings?.disabled }, listeners: {
|
||||
click: function(e: Event) {
|
||||
if(this.hasAttribute('data-disabled'))
|
||||
return;
|
||||
|
||||
state = !state;
|
||||
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
||||
settings?.change && settings.change.bind(this)(state);
|
||||
}
|
||||
}
|
||||
}, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);
|
||||
return element;
|
||||
}
|
||||
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLDivElement & { refresh: () => void }
|
||||
{
|
||||
let focus = settings?.focused ?? tabs[0]?.id;
|
||||
const titles = tabs.map(e => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
|
||||
if(this.hasAttribute('data-focus'))
|
||||
return;
|
||||
|
||||
if(settings?.switch && settings.switch(e.id) === false)
|
||||
return;
|
||||
|
||||
titles.forEach(e => e.toggleAttribute('data-focus', false));
|
||||
this.toggleAttribute('data-focus', true);
|
||||
focus = e.id;
|
||||
const _content = typeof e.content === 'function' ? e.content() : e.content;
|
||||
_content && content.replaceChildren(..._content?.filter(e => !!e));
|
||||
}}}, e.title));
|
||||
const _content = tabs.find(e => e.id === focus)?.content;
|
||||
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
|
||||
|
||||
const container = div(['flex flex-col', settings?.class?.container], [
|
||||
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
|
||||
content
|
||||
]);
|
||||
Object.defineProperty(container, 'refresh', {
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: () => {
|
||||
let _content = tabs.find(e => e.id === focus)?.content;
|
||||
_content = (typeof _content === 'function' ? _content() : _content);
|
||||
_content && content.replaceChildren(..._content.filter(e => !!e));
|
||||
}
|
||||
})
|
||||
return container as HTMLDivElement & { refresh: () => void };
|
||||
}
|
||||
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
|
||||
{
|
||||
let viewport = document.getElementById('mainContainer') ?? undefined;
|
||||
let diffX, diffY;
|
||||
let minimizeRect: DOMRect, minimized = false;
|
||||
|
||||
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
|
||||
show: ['mouseenter', 'mousemove', 'focus'],
|
||||
hide: ['mouseleave', 'blur'],
|
||||
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
|
||||
|
||||
if(settings?.pinned)
|
||||
{
|
||||
events.onshow = (state) => {
|
||||
if(!settings?.events?.onshow || settings?.events?.onshow(state))
|
||||
{
|
||||
floating.show();
|
||||
pin();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const dragstart = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if(minimized)
|
||||
return;
|
||||
|
||||
window.addEventListener('mousemove', dragmove);
|
||||
window.addEventListener('mouseup', dragend);
|
||||
|
||||
const box = floating.content.getBoundingClientRect();
|
||||
diffX = e.clientX - box.x;
|
||||
diffY = e.clientY - box.y;
|
||||
};
|
||||
const resizestart = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
window.addEventListener('mousemove', resizemove);
|
||||
window.addEventListener('mouseup', resizeend);
|
||||
};
|
||||
|
||||
const dragmove = (e: MouseEvent) => {
|
||||
const box = floating.content.getBoundingClientRect();
|
||||
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||
box.x = clamp(e.clientX - diffX!, viewbox?.left ?? 0, viewbox.right - box.width);
|
||||
box.y = clamp(e.clientY - diffY!, viewbox?.top ?? 0, viewbox.bottom - box.height);
|
||||
|
||||
Object.assign(floating.content.style, {
|
||||
left: `${box.x}px`,
|
||||
top: `${box.y}px`,
|
||||
});
|
||||
};
|
||||
const resizemove = (e: MouseEvent) => {
|
||||
const box = floating.content.getBoundingClientRect();
|
||||
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||
box.width = clamp(e.clientX - box.x, 200, Math.min(750, viewbox.right - box.x));
|
||||
box.height = clamp(e.clientY - box.y, 150, Math.min(750, viewbox.bottom - box.y));
|
||||
|
||||
Object.assign(floating.content.style, {
|
||||
width: `${box.width}px`,
|
||||
height: `${box.height}px`,
|
||||
});
|
||||
};
|
||||
|
||||
const dragend = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
window.removeEventListener('mousemove', dragmove);
|
||||
window.removeEventListener('mouseup', dragend);
|
||||
};
|
||||
const resizeend = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
window.removeEventListener('mousemove', resizemove);
|
||||
window.removeEventListener('mouseup', resizeend);
|
||||
};
|
||||
|
||||
const pin = () => {
|
||||
if(floating.content.hasAttribute('data-pinned'))
|
||||
return;
|
||||
|
||||
const box = floating.content.children.item(0)!.getBoundingClientRect();
|
||||
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
|
||||
Object.assign(floating.content.style, {
|
||||
left: `${clamp(box.left, viewbox.left, viewbox.right)}px`,
|
||||
top: `${clamp(box.top, viewbox.top, viewbox.bottom)}px`,
|
||||
width: `${box.width + 21}px`,
|
||||
height: `${box.height + 21}px`,
|
||||
});
|
||||
floating.content.attributeStyleMap.delete('bottom');
|
||||
floating.content.attributeStyleMap.delete('right');
|
||||
floating.stop();
|
||||
|
||||
floating.content.addEventListener('mousedown', function() {
|
||||
if(!floating.content.hasAttribute('data-pinned'))
|
||||
return;
|
||||
|
||||
[...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1));
|
||||
this.attributeStyleMap.set('z-index', 0);
|
||||
}, { passive: true });
|
||||
}
|
||||
const minimize = () => {
|
||||
minimized = !minimized;
|
||||
floating.content.toggleAttribute('data-minimized', minimized);
|
||||
if(minimized)
|
||||
{
|
||||
minimizeRect = floating.content.getBoundingClientRect();
|
||||
Object.assign(floating.content.style, {
|
||||
width: `150px`,
|
||||
height: `21px`,
|
||||
position: 'initial',
|
||||
});
|
||||
floating.content.style.setProperty('top', null);
|
||||
floating.content.style.setProperty('left', null);
|
||||
floating.content.style.setProperty('bottom', null);
|
||||
floating.content.style.setProperty('right', null);
|
||||
|
||||
minimizeBox.appendChild(floating.content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.assign(floating.content.style, {
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}px`,
|
||||
height: `${minimizeRect.height}px`,
|
||||
});
|
||||
floating.content.style.setProperty('position', null);
|
||||
|
||||
teleport.appendChild(floating.content);
|
||||
}
|
||||
};
|
||||
|
||||
const floating = popper(container, {
|
||||
arrow: true,
|
||||
delay: settings?.pinned ? 0 : 150,
|
||||
offset: 12,
|
||||
cover: settings?.cover,
|
||||
placement: settings?.position,
|
||||
style: settings?.style,
|
||||
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
|
||||
content: () => [
|
||||
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
|
||||
dom('span', { class: 'flex-1 w-full h-5 cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
|
||||
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
|
||||
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
|
||||
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
|
||||
e.stopImmediatePropagation();
|
||||
floating.hide();
|
||||
|
||||
floating.content.toggleAttribute('data-minimized', false);
|
||||
minimized && Object.assign(floating.content.style, {
|
||||
left: `${minimizeRect.left}px`,
|
||||
top: `${minimizeRect.top}px`,
|
||||
width: `${minimizeRect.width}px`,
|
||||
height: `${minimizeRect.height}px`,
|
||||
});
|
||||
minimized = false;
|
||||
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||
div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
|
||||
],
|
||||
viewport,
|
||||
events: events
|
||||
});
|
||||
|
||||
if(settings?.pinned === false)
|
||||
floating.content.addEventListener('dblclick', pin);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
export interface ToastConfig
|
||||
{
|
||||
closeable?: boolean
|
||||
duration: number
|
||||
title?: string
|
||||
content?: string
|
||||
timer?: boolean
|
||||
type?: ToastType
|
||||
}
|
||||
type ToastDom = ToastConfig & { dom: HTMLElement };
|
||||
export type ToastType = 'info' | 'success' | 'error';
|
||||
export class Toaster
|
||||
{
|
||||
private static _MAX_DRAG = 150;
|
||||
private static _list: Array<ToastDom> = [];
|
||||
private static _container: HTMLDivElement;
|
||||
|
||||
static init()
|
||||
{
|
||||
Toaster._container = dom('div', { attributes: { id: 'toaster' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72 empty:hidden' });
|
||||
document.body.appendChild(Toaster._container);
|
||||
}
|
||||
static add(_config: ToastConfig)
|
||||
{
|
||||
let start: number;
|
||||
const dragstart = (e: MouseEvent) => {
|
||||
window.addEventListener('mousemove', dragmove);
|
||||
window.addEventListener('mouseup', dragend);
|
||||
|
||||
start = e.clientX;
|
||||
};
|
||||
const dragmove = (e: MouseEvent) => {
|
||||
const drag = e.clientX - start;
|
||||
if(drag > Toaster._MAX_DRAG)
|
||||
{
|
||||
dragend();
|
||||
config.dom.animate([{ transform: `translateX(${drag}px)` }, { transform: `translateX(150%)` }], { duration: 100, easing: 'ease-out' });
|
||||
Toaster.close(config);
|
||||
}
|
||||
else if(drag > 0)
|
||||
{
|
||||
config.dom.style.transform = `translateX(${drag}px)`;
|
||||
}
|
||||
};
|
||||
const dragend = () => {
|
||||
window.removeEventListener('mousemove', dragmove);
|
||||
window.removeEventListener('mouseup', dragend);
|
||||
|
||||
config.dom.style.transform = `translateX(0px)`;
|
||||
};
|
||||
const config = _config as ToastDom;
|
||||
const loader = config.timer ? div('bg-light-50 dark:bg-dark-50 h-full w-full transition-[width] ease-linear') : undefined;
|
||||
loader?.animate([{ width: '0' }, { width: '100%' }], { duration: config.duration, easing: 'linear' });
|
||||
config.dom = dom('div', { class: 'ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group select-none', attributes: { 'data-type': config.type, 'data-state': 'open' } }, [
|
||||
div('grid grid-cols-8 px-3 pt-2 pb-2', [
|
||||
config.title ? dom('h4', { class: 'font-semibold text-xl col-span-7 text-light-100 dark:text-dark-100', text: config.title }) : undefined,
|
||||
config.closeable ? dom('span', { class: 'translate-x-4 text-light-100 dark:text-dark-100', listeners: { click: () => Toaster.close(config), } }, [ icon('radix-icons:cross-1', { width: 12, height: 12, noobserver: true, class: 'cursor-pointer' }) ]) : undefined,
|
||||
config.content ? dom('span', { class: 'text-sm col-span-8 text-light-100 dark:text-dark-100', text: config.content }) : undefined,
|
||||
]),
|
||||
config.timer ? dom('div', { class: 'relative overflow-hidden bg-light-25 dark:bg-dark-25 h-1 mb-0 mt-0 w-full group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50' }, [ loader ]) : undefined
|
||||
]);
|
||||
config.dom.addEventListener('mousedown', dragstart);
|
||||
config.dom.animate([{ transform: 'translateX(100%)' }, { transform: 'translateX(0)' }], { duration: 150, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
|
||||
Toaster._container?.appendChild(config.dom);
|
||||
Toaster._list.push(config);
|
||||
|
||||
setTimeout(() => Toaster.close(config), config.duration);
|
||||
}
|
||||
static clear(type?: ToastType)
|
||||
{
|
||||
Toaster._list.filter(e => e.type !== type || (Toaster.close(e), false));
|
||||
}
|
||||
private static close(config: ToastDom)
|
||||
{
|
||||
config.dom.animate([
|
||||
{ opacity: 1 }, { opacity: 0 },
|
||||
], { easing: 'ease-in', duration: 100 }).onfinish = () => config.dom.remove();
|
||||
Toaster._list = Toaster._list.filter(e => e !== config);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { safeDestr as parse } from 'destr';
|
||||
import { Canvas, CanvasEditor } from "#shared/canvas.util";
|
||||
import render from "#shared/markdown.util";
|
||||
import { confirm, contextmenu, tooltip } from "#shared/floating.util";
|
||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
|
||||
import { async, loading } from "#shared/components.util";
|
||||
import { Canvas, CanvasEditor } from "#shared/canvas";
|
||||
import render, { renderMDAsText } from "#shared/markdown";
|
||||
import { confirm, contextmenu, tooltip } from "#shared/floating";
|
||||
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom";
|
||||
import { loading } from "#shared/components";
|
||||
import prose, { h1, h2 } from "#shared/proses";
|
||||
import { getID, parsePath } from '#shared/general.util';
|
||||
import { getID, lerp, parsePath } from '~~/shared/general';
|
||||
import { TreeDOM, type Recursive } from '#shared/tree';
|
||||
import { History } from '#shared/history.util';
|
||||
import { MarkdownEditor } from '#shared/editor.util';
|
||||
import { History } from '~~/shared/history';
|
||||
import { MarkdownEditor } from '~~/shared/editor';
|
||||
import type { CanvasContent } from '~/types/canvas';
|
||||
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
@@ -16,6 +16,9 @@ import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/
|
||||
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import type { CleanupFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types';
|
||||
import type { CharacterConfig, FeatureID, MainStat, TrainingLevel } from '~/types/character';
|
||||
import { getText } from './i18n';
|
||||
import { mainStatTexts } from './character';
|
||||
|
||||
export type FileType = keyof ContentMap;
|
||||
export interface ContentMap
|
||||
@@ -138,13 +141,13 @@ export class Content
|
||||
const overview = await Content.read('overview', { create: true });
|
||||
try
|
||||
{
|
||||
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
|
||||
Content._overview = reactive(parse<Record<string, Omit<LocalContent, 'content'>>>(overview));
|
||||
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => { p[v.path] = v.id; return p; }, {} as Record<string, string>);
|
||||
await Content.pull();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
Content._overview = {};
|
||||
Content._overview = reactive({});
|
||||
await Content.pull(true);
|
||||
}
|
||||
|
||||
@@ -175,12 +178,36 @@ export class Content
|
||||
}
|
||||
static async getContent(id: string): Promise<LocalContent | undefined>
|
||||
{
|
||||
await Content.ready;
|
||||
const overview = Content._overview[id];
|
||||
|
||||
if(!overview)
|
||||
return;
|
||||
|
||||
return { ...overview, content: Content.fromString(overview, (await Content.read(id, { create: true }))!) };
|
||||
let content = await Content.read(id);
|
||||
if(content === undefined)
|
||||
{
|
||||
try
|
||||
{
|
||||
const apiContent = await useRequestFetch()(`/api/file/content/${id}`, { cache: 'no-cache' });
|
||||
if(apiContent)
|
||||
{
|
||||
content = apiContent;
|
||||
if(overview.type !== 'folder')
|
||||
Content.queue.queue(() => Content.write(id, content!, { create: true }));
|
||||
}
|
||||
else
|
||||
{
|
||||
overview.error = true;
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
overview.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...overview, content: Content.fromString(overview, content!) };
|
||||
}
|
||||
static set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>)
|
||||
{
|
||||
@@ -247,20 +274,6 @@ export class Content
|
||||
{
|
||||
Content._overview[file.id] = file;
|
||||
Content._reverseMapping[file.path] = file.id;
|
||||
|
||||
Content.queue.queue(() => {
|
||||
return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined | null) => {
|
||||
if(content)
|
||||
{
|
||||
if(file.type !== 'folder')
|
||||
Content.queue.queue(() => Content.write(file.id, content, { create: true }));
|
||||
}
|
||||
else
|
||||
Content._overview[file.id]!.error = true;
|
||||
}).catch(e => {
|
||||
Content._overview[file.id]!.error = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deletable.splice(deletable.findIndex(e => e === file.id), 1);
|
||||
@@ -280,11 +293,12 @@ export class Content
|
||||
}
|
||||
static async push()
|
||||
{
|
||||
const blocked = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
|
||||
const requested = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
|
||||
|
||||
for(const [id, value] of Object.entries(Content._overview).filter(e => !blocked.includes(e[0])))
|
||||
for(let id of requested)
|
||||
{
|
||||
if(value.type === 'folder')
|
||||
const value = Content.get(id);
|
||||
if(!value || value.type === 'folder')
|
||||
continue;
|
||||
|
||||
Content.queue.queue(() => Content.read(id).then(e => {
|
||||
@@ -578,12 +592,12 @@ export const iconByType: Record<FileType, string> = {
|
||||
export class Editor
|
||||
{
|
||||
tree!: TreeDOM;
|
||||
container: HTMLDivElement;
|
||||
container: HTMLElement;
|
||||
|
||||
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
|
||||
|
||||
private instruction: HTMLDivElement;
|
||||
private cleanup: CleanupFn;
|
||||
private instruction: HTMLElement;
|
||||
private cleanup?: CleanupFn;
|
||||
|
||||
private history: History;
|
||||
|
||||
@@ -647,6 +661,7 @@ export class Editor
|
||||
this.tree.update();
|
||||
},
|
||||
redo: (action) => {
|
||||
|
||||
this.tree.tree.remove(action.element.id);
|
||||
if(this.selected === action.element) this.select();
|
||||
action.element.cleanup();
|
||||
@@ -699,14 +714,14 @@ export class Editor
|
||||
|
||||
Content.ready.then(() => {
|
||||
this.tree = new TreeDOM((item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-hidden relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
|
||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
|
||||
])]);
|
||||
}, (item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-hidden relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
|
||||
@@ -728,20 +743,20 @@ export class Editor
|
||||
e.preventDefault();
|
||||
|
||||
const { close } = contextmenu(e.clientX, e.clientY, [
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 hover:dark:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 hover:dark:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 hover:dark:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
], { placement: 'right-start', offset: 8 });
|
||||
}
|
||||
private add(type: FileType, nextTo: Recursive<LocalContent>)
|
||||
{
|
||||
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
|
||||
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
|
||||
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
|
||||
this.history.add('overview', [{ element: item, from: undefined, to: nextTo.order + 1, event: 'add' }]);
|
||||
}
|
||||
private remove(item: LocalContent & { element?: HTMLElement })
|
||||
{
|
||||
this.history.add('overview', 'remove', [{ element: item, from: item.order, to: undefined }], true);
|
||||
this.history.add('overview', [{ element: item, from: item.order, to: undefined, event: 'remove' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
}
|
||||
private rename(item: LocalContent & { element?: HTMLElement })
|
||||
{
|
||||
@@ -756,7 +771,7 @@ export class Editor
|
||||
|
||||
input.parentElement?.replaceChild(text!, input);
|
||||
input.remove();
|
||||
if(value !== item.title) this.history.add('overview', 'rename', [{ element: item, from: item.title, to: value }], true);
|
||||
if(value !== item.title) this.history.add('overview', [{ element: item, from: item.title, to: value, event: 'rename' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
}
|
||||
}
|
||||
const text = item.element!.children[0]?.children[1];
|
||||
@@ -769,13 +784,13 @@ export class Editor
|
||||
{
|
||||
cancelPropagation(e);
|
||||
|
||||
this.history.add('overview', 'navigable', [{ element: item, from: item.navigable, to: !item.navigable }], true);
|
||||
this.history.add('overview', [{ element: item, from: item.navigable, to: !item.navigable, event: 'navigable' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
}
|
||||
private togglePrivate(e: Event, item: LocalContent & { element?: HTMLElement })
|
||||
{
|
||||
cancelPropagation(e);
|
||||
|
||||
this.history.add('overview', 'private', [{ element: item, from: item.private, to: !item.private }], true);
|
||||
this.history.add('overview', [{ element: item, from: item.private, to: !item.private, event: 'private' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
}
|
||||
private setupDnD(): CleanupFn
|
||||
{
|
||||
@@ -880,13 +895,13 @@ export class Editor
|
||||
return;
|
||||
|
||||
if (instruction.type === 'reorder-above')
|
||||
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order }}], true);
|
||||
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
|
||||
if (instruction.type === 'reorder-below')
|
||||
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order + 1 }}], true);
|
||||
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order + 1 }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
|
||||
if (instruction.type === 'make-child' && targetItem.type === 'folder')
|
||||
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: targetItem, order: 0 }}], true);
|
||||
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: targetItem, order: 0 }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
|
||||
}
|
||||
private render<T extends FileType>(item: LocalContent<T>): Node
|
||||
{
|
||||
@@ -917,7 +932,15 @@ export class Editor
|
||||
}
|
||||
unmount()
|
||||
{
|
||||
this.cleanup();
|
||||
this.cleanup && this.cleanup();
|
||||
}
|
||||
undo()
|
||||
{
|
||||
this.history.undo();
|
||||
}
|
||||
redo()
|
||||
{
|
||||
this.history.redo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,4 +954,51 @@ export function getPath(item: any): string
|
||||
return parsePath(item.title);
|
||||
else
|
||||
return parsePath(item.title) ?? item.path;
|
||||
}
|
||||
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
const config = characterConfig as CharacterConfig;
|
||||
export function buildSpellMD()
|
||||
{
|
||||
const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"];
|
||||
const SPELL_TYPE_TEXTS = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
const SPELL_ELEMENTS_TEXTS = { 'fire': 'feu', 'ice': 'glace', 'thunder': 'foudre', 'earth': 'terre', 'arcana': 'arcane', 'air': 'air', 'nature': 'nature', 'light': 'lumiere', 'psyche': 'psy' };
|
||||
const SPELL_SPEED_TEXTS = { 'action': 'action', 'reaction': 'réaction', 'number': '%1 minute(s)' };
|
||||
|
||||
return Object.values(config.spells).sort((a, b) => a.rank - b.rank || SPELL_ELEMENTS.indexOf(a.type) - SPELL_ELEMENTS.indexOf(b.type)).map(e => `- ${e.name} ${e.elements.map(el => '#' + SPELL_ELEMENTS_TEXTS[el]).join(' + ')} ${SPELL_TYPE_TEXTS[e.type]} (${e.cost} mana, ${typeof e.speed === 'number' ? SPELL_SPEED_TEXTS['number'].replace('%1', e.speed.toString()).replace('(s)', e.speed === 1 ? '' : 's') : SPELL_SPEED_TEXTS[e.speed]}${e.concentration ? ', [[1. Magie#La concentration|concentration]]' : ''}, ${e.range === 'personnal' ? 'Personnel' : e.range === 0 ? 'Toucher' : e.range + ' cases'})\nTags: ${e.tags?.join(', ') ?? '-'}\n\t${e.description.endsWith('.') ? e.description : e.description + '.'}`).join('\n\n');
|
||||
}
|
||||
export function buildTrainingTree()
|
||||
{
|
||||
const getLocalID = () => { for (var t = [], n = 0; n < 16; n++) t.push((16 * Math.random() | 0).toString(16)); return t.join(""); }, colors = ["4", "1", "6", "6"], WIDTH = 448, HEIGHT = (lines: number) => lines === 0 ? 64 : (40 + (24 * lines)), PADDING_X = 48, PADDING_Y = 80;
|
||||
const _tree = {nodes: [], edges: [] } as any;
|
||||
let _maxX = 0,_minY = 0,_maxY = 0;
|
||||
const tree = (branch: Record<TrainingLevel, FeatureID[]>, name: string, color: string) => {
|
||||
let previousLevel = {} as any, maxAmount = Object.values(branch).reduce((p, v) => Math.max(p, v.length), 0), minX = _maxX + PADDING_X, maxX = minX + (PADDING_X + WIDTH) * maxAmount + PADDING_X, minY = _minY, maxY = _minY;
|
||||
Object.entries(branch).forEach(([i, e]: [string, FeatureID[]]) => {
|
||||
const nodes = e.map((_e, _i) => ({ type: 'text', id: getLocalID(), text: getText(config.features[_e]?.description), color: colors[_i] ?? undefined, x: lerp(minX, maxX - PADDING_X - PADDING_X - WIDTH, (_i + ((maxAmount - e.length) / 2)) / (maxAmount - 1)), y: maxY, width: WIDTH, height: HEIGHT(Math.ceil(renderMDAsText(getText(config.features[_e]?.description)).length / 52)) }));
|
||||
maxY += nodes.map(e => e.height).reduce((p, v) => Math.max(p, v), 0) + PADDING_Y;
|
||||
_tree.nodes.push(...nodes);
|
||||
|
||||
nodes.forEach((_e, _i) => { if(previousLevel[_i]) _tree.edges.push({id: getLocalID(), fromNode: previousLevel[_i].id, fromSide: "bottom", toNode: _e.id, toSide: "top", color: colors[_i] ?? undefined }); else if(previousLevel[0]) _tree.edges.push({id: getLocalID(), fromNode: previousLevel[0].id, fromSide: "bottom", toNode: _e.id, toSide: "top", color: colors[_i] ?? undefined }); });
|
||||
previousLevel = nodes;
|
||||
});
|
||||
_tree.nodes.push({ type: 'group', label: name, x: minX - PADDING_X, y: minY - PADDING_Y, width: maxX - minX, height: maxY - minY + PADDING_Y, color: color })
|
||||
_maxX = Math.max(_maxX, maxX);
|
||||
_maxY = Math.max(_maxY, maxY + PADDING_Y);
|
||||
};
|
||||
tree(config.training.strength, 'Force', '1');
|
||||
tree(config.training.dexterity, 'Dextérité', '4');
|
||||
tree(config.training.constitution, 'Constitution', '2');
|
||||
tree(config.training.intelligence, 'Intelligence', '5');
|
||||
tree(config.training.curiosity, 'Curiosité', '3');
|
||||
tree(config.training.charisma, 'Charisme', '#fe39ee');
|
||||
tree(config.training.psyche, 'Psyché', '6');
|
||||
|
||||
return _tree;
|
||||
}
|
||||
export function buildTrainingFile()
|
||||
{
|
||||
return Object.entries(config.training).map(e => {
|
||||
return `# ${mainStatTexts[e[0] as MainStat]}\n` + Object.entries(e[1]).map(_e => `## Niveau ${_e[0]}\n` + _e[1].map(feature => renderMDAsText(getText(config.features[feature]!.description))).join('\nou\n')).join('\n');
|
||||
}).join('\n');
|
||||
}
|
||||
244
shared/dice.ts
Normal file
244
shared/dice.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { MainStat } from "~/types/character";
|
||||
import { MAIN_STATS, mainStatShortTexts } from "./character";
|
||||
import type { Proxy } from "./reactive";
|
||||
|
||||
//[Quantité de dés, Nb de face du dé (1 = pas besoin de roll de dé), explosif]
|
||||
export type Dice = [number, number | MainStat] | [number, number | MainStat, true];
|
||||
export type DiceRoll = Dice[];
|
||||
|
||||
enum State {
|
||||
START,
|
||||
QUANTITY,
|
||||
D_LETTER,
|
||||
FACES_NUMBER,
|
||||
FACES_STAT,
|
||||
MODIFIER_SIGN,
|
||||
MODIFIER,
|
||||
EXPLODING,
|
||||
END_DICE,
|
||||
};
|
||||
|
||||
export function parseDice(formula: string): DiceRoll
|
||||
{
|
||||
const result: DiceRoll = [];
|
||||
let state = State.START;
|
||||
|
||||
let i = 0, length = formula.length;
|
||||
|
||||
//Variables utilitaires de la State machine
|
||||
let buffer = "";
|
||||
let isExploding = false, isNegative = false, currentQty = 1, currentFaces: number | MainStat = 1;
|
||||
|
||||
const reset = () => {
|
||||
if (state !== State.START && state !== State.END_DICE)
|
||||
{
|
||||
const dice: Dice = isExploding ? [currentQty, currentFaces, true] : [currentQty, currentFaces];
|
||||
|
||||
if (isNegative)
|
||||
{
|
||||
dice[0] = -dice[0];
|
||||
isNegative = false;
|
||||
}
|
||||
|
||||
result.push(dice);
|
||||
}
|
||||
|
||||
currentQty = 1;
|
||||
currentFaces = 1;
|
||||
isExploding = false;
|
||||
buffer = "";
|
||||
};
|
||||
|
||||
while(i <= length)
|
||||
{
|
||||
const char = i < length ? formula[i]?.toLowerCase() ?? '\0' : '\0';
|
||||
const isDigit = char >= '0' && char <= '9';
|
||||
const isLetter = char >= 'a' && char <= 'z';
|
||||
const isEnd = i === length;
|
||||
|
||||
switch (state) {
|
||||
case State.START:
|
||||
if (isDigit)
|
||||
{
|
||||
buffer = char;
|
||||
state = State.QUANTITY;
|
||||
}
|
||||
else if (char === 'd')
|
||||
{
|
||||
currentQty = 1;
|
||||
state = State.D_LETTER;
|
||||
}
|
||||
else if (isLetter)
|
||||
{
|
||||
buffer = char;
|
||||
state = State.FACES_STAT;
|
||||
}
|
||||
else if (char === '+' || char === '-')
|
||||
{
|
||||
isNegative = char === '-';
|
||||
state = State.MODIFIER_SIGN;
|
||||
}
|
||||
else if (!isEnd && char !== ' ' && char !== '\t')
|
||||
throw new Error(`Caractère inattendu '${char}' à la position ${i}`);
|
||||
|
||||
break;
|
||||
|
||||
case State.QUANTITY:
|
||||
if (isDigit)
|
||||
buffer += char;
|
||||
else if (char === 'd')
|
||||
{
|
||||
currentQty = parseInt(buffer, 10) || 1;
|
||||
buffer = "";
|
||||
state = State.D_LETTER;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentQty = parseInt(buffer, 10);
|
||||
currentFaces = 1;
|
||||
reset();
|
||||
state = State.END_DICE;
|
||||
i--;
|
||||
}
|
||||
break;
|
||||
|
||||
case State.D_LETTER:
|
||||
if (isDigit)
|
||||
{
|
||||
buffer = char;
|
||||
state = State.FACES_NUMBER;
|
||||
}
|
||||
else if (isLetter)
|
||||
{
|
||||
buffer = 'd' + char;
|
||||
state = State.FACES_STAT;
|
||||
}
|
||||
else
|
||||
throw new Error(`Attendu nombre ou stat après 'd' à la position ${i}`);
|
||||
|
||||
break;
|
||||
|
||||
case State.FACES_NUMBER:
|
||||
if (isDigit)
|
||||
buffer += char;
|
||||
else if (char === '!')
|
||||
{
|
||||
currentFaces = parseInt(buffer, 10);
|
||||
state = State.EXPLODING;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentFaces = parseInt(buffer, 10);
|
||||
reset();
|
||||
state = State.END_DICE;
|
||||
i--;
|
||||
}
|
||||
break;
|
||||
|
||||
case State.FACES_STAT:
|
||||
if (isLetter && buffer.length < 3)
|
||||
buffer += char;
|
||||
else if (char === '!')
|
||||
{
|
||||
const stat = validStat(buffer);
|
||||
if (!stat)
|
||||
throw new Error(`Stat invalide '${buffer}' à la position ${i - buffer.length}`);
|
||||
|
||||
currentFaces = stat;
|
||||
state = State.EXPLODING;
|
||||
}
|
||||
else
|
||||
{
|
||||
const stat = validStat(buffer);
|
||||
if (!stat)
|
||||
throw new Error(`Stat invalide '${buffer}' à la position ${i - buffer.length}`);
|
||||
|
||||
currentFaces = stat;
|
||||
reset();
|
||||
state = State.END_DICE;
|
||||
i--;
|
||||
}
|
||||
break;
|
||||
|
||||
case State.EXPLODING:
|
||||
isExploding = true;
|
||||
reset();
|
||||
state = State.END_DICE;
|
||||
i--;
|
||||
break;
|
||||
|
||||
case State.MODIFIER_SIGN:
|
||||
if (isDigit)
|
||||
{
|
||||
buffer = char;
|
||||
state = State.MODIFIER;
|
||||
}
|
||||
else if (char === 'd')
|
||||
{
|
||||
currentQty = 1;
|
||||
state = State.D_LETTER;
|
||||
}
|
||||
else if (isLetter)
|
||||
{
|
||||
buffer = char;
|
||||
state = State.FACES_STAT;
|
||||
}
|
||||
else
|
||||
throw new Error(`Attendu nombre après '${isNegative ? "-" : "+"}' à la position ${i}`);
|
||||
break;
|
||||
|
||||
case State.MODIFIER:
|
||||
if (isDigit)
|
||||
buffer += char;
|
||||
else
|
||||
{
|
||||
currentQty = parseInt(buffer, 10);
|
||||
currentFaces = 1;
|
||||
if (isNegative) currentQty = -currentQty;
|
||||
reset();
|
||||
isNegative = false;
|
||||
state = State.END_DICE;
|
||||
i--;
|
||||
}
|
||||
break;
|
||||
|
||||
case State.END_DICE:
|
||||
if (char === '+' || char === '-')
|
||||
{
|
||||
isNegative = char === '-';
|
||||
state = State.MODIFIER_SIGN;
|
||||
}
|
||||
else if (char === 'd')
|
||||
{
|
||||
currentQty = 1;
|
||||
state = State.D_LETTER;
|
||||
}
|
||||
else if (!isEnd && char !== ' ' && char !== '\t')
|
||||
throw new Error(`Opérateur attendu, trouvé '${char}' à la position ${i}`);
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export function stringifyRoll(dices: DiceRoll, modifiers?: Proxy<Partial<Record<MainStat, number>>>, explicit?: boolean): string
|
||||
{
|
||||
const map = new Map<string, number>();
|
||||
dices.forEach(e => {
|
||||
if(e[0] === 0 || e[1] === 0) return;
|
||||
const target = typeof e[1] === 'string' ? modifiers && e[1] in modifiers ? modifiers[e[1]]! * e[0] : `@${e[1]}` : e[0]; //@ character used to sort the stat dices at the end
|
||||
const key = typeof e[1] === 'string' ? typeof target === 'string' ? e[1] : '1' : e[2] ? e[1].toString(10) + '!' : e[1].toString(10), value = map.get(key);
|
||||
if(typeof target === 'string') map.has(target) ? map.set(target, 1) : map.set(target, map.get(target)! + 1);
|
||||
else value === undefined ? map.set(key, target) : map.set(key, value + target);
|
||||
});
|
||||
|
||||
const stringify = ([face, amount]: [string, number]) => (face.startsWith('@') && MAIN_STATS.includes(face.substring(1) as MainStat)) || face === '1' ? `${amount < 0 ? '-' : '+'}${face.startsWith('@') && MAIN_STATS.includes(face.substring(1) as MainStat) ? mainStatShortTexts[face.substring(1) as MainStat]! : amount}` : `${amount < 0 ? '-' : '+'}${explicit ?? false ? amount : amount !== 1 ? amount : ''}d${face}`;
|
||||
const text = [...map.entries()].sort((a, b) => b[0].localeCompare(a[0])).reduce((p, v) => p + stringify(v), '');
|
||||
return text.startsWith('+') ? text.substring(1) : text;
|
||||
}
|
||||
function validStat(text: string): MainStat | undefined
|
||||
{
|
||||
return Object.entries(mainStatShortTexts).find(e => e[1].toLowerCase() === text.toLowerCase())?.at(0) as MainStat | undefined;
|
||||
}
|
||||
276
shared/dom.ts
Normal file
276
shared/dom.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'iconify-icon';
|
||||
import { loading } from './components';
|
||||
import { _defer, raw, reactivity, type Reactive } from './reactive';
|
||||
|
||||
export type Node = HTMLElement | SVGElement | Text | undefined;
|
||||
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
||||
|
||||
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
|
||||
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
|
||||
options?: boolean | AddEventListenerOptions;
|
||||
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
|
||||
} | undefined;
|
||||
|
||||
export interface DOMList<T> extends Array<T>{
|
||||
render(redraw?: boolean): void;
|
||||
};
|
||||
|
||||
export interface NodeProperties
|
||||
{
|
||||
attributes?: Record<string, Reactive<string | undefined | boolean | number>>;
|
||||
text?: Reactive<string | number | Text>;
|
||||
class?: Reactive<Class>;
|
||||
style?: Reactive<Record<string, string | undefined | boolean | number> | string>;
|
||||
listeners?: {
|
||||
[K in keyof HTMLElementEventMap]?: Listener<K>
|
||||
};
|
||||
}
|
||||
|
||||
function append(dom: Element, children: Node | Node[] | undefined)
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
children.forEach(e => e && dom.appendChild(e));
|
||||
}
|
||||
else
|
||||
{
|
||||
children && dom.appendChild(children);
|
||||
}
|
||||
}
|
||||
|
||||
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T];
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
{
|
||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
const _cache = new Map<U, Node | Node[] | undefined>();
|
||||
const seen = new Set<U>(); // Cache pruning utility
|
||||
|
||||
if(children)
|
||||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
reactivity(children, (_children) => {
|
||||
element.replaceChildren();
|
||||
for(const c of _children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(children.list !== undefined)
|
||||
{
|
||||
let fallback: Node | Node[] | undefined;
|
||||
children.fallback && reactivity(children.fallback, (_fallback) => {
|
||||
if(_fallback)
|
||||
{
|
||||
if(Array.isArray(fallback) && fallback.filter(e => !!e)[0] && fallback.filter(e => !!e)[0]!.parentElement === element)
|
||||
element.replaceChildren(), append(element, _fallback);
|
||||
else if(!Array.isArray(fallback) && fallback?.parentElement === element)
|
||||
element.replaceChildren(), append(element, _fallback);
|
||||
}
|
||||
fallback = _fallback;
|
||||
});
|
||||
reactivity(children.list, (list) => {
|
||||
element.replaceChildren();
|
||||
if(list.length === 0)
|
||||
{
|
||||
fallback ??= children?.fallback ? children.fallback() : undefined;
|
||||
append(element, fallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
list.forEach(e => {
|
||||
seen.add(e);
|
||||
const child = raw(children.render(e, _cache.get(e)));
|
||||
_cache.set(e, child);
|
||||
append(element, child);
|
||||
});
|
||||
|
||||
for (const key of _cache.keys()) if (!seen.has(key)) _cache.delete(key);
|
||||
seen.clear();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.attributes)
|
||||
{
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
{
|
||||
reactivity(properties.attributes[k], (attribute) => {
|
||||
if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10));
|
||||
else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.text)
|
||||
{
|
||||
reactivity(properties.text, (text) => {
|
||||
if(typeof text === 'string')
|
||||
element.textContent = text;
|
||||
else if(typeof text === 'number')
|
||||
element.textContent = text.toString();
|
||||
else
|
||||
element.appendChild(text as Text);
|
||||
})
|
||||
}
|
||||
|
||||
if(properties?.listeners)
|
||||
{
|
||||
for(let [k, v] of Object.entries(properties.listeners))
|
||||
{
|
||||
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
|
||||
if(typeof value === 'function')
|
||||
element.addEventListener(key, value.bind(element));
|
||||
else if(value)
|
||||
element.addEventListener(key, value.listener.bind(element), value.options);
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.class)
|
||||
{
|
||||
reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes)));
|
||||
}
|
||||
|
||||
if(properties?.style)
|
||||
{
|
||||
reactivity(properties.style, (style) => {
|
||||
if(typeof style === 'string') element.setAttribute('style', style);
|
||||
else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v);
|
||||
})
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
export function div(cls?: Reactive<Class>, children?: NodeChildren): HTMLElementTagNameMap['div']
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
|
||||
{
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span']
|
||||
{
|
||||
return dom("span", { class: cls, text: text });
|
||||
}
|
||||
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Array<Reactive<SVGElement>>): SVGElementTagNameMap[K]
|
||||
{
|
||||
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
||||
|
||||
if(children)
|
||||
{
|
||||
for(const c of children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.attributes)
|
||||
{
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
{
|
||||
reactivity(properties.attributes[k], (attribute) => {
|
||||
if(typeof attribute === 'string' || typeof attribute === 'number') element.setAttribute(k, attribute.toString(10));
|
||||
else if(typeof attribute === 'boolean') element.toggleAttribute(k, attribute);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(properties?.text)
|
||||
{
|
||||
reactivity(properties.text, (text) => {
|
||||
if(typeof text === 'string')
|
||||
element.textContent = text;
|
||||
else if(typeof text === 'number')
|
||||
element.textContent = text.toString();
|
||||
else
|
||||
element.appendChild(text as Text);
|
||||
})
|
||||
}
|
||||
|
||||
if(properties?.class)
|
||||
{
|
||||
reactivity(properties?.class, (classes) => element.setAttribute('class', mergeClasses(classes)));
|
||||
}
|
||||
|
||||
if(properties?.style)
|
||||
{
|
||||
reactivity(properties.style, (style) => {
|
||||
if(typeof style === 'string') element.setAttribute('style', style);
|
||||
else for(const [k, v] of Object.entries(style)) if(v !== undefined && v !== false) element.attributeStyleMap.set(k, v);
|
||||
})
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
export function text(data: Reactive<string | number>): Text
|
||||
{
|
||||
const text = document.createTextNode('');
|
||||
reactivity(data, (txt) => text.textContent = txt.toString());
|
||||
return text;
|
||||
}
|
||||
|
||||
export interface IconProperties
|
||||
{
|
||||
mode?: 'svg' | 'style' | 'bg' | 'mask';
|
||||
inline?: boolean;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
hFlip?: boolean;
|
||||
vFlip?: boolean;
|
||||
rotate?: number;
|
||||
style?: Reactive<Record<string, string | undefined> | string>;
|
||||
class?: Class;
|
||||
}
|
||||
const iconLoadingRegistry: Map<string, Promise<Required<IconifyIcon>> | null | undefined> = new Map();
|
||||
export function icon(name: Reactive<string>, properties?: IconProperties)
|
||||
{
|
||||
const element = dom('div', { class: properties?.class, style: properties?.style });
|
||||
let timeout: NodeJS.Timeout = setTimeout(() => {}, 0), target: string;
|
||||
|
||||
const build = (icon: IconifyIcon | null | undefined) => {
|
||||
if(!icon) return clearTimeout(timeout) ?? element.replaceChildren();
|
||||
const built = buildIcon(icon, properties);
|
||||
const dom = svg('svg', { attributes: built.attributes });
|
||||
dom.innerHTML = built.body;
|
||||
clearTimeout(timeout);
|
||||
element.replaceChildren(dom);
|
||||
}
|
||||
reactivity(name, (name) => {
|
||||
target = name;
|
||||
if(!iconLoaded(name))
|
||||
{
|
||||
timeout = setTimeout(() => { element.replaceChildren(loading('small')); }, 100);
|
||||
if(!iconLoadingRegistry.has(name)) iconLoadingRegistry.set(name, loadIcon(name));
|
||||
iconLoadingRegistry.get(name)?.then((icon) => target === name && build(icon));
|
||||
}
|
||||
else build(getIcon(name));
|
||||
})
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function mergeClasses(classes: Class): string
|
||||
{
|
||||
if(typeof classes === 'string')
|
||||
{
|
||||
return classes.trim();
|
||||
}
|
||||
else if(Array.isArray(classes))
|
||||
{
|
||||
return classes.map(e => mergeClasses(e)).join(' ');
|
||||
}
|
||||
else if(classes)
|
||||
{
|
||||
return Object.entries(classes).filter(e => e[1]).map(e => e[0].trim()).join(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user