Fix comrpessing bug on null buffers, make pinned floaters resizable and optimize a few things here and there

This commit is contained in:
Clément Pons 2025-10-06 17:42:16 +02:00
parent b19d2d1b41
commit 26aa0847d9
14 changed files with 133 additions and 91 deletions

View File

@ -2,7 +2,9 @@ import { Database } from "bun:sqlite";
import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from '../db/schema';
let instance: BunSQLiteDatabase<typeof schema>;
let instance: BunSQLiteDatabase<typeof schema> & {
$client: Database;
};
export default function useDatabase()
{
if(!instance)
@ -13,6 +15,7 @@ export default function useDatabase()
instance.run("PRAGMA journal_mode = WAL;");
instance.run("PRAGMA foreign_keys = true;");
instance.run("PRAGMA optimize=0x10002;");
}
return instance;

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -35,7 +35,7 @@ export default defineEventHandler(async (e) => {
return data.content;
}
return;
return null;
}
catch(_e)
{

View File

@ -16,5 +16,10 @@ export default defineEventHandler(async (event) => {
//@ts-expect-error
_end.call(event.node.res, await Bun.zstdCompress(buffer), ...args);
}
else
{
//@ts-expect-error
_end.call(event.node.res, body, ...args);
}
};
});

View File

@ -105,8 +105,8 @@ function reshapeContent(content: string, type: FileType): string | null
return content;
case "canvas":
const data = JSON.parse(content) as CanvasContent;
data.edges?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); });
data.nodes?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); });
data.edges?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
data.nodes?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
return JSON.stringify(data);
default:
case 'folder':

View File

@ -2,7 +2,7 @@ export function hasPermissions(userPermissions: string[], neededPermissions: str
{
for(let i = 0; i < neededPermissions.length; i++)
{
const list = neededPermissions[i].split(' ');
const list = neededPermissions[i]!.split(' ');
if(list.every(e => userPermissions.includes(e)))
{

View File

@ -173,7 +173,6 @@ export class Node extends EventTarget
{ border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` }
}
}
export class NodeEditable extends Node
{
edges: Set<EdgeEditable> = new Set();
@ -440,6 +439,7 @@ export class Canvas
mount()
{
const dragMove = (e: MouseEvent) => {
e.preventDefault();
this.dragMove(e);
};
const dragEnd = (e: MouseEvent) => {
@ -515,6 +515,7 @@ export class Canvas
this.container.removeEventListener('touchmove', touchmove);
};
const touchmove = (e: TouchEvent) => {
e.preventDefault();
const pos = center(e.touches);
this._x = this.visualX = this._x - (this.lastX - pos.x) / this._zoom;
this._y = this.visualY = this._y - (this.lastY - pos.y) / this._zoom;

View File

@ -502,32 +502,61 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
})
return container as HTMLDivElement & { refresh: () => void };
}
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none' })
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement) => boolean, onhide?: (this: HTMLElement) => boolean }, title?: string })
{
let viewport = document.getElementById('mainContainer') ?? undefined;
let diffX, diffY, targetWidth, targetHeight;
const dragstart = (e: MouseEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
window.addEventListener('mousemove', dragmove);
window.addEventListener('mouseup', dragend);
const box = floating.content.getBoundingClientRect();
diffX = e.clientX - box.x;
diffY = e.clientY - box.y;
};
const resizestart = (e: MouseEvent) => {
e.preventDefault();
e.stopImmediatePropagation();
window.addEventListener('mousemove', resizemove);
window.addEventListener('mouseup', resizeend);
};
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);
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
box.x = clamp(e.clientX - diffX!, viewbox?.left ?? 0, viewbox.right - box.width);
box.y = clamp(e.clientY - diffY!, viewbox?.top ?? 0, viewbox.bottom - box.height);
Object.assign(floating.content.style, {
left: `${box.x}px`,
top: `${box.y}px`,
})
});
};
const resizemove = (e: MouseEvent) => {
const box = floating.content.getBoundingClientRect();
const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight };
box.width = clamp(e.clientX - box.x, 200, Math.min(750, viewbox.right - box.x));
box.height = clamp(e.clientY - box.y, 150, Math.min(750, viewbox.bottom - box.y));
Object.assign(floating.content.style, {
width: `${box.width}px`,
height: `${box.height}px`,
});
};
const dragend = (e: MouseEvent) => {
e.preventDefault();
window.removeEventListener('mousemove', dragmove);
window.removeEventListener('mouseup', dragend);
};
const resizeend = (e: MouseEvent) => {
e.preventDefault();
window.removeEventListener('mousemove', resizemove);
window.removeEventListener('mouseup', resizeend);
};
const floating = popper(container, {
arrow: true,
@ -535,18 +564,37 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
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) ],
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
content: () => [ settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row justify-end border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'w-full cursor-move text-xs', listeners: { mousedown: dragstart }, text: settings?.title }), tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { e.stopImmediatePropagation(); floating.hide(); } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'right') ]) : undefined, div('h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) ],
viewport
});
if(settings?.pinned === false)
{
floating.content.addEventListener('click', () => {
if(floating.content.hasAttribute('data-pinned'))
return;
const box = floating.content.children.item(0)!.getBoundingClientRect();
Object.assign(floating.content.style, {
width: `${box.width + 21}px`,
height: `${box.height + 21}px`,
});
floating.stop();
floating.content.addEventListener('mousedown', function() {
if(!floating.content.hasAttribute('data-pinned'))
return;
this.parentElement?.appendChild(this);
}, { passive: true, capture: true })
}, { once: true });
}, { passive: true });
});
}
else
{
floating.stop();
floating.show();
}
return container;
}

View File

@ -124,6 +124,9 @@ export class Content
if(Content._ready)
return Promise.resolve(true);
if(Content.initPromise)
return Content.initPromise;
Content.initPromise = new Promise(async (res) => {
try
{

View File

@ -116,9 +116,6 @@ 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
@ -160,17 +157,6 @@ export function icon(name: string, properties?: IconProperties): HTMLElement
else
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 element;
}

View File

@ -21,9 +21,12 @@ export interface PopperProperties extends FloatingProperties
{
content?: NodeChildren | (() => NodeChildren);
delay?: number;
onShow?: () => boolean | void;
onHide?: () => boolean | void;
events?: {
show: Array<keyof HTMLElementEventMap>;
hide: Array<keyof HTMLElementEventMap>;
onshow?: () => boolean;
onhide?: () => boolean;
};
}
export interface ModalProperties
@ -36,13 +39,13 @@ export interface ModalProperties
let teleport: HTMLDivElement;
export function init()
{
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0' });
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
document.body.appendChild(teleport);
}
export function popper(container: HTMLElement, properties?: PopperProperties)
{
let shown = false, manualStop = false, timeout: Timer;
let state: 'shown' | 'showing' | 'hidden' | 'hiding' | 'pinned' = 'hidden', manualStop = false, timeout: Timer;
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
const content = dom('div', { class: properties?.class, style: properties?.style });
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
@ -110,7 +113,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
let _stop: () => void | undefined, empty = true;
function show()
{
if(shown || !properties?.onShow || properties?.onShow() !== false)
if(state !== 'shown' && state !== 'showing' && state !== 'pinned' && (!properties?.events?.onshow || properties?.events?.onshow() !== false))
{
if(typeof properties?.content === 'function')
properties.content = properties.content();
@ -122,9 +125,10 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
}
clearTimeout(timeout);
state = 'showing';
timeout = setTimeout(() => {
if(!shown)
if(state !== 'shown')
{
teleport!.appendChild(floater);
@ -132,6 +136,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
floater.classList.toggle('hidden', false);
update();
_stop && _stop();
_stop = FloatingUI.autoUpdate(container, floater, update, {
animationFrame: true,
layoutShift: false,
@ -139,35 +144,39 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
ancestorScroll: false,
ancestorResize: false,
});
console.log("Starting", floater);
}
shown = true;
state = 'shown';
}, properties?.delay ?? 0);
}
}
function hide()
{
if(!manualStop && (!properties?.onHide || properties?.onHide() !== false))
if(state !== 'hiding' && state !== 'pinned' && (!properties?.events?.onhide || properties?.events?.onhide() !== false))
{
clearTimeout(timeout);
state = 'hiding';
timeout = setTimeout(() => {
floater.remove();
_stop && _stop();
console.log('Stoping', floater);
floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true);
shown = false;
}, shown ? properties?.delay ?? 0 : 0);
state = 'hidden';
}, properties?.delay ?? 0);
}
}
function start()
{
manualStop = false;
state = 'hidden';
floater.toggleAttribute('data-pinned', false);
update();
_stop && _stop();
_stop = FloatingUI.autoUpdate(container, floater, update, {
animationFrame: true,
layoutShift: false,
@ -175,33 +184,28 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
ancestorScroll: false,
ancestorResize: false,
});
console.log('Starting', floater);
}
function stop()
{
manualStop = true;
state = 'pinned';
floater.toggleAttribute('data-pinned', true);
_stop && _stop();
console.log('Stoping', floater);
clearTimeout(timeout);
}
function link(element: HTMLElement) {
Object.entries({
'mouseenter': show,
'mousemove': show,
'mouseleave': hide,
'focus': show,
'blur': hide,
} as Record<keyof HTMLElementEventMap, () => void>).forEach(([event, listener]) => {
element.addEventListener(event, listener);
});
(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));
}
link(container);
link(floater);
return { container, content: floater, stop, start, show: () => {
if(!shown)
if(state !== 'shown')
{
teleport!.appendChild(floater);
@ -210,10 +214,11 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
update();
}
shown = true;
state = 'shown';
}, hide: () => {
floater.remove();
_stop && _stop();
console.log('Stoping', floater);
floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true);
@ -221,7 +226,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
manualStop = false;
floater.toggleAttribute('data-pinned', false);
shown = false;
state = 'hidden';
} };
}
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)

View File

@ -41,7 +41,7 @@ export const a: Prose = {
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
}
return div('');
})).current], { position: 'bottom-start', pinned: false }) : element;
@ -55,23 +55,13 @@ export const preview: Prose = {
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
const el = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
const element = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
...(children ?? []),
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
]);
const magicKeys = useMagicKeys();
return 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 w-full z-[45]',
{ 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]': !properties?.size || properties.size === 'large', 'max-w-[400px] max-h-[250px]': properties.size === 'small' }
],
content: () => {
return [async('large', Content.getContent(overview.id).then((_content) => {
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' });
@ -80,16 +70,17 @@ export const preview: Prose = {
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
}
return div('');
})).current];
},
onShow() {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
},
}).container : el;
})).current], { position: 'bottom-start', pinned: false,
events: {
show: ['mouseenter', 'mousemove'],
hide: ['mouseleave'],
onshow() {
return !magicKeys.current.has('control') || !magicKeys.current.has('meta');
}
}, }) : element;
}
}
export const callout: Prose = {