Remove unused components, change zod to v4 and cahnge a few character properties
This commit is contained in:
parent
5387dc66c3
commit
80a94bee86
|
|
@ -1,932 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { bezier, getBbox, opposite, posFromDir, rotation, type Box, type Direction, type Path, type Position } from '#shared/canvas.util';
|
|
||||||
import type CanvasNodeEditor from './canvas/CanvasNodeEditor.vue';
|
|
||||||
import type CanvasEdgeEditor from './canvas/CanvasEdgeEditor.vue';
|
|
||||||
import { SnapFinder, type SnapHint } from '#shared/physics.util';
|
|
||||||
import type { CanvasPreferences } from '~/types/general';
|
|
||||||
export type Element = { type: 'node' | 'edge', id: string };
|
|
||||||
|
|
||||||
interface ActionMap {
|
|
||||||
remove: CanvasNode | CanvasEdge | undefined;
|
|
||||||
create: CanvasNode | CanvasEdge | undefined;
|
|
||||||
property: CanvasNode | CanvasEdge;
|
|
||||||
}
|
|
||||||
type Action = keyof ActionMap;
|
|
||||||
interface HistoryEvent<T extends Action = Action>
|
|
||||||
{
|
|
||||||
event: T;
|
|
||||||
actions: HistoryAction<T>[];
|
|
||||||
}
|
|
||||||
interface HistoryAction<T extends Action>
|
|
||||||
{
|
|
||||||
element: Element;
|
|
||||||
from: ActionMap[T];
|
|
||||||
to: ActionMap[T];
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeEditor = InstanceType<typeof CanvasNodeEditor>;
|
|
||||||
type EdgeEditor = InstanceType<typeof CanvasEdgeEditor>;
|
|
||||||
|
|
||||||
const cancelEvent = (e: Event) => e.preventDefault();
|
|
||||||
const stopPropagation = (e: Event) => e.stopImmediatePropagation();
|
|
||||||
function center(touches: TouchList): Position
|
|
||||||
{
|
|
||||||
const pos = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
for(const touch of touches)
|
|
||||||
{
|
|
||||||
pos.x += touch.clientX;
|
|
||||||
pos.y += touch.clientY;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos.x /= touches.length;
|
|
||||||
pos.y /= touches.length;
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
function distance(touches: TouchList): number
|
|
||||||
{
|
|
||||||
const [A, B] = touches;
|
|
||||||
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import { clamp, getID, ID_SIZE } from '#shared/general.util';
|
|
||||||
import type { CanvasContent, CanvasEdge, CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
const canvas = defineModel<CanvasContent>({ required: true });
|
|
||||||
const props = defineProps<{
|
|
||||||
path: string,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5), spacing = ref<number | undefined>(32);
|
|
||||||
const focusing = ref<Element>(), editing = ref<Element>();
|
|
||||||
const canvasRef = useTemplateRef('canvasRef'), transformRef = useTemplateRef('transformRef'), patternRef = useTemplateRef('patternRef'), toolbarRef = useTemplateRef('toolbarRef'), viewportSize = useElementBounding(canvasRef);
|
|
||||||
const nodes = useTemplateRef<NodeEditor[]>('nodes'), edges = useTemplateRef<EdgeEditor[]>('edges');
|
|
||||||
const canvasSettings = useCookie<CanvasPreferences>('canvasPreference', { default: () => ({ gridSnap: true, neighborSnap: true, spacing: 32 }) });
|
|
||||||
const hints = ref<SnapHint[]>([]);
|
|
||||||
const viewport = computed<Box>(() => {
|
|
||||||
const width = viewportSize.width.value / zoom.value, height = viewportSize.height.value / zoom.value;
|
|
||||||
const movementX = viewportSize.width.value - width, movementY = viewportSize.height.value - height;
|
|
||||||
return { x: -dispX.value + movementX / 2, y: -dispY.value + movementY / 2, width, height };
|
|
||||||
});
|
|
||||||
const updateScaleVar = useDebounceFn(() => {
|
|
||||||
if(transformRef.value)
|
|
||||||
{
|
|
||||||
transformRef.value.style.setProperty('--tw-scale', zoom.value.toString());
|
|
||||||
}
|
|
||||||
if(canvasRef.value)
|
|
||||||
{
|
|
||||||
canvasRef.value.style.setProperty('--zoom-multiplier', (1 / Math.pow(zoom.value, 0.7)).toFixed(3));
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
type DragOrigin = { type: 'edge', id: string, destination: 'from' | 'to', node: string } | { type: 'node', id: string };
|
|
||||||
const fakeEdge = ref<{ from?: Position, fromSide?: Direction, to?: Position, toSide?: Direction, path?: Path, style?: { stroke: string, fill: string }, hex?: string, drag?: DragOrigin, snapped?: { node: string, side: Direction } }>({});
|
|
||||||
|
|
||||||
const focused = computed(() => focusing.value ? focusing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === focusing.value!.id) : edges.value?.find(e => !!e && e.id === focusing.value!.id) : undefined), edited = computed(() => editing.value ? editing.value?.type === 'node' ? nodes.value?.find(e => !!e && e.id === editing.value!.id) : edges.value?.find(e => !!e && e.id === editing.value!.id) : undefined);
|
|
||||||
let snapFinder: SnapFinder;
|
|
||||||
|
|
||||||
const history = ref<HistoryEvent[]>([]);
|
|
||||||
const historyPos = ref(-1);
|
|
||||||
const historyCursor = computed(() => history.value.length > 0 && historyPos.value > -1 ? history.value[historyPos.value] : undefined);
|
|
||||||
|
|
||||||
watch(props, () => {
|
|
||||||
snapFinder = new SnapFinder(hints, viewport, { gridSize: 512, preferences: canvasSettings.value, threshold: 16, cellSize: 64 });
|
|
||||||
|
|
||||||
canvas.value.nodes?.forEach((e) => {
|
|
||||||
snapFinder.update(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
focusing.value = undefined;
|
|
||||||
editing.value = undefined;
|
|
||||||
|
|
||||||
dispX.value = 0;
|
|
||||||
dispY.value = 0;
|
|
||||||
zoom.value = 0.5;
|
|
||||||
|
|
||||||
history.value = [];
|
|
||||||
historyPos.value = -1;
|
|
||||||
fakeEdge.value = {};
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
watch(canvas, () => {
|
|
||||||
updateToolbarTransform();
|
|
||||||
}, { immediate: true, deep: true, });
|
|
||||||
|
|
||||||
const reset = (_: MouseEvent) => {
|
|
||||||
zoom.value = minZoom.value;
|
|
||||||
|
|
||||||
dispX.value = 0;
|
|
||||||
dispY.value = 0;
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAction<T extends Action = Action>(event: T, actions: HistoryAction<T>[])
|
|
||||||
{
|
|
||||||
historyPos.value++;
|
|
||||||
history.value.splice(historyPos.value, history.value.length - historyPos.value);
|
|
||||||
history.value[historyPos.value] = { event, actions };
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
|
||||||
let lastX = 0, lastY = 0, lastDistance = 0;
|
|
||||||
const dragMove = (e: MouseEvent) => {
|
|
||||||
dispX.value = dispX.value - (lastX - e.clientX) / zoom.value;
|
|
||||||
dispY.value = dispY.value - (lastY - e.clientY) / zoom.value;
|
|
||||||
|
|
||||||
lastX = e.clientX;
|
|
||||||
lastY = e.clientY;
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
};
|
|
||||||
const dragEnd = (e: MouseEvent) => {
|
|
||||||
window.removeEventListener('mouseup', dragEnd);
|
|
||||||
window.removeEventListener('mousemove', dragMove);
|
|
||||||
};
|
|
||||||
canvasRef.value?.addEventListener('mouseenter', () => {
|
|
||||||
window.addEventListener('wheel', cancelEvent, { passive: false });
|
|
||||||
document.addEventListener('gesturestart', cancelEvent);
|
|
||||||
document.addEventListener('gesturechange', cancelEvent);
|
|
||||||
|
|
||||||
canvasRef.value?.addEventListener('mouseleave', () => {
|
|
||||||
window.removeEventListener('wheel', cancelEvent);
|
|
||||||
document.removeEventListener('gesturestart', cancelEvent);
|
|
||||||
document.removeEventListener('gesturechange', cancelEvent);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
canvasRef.value?.addEventListener('mousedown', (e) => {
|
|
||||||
if(e.button === 1)
|
|
||||||
{
|
|
||||||
lastX = e.clientX;
|
|
||||||
lastY = e.clientY;
|
|
||||||
|
|
||||||
window.addEventListener('mouseup', dragEnd, { passive: true });
|
|
||||||
window.addEventListener('mousemove', dragMove, { passive: true });
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('wheel', (e) => {
|
|
||||||
if((zoom.value >= 3 && e.deltaY < 0) || (zoom.value <= minZoom.value && e.deltaY > 0))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const diff = Math.exp(e.deltaY * -0.001);
|
|
||||||
const centerX = (viewportSize.x.value + viewportSize.width.value / 2), centerY = (viewportSize.y.value + viewportSize.height.value / 2);
|
|
||||||
const mousex = centerX - e.clientX, mousey = centerY - e.clientY;
|
|
||||||
|
|
||||||
dispX.value = dispX.value - (mousex / (diff * zoom.value) - mousex / zoom.value);
|
|
||||||
dispY.value = dispY.value - (mousey / (diff * zoom.value) - mousey / zoom.value);
|
|
||||||
|
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
|
||||||
spacing.value = canvasSettings.value.gridSnap ? canvasSettings.value.spacing ?? 32 : undefined;
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
}, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('touchstart', (e) => {
|
|
||||||
({ x: lastX, y: lastY } = center(e.touches));
|
|
||||||
|
|
||||||
if(e.touches.length > 1)
|
|
||||||
{
|
|
||||||
lastDistance = distance(e.touches);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasRef.value?.addEventListener('touchend', touchend, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('touchcancel', touchcancel, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('touchmove', touchmove, { passive: true });
|
|
||||||
}, { passive: true });
|
|
||||||
const touchend = (e: TouchEvent) => {
|
|
||||||
if(e.touches.length > 1)
|
|
||||||
{
|
|
||||||
({ x: lastX, y: lastY } = center(e.touches));
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
||||||
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
||||||
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
||||||
};
|
|
||||||
const touchcancel = (e: TouchEvent) => {
|
|
||||||
if(e.touches.length > 1)
|
|
||||||
{
|
|
||||||
({ x: lastX, y: lastY } = center(e.touches));
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasRef.value?.removeEventListener('touchend', touchend);
|
|
||||||
canvasRef.value?.removeEventListener('touchcancel', touchcancel);
|
|
||||||
canvasRef.value?.removeEventListener('touchmove', touchmove);
|
|
||||||
};
|
|
||||||
const touchmove = (e: TouchEvent) => {
|
|
||||||
const pos = center(e.touches);
|
|
||||||
dispX.value = dispX.value - (lastX - pos.x) / zoom.value;
|
|
||||||
dispY.value = dispY.value - (lastY - pos.y) / zoom.value;
|
|
||||||
lastX = pos.x;
|
|
||||||
lastY = pos.y;
|
|
||||||
|
|
||||||
if(e.touches.length === 2)
|
|
||||||
{
|
|
||||||
const dist = distance(e.touches);
|
|
||||||
const diff = dist / lastDistance;
|
|
||||||
|
|
||||||
zoom.value = clamp(zoom.value * diff, minZoom.value, 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTransform();
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateTransform()
|
|
||||||
{
|
|
||||||
if(transformRef.value)
|
|
||||||
{
|
|
||||||
transformRef.value.style.transform = `scale3d(${zoom.value}, ${zoom.value}, 1) translate3d(${dispX.value}px, ${dispY.value}px, 0)`;
|
|
||||||
updateScaleVar();
|
|
||||||
}
|
|
||||||
if(patternRef.value && canvasSettings.value.gridSnap)
|
|
||||||
{
|
|
||||||
patternRef.value.parentElement?.classList.remove('hidden');
|
|
||||||
patternRef.value.setAttribute("x", (viewportSize.width.value / 2 + dispX.value % spacing.value! * zoom.value).toFixed(3));
|
|
||||||
patternRef.value.setAttribute("y", (viewportSize.height.value / 2 + dispY.value % spacing.value! * zoom.value).toFixed(3));
|
|
||||||
patternRef.value.setAttribute("width", (zoom.value * spacing.value!).toFixed(3));
|
|
||||||
patternRef.value.setAttribute("height", (zoom.value * spacing.value!).toFixed(3));
|
|
||||||
|
|
||||||
patternRef.value.children[0].setAttribute('cx', (zoom.value).toFixed(3));
|
|
||||||
patternRef.value.children[0].setAttribute('cy', (zoom.value).toFixed(3));
|
|
||||||
patternRef.value.children[0].setAttribute('r', (zoom.value).toFixed(3));
|
|
||||||
}
|
|
||||||
else if(patternRef.value && !canvasSettings.value.gridSnap)
|
|
||||||
{
|
|
||||||
patternRef.value.parentElement?.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function updateToolbarTransform()
|
|
||||||
{
|
|
||||||
const offsetY = -12;
|
|
||||||
if(toolbarRef.value)
|
|
||||||
{
|
|
||||||
if(!focusing.value)
|
|
||||||
{
|
|
||||||
toolbarRef.value.style.transform = '';
|
|
||||||
}
|
|
||||||
else if(focusing.value.type === 'node')
|
|
||||||
{
|
|
||||||
const node = canvas.value.nodes!.find(e => e.id === focusing.value!.id)!;
|
|
||||||
toolbarRef.value.style.transform = `translate(${node.x}px, ${node.y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) translateX(${node.width / 2}px) scale(calc(1 / var(--tw-scale)))`;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
const path = edges.value!.find(e => e.id === focusing.value!.id)!.path;
|
|
||||||
const x = path.from.x + (path.to.x - path.from.x) / 2, y = path.from.y;
|
|
||||||
toolbarRef.value.style.transform = `translate(${x}px, ${y}px) translateY(-100%) translateY(${offsetY}px) translateX(-50%) scale(calc(1 / var(--tw-scale)))`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function moveNode(ids: string[], deltax: number, deltay: number)
|
|
||||||
{
|
|
||||||
if(ids.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions: HistoryAction<'property'>[] = [];
|
|
||||||
for(const id of ids)
|
|
||||||
{
|
|
||||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
|
||||||
snapFinder.update(node);
|
|
||||||
|
|
||||||
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay }, to: { ...node } });
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction('property', actions);
|
|
||||||
}
|
|
||||||
function resizeNode(ids: string[], deltax: number, deltay: number, deltaw: number, deltah: number)
|
|
||||||
{
|
|
||||||
if(ids.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions: HistoryAction<'property'>[] = [];
|
|
||||||
for(const id of ids)
|
|
||||||
{
|
|
||||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
|
||||||
snapFinder.update(node);
|
|
||||||
|
|
||||||
actions.push({ element: { type: 'node', id }, from: { ...node, x: node.x - deltax, y: node.y - deltay, width: node.width - deltaw, height: node.height - deltah }, to: { ...node } });
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction('property', actions);
|
|
||||||
}
|
|
||||||
function select(element: Element)
|
|
||||||
{
|
|
||||||
if(focusing.value && (focusing.value.id !== element.id || focusing.value.type !== element.type))
|
|
||||||
{
|
|
||||||
unselect();
|
|
||||||
}
|
|
||||||
|
|
||||||
focusing.value = element;
|
|
||||||
|
|
||||||
focused.value?.dom?.addEventListener('click', stopPropagation, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
|
||||||
updateToolbarTransform();
|
|
||||||
}
|
|
||||||
function edit(element: Element)
|
|
||||||
{
|
|
||||||
editing.value = element;
|
|
||||||
|
|
||||||
focused.value?.dom?.addEventListener('wheel', stopPropagation, { passive: true });
|
|
||||||
focused.value?.dom?.addEventListener('dblclick', stopPropagation, { passive: true });
|
|
||||||
canvasRef.value?.addEventListener('click', unselect, { once: true });
|
|
||||||
}
|
|
||||||
function createNode(e: MouseEvent)
|
|
||||||
{
|
|
||||||
const width = 250, height = 100;
|
|
||||||
const x = e.layerX / zoom.value - dispX.value - width / 2;
|
|
||||||
const y = e.layerY / zoom.value - dispY.value - height / 2;
|
|
||||||
const node: CanvasNode = { id: getID(ID_SIZE), x, y, width, height, type: 'text' };
|
|
||||||
|
|
||||||
if(!canvas.value.nodes)
|
|
||||||
canvas.value.nodes = [node];
|
|
||||||
else
|
|
||||||
canvas.value.nodes.push(node);
|
|
||||||
|
|
||||||
snapFinder.add(node);
|
|
||||||
addAction('create', [{ element: { type: 'node', id: node.id }, from: undefined, to: node }]);
|
|
||||||
}
|
|
||||||
function remove(elements: Element[])
|
|
||||||
{
|
|
||||||
if(elements.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions: HistoryAction<'remove'>[] = [];
|
|
||||||
focusing.value = undefined;
|
|
||||||
editing.value = undefined;
|
|
||||||
|
|
||||||
const c = canvas.value;
|
|
||||||
|
|
||||||
for(const element of elements)
|
|
||||||
{
|
|
||||||
if(element.type === 'node')
|
|
||||||
{
|
|
||||||
const edges = c.edges?.map((e, i) => ({ id: e.id, from: e.fromNode, to: e.toNode, index: i }))?.filter(e => e.from === element.id || e.to === element.id) ?? [];
|
|
||||||
for(let i = edges.length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
actions.push({ element: { type: 'edge', id: edges[i].id }, from: c.edges!.splice(edges[i].index, 1)[0], to: undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = c.nodes!.findIndex(e => e.id === element.id);
|
|
||||||
const node = c.nodes!.splice(index, 1)[0];
|
|
||||||
|
|
||||||
snapFinder.remove(node);
|
|
||||||
actions.push({ element: { type: 'node', id: element.id }, from: node, to: undefined });
|
|
||||||
}
|
|
||||||
else if(element.type === 'edge' && !actions.find(e => e.element.type === 'edge' && e.element.id === element.id))
|
|
||||||
{
|
|
||||||
const index = c.edges!.findIndex(e => e.id === element.id);
|
|
||||||
actions.push({ element: { type: 'edge', id: element.id }, from: c.edges!.splice(index, 1)[0], to: undefined });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.value = c;
|
|
||||||
|
|
||||||
addAction('remove', actions);
|
|
||||||
}
|
|
||||||
function dragEdgeTo(e: MouseEvent): void
|
|
||||||
{
|
|
||||||
(fakeEdge.value.to as Position).x += e.movementX / zoom.value;
|
|
||||||
(fakeEdge.value.to as Position).y += e.movementY / zoom.value;
|
|
||||||
|
|
||||||
const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.id, fakeEdge.value.to!.x, fakeEdge.value.to!.y);
|
|
||||||
|
|
||||||
fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
|
|
||||||
fakeEdge.value.path = bezier((fakeEdge.value.from as Position), fakeEdge.value.fromSide!, result ?? (fakeEdge.value.to as Position), result?.direction ?? fakeEdge.value.toSide!);
|
|
||||||
}
|
|
||||||
function dragEndEdgeTo(e: MouseEvent): void
|
|
||||||
{
|
|
||||||
window.removeEventListener('mousemove', dragEdgeTo);
|
|
||||||
window.removeEventListener('mouseup', dragEndEdgeTo);
|
|
||||||
|
|
||||||
if(fakeEdge.value.snapped)
|
|
||||||
{
|
|
||||||
const node = canvas.value.nodes!.find(e => e.id === fakeEdge.value.drag!.id)!;
|
|
||||||
const edge: CanvasEdge = { fromNode: fakeEdge.value.drag!.id, fromSide: fakeEdge.value.fromSide!, toNode: fakeEdge.value.snapped.node, toSide: fakeEdge.value.snapped.side, id: getID(ID_SIZE), color: node.color };
|
|
||||||
canvas.value.edges?.push(edge);
|
|
||||||
|
|
||||||
addAction('create', [{ from: undefined, to: edge, element: { id: edge.id, type: 'edge' } }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeEdge.value = {};
|
|
||||||
}
|
|
||||||
function dragStartEdgeTo(id: string, e: MouseEvent, direction: Direction): void
|
|
||||||
{
|
|
||||||
const node = canvas.value.nodes!.find(e => e.id === id)!;
|
|
||||||
fakeEdgeFromNode(node, direction);
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', dragEdgeTo, { passive: true });
|
|
||||||
window.addEventListener('mouseup', dragEndEdgeTo, { passive: true });
|
|
||||||
}
|
|
||||||
function dragEdgeSide(e: MouseEvent): void
|
|
||||||
{
|
|
||||||
if(fakeEdge.value.drag?.type === 'node')
|
|
||||||
return;
|
|
||||||
|
|
||||||
const destination = fakeEdge.value.drag!.destination;
|
|
||||||
const pos = fakeEdge.value[destination]!;
|
|
||||||
|
|
||||||
pos.x += e.movementX / zoom.value;
|
|
||||||
pos.y += e.movementY / zoom.value;
|
|
||||||
|
|
||||||
const result = snapFinder.findEdgeSnapPosition(fakeEdge.value.drag!.node, pos.x, pos.y);
|
|
||||||
|
|
||||||
fakeEdge.value.snapped = result ? { node: result.node, side: result.direction } : undefined;
|
|
||||||
fakeEdge.value.path = bezier(destination === 'from' ? (result ?? pos) : fakeEdge.value.from!, destination === 'from' ? result?.direction ?? fakeEdge.value.fromSide! : fakeEdge.value.fromSide!, destination === 'to' ? (result ?? pos) : fakeEdge.value.to!, destination === 'to' ? result?.direction ?? fakeEdge.value.toSide! : fakeEdge.value.toSide!);
|
|
||||||
}
|
|
||||||
function dragEndEdgeSide(e: MouseEvent): void
|
|
||||||
{
|
|
||||||
if(fakeEdge.value.drag?.type === 'node')
|
|
||||||
return;
|
|
||||||
|
|
||||||
window.removeEventListener('mousemove', dragEdgeSide);
|
|
||||||
window.removeEventListener('mouseup', dragEndEdgeSide);
|
|
||||||
|
|
||||||
if(fakeEdge.value.snapped)
|
|
||||||
{
|
|
||||||
const edge = canvas.value.edges!.find(e => e.id === fakeEdge.value.drag?.id)!
|
|
||||||
const old = { ... edge };
|
|
||||||
|
|
||||||
const destination = fakeEdge.value.drag!.destination;
|
|
||||||
|
|
||||||
edge.fromNode = destination === 'to' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
|
|
||||||
edge.fromSide = destination === 'to' ? fakeEdge.value.fromSide! : fakeEdge.value.snapped.side;
|
|
||||||
|
|
||||||
edge.toNode = destination === 'from' ? fakeEdge.value.drag!.node : fakeEdge.value.snapped.node;
|
|
||||||
edge.toSide = destination === 'from' ? fakeEdge.value.toSide! : fakeEdge.value.snapped.side;
|
|
||||||
|
|
||||||
addAction('property', [{ from: old, to: edge, element: { id: edge.id, type: 'edge' } }]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeEdge.value = {};
|
|
||||||
}
|
|
||||||
function dragStartEdgeSide(id: string, e: MouseEvent, direction: 'from' | 'to'): void
|
|
||||||
{
|
|
||||||
const edge = canvas.value.edges!.find(e => e.id === id)!;
|
|
||||||
fakeEdgeFromEdge(edge, direction);
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', dragEdgeSide, { passive: true });
|
|
||||||
window.addEventListener('mouseup', dragEndEdgeSide, { passive: true });
|
|
||||||
}
|
|
||||||
function fakeEdgeFromEdge(edge: CanvasEdge, direction: 'from' | 'to'): void
|
|
||||||
{
|
|
||||||
fakeEdge.value.drag = { type: 'edge', id: edge.id, destination: direction, node: direction === 'to' ? edge.fromNode : edge.toNode };
|
|
||||||
|
|
||||||
const destinationNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.fromNode)! : canvas.value.nodes!.find(e => e.id === edge.toNode)!;
|
|
||||||
const otherNode = direction === 'from' ? canvas.value.nodes!.find(e => e.id === edge.toNode)! : canvas.value.nodes!.find(e => e.id === edge.fromNode)!;
|
|
||||||
const destinationPos = posFromDir(getBbox(destinationNode), direction === 'from' ? edge.fromSide : edge.toSide);
|
|
||||||
const otherPos = posFromDir(getBbox(otherNode), direction === 'from' ? edge.toSide : edge.fromSide);
|
|
||||||
|
|
||||||
fakeEdge.value.from = direction === 'from' ? destinationPos : otherPos;
|
|
||||||
fakeEdge.value.fromSide = edge.fromSide;
|
|
||||||
|
|
||||||
fakeEdge.value.to = direction === 'to' ? destinationPos : otherPos;
|
|
||||||
fakeEdge.value.toSide = edge.toSide;
|
|
||||||
|
|
||||||
fakeEdge.value.path = bezier(destinationPos, edge.fromSide, otherPos, edge.toSide);
|
|
||||||
fakeEdge.value.hex = edge.color?.hex;
|
|
||||||
|
|
||||||
fakeEdge.value.style = edge?.color ? edge.color?.class ?
|
|
||||||
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}` } :
|
|
||||||
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
|
|
||||||
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` };
|
|
||||||
}
|
|
||||||
function fakeEdgeFromNode(node: CanvasNode, direction: Direction): void
|
|
||||||
{
|
|
||||||
const pos = posFromDir(getBbox(node), direction);
|
|
||||||
|
|
||||||
fakeEdge.value.drag = { type: 'node', id: node.id };
|
|
||||||
|
|
||||||
fakeEdge.value.from = { ... pos };
|
|
||||||
fakeEdge.value.fromSide = direction;
|
|
||||||
|
|
||||||
fakeEdge.value.to = { ... pos };
|
|
||||||
fakeEdge.value.toSide = opposite[direction];
|
|
||||||
|
|
||||||
fakeEdge.value.path = bezier(pos, fakeEdge.value.fromSide!, pos, fakeEdge.value.toSide!);
|
|
||||||
fakeEdge.value.hex = node.color?.hex;
|
|
||||||
|
|
||||||
fakeEdge.value.style = node?.color ? node.color?.class ?
|
|
||||||
{ fill: `fill-light-${node.color?.class} dark:fill-dark-${node.color?.class}`, stroke: `stroke-light-${node.color?.class} dark:stroke-dark-${node.color?.class}` } :
|
|
||||||
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
|
|
||||||
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` };
|
|
||||||
}
|
|
||||||
function editNodeProperty<T extends keyof CanvasNode>(ids: string[], property: T, value: CanvasNode[T])
|
|
||||||
{
|
|
||||||
if(ids.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions: HistoryAction<'property'>[] = [];
|
|
||||||
|
|
||||||
for(const id of ids)
|
|
||||||
{
|
|
||||||
const copy = JSON.parse(JSON.stringify(canvas.value.nodes!.find(e => e.id === id)!)) as CanvasNode;
|
|
||||||
canvas.value.nodes!.find(e => e.id === id)![property] = value;
|
|
||||||
actions.push({ element: { type: 'node', id }, from: copy, to: canvas.value.nodes!.find(e => e.id === id)! });
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction('property', actions);
|
|
||||||
}
|
|
||||||
function editEdgeProperty<T extends keyof CanvasEdge>(ids: string[], property: T, value: CanvasEdge[T])
|
|
||||||
{
|
|
||||||
if(ids.length === 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions: HistoryAction<'property'>[] = [];
|
|
||||||
|
|
||||||
for(const id of ids)
|
|
||||||
{
|
|
||||||
const copy = JSON.parse(JSON.stringify(canvas.value.edges!.find(e => e.id === id)!)) as CanvasEdge;
|
|
||||||
canvas.value.edges!.find(e => e.id === id)![property] = value;
|
|
||||||
actions.push({ element: { type: 'edge', id }, from: copy, to: canvas.value.edges!.find(e => e.id === id)! });
|
|
||||||
}
|
|
||||||
|
|
||||||
addAction('property', actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const unselect = () => {
|
|
||||||
if(focusing.value !== undefined)
|
|
||||||
{
|
|
||||||
focused.value?.dom?.removeEventListener('click', stopPropagation);
|
|
||||||
focused.value?.unselect();
|
|
||||||
updateToolbarTransform();
|
|
||||||
}
|
|
||||||
focusing.value = undefined;
|
|
||||||
|
|
||||||
if(editing.value !== undefined)
|
|
||||||
{
|
|
||||||
edited.value?.dom?.removeEventListener('wheel', stopPropagation);
|
|
||||||
edited.value?.dom?.removeEventListener('dblclick', stopPropagation);
|
|
||||||
edited.value?.dom?.removeEventListener('click', stopPropagation);
|
|
||||||
edited.value?.unselect();
|
|
||||||
}
|
|
||||||
editing.value = undefined;
|
|
||||||
};
|
|
||||||
const undo = () => {
|
|
||||||
if(!historyCursor.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for(const action of historyCursor.value.actions)
|
|
||||||
{
|
|
||||||
if(action.element.type === 'node')
|
|
||||||
{
|
|
||||||
switch(historyCursor.value.event)
|
|
||||||
{
|
|
||||||
case 'create':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'create'>;
|
|
||||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
|
||||||
snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'remove'>;
|
|
||||||
canvas.value.nodes!.push(a.from as CanvasNode);
|
|
||||||
snapFinder.add(a.from as CanvasNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'property':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'property'>;
|
|
||||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.nodes![index] = a.from as CanvasNode;
|
|
||||||
snapFinder.update(a.from as CanvasNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(action.element.type === 'edge')
|
|
||||||
{
|
|
||||||
switch(historyCursor.value.event)
|
|
||||||
{
|
|
||||||
case 'create':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'create'>;
|
|
||||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.edges!.splice(index, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'remove'>;
|
|
||||||
canvas.value.edges!.push(a.from! as CanvasEdge);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'property':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'property'>;
|
|
||||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.edges![index] = a.from as CanvasEdge;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
historyPos.value--;
|
|
||||||
};
|
|
||||||
const redo = () => {
|
|
||||||
if(!history.value || history.value.length - 1 <= historyPos.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
historyPos.value++;
|
|
||||||
|
|
||||||
if(!historyCursor.value)
|
|
||||||
{
|
|
||||||
historyPos.value--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const action of historyCursor.value.actions)
|
|
||||||
{
|
|
||||||
if(action.element.type === 'node')
|
|
||||||
{
|
|
||||||
switch(historyCursor.value.event)
|
|
||||||
{
|
|
||||||
case 'create':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'create'>;
|
|
||||||
canvas.value.nodes!.push(a.to as CanvasNode);
|
|
||||||
snapFinder.add(a.to as CanvasNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'remove'>;
|
|
||||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
|
||||||
snapFinder.remove(canvas.value.nodes!.splice(index, 1)[0]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'property':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'property'>;
|
|
||||||
const index = canvas.value.nodes!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.nodes![index] = a.to as CanvasNode;
|
|
||||||
snapFinder.update(a.to as CanvasNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(action.element.type === 'edge')
|
|
||||||
{
|
|
||||||
switch(historyCursor.value.event)
|
|
||||||
{
|
|
||||||
case 'create':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'create'>;
|
|
||||||
canvas.value.edges!.push(a.to as CanvasEdge);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'remove'>;
|
|
||||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.edges!.splice(index, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'property':
|
|
||||||
{
|
|
||||||
const a = action as HistoryAction<'property'>;
|
|
||||||
const index = canvas.value.edges!.findIndex(e => e.id === action.element.id);
|
|
||||||
canvas.value.edges![index] = a.to as CanvasEdge;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useShortcuts({
|
|
||||||
meta_z: undo,
|
|
||||||
meta_y: redo,
|
|
||||||
Delete: () => { if(focusing.value !== undefined) { remove([focusing.value]) } }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" @dblclick.left="createNode">
|
|
||||||
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4" @click="stopPropagation" @dblclick="stopPropagation">
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
||||||
<Tooltip message="Zoom avant" side="right">
|
|
||||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:plus" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Reset" side="right">
|
|
||||||
<div @click="zoom = 1; updateTransform();" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:reload" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Tout contenir" side="right">
|
|
||||||
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:corners" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Zoom arrière" side="right">
|
|
||||||
<div @click="zoom = clamp(zoom / 1.1, minZoom, 3); updateTransform()" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:minus" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
||||||
<Tooltip message="Annuler (Ctrl+Z)" side="right">
|
|
||||||
<div @click="undo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === -1 }">
|
|
||||||
<Icon icon="ph:arrow-bend-up-left" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Retablir (Ctrl+Y)" side="right">
|
|
||||||
<div @click="redo" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer" :class="{ 'text-light-50 dark:text-dark-50 !cursor-default hover:bg-transparent dark:hover:bg-transparent': historyPos === history.length - 1 }">
|
|
||||||
<Icon icon="ph:arrow-bend-up-right" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
|
||||||
<Tooltip message="Préférences" side="right">
|
|
||||||
<Dialog title="Préférences" iconClose>
|
|
||||||
<template #trigger>
|
|
||||||
<div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:gear" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<Switch v-model="canvasSettings.neighborSnap" label="S'accrocher aux voisins" @update:model-value="snapFinder.config.preferences = canvasSettings" />
|
|
||||||
<Switch v-model="canvasSettings.gridSnap" label="S'accrocher à la grille" @update:model-value="(v) => { canvasSettings.spacing = v ? 32 : undefined; snapFinder.config.preferences = canvasSettings }" />
|
|
||||||
<NumberPicker v-model="canvasSettings.spacing" label="Taille de la grille" :disabled="!canvasSettings.gridSnap" @update:model-value="(v) => { spacing = v; updateTransform(); snapFinder.config.preferences = canvasSettings}" />
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Aide" side="right">
|
|
||||||
<Dialog title="Aide" iconClose>
|
|
||||||
<template #trigger>
|
|
||||||
<div class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:question-mark-circled" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<div class="flex flex-row justify-between px-4">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<ProseH4>Ordinateur</ProseH4>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Selectionner</div>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/><Icon icon="ph:mouse-left-click-fill" class="w-6 h-6"/>: Modifier</div>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-middle-click-fill" class="w-6 h-6"/>: Déplacer</div>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:mouse-right-click-fill" class="w-6 h-6"/>: Menu</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<ProseH4>Mobile</ProseH4>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Selectionner</div>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/><Icon icon="ph:hand-tap" class="w-6 h-6"/>: Modifier</div>
|
|
||||||
<div class="flex items-center"><Icon icon="mdi:gesture-pinch" class="w-6 h-6"/>: Zoomer</div>
|
|
||||||
<div class="flex items-center"><Icon icon="ph:hand-tap" class="w-6 h-6"/> maintenu: Menu</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
|
||||||
<pattern ref="patternRef" id="canvasPattern" patternUnits="userSpaceOnUse">
|
|
||||||
<circle cx="0.75" cy="0.75" r="0.75" class="fill-light-35 dark:fill-dark-35"></circle>
|
|
||||||
</pattern>
|
|
||||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#canvasPattern)"></rect>
|
|
||||||
</svg>
|
|
||||||
<div ref="transformRef" :style="{
|
|
||||||
'transform-origin': 'center center',
|
|
||||||
}" class="h-full">
|
|
||||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none">
|
|
||||||
<div class="absolute z-20 destination-bottom" ref="toolbarRef">
|
|
||||||
<template v-if="focusing">
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-if="focusing.type === 'node'">
|
|
||||||
<PopoverRoot>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div @click="stopPropagation">
|
|
||||||
<Tooltip message="Couleur" side="top">
|
|
||||||
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="ph:palette" class="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverPortal disabled>
|
|
||||||
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editNodeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<label>
|
|
||||||
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editNodeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</PopoverPortal>
|
|
||||||
</PopoverRoot>
|
|
||||||
<Tooltip message="Supprimer" side="top">
|
|
||||||
<div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row" v-else>
|
|
||||||
<PopoverRoot>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<div @click="stopPropagation">
|
|
||||||
<Tooltip message="Couleur" side="top">
|
|
||||||
<div class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="ph:palette" class="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverPortal disabled>
|
|
||||||
<PopoverContent align="center" side="top" class="bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 m-2">
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 flex flex-row *:cursor-pointer">
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', undefined)" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-40 dark:bg-dark-40 w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'red' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-red dark:bg-dark-red w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'orange' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-orange dark:bg-dark-orange w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'yellow' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-yellow dark:bg-dark-yellow w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'green' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-green dark:bg-dark-green w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'cyan' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-cyan dark:bg-dark-cyan w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<div @click="editEdgeProperty([focusing.id], 'color', { class: 'purple' })" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span class="bg-light-purple dark:bg-dark-purple w-4 h-4 block"></span>
|
|
||||||
</div>
|
|
||||||
<label>
|
|
||||||
<div @click="stopPropagation" class="p-2 hover:bg-light-35 hover:dark:bg-dark-35">
|
|
||||||
<span style="background: conic-gradient(red, yellow, green, blue, purple, red)" class="w-4 h-4 block relative"></span><input @change="(e: Event) => editEdgeProperty([focusing!.id], 'color', { hex: (e.target as HTMLInputElement).value })" type="color" class="appearance-none w-0 h-0 absolute" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</PopoverPortal>
|
|
||||||
</PopoverRoot>
|
|
||||||
<Tooltip message="Supprimer" side="top">
|
|
||||||
<div @click="remove([focusing])" class="w-10 h-10 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:trash" class="text-light-red dark:text-dark-red w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CanvasNodeEditor v-for="node of canvas.nodes" :key="node.id" ref="nodes" :node="node" :zoom="zoom"
|
|
||||||
@select="select" @edit="edit" @move="(i, x, y) => moveNode([i], x, y)" @resize="(i, x, y, w, h) => resizeNode([i], x, y, w, h)" @input="(id, text) => editNodeProperty([id], node.type === 'group' ? 'label' : 'text', text)" :snap="snapFinder.findNodeSnapPosition.bind(snapFinder)" @edge="dragStartEdgeTo" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CanvasEdgeEditor v-for="edge of canvas.edges" :key="edge.id" ref="edges" :edge="edge" :nodes="canvas.nodes!" @select="select" @edit="edit" @input="(id, text) => editEdgeProperty([id], 'label', text)" @drag="dragStartEdgeSide" />
|
|
||||||
<div v-if="fakeEdge.path" class="absolute overflow-visible">
|
|
||||||
<svg class="absolute top-0 overflow-visible h-px w-px">
|
|
||||||
<g :style="{'--canvas-color': fakeEdge.hex}" class="z-0">
|
|
||||||
<g :style="`transform: translate(${fakeEdge.path!.to.x}px, ${fakeEdge.path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[fakeEdge.path!.side]}deg);`">
|
|
||||||
<polygon :class="fakeEdge.style?.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
|
||||||
</g>
|
|
||||||
<path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="fakeEdge.style?.stroke" class="fill-none stroke-[4px]" :d="fakeEdge.path.path"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<svg class="absolute overflow-visible top-0 h-px w-px fill-accent-purple stroke-accent-purple stroke-1 z-50">
|
|
||||||
<g v-for="hint of hints">
|
|
||||||
<circle :cx="hint.start.x" :cy="hint.start.y" r="3" />
|
|
||||||
<circle v-if="hint.end" :cx="hint.end.x" :cy="hint.end.y" r="3" />
|
|
||||||
<line v-if="hint.end" :x1="hint.start.x" :x2="hint.end.x" :y1="hint.start.y" :y2="hint.end.y"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
|
|
||||||
import { Annotation, EditorState, RangeValue, SelectionRange, type Range } from '@codemirror/state';
|
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
||||||
import { bracketMatching, defaultHighlightStyle, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
|
||||||
import { search, searchKeymap } from '@codemirror/search';
|
|
||||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
|
||||||
import { lintKeymap } from '@codemirror/lint';
|
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
||||||
import { IterMode, Tree } from '@lezer/common';
|
|
||||||
import { tags } from '@lezer/highlight';
|
|
||||||
const External = Annotation.define<boolean>();
|
|
||||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
|
||||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
|
||||||
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
|
|
||||||
|
|
||||||
const TagTag = tags.special(tags.content);
|
|
||||||
|
|
||||||
const intersects = (a: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}, b: {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}) => !(a.to < b.from || b.to < a.from);
|
|
||||||
|
|
||||||
const highlight = HighlightStyle.define([
|
|
||||||
{ tag: tags.heading1, class: 'text-5xl pt-4 pb-2 after:hidden' },
|
|
||||||
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
|
|
||||||
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
|
|
||||||
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
|
|
||||||
{ tag: tags.meta, color: "#404740" },
|
|
||||||
{ tag: tags.link, textDecoration: "underline" },
|
|
||||||
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
|
|
||||||
{ tag: tags.emphasis, fontStyle: "italic" },
|
|
||||||
{ tag: tags.strong, fontWeight: "bold" },
|
|
||||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
|
||||||
{ tag: tags.keyword, color: "#708" },
|
|
||||||
{ tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
class Decorator
|
|
||||||
{
|
|
||||||
static hiddenNodes: string[] = [
|
|
||||||
'HardBreak',
|
|
||||||
'LinkMark',
|
|
||||||
'EmphasisMark',
|
|
||||||
'CodeMark',
|
|
||||||
'CodeInfo',
|
|
||||||
'URL',
|
|
||||||
]
|
|
||||||
decorations: DecorationSet;
|
|
||||||
constructor(view: EditorView)
|
|
||||||
{
|
|
||||||
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
|
|
||||||
}
|
|
||||||
update(update: ViewUpdate)
|
|
||||||
{
|
|
||||||
if(!update.docChanged && !update.viewportChanged && !update.selectionSet)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.decorations = this.decorations.update({
|
|
||||||
filter: (f, t, v) => false,
|
|
||||||
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
|
|
||||||
sort: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
iterate(tree: Tree, visible: readonly {
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
|
|
||||||
{
|
|
||||||
const decorations: Range<Decoration>[] = [];
|
|
||||||
|
|
||||||
for (let { from, to } of visible) {
|
|
||||||
tree.iterate({
|
|
||||||
from, to, mode: IterMode.IgnoreMounts,
|
|
||||||
enter: node => {
|
|
||||||
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
else if(node.name === 'HeaderMark')
|
|
||||||
decorations.push(Hidden.range(node.from, node.to + 1));
|
|
||||||
|
|
||||||
else if(Decorator.hiddenNodes.includes(node.name))
|
|
||||||
decorations.push(Hidden.range(node.from, node.to));
|
|
||||||
|
|
||||||
else if(node.matchContext(['BulletList', 'ListItem']) && node.name === 'ListMark')
|
|
||||||
decorations.push(Bullet.range(node.from, node.to + 1));
|
|
||||||
|
|
||||||
else if(node.matchContext(['Blockquote']))
|
|
||||||
decorations.push(Blockquote.range(node.from, node.to));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return decorations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { autofocus = false } = defineProps<{
|
|
||||||
placeholder?: string
|
|
||||||
autofocus?: boolean
|
|
||||||
}>();
|
|
||||||
const model = defineModel<string>();
|
|
||||||
|
|
||||||
const editor = useTemplateRef('editor');
|
|
||||||
const view = ref<EditorView>();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if(editor.value)
|
|
||||||
{
|
|
||||||
view.value = new EditorView({
|
|
||||||
doc: model.value,
|
|
||||||
parent: editor.value,
|
|
||||||
extensions: [
|
|
||||||
markdown({
|
|
||||||
base: markdownLanguage
|
|
||||||
}),
|
|
||||||
history(),
|
|
||||||
search(),
|
|
||||||
dropCursor(),
|
|
||||||
EditorState.allowMultipleSelections.of(true),
|
|
||||||
indentOnInput(),
|
|
||||||
syntaxHighlighting(highlight),
|
|
||||||
bracketMatching(),
|
|
||||||
closeBrackets(),
|
|
||||||
crosshairCursor(),
|
|
||||||
EditorView.lineWrapping,
|
|
||||||
keymap.of([
|
|
||||||
...closeBracketsKeymap,
|
|
||||||
...defaultKeymap,
|
|
||||||
...searchKeymap,
|
|
||||||
...historyKeymap,
|
|
||||||
...foldKeymap,
|
|
||||||
...completionKeymap,
|
|
||||||
...lintKeymap
|
|
||||||
]),
|
|
||||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
|
||||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
|
||||||
{
|
|
||||||
model.value = viewUpdate.state.doc.toString();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
EditorView.contentAttributes.of({spellcheck: "true"}),
|
|
||||||
ViewPlugin.fromClass(Decorator, {
|
|
||||||
decorations: e => e.decorations,
|
|
||||||
})
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if(autofocus)
|
|
||||||
{
|
|
||||||
view.value.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (view.value)
|
|
||||||
{
|
|
||||||
view.value?.destroy();
|
|
||||||
view.value = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (model.value === void 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentValue = view.value ? view.value.state.doc.toString() : "";
|
|
||||||
if (view.value && model.value !== currentValue) {
|
|
||||||
view.value.dispatch({
|
|
||||||
changes: { from: 0, to: currentValue.length, insert: model.value || "" },
|
|
||||||
annotations: [External.of(true)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ focus: () => editor.value?.focus() });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="editor" class="flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.variant-cap
|
|
||||||
{
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
.cm-editor
|
|
||||||
{
|
|
||||||
@apply bg-transparent;
|
|
||||||
@apply flex-1 h-full;
|
|
||||||
@apply font-sans;
|
|
||||||
|
|
||||||
@apply text-light-100 dark:text-dark-100;
|
|
||||||
}
|
|
||||||
.cm-editor .cm-content
|
|
||||||
{
|
|
||||||
@apply caret-light-100 dark:caret-dark-100;
|
|
||||||
}
|
|
||||||
.cm-line
|
|
||||||
{
|
|
||||||
@apply text-base;
|
|
||||||
@apply font-sans;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CharacterConfig, MainStat, TrainingLevel } from '~/types/character';
|
|
||||||
import PreviewA from './prose/PreviewA.vue';
|
|
||||||
|
|
||||||
const { config } = defineProps<{
|
|
||||||
config: CharacterConfig
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const selection = ref<{
|
|
||||||
stat: MainStat;
|
|
||||||
level: TrainingLevel;
|
|
||||||
option: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function focusTraining(stat: MainStat, level: TrainingLevel, option: number)
|
|
||||||
{
|
|
||||||
const s = selection.value;
|
|
||||||
if(s !== undefined && s.stat === stat && s.level === level && s.option === option)
|
|
||||||
{
|
|
||||||
selection.value = undefined;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selection.value = {
|
|
||||||
stat, level, option
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<TrainingViewer :config="config" progress>
|
|
||||||
<template #default="{ stat, level, option }">
|
|
||||||
<div @click.capture="console.log" class="border border-light-40 dark:border-dark-40 hover:border-light-70 dark:hover:border-dark-70 cursor-pointer px-2 py-1 w-[400px]" :class="{ '!border-accent-blue': selection !== undefined && selection?.stat == stat && selection?.level == level && selection?.option == option }">
|
|
||||||
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</TrainingViewer>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { MAIN_STATS, mainStatTexts } from '#shared/character.util';
|
|
||||||
import type { CharacterConfig, MainStat } from '~/types/character';
|
|
||||||
|
|
||||||
const { config } = defineProps<{
|
|
||||||
config: CharacterConfig
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const position = ref(0);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20">
|
|
||||||
<div class="flex flex-shrink gap-3 items-center relative w-48 ms-12">
|
|
||||||
<span v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent
|
|
||||||
hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="position = i"></span>
|
|
||||||
<span :style="{ 'left': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position] as MainStat]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[left]
|
|
||||||
after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center"></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 flex">
|
|
||||||
<slot name="addin" :stat="MAIN_STATS[position]"></slot>
|
|
||||||
</div>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 px-8 overflow-hidden max-w-full">
|
|
||||||
<div class="relative cursor-grab active:cursor-grabbing select-none transition-[left] flex flex-1 flex-row max-w-full" :style="{ 'left': `-${position * 100}%` }">
|
|
||||||
<div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training">
|
|
||||||
<template v-for="(options, level) of stat">
|
|
||||||
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full"></div><span class="relative left-4">{{ level }}</span></div>
|
|
||||||
<div class="flex flex-row gap-4 justify-center">
|
|
||||||
<template v-for="(option, i) in options">
|
|
||||||
<slot :stat="name" :level="level" :option="i"></slot>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<!-- <div class="flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-20" v-for="(stat, name) in config.training" >
|
|
||||||
<div class="flex flex-row gap-2 justify-center relative" v-for="(options, level) in stat">
|
|
||||||
<template v-if="progress">
|
|
||||||
<div class="absolute left-0 right-0 -top-2 h-px border-t border-light-30 dark:border-dark-30 border-dashed">
|
|
||||||
<span class="absolute right-0 p-1 text-end">{{ level }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-for="(option, i) in options">
|
|
||||||
<slot :stat="name" :level="level" :option="i"></slot>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="absolute overflow-visible">
|
|
||||||
<div v-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20">{{ edge.label }}</div>
|
|
||||||
<svg class="absolute top-0 overflow-visible h-px w-px">
|
|
||||||
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
|
|
||||||
<g :style="`transform: translate(${path!.to.x}px, ${path!.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path!.side]}deg);`">
|
|
||||||
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
|
||||||
</g>
|
|
||||||
<path :style="`stroke-width: calc(3px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="fill-none stroke-[4px]" :d="path!.path"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.fill-colored
|
|
||||||
{
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
fill: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { getPath, labelCenter, rotation } from '#shared/canvas.util';
|
|
||||||
import type { CanvasEdge, CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
const { edge, nodes } = defineProps<{
|
|
||||||
edge: CanvasEdge
|
|
||||||
nodes: CanvasNode[]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const from = computed(() => nodes!.find(f => f.id === edge.fromNode));
|
|
||||||
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
|
|
||||||
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide));
|
|
||||||
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
|
|
||||||
|
|
||||||
const style = computed(() => {
|
|
||||||
return edge.color ? edge.color?.class ?
|
|
||||||
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}` } :
|
|
||||||
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]` } :
|
|
||||||
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40` }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="absolute overflow-visible group" :class="{ 'z-[1]': focusing }">
|
|
||||||
<input v-autofocus v-if="editing" @click="e => e.stopImmediatePropagation()" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" v-model="edge.label" />
|
|
||||||
<div v-else-if="edge.label" :style="{ transform: `${labelPos} translate(-50%, -50%)` }" class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20" @click.left="select" @dblclick.left="edit">{{ edge.label }}</div>
|
|
||||||
<svg ref="dom" class="absolute top-0 overflow-visible h-px w-px">
|
|
||||||
<g :style="{'--canvas-color': edge.color?.hex}" class="z-0">
|
|
||||||
<g :style="`transform: translate(${path.to.x}px, ${path.to.y}px) scale(var(--zoom-multiplier)) rotate(${rotation[path.side]}deg);`">
|
|
||||||
<polygon :class="style.fill" points="0,0 6.5,10.4 -6.5,10.4"></polygon>
|
|
||||||
</g>
|
|
||||||
<path :style="`stroke-width: calc(${focusing ? 6 : 3}px * var(--zoom-multiplier));`" style="stroke-linecap: butt;" :class="style.stroke" class="transition-[stroke-width] fill-none stroke-[4px]" :d="path.path"></path>
|
|
||||||
<path style="stroke-width: calc(22px * var(--zoom-multiplier));" class="fill-none transition-opacity z-30 opacity-0 hover:opacity-25" :class="[style.stroke, { 'opacity-25': focusing }]" :d="path.path" @click="select" @dblclick="edit"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span v-if="focusing && !editing" :style="`transform: translate(${path.from.x}px, ${path.from.y}px) translate(-50%, -50%) scale(var(--zoom-multiplier))`" @mousedown.left="(e) => dragEdge(e, 'from')" :class="style.fill" class="hidden group-hover:block z-[31] absolute rounded-full border-2 border-light-70 dark:border-dark-70 bg-light-30 dark:bg-dark-30 w-6 h-6"></span>
|
|
||||||
<span v-if="focusing && !editing" :style="`transform: translate(${path.to.x}px, ${path.to.y}px) translate(-50%, -50%) scale(var(--zoom-multiplier))`" @mousedown.left="(e) => dragEdge(e, 'to')" :class="style.fill" class="hidden group-hover:block z-[31] absolute rounded-full border-2 border-light-70 dark:border-dark-70 bg-light-30 dark:bg-dark-30 w-6 h-6"></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.fill-colored
|
|
||||||
{
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
fill: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { getPath, labelCenter, rotation } from '#shared/canvas.util';
|
|
||||||
import type { Element } from '../CanvasEditor.vue';
|
|
||||||
import type { CanvasEdge, CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
const { edge, nodes } = defineProps<{
|
|
||||||
edge: CanvasEdge
|
|
||||||
nodes: CanvasNode[]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'select', id: Element): void,
|
|
||||||
(e: 'edit', id: Element): void,
|
|
||||||
(e: 'drag', id: string, _e: MouseEvent, origin: 'from' | 'to'): void,
|
|
||||||
(e: 'input', id: string, text?: string): void,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dom = useTemplateRef('dom');
|
|
||||||
const focusing = ref(false), editing = ref(false);
|
|
||||||
|
|
||||||
const from = computed(() => nodes!.find(f => f.id === edge.fromNode));
|
|
||||||
const to = computed(() => nodes!.find(f => f.id === edge.toNode));
|
|
||||||
const path = computed(() => getPath(from.value!, edge.fromSide, to.value!, edge.toSide)!);
|
|
||||||
const labelPos = computed(() => labelCenter(from.value!, edge.fromSide, to.value!, edge.toSide));
|
|
||||||
|
|
||||||
let oldText = edge.label;
|
|
||||||
|
|
||||||
function select(e: Event) {
|
|
||||||
if(editing.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
focusing.value = true;
|
|
||||||
emit('select', { type: 'edge', id: edge.id });
|
|
||||||
}
|
|
||||||
function edit(e: Event) {
|
|
||||||
oldText = edge.label;
|
|
||||||
|
|
||||||
focusing.value = true;
|
|
||||||
editing.value = true;
|
|
||||||
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
emit('edit', { type: 'edge', id: edge.id });
|
|
||||||
}
|
|
||||||
function dragEdge(e: MouseEvent, origin: 'from' | 'to') {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
|
|
||||||
emit('drag', edge.id, e, origin);
|
|
||||||
}
|
|
||||||
function unselect() {
|
|
||||||
if(editing.value)
|
|
||||||
{
|
|
||||||
const text = edge.label;
|
|
||||||
|
|
||||||
if(text !== oldText)
|
|
||||||
{
|
|
||||||
edge.label = oldText;
|
|
||||||
|
|
||||||
emit('input', edge.id, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
focusing.value = false;
|
|
||||||
editing.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ unselect, dom, id: edge.id, path });
|
|
||||||
|
|
||||||
const style = computed(() => {
|
|
||||||
return edge.color ? edge.color?.class ?
|
|
||||||
{ fill: `fill-light-${edge.color?.class} dark:fill-dark-${edge.color?.class}`, stroke: `stroke-light-${edge.color?.class} dark:stroke-dark-${edge.color?.class}`, outline: `outline-light-${edge.color?.class} dark:outline-dark-${edge.color?.class}` } :
|
|
||||||
{ fill: `fill-colored`, stroke: `stroke-[color:var(--canvas-color)]`, outline: `outline-[color:var(--canvas-color)]` } :
|
|
||||||
{ stroke: `stroke-light-40 dark:stroke-dark-40`, fill: `fill-light-40 dark:fill-dark-40`, outline: `outline-light-40 dark:outline-dark-40` }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="absolute" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
|
||||||
<div :class="[style.border]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg">
|
|
||||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
|
||||||
<MarkdownRenderer :content="node.text" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="node.type === 'group' && node.label !== undefined" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.bg-colored
|
|
||||||
{
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
const { node } = defineProps<{
|
|
||||||
node: CanvasNode
|
|
||||||
zoom: number
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const style = computed(() => {
|
|
||||||
return node.color ? node.color?.class ?
|
|
||||||
{ bg: `bg-light-${node.color?.class} dark:bg-dark-${node.color?.class}`, border: `border-light-${node.color?.class} dark:border-dark-${node.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` }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="absolute" ref="dom" :style="{transform: `translate(${node.x}px, ${node.y}px)`, width: `${node.width}px`, height: `${node.height}px`, '--canvas-color': node.color?.hex}" :class="{'-z-10': node.type === 'group', 'z-10': node.type !== 'group'}">
|
|
||||||
<div v-if="!editing || node.type === 'group'" style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4 cursor-move': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full hover:outline-4">
|
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto" :class="style.bg" @click.left="(e) => { if(node.type !== 'group') selectNode(e) }" @dblclick.left="(e) => { if(node.type !== 'group') editNode(e) }">
|
|
||||||
<div v-if="node.text && node.text.length > 0" class="flex items-center">
|
|
||||||
<MarkdownRenderer :content="node.text" :proses="{ a: FakeA }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="focusing">
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 0, 1, 0, -1)" id="n " class="cursor-n-resize absolute -top-3 -right-3 -left-3 h-6 group">
|
|
||||||
<span @mousedown.left="(e) => dragEdge(e, 'top')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -top-1.5 left-1/2 -translate-x-3"></span>
|
|
||||||
</span> <!-- North -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 0, 1)" id="s " class="cursor-s-resize absolute -bottom-3 -right-3 -left-3 h-6 group">
|
|
||||||
<span @mousedown.left="(e) => dragEdge(e, 'bottom')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -bottom-1.5 left-1/2 -translate-x-3"></span>
|
|
||||||
</span> <!-- South -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 1, 0)" id="e " class="cursor-e-resize absolute -top-3 -bottom-3 -right-3 w-6 group">
|
|
||||||
<span @mousedown.left="(e) => dragEdge(e, 'right')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -right-1.5 top-1/2 -translate-y-3"></span>
|
|
||||||
</span> <!-- East -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 1, 0, -1, 0)" id="w " class="cursor-w-resize absolute -top-3 -bottom-3 -left-3 w-6 group">
|
|
||||||
<span @mousedown.left="(e) => dragEdge(e, 'left')" :class="[style.bg]" class="hidden group-hover:block absolute rounded-full border-2 border-light-70 dark:border-dark-70 w-6 h-6 -left-1.5 top-1/2 -translate-y-3"></span>
|
|
||||||
</span> <!-- West -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 1, 1, -1, -1)" id="nw" class="cursor-nw-resize absolute -top-4 -left-4 w-8 h-8"></span> <!-- North West -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 0, 1, 1, -1)" id="ne" class="cursor-ne-resize absolute -top-4 -right-4 w-8 h-8"></span> <!-- North East -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 0, 0, 1, 1)" id="se" class="cursor-se-resize absolute -bottom-4 -right-4 w-8 h-8"></span> <!-- South East -->
|
|
||||||
<span @mousedown.left="(e) => resizeNode(e, 1, 0, -1, 1)" id="sw" class="cursor-sw-resize absolute -bottom-4 -left-4 w-8 h-8"></span> <!-- South West -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else style="outline-style: solid;" :class="[style.border, style.outline, { '!outline-4': focusing }]" class="outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 overflow-hidden contain-strict w-full h-full flex py-2" >
|
|
||||||
<FramedEditor v-model="node.text" autofocus :gutters="false"/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!editing && node.type === 'group' && node.label !== undefined" @click.left="(e) => selectNode(e)" @dblclick.left="(e) => editNode(e)" :class="style.border" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] mb-2 px-2 py-1 font-thin">{{ node.label }}</div>
|
|
||||||
<input v-else-if="editing && node.type === 'group'" v-model="node.label" @click="e => e.stopImmediatePropagation()" v-autofocus :class="[style.border, style.outline]" style="max-width: 100%; font-size: calc(18px * var(--zoom-multiplier))" class="origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.bg-colored
|
|
||||||
{
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgba(from var(--canvas-color) r g b / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Box, Direction } from '#shared/canvas.util';
|
|
||||||
import type { Element } from '../CanvasEditor.vue';
|
|
||||||
import FakeA from '../prose/FakeA.vue';
|
|
||||||
import type { CanvasNode } from '~/types/canvas';
|
|
||||||
|
|
||||||
const { node, zoom, snap } = defineProps<{
|
|
||||||
node: CanvasNode
|
|
||||||
zoom: number,
|
|
||||||
snap: (activeNode: CanvasNode, resizeHandle?: Box) => Partial<Box>,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'select', id: Element): void,
|
|
||||||
(e: 'edit', id: Element): void,
|
|
||||||
(e: 'move', id: string, x: number, y: number): void,
|
|
||||||
(e: 'resize', id: string, x: number, y: number, w: number, h: number): void,
|
|
||||||
(e: 'input', id: string, text: string): void,
|
|
||||||
(e: 'edge', id: string, _e: MouseEvent, side: Direction): void,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const dom = useTemplateRef('dom');
|
|
||||||
const focusing = ref(false), editing = ref(false);
|
|
||||||
let oldText = node.type === 'group' ? node.label : node.text;
|
|
||||||
|
|
||||||
function selectNode(e: Event) {
|
|
||||||
if(editing.value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
focusing.value = true;
|
|
||||||
emit('select', { type: 'node', id: node.id });
|
|
||||||
|
|
||||||
dom.value?.addEventListener('mousedown', dragstart, { passive: true });
|
|
||||||
}
|
|
||||||
function editNode(e: Event) {
|
|
||||||
focusing.value = true;
|
|
||||||
editing.value = true;
|
|
||||||
|
|
||||||
oldText = node.type === 'group' ? node.label : node.text;
|
|
||||||
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
|
|
||||||
dom.value?.removeEventListener('mousedown', dragstart);
|
|
||||||
emit('edit', { type: 'node', id: node.id });
|
|
||||||
}
|
|
||||||
function resizeNode(e: MouseEvent, x: number, y: number, width: number, height: number) {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
|
|
||||||
const startx = node.x, starty = node.y, startw = node.width, starth = node.height;
|
|
||||||
let realx = node.x, realy = node.y, realw = node.width, realh = node.height;
|
|
||||||
const resizemove = (e: MouseEvent) => {
|
|
||||||
if(e.button !== 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
realx = realx + (e.movementX / zoom) * x;
|
|
||||||
realy = realy + (e.movementY / zoom) * y;
|
|
||||||
realw = Math.max(realw + (e.movementX / zoom) * width, 64);
|
|
||||||
realh = Math.max(realh + (e.movementY / zoom) * height, 64);
|
|
||||||
|
|
||||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy, width: realw, height: realh }, { x, y, width, height });
|
|
||||||
|
|
||||||
node.x = result?.x ?? realx;
|
|
||||||
node.y = result?.y ?? realy;
|
|
||||||
node.width = result?.width ?? realw;
|
|
||||||
node.height = result?.height ?? realh;
|
|
||||||
};
|
|
||||||
const resizeend = (e: MouseEvent) => {
|
|
||||||
if(e.button !== 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
emit('resize', node.id, node.x - startx, node.y - starty, node.width - startw, node.height - starth);
|
|
||||||
|
|
||||||
window.removeEventListener('mousemove', resizemove);
|
|
||||||
window.removeEventListener('mouseup', resizeend);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', resizemove);
|
|
||||||
window.addEventListener('mouseup', resizeend);
|
|
||||||
}
|
|
||||||
function dragEdge(e: MouseEvent, direction: Direction) {
|
|
||||||
e.stopImmediatePropagation();
|
|
||||||
|
|
||||||
emit('edge', node.id, e, direction)
|
|
||||||
}
|
|
||||||
function unselect() {
|
|
||||||
if(editing.value)
|
|
||||||
{
|
|
||||||
const text = node.type === 'group' ? node.label! : node.text!;
|
|
||||||
|
|
||||||
if(text !== oldText)
|
|
||||||
{
|
|
||||||
if(node.type === 'group')
|
|
||||||
node.label = oldText;
|
|
||||||
else
|
|
||||||
node.text = oldText;
|
|
||||||
|
|
||||||
emit('input', node.id, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
focusing.value = false;
|
|
||||||
editing.value = false;
|
|
||||||
|
|
||||||
dom.value?.removeEventListener('mousedown', dragstart);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastx = 0, lasty = 0;
|
|
||||||
let realx = 0, realy = 0;
|
|
||||||
const dragmove = (e: MouseEvent) => {
|
|
||||||
if(e.button !== 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
realx += e.movementX / zoom;
|
|
||||||
realy += e.movementY / zoom;
|
|
||||||
|
|
||||||
const result = e.altKey ? undefined : snap({ ...node, x: realx, y: realy });
|
|
||||||
|
|
||||||
node.x = result?.x ?? realx;
|
|
||||||
node.y = result?.y ?? realy;
|
|
||||||
};
|
|
||||||
const dragend = (e: MouseEvent) => {
|
|
||||||
if(e.button !== 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
window.removeEventListener('mousemove', dragmove);
|
|
||||||
window.removeEventListener('mouseup', dragend);
|
|
||||||
|
|
||||||
emit('move', node.id, node.x - lastx, node.y - lasty);
|
|
||||||
};
|
|
||||||
const dragstart = (e: MouseEvent) => {
|
|
||||||
if(e.button !== 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lastx = node.x, lasty = node.y;
|
|
||||||
realx = node.x, realy = node.y;
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', dragmove, { passive: true });
|
|
||||||
window.addEventListener('mouseup', dragend, { passive: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ unselect, dom, id: node.id });
|
|
||||||
|
|
||||||
const style = computed(() => {
|
|
||||||
return node.color ? node.color?.class ?
|
|
||||||
{ bg: `bg-light-${node.color?.class} dark:bg-dark-${node.color?.class}`, border: `border-light-${node.color?.class} dark:border-dark-${node.color?.class}`, outline: `outline-light-${node.color?.class} dark:outline-dark-${node.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` }
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/*
|
|
||||||
|
|
||||||
stroke-light-red
|
|
||||||
stroke-light-orange
|
|
||||||
stroke-light-yellow
|
|
||||||
stroke-light-green
|
|
||||||
stroke-light-cyan
|
|
||||||
stroke-light-purple
|
|
||||||
dark:stroke-dark-red
|
|
||||||
dark:stroke-dark-orange
|
|
||||||
dark:stroke-dark-yellow
|
|
||||||
dark:stroke-dark-green
|
|
||||||
dark:stroke-dark-cyan
|
|
||||||
dark:stroke-dark-purple
|
|
||||||
fill-light-red
|
|
||||||
fill-light-orange
|
|
||||||
fill-light-yellow
|
|
||||||
fill-light-green
|
|
||||||
fill-light-cyan
|
|
||||||
fill-light-purple
|
|
||||||
dark:fill-dark-red
|
|
||||||
dark:fill-dark-orange
|
|
||||||
dark:fill-dark-yellow
|
|
||||||
dark:fill-dark-green
|
|
||||||
dark:fill-dark-cyan
|
|
||||||
dark:fill-dark-purple
|
|
||||||
bg-light-red
|
|
||||||
bg-light-orange
|
|
||||||
bg-light-yellow
|
|
||||||
bg-light-green
|
|
||||||
bg-light-cyan
|
|
||||||
bg-light-purple
|
|
||||||
dark:bg-dark-red
|
|
||||||
dark:bg-dark-orange
|
|
||||||
dark:bg-dark-yellow
|
|
||||||
dark:bg-dark-green
|
|
||||||
dark:bg-dark-cyan
|
|
||||||
dark:bg-dark-purple
|
|
||||||
border-light-red
|
|
||||||
border-light-orange
|
|
||||||
border-light-yellow
|
|
||||||
border-light-green
|
|
||||||
border-light-cyan
|
|
||||||
border-light-purple
|
|
||||||
dark:border-dark-red
|
|
||||||
dark:border-dark-orange
|
|
||||||
dark:border-dark-yellow
|
|
||||||
dark:border-dark-green
|
|
||||||
dark:border-dark-cyan
|
|
||||||
dark:border-dark-purple
|
|
||||||
outline-light-red
|
|
||||||
outline-light-orange
|
|
||||||
outline-light-yellow
|
|
||||||
outline-light-green
|
|
||||||
outline-light-cyan
|
|
||||||
outline-light-purple
|
|
||||||
dark:outline-dark-red
|
|
||||||
dark:outline-dark-orange
|
|
||||||
dark:outline-dark-yellow
|
|
||||||
dark:outline-dark-green
|
|
||||||
dark:outline-dark-cyan
|
|
||||||
dark:outline-dark-purple
|
|
||||||
|
|
||||||
*/
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CanvasContent } from '~/types/content';
|
|
||||||
import { Canvas } from '#shared/canvas.util';
|
|
||||||
|
|
||||||
const { path } = defineProps<{
|
|
||||||
path: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { user } = useUserSession();
|
|
||||||
const { content, get } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === path) as CanvasContent | undefined);
|
|
||||||
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
|
||||||
const element = useTemplateRef('element');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
mount();
|
|
||||||
});
|
|
||||||
|
|
||||||
if(overview.value && !overview.value.content)
|
|
||||||
{
|
|
||||||
await get(path);
|
|
||||||
mount();
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = computed(() => overview.value && overview.value.content ? overview.value.content : undefined);
|
|
||||||
console.log(canvas.value);
|
|
||||||
|
|
||||||
function mount()
|
|
||||||
{
|
|
||||||
if(element.value && canvas.value)
|
|
||||||
{
|
|
||||||
const c = new Canvas(canvas.value);
|
|
||||||
element.value.appendChild(c.container);
|
|
||||||
c.mount();
|
|
||||||
}
|
|
||||||
updateScaleVar();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div ref="element"></div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
|
||||||
|
|
||||||
const { path } = defineProps<{
|
|
||||||
path: string
|
|
||||||
filter?: string,
|
|
||||||
popover?: boolean
|
|
||||||
}>();
|
|
||||||
const { user } = useUserSession();
|
|
||||||
const { content, get } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === path));
|
|
||||||
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
if(overview.value && !overview.value.content)
|
|
||||||
{
|
|
||||||
loading.value = true;
|
|
||||||
await get(path);
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-1 justify-start items-start flex-col lg:px-16 xl:px-32 2xl:px-64 py-6">
|
|
||||||
<Loading v-if="loading" />
|
|
||||||
<template v-else-if="overview">
|
|
||||||
<div v-if="!popover" class="flex flex-1 flex-row justify-between items-center">
|
|
||||||
<ProseH1>{{ overview.title }}</ProseH1>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner || hasPermissions(user?.permissions ?? [], ['admin', 'editor'])"><Button>Modifier</Button></NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<MarkdownRenderer v-if="overview.content" :content="overview.content" :filter="filter" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div><ProseH2>Impossible d'afficher le contenu demandé</ProseH2></div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<template>
|
|
||||||
<span>
|
|
||||||
<HoverCard trigger-key="Ctrl" nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
|
||||||
<template #content>
|
|
||||||
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="pathname" :filter="hash.substring(1)" popover />
|
|
||||||
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="pathname" /></div></template>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<span class="text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85"><slot v-bind="$attrs"></slot></span>
|
|
||||||
</span>
|
|
||||||
</HoverCard>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseURL } from 'ufo';
|
|
||||||
|
|
||||||
const { href } = defineProps<{
|
|
||||||
href: string
|
|
||||||
class?: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { hash, pathname } = parseURL(href);
|
|
||||||
|
|
||||||
const { content } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === pathname));
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,30 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="text-accent-blue inline-flex items-center" :class="class">
|
<span ref="container"></span>
|
||||||
<HoverCard nuxt-client class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
|
||||||
<template #content>
|
|
||||||
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
|
|
||||||
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<slot v-bind="$attrs"></slot>
|
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
|
|
||||||
</span>
|
|
||||||
</HoverCard>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseURL } from 'ufo';
|
import { parseURL } from 'ufo';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import proses, { fakeA } from '#shared/proses';
|
||||||
import { iconByType } from '#shared/content.util';
|
import { text } from '#shared/dom.util';
|
||||||
|
|
||||||
const { href } = defineProps<{
|
const { href, label } = defineProps<{
|
||||||
href: string
|
href: string,
|
||||||
class?: string
|
label: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { hash, pathname } = parseURL(href);
|
const container = useTemplateRef('container');
|
||||||
|
|
||||||
const { content } = useContent();
|
onMounted(() => {
|
||||||
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
|
queueMicrotask(() => {
|
||||||
|
container.value && container.value.appendChild(proses('a', fakeA, [ text(label) ], { href }) as HTMLElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<NuxtLink class="text-accent-blue inline-flex items-center" :to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: decodeURIComponent(hash) } : href" :class="class">
|
|
||||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" :disabled="!overview">
|
|
||||||
<template #content>
|
|
||||||
<Markdown v-if="overview?.type === 'markdown'" class="!px-6" :path="decodeURIComponent(pathname)" :filter="hash.substring(1)" popover />
|
|
||||||
<template v-else-if="overview?.type === 'canvas'"><div class="w-[600px] h-[600px] relative"><Canvas :path="decodeURIComponent(pathname)" /></div></template>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<slot v-bind="$attrs"></slot>
|
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :icon="iconByType[overview.type]" />
|
|
||||||
</span>
|
|
||||||
</HoverCard>
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseURL } from 'ufo';
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import { iconByType } from '#shared/content.util';
|
|
||||||
|
|
||||||
const { href } = defineProps<{
|
|
||||||
href: string
|
|
||||||
class?: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { hash, pathname } = parseURL(href);
|
|
||||||
|
|
||||||
const { content } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === decodeURIComponent(pathname)));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.cm-link {
|
|
||||||
@apply text-accent-blue inline-flex items-center cursor-pointer hover:text-opacity-85;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<template>
|
|
||||||
<blockquote class="empty:before:hidden ps-4 my-4 relative before:absolute before:-top-1 before:-bottom-1 before:left-0 before:w-1 before:bg-light-30 dark:before:bg-dark-30" ref="el">
|
|
||||||
<slot />
|
|
||||||
</blockquote>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-quote
|
|
||||||
{
|
|
||||||
@apply before:hidden;
|
|
||||||
}
|
|
||||||
.HyperMD-quote.hmd-inactive-line
|
|
||||||
{
|
|
||||||
@apply before:block empty:before:!hidden !pb-2 !ps-4 !relative before:!absolute before:!-top-1 before:!-bottom-1 before:!left-0 before:!w-1 before:!bg-none before:!bg-light-30 dark:before:!bg-dark-30;
|
|
||||||
}
|
|
||||||
.HyperMD-quote.HyperMD-header
|
|
||||||
{
|
|
||||||
@apply before:!hidden;
|
|
||||||
}
|
|
||||||
.hmd-inactive-line .cm-formatting-quote
|
|
||||||
{
|
|
||||||
@apply !hidden;
|
|
||||||
}
|
|
||||||
.cm-quote
|
|
||||||
{
|
|
||||||
@apply text-light-100 dark:text-dark-100;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<template>
|
|
||||||
<CollapsibleRoot :disabled="disabled" :defaultOpen="fold === true || fold === undefined" class="callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue" :data-type="type">
|
|
||||||
<CollapsibleTrigger>
|
|
||||||
<div :class="{ 'cursor-pointer': fold !== undefined }" class="flex flex-row items-center justify-start ps-2">
|
|
||||||
<Icon :icon="calloutIconByType[type] ?? defaultCalloutIcon" inline class="w-6 h-6 stroke-2 float-start me-2 flex-shrink-0" />
|
|
||||||
<span v-if="title" class="block font-bold text-start">{{ title }}</span>
|
|
||||||
<Icon icon="radix-icons:caret-right" v-if="fold !== undefined" class="transition-transform group-data-[state=open]:rotate-90 w-6 h-6 mx-6" />
|
|
||||||
</div>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent class="overflow-hidden data-[state=closed]:animate-[collapseClose_0.2s_ease-in-out] data-[state=open]:animate-[collapseOpen_0.2s_ease-in-out] data-[state=closed]:h-0">
|
|
||||||
<div class="px-2">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</CollapsibleRoot>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
const calloutIconByType: Record<string, string> = {
|
|
||||||
note: 'radix-icons:pencil-1',
|
|
||||||
abstract: 'radix-icons:file-text',
|
|
||||||
info: 'radix-icons:info-circled',
|
|
||||||
todo: 'radix-icons:check-circled',
|
|
||||||
tip: 'radix-icons:star',
|
|
||||||
success: 'radix-icons:check',
|
|
||||||
question: 'radix-icons:question-mark-circled',
|
|
||||||
warning: 'radix-icons:exclamation-triangle',
|
|
||||||
failure: 'radix-icons:cross-circled',
|
|
||||||
danger: 'radix-icons:circle-backslash',
|
|
||||||
bug: 'solar:bug-linear',
|
|
||||||
example: 'radix-icons:list-bullet',
|
|
||||||
quote: 'radix-icons:quote',
|
|
||||||
};
|
|
||||||
const defaultCalloutIcon = 'radix-icons:info-circled';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
const { type, title, fold } = defineProps<{
|
|
||||||
type: string;
|
|
||||||
title?: string;
|
|
||||||
fold?: boolean;
|
|
||||||
}>();
|
|
||||||
const disabled = computed(() => fold === undefined);
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<template>
|
|
||||||
<code><slot /></code>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.cm-inline-code
|
|
||||||
{
|
|
||||||
@apply !border-none !bg-transparent !text-light-100 dark:!text-dark-100 !p-0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<em>
|
|
||||||
<slot />
|
|
||||||
</em>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<template>
|
|
||||||
<h1 :id="parseId(id)" class="text-5xl font-thin mt-3 mb-8 first:pt-0 pt-2">
|
|
||||||
<slot />
|
|
||||||
</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-header-1
|
|
||||||
{
|
|
||||||
@apply text-5xl pt-4 pb-2 after:hidden;
|
|
||||||
}
|
|
||||||
.HyperMD-header-1 .cm-header
|
|
||||||
{
|
|
||||||
@apply font-thin;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<template>
|
|
||||||
<h2 :id="parseId(id)" class="text-4xl font-semibold mt-3 mb-6 ms-1 first:pt-0 pt-2">
|
|
||||||
<slot />
|
|
||||||
</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-header-2
|
|
||||||
{
|
|
||||||
@apply !text-4xl !pt-4 !pb-2 !ps-1 leading-loose after:hidden;
|
|
||||||
}
|
|
||||||
.HyperMD-header-2 .cm-header
|
|
||||||
{
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<template>
|
|
||||||
<h3 :id="parseId(id)" class="text-2xl font-bold mt-2 mb-4">
|
|
||||||
<slot />
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-header-3
|
|
||||||
{
|
|
||||||
@apply !text-2xl !font-bold !pt-1 after:!hidden;
|
|
||||||
}
|
|
||||||
.HyperMD-header-3 .cm-header
|
|
||||||
{
|
|
||||||
@apply font-bold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<template>
|
|
||||||
<h4 :id="parseId(id)" class="text-xl font-semibold my-2" style="font-variant: small-caps;">
|
|
||||||
<slot />
|
|
||||||
</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-header-4
|
|
||||||
{
|
|
||||||
@apply !text-xl font-semibold pt-1 after:hidden;
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
.HyperMD-header-4 .cm-header
|
|
||||||
{
|
|
||||||
@apply font-semibold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<template>
|
|
||||||
<h5 :id="parseId(id)" class="text-lg font-semibold my-1">
|
|
||||||
<slot />
|
|
||||||
</h5>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<template>
|
|
||||||
<h6 :id="parseId(id)">
|
|
||||||
<slot />
|
|
||||||
</h6>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { parseId } from '#shared/general.util';
|
|
||||||
const props = defineProps<{ id?: string }>()
|
|
||||||
|
|
||||||
const generate = computed(() => props.id)
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<template>
|
|
||||||
<Separator class="border-b border-light-35 dark:border-dark-35 m-4" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-hr
|
|
||||||
{
|
|
||||||
@apply bg-light-35 dark:bg-dark-35 h-px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<template>
|
|
||||||
<img
|
|
||||||
:src="refinedSrc"
|
|
||||||
:alt="alt"
|
|
||||||
:width="width"
|
|
||||||
:height="height"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
|
|
||||||
import { useRuntimeConfig, computed, resolveComponent } from '#imports'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
alt: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: undefined
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: undefined
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const refinedSrc = computed(() => {
|
|
||||||
if (props.src?.startsWith('/') && !props.src.startsWith('//')) {
|
|
||||||
const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
|
|
||||||
if (_base !== '/' && !props.src.startsWith(_base)) {
|
|
||||||
return joinURL(_base, props.src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return props.src
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<template>
|
|
||||||
<li class="before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4"><slot /></li>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HyperMD-list-line
|
|
||||||
{
|
|
||||||
@apply !py-1;
|
|
||||||
}
|
|
||||||
.HyperMD-list-line.hmd-inactive-line > span
|
|
||||||
{
|
|
||||||
@apply before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4;
|
|
||||||
}
|
|
||||||
.hmd-inactive-line .cm-formatting-list
|
|
||||||
{
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
.cm-hmd-list-indent
|
|
||||||
{
|
|
||||||
@apply !hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<ol>
|
|
||||||
<slot />
|
|
||||||
</ol>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<p><slot /></p>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<pre :class="$props.class"><slot /></pre>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps({
|
|
||||||
code: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
filename: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
highlights: {
|
|
||||||
type: Array as () => number[],
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
class: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
pre code .line{display:block}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-if="isDev">
|
|
||||||
Rendering the <code>script</code> element is dangerous and is disabled by default. Consider implementing your own <code>ProseScript</code> element to have control over script rendering.
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const isDev = import.meta.dev
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<small class="text-light-60 dark:text-dark-60 text-sm italic">
|
|
||||||
<slot />
|
|
||||||
</small>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<strong>
|
|
||||||
<slot />
|
|
||||||
</strong>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<table class="mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35">
|
|
||||||
<slot />
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<template>
|
|
||||||
<HoverCard nuxt-client class="min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]">
|
|
||||||
<template #content>
|
|
||||||
<Markdown class="!px-6" path="tags" :filter="tag" popover />
|
|
||||||
</template>
|
|
||||||
<span class="before:content-['#'] cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30">
|
|
||||||
<slot></slot>
|
|
||||||
</span>
|
|
||||||
</HoverCard>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.cm-hashtag.cm-hashtag-begin
|
|
||||||
{
|
|
||||||
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 ps-1 rounded-l-[12px] border border-r-0 border-accent-blue border-opacity-30;
|
|
||||||
}
|
|
||||||
.cm-hashtag.cm-hashtag-end
|
|
||||||
{
|
|
||||||
@apply bg-accent-blue bg-opacity-10 text-accent-blue text-sm pb-0.5 pe-1 rounded-r-[12px] !rounded-se-none border border-l-0 border-accent-blue border-opacity-30;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { tag } = defineProps<{
|
|
||||||
tag: string
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const { content } = useContent();
|
|
||||||
const overview = computed(() => content.value.find(e => e.path === "tags"));
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<tbody>
|
|
||||||
<slot />
|
|
||||||
</tbody>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<td class="border border-light-35 dark:border-dark-35 py-1 px-2">
|
|
||||||
<slot />
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<th class="border border-light-35 dark:border-dark-35 px-4 first:pt-0">
|
|
||||||
<slot />
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<thead>
|
|
||||||
<slot />
|
|
||||||
</thead>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<tr>
|
|
||||||
<slot />
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<ul>
|
|
||||||
<slot />
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
@ -13,7 +13,7 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { z } from 'zod';
|
import { z } from 'zod/v4';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type { SpellConfig } from '~/types/character';
|
||||||
import type { CharacterConfig } from '~/types/character';
|
import type { CharacterConfig } from '~/types/character';
|
||||||
import { CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '~/shared/character.util';
|
import { CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '~/shared/character.util';
|
||||||
import { getText } from '~/shared/i18n';
|
import { getText } from '~/shared/i18n';
|
||||||
|
import { fakeA } from '~/shared/proses';
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
||||||
|
|
@ -16,7 +17,6 @@ const { add } = useToast();
|
||||||
|
|
||||||
const { data, status, error } = await useFetch(`/api/character/${id}`);
|
const { data, status, error } = await useFetch(`/api/character/${id}`);
|
||||||
const compiler = new CharacterCompiler(data.value ?? defaultCharacter);
|
const compiler = new CharacterCompiler(data.value ?? defaultCharacter);
|
||||||
console.log(compiler);
|
|
||||||
const character = ref(compiler.compiled);
|
const character = ref(compiler.compiled);
|
||||||
/*
|
/*
|
||||||
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
|
||||||
|
|
@ -89,26 +89,26 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
|
||||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes légères">Arme légère</PreviewA>
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes légères" label="Arme légère" />
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes de jet">Arme de jet</PreviewA>
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes de jet" label="Arme de jet" />
|
||||||
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="1. Règles/99. Annexes/4. Équipement#Les armes naturelles">Arme naturelle</PreviewA>
|
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes naturelles" label="Arme naturelle" />
|
||||||
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes">Arme standard</PreviewA>
|
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes" label="Arme standard" />
|
||||||
<PreviewA v-if="character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes improvisées">Arme improvisée</PreviewA>
|
<PreviewA v-if="character.mastery.strength > 1" href="regles/annexes/equipement#Les armes improvisées" label="Arme improvisée" />
|
||||||
<PreviewA v-if="character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes lourdes">Arme lourde</PreviewA>
|
<PreviewA v-if="character.mastery.strength > 2" href="regles/annexes/equipement#Les armes lourdes" label="Arme lourde" />
|
||||||
<PreviewA v-if="character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les armes à deux mains">Arme à deux mains</PreviewA>
|
<PreviewA v-if="character.mastery.strength > 3" href="regles/annexes/equipement#Les armes à deux mains" label="Arme à deux mains" />
|
||||||
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes maniables">Arme maniable</PreviewA>
|
<PreviewA v-if="character.mastery.dexterity > 0 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes maniables" label="Arme maniable" />
|
||||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="1. Règles/99. Annexes/4. Équipement#Les armes à projectiles">Arme à projectiles</PreviewA>
|
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 1" href="regles/annexes/equipement#Les armes à projectiles" label="Arme à projectiles" />
|
||||||
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="1. Règles/99. Annexes/4. Équipement#Les armes longues">Arme longue</PreviewA>
|
<PreviewA v-if="character.mastery.dexterity > 1 && character.mastery.strength > 2" href="regles/annexes/equipement#Les armes longues" label="Arme longue" />
|
||||||
<PreviewA v-if="character.mastery.shield > 0" href="1. Règles/99. Annexes/4. Équipement#Les boucliers">Bouclier</PreviewA>
|
<PreviewA v-if="character.mastery.shield > 0" href="regles/annexes/equipement#Les boucliers" label="Bouclier" />
|
||||||
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="1. Règles/99. Annexes/4. Équipement#Les boucliers à deux mains">Bouclier à deux mains</PreviewA>
|
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="regles/annexes/equipement#Les boucliers à deux mains" label="Bouclier à deux mains" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="character.mastery.armor > 0" class="flex flex-col">
|
<div v-if="character.mastery.armor > 0" class="flex flex-col">
|
||||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
|
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
|
||||||
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
<PreviewA v-if="character.mastery.armor > 0" href="1. Règles/99. Annexes/4. Équipement#Les armures légères">Armure légère</PreviewA>
|
<PreviewA v-if="character.mastery.armor > 0" href="regles/annexes/equipement#Les armures légères" label="Armure légère" />
|
||||||
<PreviewA v-if="character.mastery.armor > 1" href="1. Règles/99. Annexes/4. Équipement#Les armures">Armure standard</PreviewA>
|
<PreviewA v-if="character.mastery.armor > 1" href="regles/annexes/equipement#Les armures" label="Armure standard" />
|
||||||
<PreviewA v-if="character.mastery.armor > 2" href="1. Règles/99. Annexes/4. Équipement#Les armures lourdes">Armure lourde</PreviewA>
|
<PreviewA v-if="character.mastery.armor > 2" href="regles/annexes/equipement#Les armures lourdes" label="Armure lourde" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
@ -137,22 +137,22 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-semibold">Actions</span>
|
<span class="text-lg font-semibold">Actions</span>
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
|
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
|
||||||
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" />
|
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-semibold">Réactions</span>
|
<span class="text-lg font-semibold">Réactions</span>
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
|
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
|
||||||
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" />
|
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-semibold">Actions libre</span>
|
<span class="text-lg font-semibold">Actions libre</span>
|
||||||
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
|
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
|
||||||
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" />
|
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-lg font-semibold">Aptitudes</span>
|
<span class="text-lg font-semibold">Aptitudes</span>
|
||||||
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" />
|
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" :properties="{ tags: { a: fakeA } }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import type { CharacterConfig } from '~/types/character';
|
import type { CharacterConfig } from '~/types/character';
|
||||||
|
|
||||||
|
|
@ -7,7 +6,6 @@ definePageMeta({
|
||||||
guestsGoesTo: '/user/login',
|
guestsGoesTo: '/user/login',
|
||||||
})
|
})
|
||||||
const { add } = useToast();
|
const { add } = useToast();
|
||||||
const { user } = useUserSession();
|
|
||||||
|
|
||||||
const { data: characters, error, status } = await useFetch(`/api/character`);
|
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
|
||||||
|
|
@ -23,54 +23,4 @@ onMounted(() => {
|
||||||
<Title>d[any] - Edition de données</Title>
|
<Title>d[any] - Edition de données</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<div ref="container" class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full"></div>
|
<div ref="container" class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full"></div>
|
||||||
<!-- <TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="features">
|
|
||||||
<TabsList class="flex flex-row gap-4 self-center relative px-4">
|
|
||||||
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
|
|
||||||
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples ({{ config.peoples.length }})</TabsTrigger>
|
|
||||||
<TabsTrigger value="training" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Entrainement</TabsTrigger>
|
|
||||||
<TabsTrigger value="abilities" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Compétences ({{ Object.keys(config.abilities).length }})</TabsTrigger>
|
|
||||||
<TabsTrigger value="aspects" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aspects ({{ config.aspects.length }})</TabsTrigger>
|
|
||||||
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts ({{ config.spells.length }})</TabsTrigger>
|
|
||||||
<TabsTrigger value="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Features ({{ Object.keys(config.features).length }})</TabsTrigger>
|
|
||||||
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
|
|
||||||
</TabsList>
|
|
||||||
<div class="flex flex-row outline-none max-w-full w-full relative overflow-hidden">
|
|
||||||
<div class="flex flex-1 outline-none max-w-full overflow-hidden">
|
|
||||||
<TabsContent value="peoples" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class=""></div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="training" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class="">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="abilities" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class=""></div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="aspects" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class=""></div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="spells" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class=""></div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="features" class="outline-none flex gap-4 flex-col overflow-hidden">
|
|
||||||
<div class="flex flex-col w-full gap-2 justify-end items-end relative">
|
|
||||||
<Button icon @click="createFeature"><Icon icon="radix-icons:plus" class="w-6 h-6" /></Button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 overflow-x-hidden pe-2">
|
|
||||||
<div class="flex flex-row gap-2 w-full border-b border-light-35 dark:border-dark-35 pb-2" v-for="feature of config.features">
|
|
||||||
<div class="w-full flex flex-row px-4 gap-8 items-center">
|
|
||||||
<span class="font-mono">{{ feature.id }}</span>
|
|
||||||
<span class="truncate">{{ feature.description }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row gap-2 items-center">
|
|
||||||
<Button icon @click="editFeature(feature.id)"><Icon icon="radix-icons:pencil-1" /></Button>
|
|
||||||
<Button icon @click="deleteFeature(feature.id)"><Icon icon="radix-icons:trash" /></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsRoot> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -95,193 +95,4 @@ onMounted(async () => {
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
editor?.unmount();
|
editor?.unmount();
|
||||||
});
|
});
|
||||||
/* import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
|
|
||||||
import type { FileType, LocalContent, TreeItem } from '#shared/content.util';
|
|
||||||
import { DEFAULT_CONTENT, iconByType, Content, getPath } from '#shared/content.util';
|
|
||||||
import type { Preferences } from '~/types/general';
|
|
||||||
import { fakeA as proseA } from '#shared/proses';
|
|
||||||
import { parsePath } from '~/shared/general.util';
|
|
||||||
import type { CanvasContent } from '~/types/canvas';
|
|
||||||
|
|
||||||
export type TreeItemEditable = LocalContent &
|
|
||||||
{
|
|
||||||
parent?: string;
|
|
||||||
name?: string;
|
|
||||||
customPath?: boolean;
|
|
||||||
children?: TreeItemEditable[];
|
|
||||||
}
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
rights: ['admin', 'editor'],
|
|
||||||
layout: 'null',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { user } = useUserSession();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const open = ref(true), topOpen = ref(true);
|
|
||||||
|
|
||||||
const toaster = useToast();
|
|
||||||
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
|
||||||
|
|
||||||
let navigation = Content.tree as TreeItemEditable[];
|
|
||||||
const selected = ref<TreeItemEditable>();
|
|
||||||
|
|
||||||
const preferences = useCookie<Preferences>('preferences', { default: () => ({ markdown: { editing: 'split' }, canvas: { gridSnap: true, neighborSnap: true, spacing: 32 } }), watch: true, maxAge: 60*60*24*31 });
|
|
||||||
|
|
||||||
watch(selected, async (value, old) => {
|
|
||||||
if(selected.value)
|
|
||||||
{
|
|
||||||
if(!selected.value.content && selected.value.path)
|
|
||||||
{
|
|
||||||
selected.value = await Content.content(selected.value.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
router.replace({ hash: '#' + selected.value!.path || getPath(selected.value!) });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
router.replace({ hash: '' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const debouncedSave = useDebounceFn(save, 60000, { maxWait: 180000 });
|
|
||||||
|
|
||||||
useShortcuts({
|
|
||||||
//meta_s: { usingInput: true, handler: () => save(), prevent: true },
|
|
||||||
meta_n: { usingInput: true, handler: () => add('markdown'), prevent: true },
|
|
||||||
meta_shift_n: { usingInput: true, handler: () => add('folder'), prevent: true },
|
|
||||||
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: 'index' } }), prevent: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
function add(type: FileType): void
|
|
||||||
{
|
|
||||||
if(!navigation)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const news = [...tree.search(navigation, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
|
|
||||||
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
|
|
||||||
const item: TreeItemEditable = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: [], customPath: false, content: DEFAULT_CONTENT[type], owner: -1, timestamp: new Date(), visit: 0 };
|
|
||||||
|
|
||||||
if(!selected.value)
|
|
||||||
{
|
|
||||||
navigation = [...navigation, item];
|
|
||||||
}
|
|
||||||
else if(selected.value?.children)
|
|
||||||
{
|
|
||||||
item.parent = getPath(selected.value);
|
|
||||||
navigation = tree.insertChild(navigation, item.parent, item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
navigation = tree.insertAfter(navigation, getPath(selected.value), item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function updateTree(instruction: Instruction, itemId: string, targetId: string) : TreeItemEditable[] | undefined {
|
|
||||||
if(!navigation)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const item = tree.find(navigation, itemId);
|
|
||||||
const target = tree.find(navigation, targetId);
|
|
||||||
|
|
||||||
if(!item)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (instruction.type === 'reparent') {
|
|
||||||
const path = tree.getPathToItem({
|
|
||||||
current: navigation,
|
|
||||||
targetId: targetId,
|
|
||||||
});
|
|
||||||
if (!path) {
|
|
||||||
console.error(`missing ${path}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const desiredId = path[instruction.desiredLevel];
|
|
||||||
let result = tree.remove(navigation, itemId);
|
|
||||||
result = tree.insertAfter(result, desiredId, item);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// the rest of the actions require you to drop on something else
|
|
||||||
if (itemId === targetId)
|
|
||||||
return navigation;
|
|
||||||
|
|
||||||
if (instruction.type === 'reorder-above') {
|
|
||||||
let result = tree.remove(navigation, itemId);
|
|
||||||
result = tree.insertBefore(result, targetId, item);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.type === 'reorder-below') {
|
|
||||||
let result = tree.remove(navigation, itemId);
|
|
||||||
result = tree.insertAfter(result, targetId, item);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instruction.type === 'make-child') {
|
|
||||||
if(!target || target.type !== 'folder')
|
|
||||||
return;
|
|
||||||
|
|
||||||
let result = tree.remove(navigation, itemId);
|
|
||||||
result = tree.insertChild(result, targetId, item);
|
|
||||||
rebuildPath([item], targetId);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return navigation;
|
|
||||||
}
|
|
||||||
function transform(items: TreeItem[] | undefined): TreeItemEditable[] | undefined
|
|
||||||
{
|
|
||||||
return items?.map(e => ({
|
|
||||||
...e,
|
|
||||||
parent: e.path.substring(0, e.path.lastIndexOf('/')),
|
|
||||||
name: e.path.substring(e.path.lastIndexOf('/') + 1),
|
|
||||||
customPath: e.path.substring(e.path.lastIndexOf('/') + 1) !== parsePath(e.title),
|
|
||||||
children: transform(e.children)
|
|
||||||
})) as TreeItemEditable[] | undefined;
|
|
||||||
}
|
|
||||||
function flatten(items: TreeItemEditable[] | undefined): TreeItemEditable[]
|
|
||||||
{
|
|
||||||
return items?.flatMap(e => [e, ...flatten(e.children)]) ?? [];
|
|
||||||
}
|
|
||||||
function drop(instruction: Instruction, itemId: string, targetId: string)
|
|
||||||
{
|
|
||||||
navigation = updateTree(instruction, itemId, targetId) ?? navigation ?? [];
|
|
||||||
}
|
|
||||||
function rebuildPath(tree: TreeItemEditable[] | null | undefined, parentPath: string)
|
|
||||||
{
|
|
||||||
if(!tree)
|
|
||||||
return;
|
|
||||||
|
|
||||||
tree.forEach(e => {
|
|
||||||
e.parent = parentPath;
|
|
||||||
rebuildPath(e.children, getPath(e));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function save()
|
|
||||||
{
|
|
||||||
if(selected.value && selected.value.content)
|
|
||||||
{
|
|
||||||
selected.value.path = getPath(selected.value);
|
|
||||||
Content.save(selected.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultExpanded = computed(() => {
|
|
||||||
if(router.currentRoute.value.hash)
|
|
||||||
{
|
|
||||||
const split = router.currentRoute.value.hash.substring(1).split('/');
|
|
||||||
split.forEach((e, i) => { if(i !== 0) split[i] = split[i - 1] + '/' + e });
|
|
||||||
return split;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
/*watch(router.currentRoute, (value) => {
|
|
||||||
if(value && value.hash && navigation)
|
|
||||||
selected.value = tree.find(navigation, value.hash.substring(1));
|
|
||||||
else
|
|
||||||
selected.value = undefined;
|
|
||||||
}, { immediate: true }); */
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ZodError } from 'zod';
|
import type { ZodError } from 'zod/v4';
|
||||||
import { schema, type Login } from '~/schemas/login';
|
import { schema, type Login } from '~/schemas/login';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod/v4';
|
||||||
import { schema, type Registration } from '~/schemas/registration';
|
import { schema, type Registration } from '~/schemas/registration';
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { schema } from '~/schemas/login';
|
import { schema } from '~/schemas/login';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod/v4';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import { usersDataTable, usersTable } from '~/db/schema';
|
import { usersTable } from '~/db/schema';
|
||||||
import { eq, or, sql } from 'drizzle-orm';
|
import { eq, or, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
|
|
@ -50,7 +50,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
await clearUserSession(e);
|
await clearUserSession(e);
|
||||||
|
|
||||||
setResponseStatus(e, 401);
|
setResponseStatus(e, 401);
|
||||||
return { success: false, error: new ZodError([{ code: 'custom', path: ['username'], message: 'Identifiant inconnu' }]) };
|
return { success: false, error: new ZodError([{ code: 'custom', input: undefined, path: ['username'], message: 'Identifiant inconnu' }]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = await Bun.password.verify(body.data.password, id.hash);
|
const valid = await Bun.password.verify(body.data.password, id.hash);
|
||||||
|
|
@ -60,7 +60,7 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
await clearUserSession(e);
|
await clearUserSession(e);
|
||||||
|
|
||||||
setResponseStatus(e, 401);
|
setResponseStatus(e, 401);
|
||||||
return { success: false, error: new ZodError([{ code: 'custom', path: ['password'], message: 'Mot de passe incorrect' }]) };
|
return { success: false, error: new ZodError([{ code: 'custom', input: undefined, path: ['password'], message: 'Mot de passe incorrect' }]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = db.query.usersTable.findFirst({
|
const user = db.query.usersTable.findFirst({
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { count, eq, sql } from 'drizzle-orm';
|
import { count, eq, sql } from 'drizzle-orm';
|
||||||
import { ZodError, type ZodIssue } from 'zod';
|
import { ZodError } from 'zod/v4';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { usersDataTable, usersTable } from '~/db/schema';
|
import { usersDataTable, usersTable } from '~/db/schema';
|
||||||
import { schema } from '~/schemas/registration';
|
import { schema } from '~/schemas/registration';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
import sendMail from '~/server/tasks/mail';
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
import type { $ZodIssue } from 'zod/v4/core';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
{
|
{
|
||||||
|
|
@ -47,11 +48,11 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
||||||
|
|
||||||
const errors: ZodIssue[] = [];
|
const errors: $ZodIssue[] = [];
|
||||||
if(!checkUsername || checkUsername.count !== 0)
|
if(!checkUsername || checkUsername.count !== 0)
|
||||||
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
errors.push({ code: 'custom', input: undefined, path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||||
if(!checkEmail || checkEmail.count !== 0)
|
if(!checkEmail || checkEmail.count !== 0)
|
||||||
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
errors.push({ code: 'custom', input: undefined, path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
||||||
|
|
||||||
if(errors.length > 0)
|
if(errors.length > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { hash } from 'bun';
|
import { hash } from 'bun';
|
||||||
import { eq, or } from 'drizzle-orm';
|
import { eq, or } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod/v4';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { usersTable } from '~/db/schema';
|
import { usersTable } from '~/db/schema';
|
||||||
import sendMail from '~/server/tasks/mail';
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { count, eq, sql } from 'drizzle-orm';
|
import { count, eq, sql } from 'drizzle-orm';
|
||||||
import { ZodError, type ZodIssue } from 'zod';
|
import { ZodError } from 'zod/v4';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { usersDataTable, usersTable } from '~/db/schema';
|
import { usersDataTable, usersTable } from '~/db/schema';
|
||||||
import { schema } from '~/schemas/registration';
|
import { schema } from '~/schemas/registration';
|
||||||
import { checkSession, logSession } from '~/server/utils/user';
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
import sendMail from '~/server/tasks/mail';
|
import sendMail from '~/server/tasks/mail';
|
||||||
|
import type { $ZodIssue } from 'zod/v4/core';
|
||||||
|
|
||||||
interface SuccessHandler
|
interface SuccessHandler
|
||||||
{
|
{
|
||||||
|
|
@ -47,11 +48,11 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
||||||
|
|
||||||
const errors: ZodIssue[] = [];
|
const errors: $ZodIssue[] = [];
|
||||||
if(!checkUsername || checkUsername.count !== 0)
|
if(!checkUsername || checkUsername.count !== 0)
|
||||||
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
errors.push({ code: 'custom', input: undefined, path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||||
if(!checkEmail || checkEmail.count !== 0)
|
if(!checkEmail || checkEmail.count !== 0)
|
||||||
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
errors.push({ code: 'custom', input: undefined, path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
||||||
|
|
||||||
if(errors.length > 0)
|
if(errors.length > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod/v4';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||||
import { CharacterValidation } from '#shared/character.util';
|
import { CharacterValidation } from '#shared/character.util';
|
||||||
|
|
@ -38,17 +38,14 @@ export default defineEventHandler(async (e) => {
|
||||||
thumbnail: body.data.thumbnail,
|
thumbnail: body.data.thumbnail,
|
||||||
}).returning({ id: characterTable.id }).get().id;
|
}).returning({ id: characterTable.id }).get().id;
|
||||||
|
|
||||||
if(body.data.leveling.length > 0) tx.insert(characterLevelingTable).values(body.data.leveling.map(e => ({ character: id, level: e[0], choice: e[1] }))).run();
|
if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
|
||||||
|
|
||||||
const training = Object.entries(body.data.training).flatMap(e => e[1].map(_e => ({ character: id, stat: e[0] as MainStat, level: _e[0], choice: _e[1] })));
|
const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
|
||||||
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
|
||||||
|
|
||||||
const modifiers = Object.entries(body.data.modifiers).map((e) => ({ character: id, modifier: e[0] as MainStat, value: e[1] }));
|
|
||||||
if(modifiers.length > 0) tx.insert(characterModifiersTable).values(modifiers).run();
|
|
||||||
|
|
||||||
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
|
if(body.data.spells.length > 0) tx.insert(characterSpellsTable).values(body.data.spells.map(e => ({ character: id, value: e }))).run();
|
||||||
|
|
||||||
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1][0], max: e[1][1] }));
|
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
|
||||||
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { and, count, eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod/v4';
|
||||||
import { usersTable } from '~/db/schema';
|
import { usersTable } from '~/db/schema';
|
||||||
import { schema as registration } from '~/schemas/registration';
|
import { schema as registration } from '~/schemas/registration';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
@ -54,12 +54,12 @@ export default defineEventHandler(async (e) => {
|
||||||
message: 'Unauthorized',
|
message: 'Unauthorized',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(!await Bun.password.verify(body.data.oldPassword, check.hash))
|
if(!await Bun.password.verify(body.data.oldPassword as string, check.hash))
|
||||||
{
|
{
|
||||||
return { success: false, error: "Ancien mot de passe incorrect" };
|
return { success: false, error: "Ancien mot de passe incorrect" };
|
||||||
}
|
}
|
||||||
|
|
||||||
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.newPassword) }).where(eq(usersTable.id, session.user.id)).run();
|
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.newPassword as string) }).where(eq(usersTable.id, session.user.id)).run();
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { eq, getTableColumns, lte } from 'drizzle-orm';
|
import { eq, getTableColumns, lte } from 'drizzle-orm';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod/v4';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { emailValidationTable, usersTable } from '~/db/schema';
|
import { emailValidationTable, usersTable } from '~/db/schema';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ function center(touches: TouchList): Position
|
||||||
function distance(touches: TouchList): number
|
function distance(touches: TouchList): number
|
||||||
{
|
{
|
||||||
const [A, B] = touches;
|
const [A, B] = touches;
|
||||||
|
if(!A || !B) return 0;
|
||||||
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
return Math.hypot(B.clientX - A.clientX, B.clientY - A.clientY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,10 +173,9 @@ export class Node extends EventTarget
|
||||||
{ 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 dark:bg-dark-40` }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NodeEditable extends Node
|
export class NodeEditable extends Node
|
||||||
{
|
{
|
||||||
private static input: HTMLInputElement = dom('input', { class: 'origin-bottom-left tracking-wider border-4 truncate inline-block text-light-100 dark:text-dark-100 absolute bottom-[100%] appearance-none bg-transparent outline-4 mb-2 px-2 py-1 font-thin min-w-4', style: { 'max-width': '100%', 'font-size': 'calc(18px * var(--zoom-multiplier))' }, listeners: { click: e => e.stopImmediatePropagation() } });
|
|
||||||
|
|
||||||
edges: Set<EdgeEditable> = new Set();
|
edges: Set<EdgeEditable> = new Set();
|
||||||
|
|
||||||
private dirty: boolean = false;
|
private dirty: boolean = false;
|
||||||
|
|
@ -310,7 +310,6 @@ export class Edge extends EventTarget
|
||||||
}
|
}
|
||||||
export class EdgeEditable extends Edge
|
export class EdgeEditable extends Edge
|
||||||
{
|
{
|
||||||
private static input: HTMLInputElement = dom('input', { class: 'relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 z-20 -translate-x-1/2 -translate-y-1/2', listeners: { click: e => e.stopImmediatePropagation() } });
|
|
||||||
private focusing: boolean = false;
|
private focusing: boolean = false;
|
||||||
private editing: boolean = false;
|
private editing: boolean = false;
|
||||||
|
|
||||||
|
|
@ -879,9 +878,9 @@ export class CanvasEditor extends Canvas
|
||||||
this.pattern.setAttribute("width", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
this.pattern.setAttribute("width", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
||||||
this.pattern.setAttribute("height", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
this.pattern.setAttribute("height", (this._zoom * CanvasEditor.SPACING).toFixed(3));
|
||||||
|
|
||||||
this.pattern.children[0].setAttribute('cx', (this._zoom).toFixed(3));
|
this.pattern.children[0]!.setAttribute('cx', (this._zoom).toFixed(3));
|
||||||
this.pattern.children[0].setAttribute('cy', (this._zoom).toFixed(3));
|
this.pattern.children[0]!.setAttribute('cy', (this._zoom).toFixed(3));
|
||||||
this.pattern.children[0].setAttribute('r', (this._zoom).toFixed(3));
|
this.pattern.children[0]!.setAttribute('r', (this._zoom).toFixed(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override mount()
|
override mount()
|
||||||
|
|
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"fire"
|
"fire"
|
||||||
],
|
],
|
||||||
"cost": 3,
|
"cost": 3,
|
||||||
"speed": "10 ",
|
"speed": 10,
|
||||||
"concentration": false,
|
"concentration": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"utilitary"
|
"utilitary"
|
||||||
|
|
@ -1474,7 +1474,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "t0uorr9gpk3n2325s4bgozwcjlk4g7af",
|
"id": "t0uorr9gpk3n2325s4bgozwcjlk4g7af",
|
||||||
"name": "No name",
|
"name": "Glisse gracieuse",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1538,7 +1538,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "p2o6hrfw5fgf22wpm7ocsxh6inhul3b1",
|
"id": "p2o6hrfw5fgf22wpm7ocsxh6inhul3b1",
|
||||||
"name": "No name",
|
"name": "Menace statique",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1554,7 +1554,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "31neb3hkr8o2pc6wq5juhkfywqpufi7c",
|
"id": "31neb3hkr8o2pc6wq5juhkfywqpufi7c",
|
||||||
"name": "No name",
|
"name": "Vrombissement assourdissant",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1570,7 +1570,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ci60uqd0xv6bvfpaugu5f1m9c0f4b2ta",
|
"id": "ci60uqd0xv6bvfpaugu5f1m9c0f4b2ta",
|
||||||
"name": "No name",
|
"name": "Pilier de force",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1586,7 +1586,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "q8bn095qcyuqk10wru6oe0mdg6ilsqke",
|
"id": "q8bn095qcyuqk10wru6oe0mdg6ilsqke",
|
||||||
"name": "No name",
|
"name": "Choc de roche",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1618,7 +1618,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1yg0jfxt9tomjk8nfv6refiranzfcmqp",
|
"id": "1yg0jfxt9tomjk8nfv6refiranzfcmqp",
|
||||||
"name": "No name",
|
"name": "Peau de pierre",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1641,7 +1641,7 @@
|
||||||
"earth"
|
"earth"
|
||||||
],
|
],
|
||||||
"cost": null,
|
"cost": null,
|
||||||
"speed": "10 ",
|
"speed": 10,
|
||||||
"concentration": false,
|
"concentration": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"utilitary"
|
"utilitary"
|
||||||
|
|
@ -1689,7 +1689,7 @@
|
||||||
"arcana"
|
"arcana"
|
||||||
],
|
],
|
||||||
"cost": 2,
|
"cost": 2,
|
||||||
"speed": "1 ",
|
"speed": 1,
|
||||||
"concentration": false,
|
"concentration": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"utilitary"
|
"utilitary"
|
||||||
|
|
@ -1705,7 +1705,7 @@
|
||||||
"arcana"
|
"arcana"
|
||||||
],
|
],
|
||||||
"cost": 3,
|
"cost": 3,
|
||||||
"speed": "1 ",
|
"speed": 1,
|
||||||
"concentration": true,
|
"concentration": true,
|
||||||
"tags": [
|
"tags": [
|
||||||
"utilitary"
|
"utilitary"
|
||||||
|
|
@ -1762,7 +1762,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ufggg6m2fbrkb8hsmqmn5p57eow5z4du",
|
"id": "ufggg6m2fbrkb8hsmqmn5p57eow5z4du",
|
||||||
"name": "No name",
|
"name": "Insaisissable",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1785,7 +1785,7 @@
|
||||||
"nature"
|
"nature"
|
||||||
],
|
],
|
||||||
"cost": 2,
|
"cost": 2,
|
||||||
"speed": "1 ",
|
"speed": 1,
|
||||||
"concentration": false,
|
"concentration": false,
|
||||||
"tags": [
|
"tags": [
|
||||||
"utilitary"
|
"utilitary"
|
||||||
|
|
@ -1794,7 +1794,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ia1l0huseavhkm1uhkea50d4j5isz98a",
|
"id": "ia1l0huseavhkm1uhkea50d4j5isz98a",
|
||||||
"name": "No name",
|
"name": "Echange d'énergie",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1810,7 +1810,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "67cka4v8izu8qdfo2mqulkq26fp99etg",
|
"id": "67cka4v8izu8qdfo2mqulkq26fp99etg",
|
||||||
"name": "No name",
|
"name": "Corrosion",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1826,7 +1826,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6mkmnwb4m28kx4n44yy4st9qut218pwc",
|
"id": "6mkmnwb4m28kx4n44yy4st9qut218pwc",
|
||||||
"name": "No name",
|
"name": "Appel de la nature",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1890,7 +1890,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "79l392k10xs5871a26rd5omm7djswuoz",
|
"id": "79l392k10xs5871a26rd5omm7djswuoz",
|
||||||
"name": "No name",
|
"name": "Visions de terreur",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1922,7 +1922,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ewyy5jeqa5zvffqpa2hxg9ehifg2nwj7",
|
"id": "ewyy5jeqa5zvffqpa2hxg9ehifg2nwj7",
|
||||||
"name": "No name",
|
"name": "Manteau de flamme",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -1970,7 +1970,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kxk0yq1qd0b4sl2yyfi9xje6vfkxh0aa",
|
"id": "kxk0yq1qd0b4sl2yyfi9xje6vfkxh0aa",
|
||||||
"name": "No name",
|
"name": "Gel encombrant",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2034,7 +2034,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "54z5hr40pfmsbbeyksgj1ybvdzy2lrg1",
|
"id": "54z5hr40pfmsbbeyksgj1ybvdzy2lrg1",
|
||||||
"name": "No name",
|
"name": "Choc auditif",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2050,7 +2050,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "awt4tmkif5valsga4bcbezhjhckm7mxq",
|
"id": "awt4tmkif5valsga4bcbezhjhckm7mxq",
|
||||||
"name": "No name",
|
"name": "Aura statique",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2066,7 +2066,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "vubi6idvry4vim1oavd9xneakek6jnnq",
|
"id": "vubi6idvry4vim1oavd9xneakek6jnnq",
|
||||||
"name": "No name",
|
"name": "Lame de roc",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2082,7 +2082,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "unqrb8i8erotcaqt9g7e2jn6g0go9kfu",
|
"id": "unqrb8i8erotcaqt9g7e2jn6g0go9kfu",
|
||||||
"name": "No name",
|
"name": "Torgnole rocailleuse",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2098,7 +2098,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1o219d03xifhmcf7v5tb6z1n2dfv9cfh",
|
"id": "1o219d03xifhmcf7v5tb6z1n2dfv9cfh",
|
||||||
"name": "No name",
|
"name": "Faiblesse d'éther",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2162,7 +2162,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "ewkikv0f2p1332ic8m8tlf52f6k4mn3u",
|
"id": "ewkikv0f2p1332ic8m8tlf52f6k4mn3u",
|
||||||
"name": "No name",
|
"name": "Partage d'esprit",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2178,7 +2178,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "xockmmr8iu687kpjw09fua3835gwfzog",
|
"id": "xockmmr8iu687kpjw09fua3835gwfzog",
|
||||||
"name": "No name",
|
"name": "Air chaotique",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2194,7 +2194,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c3f71icemysqn30kcts3sup4rsq3nx7e",
|
"id": "c3f71icemysqn30kcts3sup4rsq3nx7e",
|
||||||
"name": "No name",
|
"name": "Bénédiction des vents",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2210,7 +2210,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "zno89j1hd5jkgafhhar3x5tt17we1rzl",
|
"id": "zno89j1hd5jkgafhhar3x5tt17we1rzl",
|
||||||
"name": "No name",
|
"name": "Pression descendante",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2226,7 +2226,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bov1z1ieb4s7gbzbgps21e1pvouohdwk",
|
"id": "bov1z1ieb4s7gbzbgps21e1pvouohdwk",
|
||||||
"name": "No name",
|
"name": "Bourrasque opposante",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2242,7 +2242,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "p6kpqeqrrhw8iopl9b9jmuxuabtquvps",
|
"id": "p6kpqeqrrhw8iopl9b9jmuxuabtquvps",
|
||||||
"name": "No name",
|
"name": "Epuisement spontané",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2258,7 +2258,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kwy42uq39xk85us5ozf0fm9t79041127",
|
"id": "kwy42uq39xk85us5ozf0fm9t79041127",
|
||||||
"name": "No name",
|
"name": "Echange d'énergie supérieur",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2274,7 +2274,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "o5xy0qagub9x7hsityncleza1ysrglm2",
|
"id": "o5xy0qagub9x7hsityncleza1ysrglm2",
|
||||||
"name": "No name",
|
"name": "Vision dans le noir",
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2402,7 +2402,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bm9ywuo2gdtnzrigr1hg7y4falkrgm10",
|
"id": "bm9ywuo2gdtnzrigr1hg7y4falkrgm10",
|
||||||
"name": "No name",
|
"name": "Permutation",
|
||||||
"rank": 3,
|
"rank": 3,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2450,7 +2450,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fchuvggmgrh89dyx06kk3433a1wf4u3m",
|
"id": "fchuvggmgrh89dyx06kk3433a1wf4u3m",
|
||||||
"name": "No name",
|
"name": "Erection de matière",
|
||||||
"rank": 3,
|
"rank": 3,
|
||||||
"type": "knowledge",
|
"type": "knowledge",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2466,7 +2466,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "kfz4nibuso2um6na7v5z2lc22ng1lgca",
|
"id": "kfz4nibuso2um6na7v5z2lc22ng1lgca",
|
||||||
"name": "No name",
|
"name": "Densité tranchante",
|
||||||
"rank": 3,
|
"rank": 3,
|
||||||
"type": "precision",
|
"type": "precision",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2562,7 +2562,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6z02eebtw1p7wlfc4jned3iwo16bb772",
|
"id": "6z02eebtw1p7wlfc4jned3iwo16bb772",
|
||||||
"name": "No name",
|
"name": "Redirection",
|
||||||
"rank": 3,
|
"rank": 3,
|
||||||
"type": "instinct",
|
"type": "instinct",
|
||||||
"elements": [
|
"elements": [
|
||||||
|
|
@ -2614,10 +2614,7 @@
|
||||||
"name": "Akkatom",
|
"name": "Akkatom",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "strength",
|
"stat": "strength",
|
||||||
"alignment": {
|
"alignment": "neutral_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2638,10 +2635,7 @@
|
||||||
"name": "Anseilid",
|
"name": "Anseilid",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "chaotic_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 13,
|
"difficulty": 13,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2662,10 +2656,7 @@
|
||||||
"name": "Arsinam",
|
"name": "Arsinam",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "constitution",
|
"stat": "constitution",
|
||||||
"alignment": {
|
"alignment": "chaotic_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2686,10 +2677,7 @@
|
||||||
"name": "Asnol",
|
"name": "Asnol",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2710,10 +2698,7 @@
|
||||||
"name": "Beth'oit",
|
"name": "Beth'oit",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "charisma",
|
"stat": "charisma",
|
||||||
"alignment": {
|
"alignment": "loyal_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2734,10 +2719,7 @@
|
||||||
"name": "Brukaur",
|
"name": "Brukaur",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "constitution",
|
"stat": "constitution",
|
||||||
"alignment": {
|
"alignment": "chaotic_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2758,10 +2740,7 @@
|
||||||
"name": "Calderan",
|
"name": "Calderan",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "loyal_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2782,10 +2761,7 @@
|
||||||
"name": "Dao Tua",
|
"name": "Dao Tua",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "charisma",
|
"stat": "charisma",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2806,10 +2782,7 @@
|
||||||
"name": "Digride",
|
"name": "Digride",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "dexterity",
|
"stat": "dexterity",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2830,10 +2803,7 @@
|
||||||
"name": "Drinbuur",
|
"name": "Drinbuur",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "neutral_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2854,10 +2824,7 @@
|
||||||
"name": "Franeline",
|
"name": "Franeline",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "dexterity",
|
"stat": "dexterity",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2878,10 +2845,7 @@
|
||||||
"name": "Goldreg",
|
"name": "Goldreg",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "loyal_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2902,10 +2866,7 @@
|
||||||
"name": "Hashura",
|
"name": "Hashura",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "charisma",
|
"stat": "charisma",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2926,10 +2887,7 @@
|
||||||
"name": "Incabat",
|
"name": "Incabat",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "constitution",
|
"stat": "constitution",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2950,10 +2908,7 @@
|
||||||
"name": "Kaha Bii",
|
"name": "Kaha Bii",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "curiosity",
|
"stat": "curiosity",
|
||||||
"alignment": {
|
"alignment": "loyal_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2974,10 +2929,7 @@
|
||||||
"name": "Kronian",
|
"name": "Kronian",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -2998,10 +2950,7 @@
|
||||||
"name": "Kuelid",
|
"name": "Kuelid",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "loyal_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3022,10 +2971,7 @@
|
||||||
"name": "Lonidae",
|
"name": "Lonidae",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "chaotic_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3046,10 +2992,7 @@
|
||||||
"name": "Miador",
|
"name": "Miador",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "dexterity",
|
"stat": "dexterity",
|
||||||
"alignment": {
|
"alignment": "loyal_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3070,10 +3013,7 @@
|
||||||
"name": "Mul'dekar",
|
"name": "Mul'dekar",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "curiosity",
|
"stat": "curiosity",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3094,10 +3034,7 @@
|
||||||
"name": "Nigiak",
|
"name": "Nigiak",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "charisma",
|
"stat": "charisma",
|
||||||
"alignment": {
|
"alignment": "loyal_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3118,10 +3055,7 @@
|
||||||
"name": "Nyelis",
|
"name": "Nyelis",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "curiosity",
|
"stat": "curiosity",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3142,10 +3076,7 @@
|
||||||
"name": "Onimee",
|
"name": "Onimee",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "dexterity",
|
"stat": "dexterity",
|
||||||
"alignment": {
|
"alignment": "chaotic_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 7,
|
"difficulty": 7,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3166,10 +3097,7 @@
|
||||||
"name": "Othompa",
|
"name": "Othompa",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "neutral_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3190,10 +3118,7 @@
|
||||||
"name": "Promolide",
|
"name": "Promolide",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "constitution",
|
"stat": "constitution",
|
||||||
"alignment": {
|
"alignment": "chaotic_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 11,
|
"difficulty": 11,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3214,10 +3139,7 @@
|
||||||
"name": "Qua'faltar",
|
"name": "Qua'faltar",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "chaotic_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3238,10 +3160,7 @@
|
||||||
"name": "Rudnar",
|
"name": "Rudnar",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "dexterity",
|
"stat": "dexterity",
|
||||||
"alignment": {
|
"alignment": "chaotic_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3262,10 +3181,7 @@
|
||||||
"name": "Shelfine",
|
"name": "Shelfine",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "intelligence",
|
"stat": "intelligence",
|
||||||
"alignment": {
|
"alignment": "chaotic_good",
|
||||||
"kindness": "good",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3286,10 +3202,7 @@
|
||||||
"name": "Shlahog",
|
"name": "Shlahog",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "strength",
|
"stat": "strength",
|
||||||
"alignment": {
|
"alignment": "chaotic_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3310,10 +3223,7 @@
|
||||||
"name": "Thymeïr",
|
"name": "Thymeïr",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "strength",
|
"stat": "strength",
|
||||||
"alignment": {
|
"alignment": "chaotic_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3334,10 +3244,7 @@
|
||||||
"name": "Urdi'rik",
|
"name": "Urdi'rik",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "constitution",
|
"stat": "constitution",
|
||||||
"alignment": {
|
"alignment": "loyal_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3358,10 +3265,7 @@
|
||||||
"name": "Vadeaxil",
|
"name": "Vadeaxil",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "strength",
|
"stat": "strength",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3382,10 +3286,7 @@
|
||||||
"name": "Vernil",
|
"name": "Vernil",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "curiosity",
|
"stat": "curiosity",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 8,
|
"difficulty": 8,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3406,10 +3307,7 @@
|
||||||
"name": "Yinkovn",
|
"name": "Yinkovn",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "psyche",
|
"stat": "psyche",
|
||||||
"alignment": {
|
"alignment": "neutral_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "neutral"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3430,10 +3328,7 @@
|
||||||
"name": "Zaliax",
|
"name": "Zaliax",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "strength",
|
"stat": "strength",
|
||||||
"alignment": {
|
"alignment": "loyal_evil",
|
||||||
"kindness": "evil",
|
|
||||||
"loyalty": "loyal"
|
|
||||||
},
|
|
||||||
"magic": false,
|
"magic": false,
|
||||||
"difficulty": 9,
|
"difficulty": 9,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
@ -3454,10 +3349,7 @@
|
||||||
"name": "Zeniom",
|
"name": "Zeniom",
|
||||||
"description": "",
|
"description": "",
|
||||||
"stat": "charisma",
|
"stat": "charisma",
|
||||||
"alignment": {
|
"alignment": "chaotic_neutral",
|
||||||
"kindness": "neutral",
|
|
||||||
"loyalty": "chaotic"
|
|
||||||
},
|
|
||||||
"magic": true,
|
"magic": true,
|
||||||
"difficulty": 10,
|
"difficulty": 10,
|
||||||
"physic": {
|
"physic": {
|
||||||
|
|
|
||||||
|
|
@ -180,11 +180,10 @@ export const CharacterValidation = z.object({
|
||||||
notes: z.string().nullable().optional(),
|
notes: z.string().nullable().optional(),
|
||||||
health: z.number().default(0),
|
health: z.number().default(0),
|
||||||
mana: z.number().default(0),
|
mana: z.number().default(0),
|
||||||
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())),
|
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number())),
|
||||||
leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
|
leveling: z.record(z.enum(LEVELS.map(String)), z.number()),
|
||||||
abilities: z.record(z.enum(ABILITIES), z.number().optional()),
|
abilities: z.record(z.enum(ABILITIES), z.number()),
|
||||||
spells: z.string().array(),
|
spells: z.string().array(),
|
||||||
modifiers: z.record(z.enum(MAIN_STATS), z.number().optional()),
|
|
||||||
choices: z.record(z.string(), z.array(z.number())),
|
choices: z.record(z.string(), z.array(z.number())),
|
||||||
owner: z.number(),
|
owner: z.number(),
|
||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
|
|
@ -429,7 +428,7 @@ export class CharacterBuilder extends CharacterCompiler
|
||||||
}
|
}
|
||||||
private render()
|
private render()
|
||||||
{
|
{
|
||||||
this._steps = [
|
/*this._steps = [
|
||||||
PeoplePicker,
|
PeoplePicker,
|
||||||
LevelPicker,
|
LevelPicker,
|
||||||
TrainingPicker,
|
TrainingPicker,
|
||||||
|
|
@ -438,9 +437,9 @@ export class CharacterBuilder extends CharacterCompiler
|
||||||
];
|
];
|
||||||
this._stepsHeader = this._steps.map((e, i) =>
|
this._stepsHeader = this._steps.map((e, i) =>
|
||||||
dom("div", { class: "group flex items-center", }, [
|
dom("div", { class: "group flex items-center", }, [
|
||||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.name)]),
|
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
|
||||||
])
|
])
|
||||||
);
|
);*/
|
||||||
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
|
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
|
||||||
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
||||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||||
|
|
@ -461,15 +460,15 @@ export class CharacterBuilder extends CharacterCompiler
|
||||||
if(step < 0 || step >= this._stepsHeader.length)
|
if(step < 0 || step >= this._stepsHeader.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
|
//if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
|
||||||
return;
|
// return;
|
||||||
|
|
||||||
this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
|
//this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
|
||||||
this._stepsHeader[step]!.setAttribute('data-state', 'active');
|
//this._stepsHeader[step]!.setAttribute('data-state', 'active');
|
||||||
|
|
||||||
this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
|
//this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
|
||||||
|
|
||||||
this._helperText.textContent = this._steps[step]!.description;
|
//this._helperText.textContent = this._steps[step]!.description;
|
||||||
}
|
}
|
||||||
async save(leave: boolean = true)
|
async save(leave: boolean = true)
|
||||||
{
|
{
|
||||||
|
|
@ -672,7 +671,7 @@ export class PickableFeature
|
||||||
abstract class BuilderTab {
|
abstract class BuilderTab {
|
||||||
protected _builder: CharacterBuilder;
|
protected _builder: CharacterBuilder;
|
||||||
protected _content!: Array<Node | string>;
|
protected _content!: Array<Node | string>;
|
||||||
static name: string;
|
static header: string;
|
||||||
static description: string;
|
static description: string;
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder) { this._builder = builder; }
|
constructor(builder: CharacterBuilder) { this._builder = builder; }
|
||||||
|
|
@ -694,7 +693,7 @@ class PeoplePicker extends BuilderTab
|
||||||
|
|
||||||
private _activeOption?: HTMLDivElement;
|
private _activeOption?: HTMLDivElement;
|
||||||
|
|
||||||
static override name = 'Peuple';
|
static override header = 'Peuple';
|
||||||
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
|
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder)
|
constructor(builder: CharacterBuilder)
|
||||||
|
|
@ -756,7 +755,7 @@ class LevelPicker extends BuilderTab
|
||||||
private _manaText: Text;
|
private _manaText: Text;
|
||||||
private _options: HTMLDivElement[][];
|
private _options: HTMLDivElement[][];
|
||||||
|
|
||||||
static override name = 'Niveaux';
|
static override header = 'Niveaux';
|
||||||
static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.';
|
static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.';
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder)
|
constructor(builder: CharacterBuilder)
|
||||||
|
|
@ -836,7 +835,7 @@ class TrainingPicker extends BuilderTab
|
||||||
private _statIndicator: HTMLSpanElement;
|
private _statIndicator: HTMLSpanElement;
|
||||||
private _statContainer: HTMLDivElement;
|
private _statContainer: HTMLDivElement;
|
||||||
|
|
||||||
static override name = 'Entrainement';
|
static override header = 'Entrainement';
|
||||||
static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.';
|
static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.';
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder)
|
constructor(builder: CharacterBuilder)
|
||||||
|
|
@ -914,7 +913,7 @@ class AbilityPicker extends BuilderTab
|
||||||
private _tooltips: Text[] = [];
|
private _tooltips: Text[] = [];
|
||||||
private _maxs: HTMLElement[] = [];
|
private _maxs: HTMLElement[] = [];
|
||||||
|
|
||||||
static override name = 'Compétences';
|
static override header = 'Compétences';
|
||||||
static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.';
|
static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.';
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder)
|
constructor(builder: CharacterBuilder)
|
||||||
|
|
@ -1025,7 +1024,7 @@ class AspectPicker extends BuilderTab
|
||||||
|
|
||||||
private _options: HTMLDivElement[];
|
private _options: HTMLDivElement[];
|
||||||
|
|
||||||
static override name = 'Aspect';
|
static override header = 'Aspect';
|
||||||
static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.';
|
static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.';
|
||||||
|
|
||||||
constructor(builder: CharacterBuilder)
|
constructor(builder: CharacterBuilder)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig } from "~/types/character";
|
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
|
||||||
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
|
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
|
||||||
import { MarkdownEditor } from "#shared/editor.util";
|
import { MarkdownEditor } from "#shared/editor.util";
|
||||||
import { fakeA } from "#shared/proses";
|
import { fakeA } from "#shared/proses";
|
||||||
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
||||||
import { fullblocker, tooltip } from "#shared/floating.util";
|
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { ALIGNMENTS, alignmentTexts, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
import { ALIGNMENTS, alignmentTexts, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
||||||
import characterConfig from "#shared/character-config.json";
|
import characterConfig from "#shared/character-config.json";
|
||||||
import { getID, ID_SIZE } from "#shared/general.util";
|
import { getID, ID_SIZE } from "#shared/general.util";
|
||||||
|
|
@ -139,6 +139,13 @@ class TrainingEditor extends BuilderTab
|
||||||
this._builder.edit(config.features[option]!).then(e => {
|
this._builder.edit(config.features[option]!).then(e => {
|
||||||
element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }));
|
element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }));
|
||||||
});
|
});
|
||||||
|
}, contextmenu: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const context = contextmenu(e.clientX, e.clientY, [
|
||||||
|
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau avant') ]),
|
||||||
|
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau après') ]),
|
||||||
|
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { delete config.training[stat][level[0] as any as TrainingLevel]; /* redraw */ } }) } } }, [ text('Supprimer') ])
|
||||||
|
], { placement: "right-start", priority: false });
|
||||||
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
|
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
|
||||||
return element;
|
return element;
|
||||||
})),
|
})),
|
||||||
|
|
@ -197,9 +204,9 @@ class AspectEditor extends BuilderTab
|
||||||
alignment: select(ALIGNMENTS.map(f => ({ text: alignmentTexts[f], value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }),
|
alignment: select(ALIGNMENTS.map(f => ({ text: alignmentTexts[f], value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }),
|
||||||
magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }),
|
magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }),
|
||||||
difficulty: numberpicker({ min: 6, max: 13, input: (value) => aspect.difficulty = value, defaultValue: aspect.difficulty, class: '!m-0 w-full' }),
|
difficulty: numberpicker({ min: 6, max: 13, input: (value) => aspect.difficulty = value, defaultValue: aspect.difficulty, class: '!m-0 w-full' }),
|
||||||
physic: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value }) ]),
|
physic: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value, class: '!m-0' }) ]),
|
||||||
mental: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value }) ]),
|
mental: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value, class: '!m-0' }) ]),
|
||||||
personality: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value }) ]),
|
personality: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value, class: '!m-0' }) ]),
|
||||||
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:file-text'), () => {}, 'p-1'), button(icon('radix-icons:trash'), () => remove(aspect), 'p-1') ])
|
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:file-text'), () => {}, 'p-1'), button(icon('radix-icons:trash'), () => remove(aspect), 'p-1') ])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import render from "#shared/markdown.util";
|
||||||
import { popper } from "#shared/floating.util";
|
import { popper } from "#shared/floating.util";
|
||||||
import { Canvas } from "#shared/canvas.util";
|
import { Canvas } from "#shared/canvas.util";
|
||||||
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
||||||
import { unifySlug } from "#shared/general.util";
|
import { parsePath, unifySlug } from "#shared/general.util";
|
||||||
import { loading } from "./components.util";
|
import { loading } from "./components.util";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,6 +84,7 @@ export const fakeA: Prose = {
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
if(!!overview)
|
if(!!overview)
|
||||||
{
|
{
|
||||||
const magicKeys = useMagicKeys();
|
const magicKeys = useMagicKeys();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue