diff --git a/db.sqlite b/db.sqlite index e801226..2f5a8d2 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index 944e302..fe9ac28 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index 0c6d66e..e69de29 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/shared/character.util.ts b/shared/character.util.ts index 5f23067..24b8b13 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1368,7 +1368,7 @@ export class CharacterSheet div("flex flex-col gap-4 py-1 w-60", [ div("flex flex-col py-1 gap-4", [ div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]), + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") ]), @@ -1383,29 +1383,29 @@ export class CharacterSheet div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]), + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") ]), character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', size: 'small' }) : undefined, - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', size: 'small' }) : undefined, - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', size: 'small' }) : undefined, - character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', size: 'small' }) : undefined, - character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', size: 'small' }) : undefined, - character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', size: 'small' }) : undefined, - character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', size: 'small' }) : undefined, - character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', size: 'small' }) : undefined, - character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', size: 'small' }) : undefined, - character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', size: 'small' }) : undefined, - character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', size: 'small' }) : undefined, - character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', size: 'small' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée' }) : undefined, + character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde' }) : undefined, + character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains' }) : undefined, + character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue' }) : undefined, + character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier' }) : undefined, + character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains' }) : undefined, ]) : undefined, character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ - character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', size: 'small' }) : undefined, - character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', size: 'small' }) : undefined, - character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', size: 'small' }) : undefined, + character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère' }) : undefined, + character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard' }) : undefined, + character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde' }) : undefined, ]) : undefined, div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ diff --git a/shared/components.util.ts b/shared/components.util.ts index 9349301..abd15e8 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -1,6 +1,6 @@ -import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router"; +import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router"; import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util"; -import { contextmenu, followermenu, popper, tooltip } from "./floating.util"; +import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util"; import { clamp } from "./general.util"; import { Tree } from "./tree"; import type { Placement } from "@floating-ui/dom"; @@ -502,14 +502,18 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: }) return container as HTMLDivElement & { refresh: () => void }; } -export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array, hide: Array, onshow?: (this: HTMLElement) => boolean, onhide?: (this: HTMLElement) => boolean }, title?: string }) +export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array, hide: Array, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean }, title?: string }) { let viewport = document.getElementById('mainContainer') ?? undefined; - let diffX, diffY, targetWidth, targetHeight; + let diffX, diffY; + let minimizeBox: DOMRect, minimized = false; const dragstart = (e: MouseEvent) => { e.preventDefault(); - e.stopImmediatePropagation(); + + if(minimized) + return; + window.addEventListener('mousemove', dragmove); window.addEventListener('mouseup', dragend); @@ -519,7 +523,6 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N }; const resizestart = (e: MouseEvent) => { e.preventDefault(); - e.stopImmediatePropagation(); window.addEventListener('mousemove', resizemove); window.addEventListener('mouseup', resizeend); }; @@ -557,6 +560,31 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N window.removeEventListener('mousemove', resizemove); window.removeEventListener('mouseup', resizeend); }; + + const minimize = () => { + minimized = !minimized; + floating.content.toggleAttribute('data-minimized', minimized); + if(minimized) + { + minimizeBox = floating.content.getBoundingClientRect(); + Object.assign(floating.content.style, { + left: `0px`, + top: `initial`, + bottom: `0px`, + width: `150px`, + height: `21px`, + }); + } + else + { + Object.assign(floating.content.style, { + left: `${minimizeBox.left}px`, + top: `${minimizeBox.top}px`, + width: `${minimizeBox.width}px`, + height: `${minimizeBox.height}px`, + }); + } + }; const floating = popper(container, { arrow: true, @@ -565,28 +593,38 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N cover: settings?.cover, placement: settings?.position, class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full', - content: () => [ settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row justify-end border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'w-full cursor-move text-xs', listeners: { mousedown: dragstart }, text: settings?.title }), tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { e.stopImmediatePropagation(); floating.hide(); } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'right') ]) : undefined, div('h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) ], - viewport + content: () => [ + settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }), settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined, settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { useRouter().push(settings.href!); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined, tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { e.stopImmediatePropagation(); floating.hide(); } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined, + div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) + ], + viewport, + events: settings?.events, }); if(settings?.pinned === false) { - floating.content.addEventListener('click', () => { + floating.content.addEventListener('dblclick', () => { if(floating.content.hasAttribute('data-pinned')) return; const box = floating.content.children.item(0)!.getBoundingClientRect(); + const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight }; Object.assign(floating.content.style, { + left: `${clamp(box.left, viewbox.left, viewbox.right)}px`, + top: `${clamp(box.top, viewbox.top, viewbox.bottom)}px`, width: `${box.width + 21}px`, height: `${box.height + 21}px`, }); + floating.content.attributeStyleMap.delete('bottom'); + floating.content.attributeStyleMap.delete('right'); floating.stop(); floating.content.addEventListener('mousedown', function() { if(!floating.content.hasAttribute('data-pinned')) return; - this.parentElement?.appendChild(this); + [...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1)); + this.attributeStyleMap.set('z-index', 0); }, { passive: true }); }); } diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 8fb5808..f841adc 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -17,6 +17,7 @@ export interface FollowerProperties extends FloatingProperties blur?: () => void; priority?: boolean; } +export type FloatState = 'shown' | 'showing' | 'hidden' | 'hiding' | 'pinned'; export interface PopperProperties extends FloatingProperties { content?: NodeChildren | (() => NodeChildren); @@ -24,8 +25,8 @@ export interface PopperProperties extends FloatingProperties events?: { show: Array; hide: Array; - onshow?: () => boolean; - onhide?: () => boolean; + onshow?: (state: FloatState) => boolean; + onhide?: (state: FloatState) => boolean; }; } @@ -45,7 +46,7 @@ export function init() export function popper(container: HTMLElement, properties?: PopperProperties) { - let state: 'shown' | 'showing' | 'hidden' | 'hiding' | 'pinned' = 'hidden', manualStop = false, timeout: Timer; + let state: FloatState = 'hidden', manualStop = false, timeout: Timer; const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]); const content = dom('div', { class: properties?.class, style: properties?.style }); const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]); @@ -113,7 +114,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties) let _stop: () => void | undefined, empty = true; function show() { - if(state !== 'shown' && state !== 'showing' && state !== 'pinned' && (!properties?.events?.onshow || properties?.events?.onshow() !== false)) + if(state !== 'shown' && state !== 'showing' && state !== 'pinned' && (!properties?.events?.onshow || properties?.events?.onshow(state) !== false)) { if(typeof properties?.content === 'function') properties.content = properties.content(); diff --git a/shared/proses.ts b/shared/proses.ts index 6516c48..ed65407 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -1,11 +1,11 @@ import { dom, icon, type NodeChildren, type Node, div, type Class } from "#shared/dom.util"; import { parseURL } from 'ufo'; import render from "#shared/markdown.util"; -import { popper } from "#shared/floating.util"; import { Canvas } from "#shared/canvas.util"; import { Content, iconByType, type LocalContent } from "#shared/content.util"; -import { parsePath, unifySlug } from "#shared/general.util"; -import { async, floater, loading } from "./components.util"; +import { unifySlug } from "#shared/general.util"; +import { async, floater } from "./components.util"; +import type { FloatState } from "./floating.util"; export type CustomProse = (properties: any, children: NodeChildren) => Node; @@ -44,11 +44,11 @@ export const a: Prose = { return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); } return div(''); - })).current], { position: 'bottom-start', pinned: false }) : element; + })).current], { position: 'bottom-start', pinned: false, title: properties?.label }) : element; } } export const preview: Prose = { - custom(properties: { href: string, class?: Class, size?: 'small' | 'large' }, children) { + custom(properties: { href: string, class?: Class, label: string }, children) { const href = properties.href as string; const { hash, pathname } = parseURL(href); const router = useRouter(); @@ -77,10 +77,10 @@ export const preview: Prose = { events: { show: ['mouseenter', 'mousemove'], hide: ['mouseleave'], - onshow() { - return !magicKeys.current.has('control') || !magicKeys.current.has('meta'); + onshow(state: FloatState) { + return state === 'shown' || state === 'hiding' || magicKeys.current.has('control') || magicKeys.current.has('meta'); } - }, }) : element; + }, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element; } } export const callout: Prose = {