diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 8c2a12c6a..36277bbb1 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -1,8 +1,9 @@ import { dayjs } from "@triliumnext/commons"; -import type { ViewMode, ViewScope } from "./link.js"; -import FNote from "../entities/fnote"; import { snapdom } from "@zumer/snapdom"; +import FNote from "../entities/fnote"; +import type { ViewMode, ViewScope } from "./link.js"; + const SVG_MIME = "image/svg+xml"; export const isShare = !window.glob; @@ -113,9 +114,9 @@ function formatDateISO(date: Date) { export function formatDateTime(date: Date, userSuppliedFormat?: string): string { if (userSuppliedFormat?.trim()) { return dayjs(date).format(userSuppliedFormat); - } else { - return `${formatDate(date)} ${formatTime(date)}`; - } + } + return `${formatDate(date)} ${formatTime(date)}`; + } function localNowDateTime() { @@ -191,9 +192,9 @@ export function formatSize(size: number | null | undefined) { if (size < 1024) { return `${size} KiB`; - } else { - return `${Math.round(size / 102.4) / 10} MiB`; - } + } + return `${Math.round(size / 102.4) / 10} MiB`; + } function toObject(array: T[], fn: (arg0: T) => [key: string, value: R]) { @@ -297,18 +298,18 @@ function formatHtml(html: string) { let indent = "\n"; const tab = "\t"; let i = 0; - let pre: { indent: string; tag: string }[] = []; + const pre: { indent: string; tag: string }[] = []; html = html - .replace(new RegExp("
([\\s\\S]+?)?
"), function (x) { + .replace(new RegExp("
([\\s\\S]+?)?
"), (x) => { pre.push({ indent: "", tag: x }); - return "<--TEMPPRE" + i++ + "/-->"; + return `<--TEMPPRE${ i++ }/-->`; }) - .replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) { + .replace(new RegExp("<[^<>]+>[^<]?", "g"), (x) => { let ret; const tagRegEx = /<\/?([^\s/>]+)/.exec(x); - let tag = tagRegEx ? tagRegEx[1] : ""; - let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x); + const tag = tagRegEx ? tagRegEx[1] : ""; + const p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x); if (p) { const pInd = parseInt(p[1]); @@ -318,24 +319,22 @@ function formatHtml(html: string) { if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) { // self closing tag ret = indent + x; + } else if (x.indexOf("") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length); + else ret = indent + x; + !p && (indent += tab); } else { - if (x.indexOf("") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length); - else ret = indent + x; - !p && (indent += tab); - } else { - //close tag - indent = indent.substr(0, indent.length - 1); - if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length); - else ret = indent + x; - } + //close tag + indent = indent.substr(0, indent.length - 1); + if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length); + else ret = indent + x; } return ret; }); for (i = pre.length; i--;) { - html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("
", "
\n").replace("
", pre[i].indent + "
")); + html = html.replace(`<--TEMPPRE${ i }/-->`, pre[i].tag.replace("
", "
\n").replace("
", `${pre[i].indent }
`)); } return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html; @@ -364,11 +363,11 @@ type dynamicRequireMappings = { export function dynamicRequire(moduleName: T): Awaited{ if (typeof __non_webpack_require__ !== "undefined") { return __non_webpack_require__(moduleName); - } else { - // explicitly pass as string and not as expression to suppress webpack warning - // 'Critical dependency: the request of a dependency is an expression' - return require(`${moduleName}`); - } + } + // explicitly pass as string and not as expression to suppress webpack warning + // 'Critical dependency: the request of a dependency is an expression' + return require(`${moduleName}`); + } function timeLimit(promise: Promise, limitMs: number, errorMessage?: string) { @@ -509,8 +508,8 @@ export function escapeRegExp(str: string) { function areObjectsEqual(...args: unknown[]) { let i; let l; - let leftChain: Object[]; - let rightChain: Object[]; + let leftChain: object[]; + let rightChain: object[]; function compare2Objects(x: unknown, y: unknown) { let p; @@ -695,9 +694,9 @@ async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | S try { const result = await snapdom(element, { - backgroundColor: "transparent", - scale: 2 - }); + backgroundColor: "transparent", + scale: 2 + }); triggerDownload(`${nameWithoutExtension}.svg`, result.url); } finally { cleanup(); @@ -733,9 +732,9 @@ async function downloadAsPng(nameWithoutExtension: string, svgSource: string | S try { const result = await snapdom(element, { - backgroundColor: "transparent", - scale: 2 - }); + backgroundColor: "transparent", + scale: 2 + }); const pngImg = await result.toPng(); await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src); } finally { @@ -763,11 +762,11 @@ export function getSizeFromSvg(svgContent: string) { return { width: parseFloat(width), height: parseFloat(height) - } - } else { - console.warn("SVG export error", svgDocument.documentElement); - return null; - } + }; + } + console.warn("SVG export error", svgDocument.documentElement); + return null; + } /** @@ -896,9 +895,9 @@ export function mapToKeyValueArray(map: R export function getErrorMessage(e: unknown) { if (e && typeof e === "object" && "message" in e && typeof e.message === "string") { return e.message; - } else { - return "Unknown error"; - } + } + return "Unknown error"; + } /** @@ -913,6 +912,12 @@ export function handleRightToLeftPlacement(placement: T) { return placement; } +export function clamp(value: number, min: number, max: number) { + if (value < min) return min; + if (value > max) return max; + return value; +} + export default { reloadFrontendApp, restartDesktopApp, diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx index 8d41929cc..8a708ea7a 100644 --- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx +++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx @@ -7,6 +7,7 @@ import { useEffect, useRef } from "preact/hooks"; import options from "../../services/options"; import { DEFAULT_GUTTER_SIZE } from "../../services/resizer"; +import { clamp } from "../../services/utils"; import HighlightsList from "./HighlightsList"; import TableOfContents from "./TableOfContents"; @@ -64,31 +65,113 @@ export default function RightPanelContainer() { const children = Array.from(rightPaneEl?.querySelectorAll(":scope > .card") ?? []); const pos = children.indexOf(cardEl); if (pos === -1) return; - const sizes = splitInstance.getSizes(); + + const sizes = splitInstance.getSizes(); // percentages + const COLLAPSED_SIZE = 0; // keep your current behavior; consider a small min later + + // Choose recipients/donors: nearest expanded panes first; if none, all except pos. + const recipients = getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE); + const fallback = getExpandedIndices(sizes, pos, -Infinity); // all other panes + const targets = recipients.length ? recipients : fallback; + if (!expanded) { const sizeBeforeCollapse = sizes[pos]; sizesBeforeCollapse.current.set(cardEl, sizeBeforeCollapse); - sizes[pos] = 0; - const itemToExpand = pos > 0 ? pos - 1 : pos + 1; - if (sizes[itemToExpand] > COLLAPSED_SIZE) { - sizes[itemToExpand] += sizeBeforeCollapse; - } + // Collapse + sizes[pos] = COLLAPSED_SIZE; + + // Give freed space to other panes + const freed = sizeBeforeCollapse - COLLAPSED_SIZE; + distributeInto(sizes, targets, freed); } else { - const itemToExpand = pos > 0 ? pos - 1 : pos + 1; - const sizeBeforeCollapse = sizesBeforeCollapse.current.get(cardEl) ?? 50; + const want = sizesBeforeCollapse.current.get(cardEl) ?? 50; - if (sizes[itemToExpand] > COLLAPSED_SIZE) { - sizes[itemToExpand] -= sizeBeforeCollapse; - } - sizes[pos] = sizeBeforeCollapse; + // Take space back from other panes to expand this one + const took = takeFrom(sizes, targets, want); + + sizes[pos] = COLLAPSED_SIZE + took; // if donors couldn't provide all, expand partially } - console.log("Set sizes to ", sizes); + + // Optional: tiny cleanup to avoid negatives / floating drift + for (let i = 0; i < sizes.length; i++) sizes[i] = clamp(sizes[i], 0, 100); + + // Normalize to sum to 100 (Split.js likes this) + const sum = sizes.reduce((a, b) => a + b, 0); + if (sum > 0) { + for (let i = 0; i < sizes.length; i++) sizes[i] = (sizes[i] / sum) * 100; + } + splitInstance.setSizes(sizes); - }, + } }}> {items} ); } + +function getExpandedIndices(sizes, skipIndex, COLLAPSED_SIZE) { + const idxs = []; + for (let i = 0; i < sizes.length; i++) { + if (i === skipIndex) continue; + if (sizes[i] > COLLAPSED_SIZE) idxs.push(i); + } + return idxs; +} + +// Prefer nearby panes (VS Code-ish). Falls back to "all expanded panes". +function getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE) { + const recipients = []; + for (let d = 1; d < sizes.length; d++) { + const left = pos - d; + const right = pos + d; + if (left >= 0 && sizes[left] > COLLAPSED_SIZE) recipients.push(left); + if (right < sizes.length && sizes[right] > COLLAPSED_SIZE) recipients.push(right); + } + return recipients; +} + +// Distribute `amount` into `recipients` proportionally to their current sizes. +function distributeInto(sizes, recipients, amount) { + if (amount === 0 || recipients.length === 0) return; + const total = recipients.reduce((sum, i) => sum + sizes[i], 0); + if (total <= 0) { + // equal split fallback + const delta = amount / recipients.length; + recipients.forEach(i => (sizes[i] += delta)); + return; + } + recipients.forEach(i => { + const share = (sizes[i] / total) * amount; + sizes[i] += share; + }); +} + +// Take `amount` out of `donors` proportionally, without driving anyone below 0. +// Returns how much was actually taken. +function takeFrom(sizes, donors, amount) { + if (amount <= 0 || donors.length === 0) return 0; + + // max each donor can contribute (don’t go below 0 here; you can change min if you want) + const caps = donors.map(i => ({ i, cap: Math.max(0, sizes[i]) })); + let remaining = amount; + + // iterative proportional take with caps + for (let iter = 0; iter < 5 && remaining > 1e-9; iter++) { + const active = caps.filter(x => x.cap > 1e-9); + if (active.length === 0) break; + + const total = active.reduce((s, x) => s + sizes[x.i], 0) || active.length; + for (const x of active) { + const weight = total === active.length ? 1 / active.length : (sizes[x.i] / total); + const want = remaining * weight; + const took = Math.min(x.cap, want); + sizes[x.i] -= took; + x.cap -= took; + remaining -= took; + if (remaining <= 1e-9) break; + } + } + return amount - remaining; +}