Updated legal stuff, added floating popup that can be pin and move. Fix character compiler modifier updates not dirtying all dependents.

This commit is contained in:
2025-10-05 23:54:37 +02:00
parent 89c4476ffb
commit b19d2d1b41
21 changed files with 420 additions and 446 deletions

View File

@@ -1,8 +1,8 @@
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
import { clamp, lerp } from "#shared/general.util";
import { dom, icon, svg, text } from "#shared/dom.util";
import { dom, icon, svg } from "#shared/dom.util";
import render from "#shared/markdown.util";
import { popper, tooltip } from "#shared/floating.util";
import { tooltip } from "#shared/floating.util";
import { History } from "#shared/history.util";
import { preview } from "#shared/proses";
import { SpatialGrid } from "#shared/physics.util";

File diff suppressed because one or more lines are too long

View File

@@ -253,7 +253,15 @@ export class CharacterCompiler
{
protected _character!: Character;
protected _result!: CompiledCharacter;
protected _buffer: Record<string, PropertySum> = {};
protected _buffer: Record<string, PropertySum> = {
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
};
private _variableDirty: boolean = false;
constructor(character: Character)
@@ -265,7 +273,15 @@ export class CharacterCompiler
{
this._character = value;
this._result = defaultCompiledCharacter(value);
this._buffer = {};
this._buffer = {
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/intelligence': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/curiosity': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
};
if(value.people !== undefined)
{
@@ -352,8 +368,8 @@ export class CharacterCompiler
case "list":
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
this._result.lists[feature.list]!.push(feature.item);
else
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter((e: string) => e !== feature.item);
else if(feature.action === 'remove')
this._result.lists[feature.list] = this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e !== feature.item), 1);
return;
case "value":
@@ -364,6 +380,9 @@ export class CharacterCompiler
this._buffer[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return;
case "choice":
const choice = this._character.choices[feature.id];
@@ -386,8 +405,8 @@ export class CharacterCompiler
case "list":
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
this._result.lists[feature.list]!.push(feature.item);
else
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter(e => e !== feature.item);
else if(feature.action === 'add')
this._result.lists[feature.list] = this._result.lists[feature.list]!.splice(this._result.lists[feature.list]!.findIndex((e: string) => e !== feature.item), 1)
return;
case "value":
@@ -398,6 +417,9 @@ export class CharacterCompiler
this._buffer[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return;
case "choice":
const choice = this._character.choices[feature.id];
@@ -410,60 +432,53 @@ export class CharacterCompiler
return;
}
}
protected compile(properties: string[])
protected compile(queue: string[])
{
const queue = properties;
for(let i = 0; i < queue.length; i++)
{
if(queue[i] === undefined || queue[i] === "") continue;
const property = queue[i]!;
const buffer = this._buffer[property];
if(property === "")
continue
if(buffer && buffer._dirty === true)
{
let sum = 0, shortcut = false;
for(let j = 0; j < buffer.list.length; j++)
{
if(typeof buffer.list[j]!.value === 'string') // Add or set a modifier
{
const modifier = this._buffer[buffer.list[j]!.value as string];
if(!modifier)
{
if(!queue.includes(buffer.list[j]!.value as string))
this._buffer[buffer.list[j]!.value as string] = { _dirty: false, list: [], min: -Infinity, value: 0 };
const item = buffer.list[j];
if(!item)
continue;
queue.push(property);
shortcut = true;
break;
}
else if(modifier._dirty)
if(typeof item.value === 'string') // Add or set a modifier
{
const modifier = this._buffer[item.value as string]!;
if(modifier._dirty)
{
//Put it back in queue since its dependencies haven't been resolved yet
queue.push(buffer.list[j]!.value as string);
queue.push(item.value as string);
queue.push(property);
shortcut = true;
break;
}
else
{
if(buffer.list[j]?.operation === 'add')
if(item.operation === 'add')
sum += modifier.value;
else if(buffer.list[j]?.operation === 'set')
else if(item.operation === 'set')
sum = modifier.value;
else if(buffer.list[j]?.operation === 'min')
this._buffer[property]!.min = modifier.value;
else if(item.operation === 'min')
buffer.min = modifier.value;
}
}
else
{
if(buffer.list[j]?.operation === 'add')
sum += buffer.list[j]!.value as number;
else if(buffer.list[j]?.operation === 'set')
sum = buffer.list[j]!.value as number;
else if(buffer.list[j]?.operation === 'min')
this._buffer[property]!.min = buffer.list[j]!.value as number;
if(item.operation === 'add')
sum += item.value as number;
else if(item.operation === 'set')
sum = item.value as number;
else if(item.operation === 'min')
buffer.min = item.value as number;
}
}
@@ -573,7 +588,7 @@ export class CharacterBuilder extends CharacterCompiler
}
async save(leave: boolean = true)
{
if(this.id === 'new')
if(this.id === 'new' || this.id === '-1')
{
//@ts-ignore
this.id = this._character.id = this._result.id = await useRequestFetch()(`/api/character`, {
@@ -1052,8 +1067,6 @@ class AbilityPicker extends BuilderTab
const values = builder.values, compiled = builder.compiled;
const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0);
console.log(ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)));
return ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)).every(e => e) && (values.ability ?? 0) - abilities >= 0;
}
}
@@ -1194,7 +1207,8 @@ export class CharacterSheet
}
else
throw new Error();
}).catch(() => {
}).catch((e) => {
console.error(e);
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
span('text-2xl font-bold tracking-wider', 'Personnage introuvable'),
span(undefined, 'Ce personnage n\'existe pas ou est privé.'),

View File

@@ -1,8 +1,9 @@
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
import { contextmenu, followermenu } from "./floating.util";
import { contextmenu, followermenu, popper, tooltip } from "./floating.util";
import { clamp } from "./general.util";
import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom";
export function link(properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>, children?: NodeChildren)
{
@@ -20,20 +21,21 @@ export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElem
{
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
}
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>): HTMLElement
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>)
{
const load = loading(size);
let state = { current: loading(size) };
fn.then((element) => {
load.replaceWith(element);
state.current.replaceWith(element);
state.current = element;
}).catch(e => {
console.error(e);
load.remove();
state.current.remove();
})
return load;
return state;
}
export function button(content: Node, onClick?: () => void, cls?: Class)
export function button(content: Node, onClick?: (this: HTMLElement) => void, cls?: Class)
{
/*
text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
@@ -44,7 +46,7 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]);
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, [ content ]);
let disabled = false;
Object.defineProperty(btn, 'disabled', {
get: () => disabled,
@@ -75,6 +77,14 @@ export function buttongroup<T extends any>(options: Array<{ text: string, value:
}}}))
return div(['flex flex-row', settings?.class?.container], elements);
}
export function optionmenu(options: Array<{ title: string, click: () => void }>, settings?: { position?: Placement, class?: { container?: Class, option?: Class } }): (target?: HTMLElement) => void
{
let close: () => void;
const element = div(['flex flex-col divide-y divide-light-30 dark:divide-dark-30 text-light-100 dark:text-dark-100', settings?.class?.container], options.map(e => dom('div', { class: ['flex flex-row px-2 py-1 hover:bg-light-35 dark:hover:bg-dark-35 cursor-pointer', settings?.class?.option], text: e.title, listeners: { click: () => { e.click(); close() } } })));
return function(this: HTMLElement, target?: HTMLElement) {
close = followermenu(target ?? this, [ element ], { arrow: true, placement: settings?.position, offset: 8 }).close;
}
}
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
@@ -427,8 +437,7 @@ export function foldable(content: NodeChildren | (() => NodeChildren), title: No
if(state && !_content)
{
_content = typeof content === 'function' ? content() : content;
//@ts-ignore
contentContainer.replaceChildren(..._content);
_content && contentContainer.replaceChildren(..._content.filter(e => !!e));
}
}
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
@@ -472,8 +481,7 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
this.toggleAttribute('data-focus', true);
focus = e.id;
const _content = typeof e.content === 'function' ? e.content() : e.content;
//@ts-expect-error
content.replaceChildren(..._content);
_content && content.replaceChildren(..._content?.filter(e => !!e));
}}}, e.title));
const _content = tabs.find(e => e.id === focus)?.content;
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
@@ -487,13 +495,61 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
configurable: false,
enumerable: false,
value: () => {
const _content = tabs.find(e => e.id === focus)?.content;
//@ts-expect-error
_content && content.replaceChildren(...(typeof _content === 'function' ? _content() : _content))
let _content = tabs.find(e => e.id === focus)?.content;
_content = (typeof _content === 'function' ? _content() : _content);
_content && content.replaceChildren(..._content.filter(e => !!e));
}
})
return container as HTMLDivElement & { refresh: () => void };
}
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none' })
{
let viewport = document.getElementById('mainContainer') ?? undefined;
const dragstart = (e: MouseEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
window.addEventListener('mousemove', dragmove);
window.addEventListener('mouseup', dragend);
};
const dragmove = (e: MouseEvent) => {
const box = floating.content.getBoundingClientRect();
const viewbox = viewport?.getBoundingClientRect();
box.x = clamp(box.x + e.movementX, viewbox?.left ?? 0, (viewbox?.right ?? Infinity) - box.width);
box.y = clamp(box.y + e.movementY, viewbox?.top ?? 0, (viewbox?.bottom ?? Infinity) - box.height);
Object.assign(floating.content.style, {
left: `${box.x}px`,
top: `${box.y}px`,
})
};
const dragend = (e: MouseEvent) => {
e.preventDefault();
window.removeEventListener('mousemove', dragmove);
window.removeEventListener('mouseup', dragend);
};
const floating = popper(container, {
arrow: true,
delay: 150,
offset: 12,
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]',
content: () => [ settings?.pinned !== undefined ? dom('div', { class: 'hidden group-data-[pinned]:flex flex-row gap-4 justify-end border-b border-light-35 dark:border-dark-35 cursor-move', listeners: { mousedown: dragstart } }, [ tooltip(icon('radix-icons:cross-1', { width: 12, height: 12, listeners: { click: (e) => { e.preventDefault(); floating.hide(); } }, class: 'p-1 cursor-pointer' }), 'Fermer', 'right') ]) : undefined, div('min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto box-content', typeof content === 'function' ? content() : content) ],
viewport
});
if(settings?.pinned === false)
floating.content.addEventListener('click', () => {
floating.stop();
floating.content.addEventListener('mousedown', function() {
this.parentElement?.appendChild(this);
}, { passive: true, capture: true })
}, { once: true });
return container;
}
export interface ToastConfig
{
@@ -508,13 +564,13 @@ type ToastDom = ToastConfig & { dom: HTMLElement };
export type ToastType = 'info' | 'success' | 'error';
export class Toaster
{
private static _MAX_DRAG = 130;
private static _MAX_DRAG = 150;
private static _list: Array<ToastDom> = [];
private static _container: HTMLDivElement;
static init()
{
Toaster._container = div('fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72');
Toaster._container = dom('div', { attributes: { id: 'toaster' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72' });
document.body.appendChild(Toaster._container);
}
static add(_config: ToastConfig)

View File

@@ -764,11 +764,11 @@ export class Editor
if (location.current.dropTargets.length === 0)
return;
const target = location.current.dropTargets[0];
const target = location.current.dropTargets[0]!;
const instruction = extractInstruction(target.data);
if (instruction !== null)
this.updateTree(instruction, location.initial.dropTargets[0].data.id as string, target.data.id as string);
this.updateTree(instruction, location.initial.dropTargets[0]!.data.id as string, target.data.id as string);
},
}), autoScrollForElements({
element: this.tree.container,

View File

@@ -1,7 +1,7 @@
import { iconExists, loadIcon } from 'iconify-icon';
export type Node = HTMLElement | SVGElement | Text | undefined;
export type NodeChildren = Array<Node>;
export type NodeChildren = Array<Node> | undefined;
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
@@ -60,7 +60,7 @@ export function span(cls?: Class, text?: string): HTMLSpanElement
{
return dom("span", { class: cls, text: text });
}
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Omit<NodeChildren, 'HTMLElement' | 'Text'>): SVGElementTagNameMap[K]
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: SVGElement[]): SVGElementTagNameMap[K]
{
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
@@ -116,49 +116,63 @@ export interface IconProperties
rotate?: number|string;
style?: Record<string, string | undefined> | string;
class?: Class;
listeners?: {
[K in keyof HTMLElementEventMap]?: Listener<K>
};
}
const iconCache: Map<string, HTMLElement> = new Map();
export function icon(name: string, properties?: IconProperties): HTMLElement
{
let el;
let element;
if(iconCache.has(name))
el = iconCache.get(name)!.cloneNode() as HTMLElement;
element = iconCache.get(name)!.cloneNode() as HTMLElement;
else
{
el = document.createElement('iconify-icon');
element = document.createElement('iconify-icon');
if(!iconExists(name))
loadIcon(name);
el.setAttribute('icon', name);
element.setAttribute('icon', name);
iconCache.set(name, el.cloneNode() as HTMLElement);
iconCache.set(name, element.cloneNode() as HTMLElement);
}
properties?.mode && el.setAttribute('mode', properties?.mode.toString());
properties?.inline && el.toggleAttribute('inline', properties?.inline);
el.toggleAttribute('noobserver', properties?.noobserver ?? true);
properties?.width && el.setAttribute('width', properties?.width.toString());
properties?.height && el.setAttribute('height', properties?.height.toString());
properties?.flip && el.setAttribute('flip', properties?.flip.toString());
properties?.rotate && el.setAttribute('rotate', properties?.rotate.toString());
properties?.mode && element.setAttribute('mode', properties?.mode.toString());
properties?.inline && element.toggleAttribute('inline', properties?.inline);
element.toggleAttribute('noobserver', properties?.noobserver ?? true);
properties?.width && element.setAttribute('width', properties?.width.toString());
properties?.height && element.setAttribute('height', properties?.height.toString());
properties?.flip && element.setAttribute('flip', properties?.flip.toString());
properties?.rotate && element.setAttribute('rotate', properties?.rotate.toString());
if(properties?.class)
{
el.setAttribute('class', mergeClasses(properties.class));
element.setAttribute('class', mergeClasses(properties.class));
}
if(properties?.style)
{
if(typeof properties.style === 'string')
{
el.setAttribute('style', properties.style);
element.setAttribute('style', properties.style);
}
else
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) el.attributeStyleMap.set(k, v);
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) element.attributeStyleMap.set(k, v);
}
if(properties?.listeners)
{
for(let [k, v] of Object.entries(properties.listeners))
{
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
if(typeof value === 'function')
element.addEventListener(key, value.bind(element));
else if(value)
element.addEventListener(key, value.listener.bind(element), value.options);
}
}
return el;
return element;
}
export function mergeClasses(classes: Class): string

View File

@@ -2,15 +2,14 @@ import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureChoice, Fe
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util";
import { preview } from "#shared/proses";
import { button, combobox, foldable, input, multiselect, numberpicker, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
import { button, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import characterConfig from "#shared/character-config.json";
import { getID } from "#shared/general.util";
import renderMarkdown, { renderText } from "#shared/markdown.util";
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
import { Tree } from "#shared/tree";
import { getText } from "#shared/i18n";
import markdown from "#shared/markdown.util";
const config = characterConfig as CharacterConfig;
export class HomebrewBuilder
@@ -33,7 +32,7 @@ export class HomebrewBuilder
{ id: 'spells', title: [ text("Sorts") ], content: () => this.spells() },
{ id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() },
{ id: 'actions', title: [ text("Actions") ], content: () => this.actions() },
], { focused: 'actions', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } });
], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } });
this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom'))
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
@@ -232,6 +231,7 @@ export class HomebrewBuilder
elements: [],
effect: '',
concentration: false,
range: 0,
tags: [],
});
@@ -243,7 +243,7 @@ export class HomebrewBuilder
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
if(e)
{
config.spells = config.spells.filter(e => e !== spell);
this._config.spells = this._config.spells.filter(e => e !== spell);
const element = redraw();
content.parentElement?.replaceChild(element, content);
@@ -259,41 +259,98 @@ export class HomebrewBuilder
{
let editing: { type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string } | undefined;
const render = (type: 'action' | 'reaction' | 'freeaction' | 'passive', feature: { id: string, name: string, description: string, cost?: number }) => {
return div('flex flex-col gap-1', [
div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => feature.name = value, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1' }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:pencil-1'), () => {
}, 'p-1'), 'Modifier', 'left'), tooltip(button(icon('radix-icons:trash'), () => remove(type, feature.id), 'p-1'), 'Supprimer', 'right') ]) ])]),
markdown(getText(feature.description), undefined, { tags: { a: preview }, class: 'px-2' }),
]);
const md = markdownReference(getText(feature.description), undefined, { tags: { a: preview }, class: 'ms-2 px-2 py-1 border-l-4 border-light-30 dark:border-dark-30' });
const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:pencil-1'), () => edit(type, feature.id), 'p-1'), 'Modifier', 'left'), tooltip(button(icon('radix-icons:trash'), () => remove(type, feature.id), 'p-1'), 'Supprimer', 'right') ]);
return {
dom: div('flex flex-col gap-2', [
div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => feature.name = value, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1', max: type === 'action' ? 3 : 2, min: 0 }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, buttons ])]),
md.current,
]),
buttons,
md,
type,
id: feature.id,
};
}
const add = (type: 'action' | 'reaction' | 'freeaction' | 'passive') => {
const feature: { id: string, name: string, description: string, cost?: number } = {
id: getID(),
name: '',
description: getID(),
description: getID(), // i18nID
cost: type === 'action' || type === 'reaction' ? 1 : undefined,
}
this._config.texts[feature.description] = { 'fr_FR': '', default: '' };
this._config[type][feature.id] = feature;
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
const option = render(type, feature);
options.push(option);
optionHolder.appendChild(option.dom);
};
const remove = (type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string) => {
confirm('Voulez vous vraiment supprimer cet effet ?').then(e => {
const feature = this._config[type][id]!;
confirm(`Voulez vous vraiment supprimer l'effet "${feature.name}" ?`).then(e => {
if(e)
{
delete this._config.texts[feature.description];
delete this._config[type][id];
const element = redraw();
content.replaceWith(element);
content = element;
const idx = options.findIndex(e => e.type === type && e.id === id);
options.splice(idx, 1)[0]?.dom.remove();
}
});
};
const redraw = () => div('flex flex-col gap-4', [...Object.values(this._config.action).map(e => render('action', e)), ...Object.values(this._config.reaction).map(e => render('reaction', e)), ...Object.values(this._config.freeaction).map(e => render('freeaction', e)), ...Object.values(this._config.passive).map(e => render('passive', e))]);
let content = redraw();
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), () => add('passive'), 'p-1') ]), content ] ) ];
const edit = (type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string) => {
const feature = this._config[type][id]!;
const option = options.find(e => e.type === type && e.id === id);
if(editing)
{
const idx = options.findIndex(e => e.id === editing!.id && e.type === editing!.type);
const rerender = render(editing.type, this._config[editing.type][editing.id]!);
options[idx]?.dom.replaceWith(rerender.dom);
options[idx] = rerender;
}
editing = { id, type };
const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:check'), () => {
this._config.texts[feature.description].default = editor.content;
this._config.texts[feature.description]['fr_FR'] = editor.content;
const rerender = render(type, feature);
option!.buttons.replaceWith(rerender.buttons);
option!.buttons = rerender.buttons;
option!.md.current.replaceWith(rerender.md.current);
option!.md = rerender.md;
editing = undefined;
}, 'p-1'), 'Valider', 'left'), tooltip(button(icon('radix-icons:cross-1'), () => {
const rerender = render(type, feature);
option!.buttons.replaceWith(rerender.buttons);
option!.buttons = rerender.buttons;
option!.md.current.replaceWith(rerender.md.current);
option!.md = rerender.md;
editing = undefined;
}, 'p-1'), 'Rejeter', 'right') ]);
option!.buttons.replaceWith(buttons);
option!.buttons = buttons;
const editor = MarkdownEditor.singleton;
editor.content = getText(feature.description);
editor.onChange = (value) => {};
const editorDom = div('p-1 border border-light-35 dark:border-dark-35', [ editor.dom ]);
option!.md.current.replaceWith(editorDom);
option!.md.current = editorDom;
}
const options = [...Object.values(this._config.action).map(e => render('action', e)), ...Object.values(this._config.reaction).map(e => render('reaction', e)), ...Object.values(this._config.freeaction).map(e => render('freeaction', e)), ...Object.values(this._config.passive).map(e => render('passive', e))];
const optionHolder = div('flex flex-col gap-4', options.map(e => e.dom));
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Action', click: () => add('action') }, { title: 'Réaction', click: () => add('reaction') }, { title: 'Action libre', click: () => add('freeaction') }, { title: 'Passif', click: () => add('passive') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ];
}
edit(feature: Feature): Promise<Feature>
{
@@ -384,7 +441,7 @@ export class FeatureEditor
private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
{
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: preview } }) ]),
div('px-4 flex items-center h-full', [ markdown(textFromEffect(effect), undefined, { tags: { a: preview } }) ]),
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
content.replaceWith(this._edit(effect));
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifieur", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
@@ -458,20 +515,16 @@ export class FeatureEditor
{
if(buffer.list === 'spells')
{
bottom = [ combobox(config.spells.map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
bottom = [ combobox(config.spells.map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
}
else
else if(buffer.list)
{
const editor = new MarkdownEditor();
editor.content = getText(buffer.item);
editor.onChange = (item) => (buffer as FeatureList).item = item;
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ];
bottom = [ combobox(Object.values(config[buffer.list]).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
}
}
else
{
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract<FeatureItem, { category: 'list' }>).item)) : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: (e as Extract<FeatureItem, { category: 'list' }>).item })), { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ];
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map((e) => (e as FeatureList).list === 'spells' ? config.spells.find(f => f.id === (e as FeatureList).item) : config[(e as FeatureList).list][(e as FeatureList).item]).map((e) => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ];
}
break;
case 'choice':
@@ -735,7 +788,7 @@ function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
switch(splited[1])
{
case 'defense':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${resistanceTexts[splited[2] as Resistance]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${resistanceTexts[splited[2] as Resistance]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${resistanceTexts[splited[2] as Resistance]} = interdit).` });
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${mainStatTexts[splited[2] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${mainStatTexts[splited[2] as MainStat]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${mainStatTexts[splited[2] as MainStat]} = interdit).` });
case 'abilities':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` });
default: return 'Bonus inconnu';

View File

@@ -40,16 +40,17 @@ export function init()
document.body.appendChild(teleport);
}
export function popper(container: HTMLElement, properties?: PopperProperties): HTMLElement
export function popper(container: HTMLElement, properties?: PopperProperties)
{
let shown = false, timeout: Timer;
const arrow = svg('svg', { class: '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: ['fixed hidden', properties?.class], style: properties?.style, attributes: { 'data-state': 'closed' } });
let shown = false, 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 ]);
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
function update()
{
FloatingUI.computePosition(container, content, {
FloatingUI.computePosition(container, floater, {
placement: properties?.placement,
strategy: 'fixed',
middleware: [
@@ -59,14 +60,14 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
FloatingUI.flip({ rootBoundary: rect }),
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
if(properties?.cover === 'width' || properties?.cover === 'all')
content.style.width = `${availableWidth}px`;
floater.style.maxWidth = `${availableWidth}px`;
if(properties?.cover === 'height' || properties?.cover === 'all')
content.style.height = `${availableHeight}px`;
floater.style.maxHeight = `${availableHeight}px`;
} }),
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(content.style, {
Object.assign(floater.style, {
left: `${x}px`,
top: `${y}px`,
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
@@ -74,7 +75,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
const side = placement.split('-')[0] as FloatingUI.Side;
content.setAttribute('data-side', side);
floater.setAttribute('data-side', side);
if(middlewareData.arrow)
{
@@ -99,25 +100,25 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
top: arrowY != null ? `${arrowY}px` : '',
right: '',
bottom: '',
[staticSide]: `-8px`,
[staticSide]: `-7px`,
transform: `rotate(${rotation}deg)`,
});
}
});
}
let stop: () => void | undefined;
let _stop: () => void | undefined, empty = true;
function show()
{
if(shown || !properties?.onShow || properties?.onShow() !== false)
{
if(typeof properties?.content === 'function')
{
properties.content = properties.content();
}
if(content.children.length === 0 && (properties?.content && properties.content.length > 0 || properties?.arrow))
if(properties?.content && empty)
{
content.replaceChildren(...(properties!.content as Node[]), arrow);
content.replaceChildren(...properties!.content.filter(e => !!e));
empty = false;
}
clearTimeout(timeout);
@@ -125,13 +126,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
timeout = setTimeout(() => {
if(!shown)
{
teleport!.appendChild(content);
teleport!.appendChild(floater);
content.setAttribute('data-state', 'open');
content.classList.toggle('hidden', false);
floater.setAttribute('data-state', 'open');
floater.classList.toggle('hidden', false);
update();
stop = FloatingUI.autoUpdate(container, content, update, {
_stop = FloatingUI.autoUpdate(container, floater, update, {
animationFrame: true,
layoutShift: false,
elementResize: false,
@@ -146,18 +147,43 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
function hide()
{
if(!properties?.onHide || properties?.onHide() !== false)
if(!manualStop && (!properties?.onHide || properties?.onHide() !== false))
{
clearTimeout(timeout);
timeout = setTimeout(() => {
content.remove();
stop && stop();
floater.remove();
_stop && _stop();
floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true);
shown = false;
}, shown ? properties?.delay ?? 0 : 0);
}
}
function start()
{
manualStop = false;
floater.toggleAttribute('data-pinned', false);
update();
_stop = FloatingUI.autoUpdate(container, floater, update, {
animationFrame: true,
layoutShift: false,
elementResize: false,
ancestorScroll: false,
ancestorResize: false,
});
}
function stop()
{
manualStop = true;
floater.toggleAttribute('data-pinned', true);
_stop && _stop();
clearTimeout(timeout);
}
function link(element: HTMLElement) {
Object.entries({
@@ -172,9 +198,31 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
}
link(container);
link(content);
link(floater);
return container;
return { container, content: floater, stop, start, show: () => {
if(!shown)
{
teleport!.appendChild(floater);
floater.setAttribute('data-state', 'open');
floater.classList.toggle('hidden', false);
update();
}
shown = true;
}, hide: () => {
floater.remove();
_stop && _stop();
floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true);
manualStop = false;
floater.toggleAttribute('data-pinned', false);
shown = false;
} };
}
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
{
@@ -290,14 +338,17 @@ export function tooltip(container: HTMLElement, txt: string | Text, placement: F
arrow: true,
offset: 8,
delay: delay,
content: [ typeof txt === 'string' ? text(txt) : txt ],
content: () => [ typeof txt === 'string' ? text(txt) : txt ],
placement: placement,
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50 max-w-96"
});
class: "border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50 max-w-96"
}).container;
}
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
{
if(!content)
return { close: () => {} };
const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove();
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } });
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);

View File

@@ -4,13 +4,13 @@ import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td
import { heading } from "hast-util-heading";
import { headingRank } from "hast-util-heading-rank";
import { parseId } from "#shared/general.util";
import { async, loading } from "#shared/components.util";
import { async } from "#shared/components.util";
export function renderMarkdown(markdown: Root, proses: Record<string, Prose>): HTMLDivElement
{
return dom('div', {}, markdown.children.map(e => renderContent(e, proses)));
}
export function renderText(markdown: string): string
export function renderMDAsText(markdown: string): string
{
return useMarkdown().text(markdown);
}
@@ -43,9 +43,9 @@ export interface MDProperties
style?: string | Record<string, string>;
tags?: Record<string, Prose>;
}
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
export function markdownReference(content: string, filter?: string, properties?: MDProperties)
{
return async('large', useMarkdown().parse(content).then(data => {
const state = async('large', useMarkdown().parse(content).then(data => {
if(filter)
{
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
@@ -53,11 +53,11 @@ export default function(content: string, filter?: string, properties?: MDPropert
if(start !== -1)
{
let end = start;
const rank = headingRank(data.children[start])!;
const rank = headingRank(data.children[start]!)!;
while(end < data.children.length)
{
end++;
if(heading(data.children[end]) && headingRank(data.children[end])! <= rank)
if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank)
break;
}
data = { ...data, children: data.children.slice(start, end) };
@@ -70,4 +70,9 @@ export default function(content: string, filter?: string, properties?: MDPropert
return el;
}));
return state;
}
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
{
return markdownReference(content, filter, properties).current;
}

View File

@@ -5,7 +5,7 @@ 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, loading } from "./components.util";
import { async, floater, loading } from "./components.util";
export type CustomProse = (properties: any, children: NodeChildren) => Node;
@@ -20,7 +20,7 @@ export const a: Prose = {
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
const el = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
const element = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: {
'click': (e) => {
e.preventDefault();
router.push(link);
@@ -32,35 +32,19 @@ export const a: Prose = {
])
]);
if(!!overview)
{
popper(el, {
arrow: true,
delay: 150,
offset: 12,
cover: "height",
placement: 'bottom-start',
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]',
content: () => {
return [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
}
if(_content?.type === 'canvas')
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
}
return div('');
}))];
},
viewport: document.getElementById('mainContainer') ?? undefined
});
}
return el;
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
}
if(_content?.type === 'canvas')
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
}
return div('');
})).current], { position: 'bottom-start', pinned: false }) : element;
}
}
export const preview: Prose = {
@@ -99,13 +83,13 @@ export const preview: Prose = {
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
}
return div('');
}))];
})).current];
},
onShow() {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
},
}) : el;
}).container : el;
}
}
export const callout: Prose = {