Rename RedrawableHTML, remove File API rate limite and fix pull job transaction.

This commit is contained in:
Clément Pons 2026-01-27 17:13:40 +01:00
parent e9a892076d
commit a412116b9c
14 changed files with 153 additions and 117 deletions

View File

@ -16,6 +16,7 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
import { z } from 'zod/v4';
import { Icon } from '@iconify/vue';
import { Toaster } from '~~/shared/components';
import { Content } from '~~/shared/content';
definePageMeta({
rights: ['admin'],
@ -51,6 +52,9 @@ async function fetch()
error.value = null;
success.value = true;
if(job.value === 'pull')
await Content.pull(true);
Toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
}
catch(e)

BIN
db.sqlite

Binary file not shown.

View File

@ -178,6 +178,11 @@ export default defineNuxtConfig({
tokensPerInterval: 1
},
}
},
'/api/file/**': {
security: {
rateLimiter: false,
}
}
},
security: {

View File

@ -71,10 +71,10 @@ export default defineTask({
const db = useDatabase();
db.transaction(tx => {
db.delete(projectFilesTable).run();
db.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
db.delete(projectContentTable).run();
db.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
tx.delete(projectFilesTable).run();
tx.insert(projectFilesTable).values(files.map(e => {const { content, ...rest } = e; return rest; })).run();
tx.delete(projectContentTable).run();
tx.insert(projectContentTable).values(files.map(e => ({ content: e.content ? Buffer.from(e.content as string) : null, id: e.id }))).run();
});
return { result: true };

View File

@ -2,7 +2,7 @@ import { z } from "zod/v4";
import type { User } from "~/types/auth";
import characterConfig from '#shared/character-config.json';
import type { Campaign } from "~/types/campaign";
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom";
import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom";
import { button, foldable, loading, numberpicker, tabgroup, Toaster } from "#shared/components";
import { CharacterCompiler, colorByRarity, stateFactory, subnameFactory } from "#shared/character";
import { modal, tooltip } from "#shared/floating";
@ -27,7 +27,7 @@ export const CampaignValidation = z.object({
class CharacterPrinter
{
compiler?: CharacterCompiler;
container: RedrawableHTML;
container: HTMLElement;
name: string;
id: number;
constructor(character: number, name: string)
@ -64,7 +64,7 @@ export class CampaignSheet
private campaign?: Campaign;
private characters!: Array<CharacterPrinter>;
container: RedrawableHTML = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6');
container: HTMLElement = div('flex flex-col flex-1 h-full w-full items-center justify-start gap-6');
ws?: Socket;
constructor(id: string, user: ComputedRef<User | null>)

View File

@ -1,6 +1,6 @@
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
import { clamp, lerp } from "#shared/general";
import { dom, icon, svg, type RedrawableHTML } from "#shared/dom";
import { dom, icon, svg, type HTMLElement } from "#shared/dom";
import render from "#shared/markdown";
import { tooltip } from "#shared/floating";
import { History } from "#shared/history";
@ -200,7 +200,7 @@ export class Node extends EventTarget
{
properties: CanvasNode;
nodeDom?: RedrawableHTML;
nodeDom?: HTMLElement;
constructor(properties: CanvasNode)
{
@ -334,7 +334,7 @@ export class Edge extends EventTarget
{
properties: CanvasEdge;
edgeDom?: RedrawableHTML;
edgeDom?: HTMLElement;
protected from: Node;
protected to: Node;
protected path: Path;
@ -388,7 +388,7 @@ export class EdgeEditable extends Edge
private editing: boolean = false;
private pathDom?: SVGPathElement;
private inputDom?: RedrawableHTML;
private inputDom?: HTMLElement;
constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable)
{
super(properties, from, to);
@ -441,8 +441,8 @@ export class Canvas
protected tweener: Tweener = new Tweener();
private debouncedTimeout: Timer = setTimeout(() => {}, 0);
protected transform!: RedrawableHTML;
container!: RedrawableHTML;
protected transform!: HTMLElement;
container!: HTMLElement;
protected firstX = 0;
protected firstY = 0;

View File

@ -3,7 +3,7 @@ import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses";
import { button, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components";
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom";
import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom";
import { followermenu, fullblocker, tooltip } from "#shared/floating";
import { clamp } from "#shared/general";
import markdown from "#shared/markdown";
@ -701,16 +701,16 @@ function setProperty<T>(root: any, path: string, value: T | ((old: T) => T), for
}
export class CharacterBuilder extends CharacterCompiler
{
private _container: RedrawableHTML;
private _content?: RedrawableHTML;
private _stepsHeader: RedrawableHTML[] = [];
private _container: HTMLElement;
private _content?: HTMLElement;
private _stepsHeader: HTMLElement[] = [];
private _steps: Array<BuilderTabConstructor> = [];
private _stepContent: Array<BuilderTab> = [];
private _currentStep: number = 0;
private _helperText!: Text;
private id?: string;
constructor(container: RedrawableHTML, id?: string)
constructor(container: HTMLElement, id?: string)
{
super(Object.assign({}, defaultCharacter));
this.id = id;
@ -944,7 +944,7 @@ export class CharacterBuilder extends CharacterCompiler
this.add(config.training[stat][level][choice]);
}
}
handleChoice(element: RedrawableHTML, feature: string)
handleChoice(element: HTMLElement, feature: string)
{
const choices = config.features[feature]!.effect.filter(e => e.category === 'choice');
if(choices.length === 0)
@ -981,8 +981,8 @@ type BuilderTabConstructor = {
class PeoplePicker extends BuilderTab
{
private _nameInput: HTMLInputElement;
private _visibilityInput: RedrawableHTML;
private _options: RedrawableHTML[];
private _visibilityInput: HTMLElement;
private _options: HTMLElement[];
static override header = 'Peuple';
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
@ -1043,7 +1043,7 @@ class LevelPicker extends BuilderTab
private _levelInput: HTMLInputElement;
private _pointsInput: HTMLInputElement;
private _options: RedrawableHTML[][];
private _options: HTMLElement[][];
static override header = 'Niveaux';
static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.';
@ -1112,8 +1112,8 @@ class LevelPicker extends BuilderTab
e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
e[1]?.childNodes.forEach((option, j) => {
'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j));
'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j));
});
});
}
@ -1125,11 +1125,11 @@ class LevelPicker extends BuilderTab
class TrainingPicker extends BuilderTab
{
private _pointsInput: HTMLInputElement;
private _options: Record<MainStat, RedrawableHTML[][]>;
private _options: Record<MainStat, HTMLElement[][]>;
private _tab: number = 0;
private _statIndicator: RedrawableHTML;
private _statContainer: RedrawableHTML;
private _statIndicator: HTMLElement;
private _statContainer: HTMLElement;
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.';
@ -1156,7 +1156,7 @@ class TrainingPicker extends BuilderTab
this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, RedrawableHTML[][]>);
this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLElement[][]>);
this._statIndicator = dom('span', { 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' });
this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
@ -1207,7 +1207,7 @@ class TrainingPicker extends BuilderTab
e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
e[1]?.childNodes.forEach((option, j) => {
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as RedrawableHTML).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j)));
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j)));
})
})
});
@ -1223,9 +1223,9 @@ class TrainingPicker extends BuilderTab
class AbilityPicker extends BuilderTab
{
private _pointsInput: HTMLInputElement;
private _options: RedrawableHTML[];
private _options: HTMLElement[];
private _maxs: RedrawableHTML[] = [];
private _maxs: HTMLElement[] = [];
static override header = 'Compétences';
static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.';
@ -1266,7 +1266,7 @@ class AbilityPicker extends BuilderTab
ABILITIES.forEach((e, i) => {
const max = (values[`bonus/abilities/${e}`] ?? 0);
const load = this._options[i]?.lastElementChild as RedrawableHTML | undefined;
const load = this._options[i]?.lastElementChild as HTMLElement | undefined;
const valid = (compiled.abilities[e] ?? 0) <= max;
if(load)
{
@ -1293,7 +1293,7 @@ class AspectPicker extends BuilderTab
private _filter: boolean = true;
private _options: RedrawableHTML[];
private _options: HTMLElement[];
static override header = 'Aspect';
static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.';
@ -1370,7 +1370,7 @@ class AspectPicker extends BuilderTab
this._mentalInput.value = mental.toString();
this._personalityInput.value = personality.toString();
(this._content[1] as RedrawableHTML).replaceChildren(...this._options.filter(e => {
(this._content[1] as HTMLElement).replaceChildren(...this._options.filter(e => {
const id = e.getAttribute('data-aspect')!;
const aspect = config.aspects[id]!;
@ -1498,8 +1498,8 @@ export class CharacterSheet
{
private user: ComputedRef<User | null>;
private character?: CharacterCompiler;
container: RedrawableHTML = div('flex flex-1 h-full w-full items-start justify-center');
private tabs?: RedrawableHTML;
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center');
private tabs?: HTMLElement;
private tab: string = 'actions';
ws?: Socket;
@ -1582,7 +1582,7 @@ export class CharacterSheet
publicNotes.content = this.character!.character.notes!.public!;
privateNotes.content = this.character!.character.notes!.private!;
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: RedrawableHTML }) => {
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => {
character.variables[property] = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
obj.edit.value = (character[property] - character.variables[property]).toString();
obj.edit.replaceWith(obj.readonly);
@ -1624,12 +1624,19 @@ export class CharacterSheet
{ id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab(character) },
{ id: 'notes', title: [ text('Notes') ], content: () => [
div('flex flex-col gap-2', [
div('flex flex-col gap-2 border-b border-light-35 dark:border-dark-35 pb-4', [ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ publicNotes.dom ]) ]),
div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
div('flex flex-col h-full divide-y divide-light-30 dark:divide-dark-30', [
foldable([ div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 flex-1', [ publicNotes.dom ]) ],
[ div('flex flex-row w-full items-center justify-between', [ span('text-lg font-bold', 'Notes publics'), tooltip(button(loadableIcon, saveNotes, 'p-1 items-center justify-center'), 'Enregistrer', 'right') ]), ], {
class: { container: 'flex flex-col gap-2 data-[active]:flex-1 py-2', content: 'h-full' }, open: true
}),
foldable([ div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 flex-1', [ privateNotes.dom ]) ],
[ span('text-lg font-bold', 'Notes privés'), ], {
class: { container: 'flex flex-col gap-2 data-[active]:flex-1 py-2', content: 'h-full' }, open: false
}),
//div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
])
] },
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto' }, switch: v => { this.tab = v; } });
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; } });
this.container.replaceChildren(div('flex flex-col justify-start gap-1 h-full', [
div("flex flex-row gap-4 justify-between", [
@ -1791,7 +1798,7 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.action }),
]),
@ -1806,7 +1813,7 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.reaction }),
]),
@ -1820,7 +1827,7 @@ export class CharacterSheet
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.freeaction[e]?.name }) ]),
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.reaction })
]),
@ -1832,7 +1839,7 @@ export class CharacterSheet
{
return [
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
div('flex flex-row justify-between', [dom('span', { class: 'text-lg font-semibold', text: config.passive[e]?.name }) ]),
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.passive }),
];

View File

@ -1,5 +1,5 @@
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type RedrawableHTML } from "#shared/dom";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type HTMLElement } from "#shared/dom";
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "#shared/floating";
import { clamp } from "#shared/general";
import { Tree } from "#shared/tree";
@ -18,11 +18,11 @@ export function link(children: NodeChildren, properties?: NodeProperties & { act
}
} : undefined }, children);
}
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): RedrawableHTML
export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElement
{
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
}
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<RedrawableHTML>)
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>)
{
let state = { current: loading(size) };
@ -36,7 +36,7 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise
return state;
}
export function button(content: Node | NodeChildren, onClick?: (this: RedrawableHTML) => void, cls?: Class)
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class)
{
const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
@ -73,17 +73,17 @@ 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?: RedrawableHTML) => void
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: RedrawableHTML, target?: RedrawableHTML) {
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?: () => RedrawableHTML, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: RedrawableHTML, container?: RedrawableHTML, 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 }): RedrawableHTML
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
{
let context: { close: Function };
let focused: number | undefined;
@ -151,7 +151,7 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
})
return select;
}
export function multiselect<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): RedrawableHTML
export function multiselect<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
{
let context: { close: Function };
let focused: number | undefined;
@ -225,7 +225,7 @@ export function multiselect<T extends NonNullable<any>>(options: Array<{ text: s
}
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' })
{
let context: { container: RedrawableHTML, content: NodeChildren, close: () => void };
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
let selected = true, tree: StoredOption<T>[] = [];
let focused: number | undefined;
let currentOptions: StoredOption<T>[] = [];
@ -470,10 +470,10 @@ export function foldable(content: Reactive<NodeChildren>, title: NodeChildren, s
fold.toggleAttribute('data-active', settings?.open ?? true);
return fold;
}
type TableRow = Record<string, (() => RedrawableHTML) | RedrawableHTML | string>;
type TableRow = Record<string, (() => HTMLElement) | HTMLElement | string>;
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
{
const render = (item: (() => RedrawableHTML) | RedrawableHTML | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
}
export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } })
@ -494,7 +494,7 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
return element;
}
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: RedrawableHTML, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } })
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: boolean, class?: { container?: Class, icon?: Class } })
{
let state = settings?.defaultValue ?? false;
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
@ -512,7 +512,7 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: Re
}, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);
return element;
}
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): RedrawableHTML
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: Reactive<NodeChildren> }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class }, switch?: (tab: string) => void | boolean }): HTMLElement
{
let focus = settings?.focused ?? tabs[0]?.id;
const titles = tabs.map(e => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
@ -535,15 +535,15 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
content
]);
return container as RedrawableHTML;
return container as HTMLElement;
}
export function floater(container: RedrawableHTML, content: NodeChildren | (() => NodeChildren), settings?: { delay?: number, href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean | { width: number, height: number }, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { delay?: number, href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean | { width: number, height: number }, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
{
let viewport = document.getElementById('mainContainer') ?? undefined;
let diffX, diffY;
let minimizeRect: DOMRect, minimized = false;
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: RedrawableHTML, state: FloatState) => boolean, onhide?: (this: RedrawableHTML, state: FloatState) => boolean } = Object.assign({
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
show: ['mouseenter', 'mousemove', 'focus'],
hide: ['mouseleave', 'blur'],
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
@ -634,7 +634,7 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() =
if(!floating.content.hasAttribute('data-pinned'))
return;
[...this.parentElement?.children ?? []].forEach(e => (e as any as RedrawableHTML).attributeStyleMap.set('z-index', -1));
[...this.parentElement?.children ?? []].forEach(e => (e as any as HTMLElement).attributeStyleMap.set('z-index', -1));
this.attributeStyleMap.set('z-index', 0);
}, { passive: true });
}
@ -717,13 +717,13 @@ export interface ToastConfig
timer?: boolean
type?: ToastType
}
type ToastDom = ToastConfig & { dom: RedrawableHTML };
type ToastDom = ToastConfig & { dom: HTMLElement };
export type ToastType = 'info' | 'success' | 'error';
export class Toaster
{
private static _MAX_DRAG = 150;
private static _list: Array<ToastDom> = [];
private static _container: RedrawableHTML;
private static _container: HTMLElement;
static init()
{
@ -794,3 +794,22 @@ export class Toaster
Toaster._list = Toaster._list.filter(e => e !== config);
}
}
export class DiceRoller
{
private static _dom: HTMLElement;
static init()
{
DiceRoller.dispose();
DiceRoller._dom = dom('div', { attributes: { id: 'dice-roller' }, class: 'fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72 empty:hidden' });
document.body.appendChild(DiceRoller._dom);
}
static dispose()
{
DiceRoller._dom?.remove();
}
static get dom()
{
return DiceRoller._dom;
}
}

View File

@ -2,7 +2,7 @@ import { safeDestr as parse } from 'destr';
import { Canvas, CanvasEditor } from "#shared/canvas";
import render, { renderMDAsText } from "#shared/markdown";
import { confirm, contextmenu, tooltip } from "#shared/floating";
import { cancelPropagation, dom, icon, text, type Node, type RedrawableHTML } from "#shared/dom";
import { cancelPropagation, dom, icon, text, type Node, type HTMLElement } from "#shared/dom";
import { loading } from "#shared/components";
import prose, { h1, h2 } from "#shared/proses";
import { getID, lerp, parsePath } from '~~/shared/general';
@ -381,7 +381,7 @@ export class Content
return handlers[overview.type].fromString(content);
}
static async render(parent: RedrawableHTML, path: string): Promise<Omit<LocalContent, 'content'> | undefined>
static async render(parent: HTMLElement, path: string): Promise<Omit<LocalContent, 'content'> | undefined>
{
parent.appendChild(dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]))
@ -489,7 +489,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
return c.container;
},
renderEditor: (content) => {
let element: RedrawableHTML;
let element: HTMLElement;
if(content.hasOwnProperty('content'))
{
const c = new CanvasEditor(content.content);
@ -527,7 +527,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
])
},
renderEditor: (content) => {
let element: RedrawableHTML;
let element: HTMLElement;
if(content.hasOwnProperty('content'))
{
MarkdownEditor.singleton.content = content.content;
@ -582,11 +582,11 @@ export const iconByType: Record<FileType, string> = {
export class Editor
{
tree!: TreeDOM;
container: RedrawableHTML;
container: HTMLElement;
selected?: Recursive<LocalContent & { element?: RedrawableHTML }>;
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
private instruction: RedrawableHTML;
private instruction: HTMLElement;
private cleanup?: CleanupFn;
private history: History;
@ -638,7 +638,7 @@ export class Editor
if(!action.element)
{
const depth = getPath(action.element as LocalContent).split('/').length;
action.element.element = this.tree.render(action.element as LocalContent, depth) as RedrawableHTML;
action.element.element = this.tree.render(action.element as LocalContent, depth) as HTMLElement;
this.dragndrop(action.element as LocalContent, depth, (action.element as Recursive<LocalContent>).parent);
}
this.tree.tree.insertAt(action.element as Recursive<LocalContent>, action.to as number);
@ -718,7 +718,7 @@ export class Editor
])]);
});
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: RedrawableHTML }> | undefined);
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
this.cleanup = this.setupDnD();
});
@ -740,14 +740,14 @@ export class Editor
private add(type: FileType, nextTo: Recursive<LocalContent>)
{
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: RedrawableHTML }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
}
private remove(item: LocalContent & { element?: RedrawableHTML })
private remove(item: LocalContent & { element?: HTMLElement })
{
this.history.add('overview', 'remove', [{ element: item, from: item.order, to: undefined }], true);
}
private rename(item: LocalContent & { element?: RedrawableHTML })
private rename(item: LocalContent & { element?: HTMLElement })
{
let exists = true;
const change = () =>
@ -769,13 +769,13 @@ export class Editor
text?.parentElement?.replaceChild(input, text);
input.focus();
}
private toggleNavigable(e: Event, item: LocalContent & { element?: RedrawableHTML })
private toggleNavigable(e: Event, item: LocalContent & { element?: HTMLElement })
{
cancelPropagation(e);
this.history.add('overview', 'navigable', [{ element: item, from: item.navigable, to: !item.navigable }], true);
}
private togglePrivate(e: Event, item: LocalContent & { element?: RedrawableHTML })
private togglePrivate(e: Event, item: LocalContent & { element?: HTMLElement })
{
cancelPropagation(e);
@ -800,7 +800,7 @@ export class Editor
element: this.tree.container,
}));
}
private dragndrop(item: Omit<LocalContent & { element?: RedrawableHTML, cleanup?: () => void }, "content">, depth: number, parent?: Omit<LocalContent & { element?: RedrawableHTML }, "content">): CleanupFn
private dragndrop(item: Omit<LocalContent & { element?: HTMLElement, cleanup?: () => void }, "content">, depth: number, parent?: Omit<LocalContent & { element?: HTMLElement }, "content">): CleanupFn
{
item.cleanup && item.cleanup();
@ -896,7 +896,7 @@ export class Editor
{
return handlers[item.type].renderEditor(item);
}
private select(item?: LocalContent & { element?: RedrawableHTML })
private select(item?: LocalContent & { element?: HTMLElement })
{
if(this.selected && item)
{
@ -917,7 +917,7 @@ export class Editor
useRouter().push({ hash: this.selected ? '#' + this.selected.id : '' })
this.container.firstElementChild!.replaceChildren();
this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as RedrawableHTML);
this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as HTMLElement);
}
unmount()
{
@ -937,8 +937,9 @@ export function getPath(item: any): string
return parsePath(item.title) ?? item.path;
}
/* export function buildSpellMD()
import characterConfig from '#shared/character-config.json';
const config = characterConfig as CharacterConfig;
export function buildSpellMD()
{
const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"];
const SPELL_TYPE_TEXTS = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
@ -981,4 +982,4 @@ export function buildTrainingFile()
return Object.entries(config.training).map(e => {
return `# ${mainStatTexts[e[0] as MainStat]}\n` + Object.entries(e[1]).map(_e => `## Niveau ${_e[0]}\n` + _e[1].map(feature => renderMDAsText(getText(config.features[feature]!.description))).join('\nou\n')).join('\n');
}).join('\n');
} */
}

View File

@ -2,14 +2,14 @@ import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'icon
import { loading } from './components';
import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive';
export type RedrawableHTML = HTMLElement;
export type HTMLElement = HTMLElement;
export type Node = HTMLElement | SVGElement | Text | undefined;
export type NodeChildren = Array<Reactive<Node>> | undefined;
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
type Listener<K extends keyof HTMLElementEventMap> = | ((this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any) | {
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
options?: boolean | AddEventListenerOptions;
listener: (this: RedrawableHTML, ev: HTMLElementEventMap[K]) => any;
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
} | undefined;
export interface DOMList<T> extends Array<T>{

View File

@ -7,7 +7,7 @@ import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, c
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
import { tags } from '@lezer/highlight';
import { div, dom, icon, span, type RedrawableHTML } from '~~/shared/dom';
import { div, dom, icon, span, type HTMLElement } from '~~/shared/dom';
import { callout as calloutExtension, calloutKeymap } from '#shared/grammar/callout.extension';
import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension';
import renderMarkdown from '~~/shared/markdown';
@ -56,7 +56,7 @@ class CalloutWidget extends WidgetType
foldable?: boolean;
content: string;
contentMD: RedrawableHTML;
contentMD: HTMLElement;
static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined
{

View File

@ -1,5 +1,5 @@
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom";
import { div, dom, icon, span, text, type NodeChildren, type HTMLElement } from "#shared/dom";
import { MarkdownEditor } from "#shared/editor";
import { preview } from "#shared/proses";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components";
@ -18,10 +18,10 @@ type Rarity = ItemConfig['rarity'];
const config = reactive(characterConfig as CharacterConfig);
export class HomebrewBuilder
{
private _container: RedrawableHTML;
private _tabs: RedrawableHTML;
private _container: HTMLElement;
private _tabs: HTMLElement;
constructor(container: RedrawableHTML)
constructor(container: HTMLElement)
{
this._container = container;
@ -58,7 +58,7 @@ export class HomebrewBuilder
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
};
config.peoples[people.id] = people;
(content[0] as RedrawableHTML).appendChild(peopleRender(people));
(content[0] as HTMLElement).appendChild(peopleRender(people));
}
const render = (people: string, level: Level, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
@ -144,7 +144,7 @@ export class HomebrewBuilder
])
}
const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, RedrawableHTML[][]>);
const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLElement[][]>);
const _statIndicator = dom('span', { 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' });
const _statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(_options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
@ -507,7 +507,7 @@ class FeatureEditor
private _arr: boolean;
private option!: FeatureOption;
container!: RedrawableHTML;
container!: HTMLElement;
constructor(list: Record<string, FeatureOption> | FeatureOption[], id: string, draft: boolean)
{
@ -795,7 +795,7 @@ export class FeaturePanel
}
static edit(feature: Feature): Promise<Feature>
{
let container: RedrawableHTML, close: Function;
let container: HTMLElement, close: Function;
return new Promise<Feature>((success, failure) => {
container = FeaturePanel.render(feature, success, failure);
close = fullblocker([container], {
@ -869,7 +869,7 @@ export class ItemPanel
}
static edit(item: ItemConfig): Promise<ItemConfig>
{
let container: RedrawableHTML, close: Function;
let container: HTMLElement, close: Function;
return new Promise<ItemConfig>((success, failure) => {
container = ItemPanel.render(item, success, failure);
close = fullblocker([container], {

View File

@ -1,5 +1,5 @@
import * as FloatingUI from "@floating-ui/dom";
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type RedrawableHTML } from "./dom";
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type HTMLElement } from "./dom";
import { button } from "./components";
import type { Reactive } from "./reactive";
@ -10,7 +10,7 @@ export interface FloatingProperties
arrow?: boolean;
class?: Class;
style?: Record<string, string | undefined | boolean | number> | string;
viewport?: RedrawableHTML;
viewport?: HTMLElement;
cover?: 'width' | 'height' | 'all' | 'none';
persistant?: boolean;
}
@ -41,8 +41,8 @@ export interface ModalProperties
onClose?: () => boolean | void;
}
type ModalInternals = {
container: RedrawableHTML;
content: RedrawableHTML;
container: HTMLElement;
content: HTMLElement;
stop: Function;
start: Function;
show: Function;
@ -50,7 +50,7 @@ type ModalInternals = {
persistant: boolean;
};
export let teleport: RedrawableHTML, minimizeBox: RedrawableHTML, cache: ModalInternals[] = [], hook: VoidFunction = () => {};
export let teleport: HTMLElement, minimizeBox: HTMLElement, cache: ModalInternals[] = [], hook: VoidFunction = () => {};
export function init()
{
dispose();
@ -76,7 +76,7 @@ function clear()
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
}
export function popper(container: RedrawableHTML, properties?: PopperProperties)
export function popper(container: HTMLElement, properties?: PopperProperties)
{
let state: FloatState = 'hidden', 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" } })]);
@ -233,7 +233,7 @@ export function popper(container: RedrawableHTML, properties?: PopperProperties)
clearTimeout(timeout);
}
function link(element: RedrawableHTML) {
function link(element: HTMLElement) {
(properties?.events?.show ?? ['mouseenter', 'mousemove', 'focus']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, show));
(properties?.events?.hide ?? ['mouseleave', 'blur']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, hide));
}
@ -384,7 +384,7 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
},
}, content, properties);
}
export function tooltip(container: RedrawableHTML, txt: string | Text, placement: FloatingUI.Placement, delay?: number): RedrawableHTML
export function tooltip(container: HTMLElement, txt: string | Text, placement: FloatingUI.Placement, delay?: number): HTMLElement
{
return popper(container, {
arrow: true,

View File

@ -1,5 +1,5 @@
import { Content, type LocalContent } from "./content";
import { dom, type RedrawableHTML } from "./dom";
import { dom, type HTMLElement } from "./dom";
import { clamp } from "./general";
export type Recursive<T> = T & {
@ -138,14 +138,14 @@ export class Tree<T extends Omit<LocalContent, 'content'>>
}
export class TreeDOM
{
container: RedrawableHTML;
tree: Tree<Omit<LocalContent & { element?: RedrawableHTML }, "content">>;
container: HTMLElement;
tree: Tree<Omit<LocalContent & { element?: HTMLElement }, "content">>;
private filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined;
private folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML;
private leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML;
private folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;
private leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;
constructor(folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML, leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => RedrawableHTML, filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined)
constructor(folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined)
{
this.tree = new Tree(Content.tree);
@ -156,7 +156,7 @@ export class TreeDOM
const elements = this.tree.accumulate(this.render.bind(this));
this.container = dom('div', { class: 'list-none select-none text-light-100 dark:text-dark-100 text-sm ps-2' }, elements);
}
render(item: Recursive<Omit<LocalContent & { element?: RedrawableHTML }, "content">>, depth: number): RedrawableHTML | undefined
render(item: Recursive<Omit<LocalContent & { element?: HTMLElement }, "content">>, depth: number): HTMLElement | undefined
{
if(this.filter && !(this.filter(item, depth) ?? true))
return;
@ -187,7 +187,7 @@ export class TreeDOM
{
this.container.replaceChildren(...this.tree.flatten.map(e => e.element!));
}
toggle(item?: Omit<LocalContent & { element?: RedrawableHTML }, 'content'>, state?: boolean)
toggle(item?: Omit<LocalContent & { element?: HTMLElement }, 'content'>, state?: boolean)
{
if(item && item.type === 'folder')
{
@ -202,7 +202,7 @@ export class TreeDOM
});
}
}
opened(item?: Omit<LocalContent & { element?: RedrawableHTML }, 'content'>): boolean | undefined
opened(item?: Omit<LocalContent & { element?: HTMLElement }, 'content'>): boolean | undefined
{
return item ? item.element!.getAttribute('data-state') === 'open' : undefined;
}