mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00

# Description of Changes - Added the all tools sidebar - Added a TextFit component that shrinks text to fit containers - Added a TopToolIcon on the nav, that animates down to give users feedback on what tool is selected - Added the baseToolRegistry, to replace the old pattern of listing tools, allowing us to clean up the ToolRegistry code - Fixed Mantine light/dark theme race condition - General styling tweaks --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import { RefObject, useEffect } from 'react';
|
|
|
|
export type AdjustFontSizeOptions = {
|
|
/** Max font size to start from. Defaults to the element's computed font size. */
|
|
maxFontSizePx?: number;
|
|
/** Minimum scale relative to max size (like React Native's minimumFontScale). Default 0.7 */
|
|
minFontScale?: number;
|
|
/** Step as a fraction of max size used while shrinking. Default 0.05 (5%). */
|
|
stepScale?: number;
|
|
/** Limit the number of lines to fit. If omitted, only width is considered for multi-line. */
|
|
maxLines?: number;
|
|
/** If true, force single-line fitting (uses nowrap). Default false. */
|
|
singleLine?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Imperative util: progressively reduces font-size until content fits within the element
|
|
* (width and optional line count). Returns a cleanup that disconnects observers.
|
|
*/
|
|
export function adjustFontSizeToFit(
|
|
element: HTMLElement,
|
|
options: AdjustFontSizeOptions = {}
|
|
): () => void {
|
|
if (!element) return () => {};
|
|
|
|
const computed = window.getComputedStyle(element);
|
|
const baseFontPx = options.maxFontSizePx ?? parseFloat(computed.fontSize || '16');
|
|
const minScale = Math.max(0.1, options.minFontScale ?? 0.7);
|
|
const stepScale = Math.max(0.005, options.stepScale ?? 0.05);
|
|
const singleLine = options.singleLine ?? false;
|
|
const maxLines = options.maxLines;
|
|
|
|
// Ensure measurement is consistent
|
|
if (singleLine) {
|
|
element.style.whiteSpace = 'nowrap';
|
|
}
|
|
// Never split within words; only allow natural breaks (spaces) or explicit soft breaks
|
|
element.style.wordBreak = 'keep-all';
|
|
element.style.overflowWrap = 'normal';
|
|
// Disable automatic hyphenation to avoid mid-word breaks; use only manual opportunities
|
|
element.style.setProperty('hyphens', 'manual');
|
|
element.style.overflow = 'visible';
|
|
|
|
const minFontPx = baseFontPx * minScale;
|
|
const stepPx = Math.max(0.5, baseFontPx * stepScale);
|
|
|
|
const fit = () => {
|
|
// Reset to largest before measuring
|
|
element.style.fontSize = `${baseFontPx}px`;
|
|
|
|
// Calculate target height threshold for line limit
|
|
let maxHeight = Number.POSITIVE_INFINITY;
|
|
if (typeof maxLines === 'number' && maxLines > 0) {
|
|
const cs = window.getComputedStyle(element);
|
|
const lineHeight = parseFloat(cs.lineHeight) || baseFontPx * 1.2;
|
|
maxHeight = lineHeight * maxLines + 0.1; // small epsilon
|
|
}
|
|
|
|
let current = baseFontPx;
|
|
// Guard against excessive loops
|
|
let iterations = 0;
|
|
while (iterations < 200) {
|
|
const fitsWidth = element.scrollWidth <= element.clientWidth + 1; // tolerance
|
|
const fitsHeight = element.scrollHeight <= maxHeight + 1;
|
|
const fits = fitsWidth && fitsHeight;
|
|
if (fits || current <= minFontPx) break;
|
|
current = Math.max(minFontPx, current - stepPx);
|
|
element.style.fontSize = `${current}px`;
|
|
iterations += 1;
|
|
}
|
|
};
|
|
|
|
// Defer to next frame to ensure layout is ready
|
|
const raf = requestAnimationFrame(fit);
|
|
|
|
const ro = new ResizeObserver(() => fit());
|
|
ro.observe(element);
|
|
if (element.parentElement) ro.observe(element.parentElement);
|
|
|
|
const mo = new MutationObserver(() => fit());
|
|
mo.observe(element, { characterData: true, childList: true, subtree: true });
|
|
|
|
return () => {
|
|
cancelAnimationFrame(raf);
|
|
try { ro.disconnect(); } catch {}
|
|
try { mo.disconnect(); } catch {}
|
|
};
|
|
}
|
|
|
|
/** React hook wrapper for convenience */
|
|
export function useAdjustFontSizeToFit(
|
|
ref: RefObject<HTMLElement | null>,
|
|
options: AdjustFontSizeOptions = {}
|
|
) {
|
|
useEffect(() => {
|
|
if (!ref.current) return;
|
|
const cleanup = adjustFontSizeToFit(ref.current, options);
|
|
return cleanup;
|
|
}, [ref, options.maxFontSizePx, options.minFontScale, options.stepScale, options.maxLines, options.singleLine]);
|
|
}
|
|
|
|
|