Remove unused components, change zod to v4 and cahnge a few character properties

This commit is contained in:
Clément Pons 2025-08-26 13:21:42 +02:00
parent 5387dc66c3
commit 80a94bee86
60 changed files with 170 additions and 2742 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<template>
<em>
<slot />
</em>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<template>
<ol>
<slot />
</ol>
</template>

View File

@ -1,3 +0,0 @@
<template>
<p><slot /></p>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<template>
<small class="text-light-60 dark:text-dark-60 text-sm italic">
<slot />
</small>
</template>

View File

@ -1,5 +0,0 @@
<template>
<strong>
<slot />
</strong>
</template>

View File

@ -1,5 +0,0 @@
<template>
<table class="mx-4 my-8 border-collapse border border-light-35 dark:border-dark-35">
<slot />
</table>
</template>

View File

@ -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>

View File

@ -1,5 +0,0 @@
<template>
<tbody>
<slot />
</tbody>
</template>

View File

@ -1,5 +0,0 @@
<template>
<td class="border border-light-35 dark:border-dark-35 py-1 px-2">
<slot />
</td>
</template>

View File

@ -1,5 +0,0 @@
<template>
<th class="border border-light-35 dark:border-dark-35 px-4 first:pt-0">
<slot />
</th>
</template>

View File

@ -1,5 +0,0 @@
<template>
<thead>
<slot />
</thead>
</template>

View File

@ -1,5 +0,0 @@
<template>
<tr>
<slot />
</tr>
</template>

View File

@ -1,5 +0,0 @@
<template>
<ul>
<slot />
</ul>
</template>

BIN
db.sqlite

Binary file not shown.

View File

@ -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({

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -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';

View File

@ -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({

View File

@ -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)
{ {

View File

@ -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';

View File

@ -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)
{ {

View File

@ -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;

View File

@ -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 };
} }

View File

@ -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';

View File

@ -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()

View File

@ -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": {

View File

@ -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)

View File

@ -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') ])
}; };
} }

View File

@ -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();