npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

snugtext

v1.0.3

Published

Динамический расчёт размера шрифта для идеальной подгонки текста в контейнер

Readme

SnugText

🎯 Динамический расчёт размера шрифта для идеальной подгонки текста в контейнер

Автоматически подбирает оптимальный font-size, чтобы текст идеально помещался в заданные размеры контейнера. Production-ready с кэшированием, debounce, lazy loading и всеми современными оптимизациями.


📦 Установка

# npm
npm install snugtext

# yarn
yarn add snugtext

# pnpm
pnpm add snugtext

🚀 Быстрый старт (5 минут)

Vanilla JavaScript

import { fitText } from 'snugtext'

const result = fitText('Hello World', {
  maxWidth: 300,
  maxHeight: 100
})

console.log(result.fontSize) // 42.5 (px)

// Применяем к элементу
const element = document.querySelector('.title')
element.style.fontSize = `${result.fontSize}px`
element.textContent = 'Hello World'

React

import { AutoFitText } from 'snugtext/react'

function MyComponent() {
  return (
    <AutoFitText 
      style={{ width: 300, height: 100 }}
      minFontSize={12}
      maxFontSize={48}
    >
      Hello World
    </AutoFitText>
  )
}

HTML Setup

<!DOCTYPE html>
<html>
<head>
  <script type="module">
    import { fitText } from 'https://cdn.jsdelivr.net/npm/snugtext'
    
    const result = fitText('Hello', { maxWidth: 400, maxHeight: 100 })
    document.querySelector('.text').style.fontSize = `${result.fontSize}px`
  </script>
</head>
<body>
  <div class="text">Hello</div>
</body>
</html>

📚 Таблица содержания


🎯 fitText()

Вычисляет оптимальный размер шрифта для однострочного текста.

Пример 1: Базовое использование

import { fitText } from 'snugtext'

const result = fitText('My Text', {
  maxWidth: 400,
  maxHeight: 100
})

console.log(result)
// {
//   fontSize: 38.25,      // В пикселях
//   width: 398,           // Реальная ширина
//   height: 95            // Реальная высота
// }

Пример 2: С кастомным шрифтом

const result = fitText('Bold Text', {
  maxWidth: 500,
  maxHeight: 150,
  fontFamily: 'Roboto',
  fontWeight: 'bold',
  fontStyle: 'italic'
})

// Применяем результат
const element = document.querySelector('.text')
element.style.fontSize = `${result.fontSize}px`
element.style.fontFamily = 'Roboto'
element.style.fontWeight = 'bold'
element.style.fontStyle = 'italic'
element.textContent = 'Bold Text'

Пример 3: С ограничениями размера

const result = fitText('Limited Size', {
  maxWidth: 600,
  maxHeight: 200,
  minFontSize: 16,    // Не меньше 16px
  maxFontSize: 72     // Не больше 72px
})

console.log(result.fontSize) // Будет между 16 и 72

Пример 4: С единицами rem/em

// Результат в rem (базовый размер 16px)
const result = fitText('Responsive Text', {
  maxWidth: 400,
  maxHeight: 100,
  unit: 'rem'  // 'px' | 'rem' | 'em'
})

console.log(result.fontSize) // 2.5 (вместо 40px)

// В HTML
element.style.fontSize = `${result.fontSize}rem`

Пример 5: Дополнительные стили

const result = fitText('Special', {
  maxWidth: 300,
  maxHeight: 100,
  fontFamily: 'Arial',
  fontWeight: '900',
  lineHeight: 1.2,
  additionalStyles: {
    'letter-spacing': '0.1em',
    'text-transform': 'uppercase',
    'word-spacing': '0.2em'
  }
})

element.style.fontSize = `${result.fontSize}px`
element.style.letterSpacing = '0.1em'
element.style.textTransform = 'uppercase'

Пример 6: Разные контейнеры

// Для узкого контейнера
const narrowResult = fitText('Text', {
  maxWidth: 100,
  maxHeight: 50
})
console.log(narrowResult.fontSize) // Очень маленький

// Для широкого контейнера
const wideResult = fitText('Text', {
  maxWidth: 800,
  maxHeight: 300
})
console.log(wideResult.fontSize) // Большой размер

🎯 fitTextMultiline()

Для многострочного текста с переносами и line-height.

Пример 1: С явными переносами

import { fitTextMultiline } from 'snugtext'

const result = fitTextMultiline(
  'Строка 1\nСтрока 2\nСтрока 3',
  {
    maxWidth: 400,
    maxHeight: 300,
    lineHeight: 1.5
  }
)

console.log(result.fontSize) // Размер подогнан для трёх строк

// Применяем
const element = document.querySelector('.text')
element.style.fontSize = `${result.fontSize}px`
element.style.lineHeight = '1.5'
element.textContent = 'Строка 1\nСтрока 2\nСтрока 3'

Пример 2: Автоматический перенос

const text = 'Это длинный текст который автоматически переносится на новые строки чтобы уместиться в контейнер'

const result = fitTextMultiline(text, {
  maxWidth: 250,           // Узкий контейнер
  maxHeight: 400,
  fontFamily: 'Arial',
  lineHeight: 1.4
})

element.style.fontSize = `${result.fontSize}px`
element.style.lineHeight = '1.4'
element.style.width = '250px'
element.textContent = text

Пример 3: Разный line-height

// Плотный текст
const dense = fitTextMultiline(text, {
  maxWidth: 300,
  maxHeight: 200,
  lineHeight: 1.2  // Плотно
})

// Просторный текст
const spacious = fitTextMultiline(text, {
  maxWidth: 300,
  maxHeight: 200,
  lineHeight: 1.8  // Редко
})

console.log(dense.fontSize)    // Больше
console.log(spacious.fontSize) // Меньше

Пример 4: С подзаголовками

// Заголовок
const titleResult = fitTextMultiline('Main Title', {
  maxWidth: 600,
  maxHeight: 150,
  fontWeight: 'bold',
  lineHeight: 1.2
})

// Описание под заголовком
const descResult = fitTextMultiline(
  'Длинное описание которое может быть\nна нескольких строках',
  {
    maxWidth: 600,
    maxHeight: 300,
    lineHeight: 1.5,
    fontWeight: 'normal'
  }
)

🎯 autoFitText()

Автоматически извлекает стили из CSS элемента.

Пример 1: Базовое использование

<style>
  .title {
    font-family: 'Montserrat', sans-serif;
    font-weight: bold;
    font-size: 16px;
    width: 400px;
    height: 100px;
    color: #333;
  }
</style>

<div class="title">My Beautiful Title</div>
import { autoFitText } from 'snugtext'

const element = document.querySelector('.title')

// ✅ Автоматически использует:
// - font-family: 'Montserrat', sans-serif
// - font-weight: bold
// - width: 400px
// - height: 100px
autoFitText(element)

// element.style.fontSize будет установлен автоматически

Пример 2: С переопределением параметров

autoFitText(element, {
  minFontSize: 20,   // Переопределяем минимум
  maxFontSize: 64,   // Переопределяем максимум
  unit: 'rem'        // Результат в rem
})

Пример 3: Для многострочного текста

import { autoFitTextMultiline } from 'snugtext'

const paragraph = document.querySelector('.description')

autoFitTextMultiline(paragraph, {
  lineHeight: 1.6,
  minFontSize: 14,
  maxFontSize: 24
})

Пример 4: На разных элементах

// Заголовок
const h1 = document.querySelector('h1')
autoFitText(h1, { 
  minFontSize: 32, 
  maxFontSize: 96 
})

// Подзаголовок
const h2 = document.querySelector('h2')
autoFitText(h2, { 
  minFontSize: 24, 
  maxFontSize: 64 
})

// Параграф
const p = document.querySelector('p')
autoFitTextMultiline(p, { 
  minFontSize: 12, 
  maxFontSize: 18,
  lineHeight: 1.5
})

Пример 5: С обработкой ошибок

try {
  const element = document.querySelector('.maybe-missing')
  
  if (element) {
    autoFitText(element, {
      minFontSize: 10,
      maxFontSize: 50
    })
    console.log('✅ Размер подогнан')
  } else {
    console.warn('⚠️ Элемент не найден')
  }
} catch (error) {
  console.error('❌ Ошибка:', error.message)
}

🎯 fitTextAll()

Применяет fitText ко всем элементам по CSS селектору.

Пример 1: Базовое использование

<div class="card">
  <h3 class="card-title">Product 1</h3>
</div>
<div class="card">
  <h3 class="card-title">Product 2</h3>
</div>
<div class="card">
  <h3 class="card-title">Product 3</h3>
</div>
import { fitTextAll } from 'snugtext'

// ✅ Один вызов для всех элементов!
fitTextAll('.card-title', {
  minFontSize: 14,
  maxFontSize: 32
})

// Все три h3 получили оптимальный размер шрифта

Пример 2: Разные селекторы

// Заголовки
fitTextAll('h1, h2, h3', {
  fontWeight: 'bold',
  minFontSize: 24,
  maxFontSize: 72
})

// Описания
fitTextAll('.description', {
  minFontSize: 12,
  maxFontSize: 18,
  lineHeight: 1.5
})

// Цены
fitTextAll('.price', {
  fontWeight: '900',
  minFontSize: 18,
  maxFontSize: 42
})

Пример 3: Селекторы с контекстом

// Только заголовки в карточках
fitTextAll('.product-card .title', {
  minFontSize: 16,
  maxFontSize: 28
})

// Только первый заголовок каждой карточки
fitTextAll('.card:first-child h2', {
  fontWeight: 'bold'
})

// Все заголовки, кроме скрытых
fitTextAll('h1:not(.hidden)', {
  minFontSize: 24,
  maxFontSize: 64
})

Пример 4: С проверкой

// Проверяем сколько элементов найдено
const elements = document.querySelectorAll('.title')
console.log(`Найдено элементов: ${elements.length}`)

fitTextAll('.title', {
  minFontSize: 20,
  maxFontSize: 56
})

// Проверяем результаты
elements.forEach((el, i) => {
  const fontSize = window.getComputedStyle(el).fontSize
  console.log(`${i + 1}. Font-size: ${fontSize}`)
})

Пример 5: При динамическом добавлении элементов

// Исходные элементы
fitTextAll('.dynamic-title')

// Добавили новые элементы
function addNewCard(title) {
  const card = document.createElement('div')
  card.className = 'card'
  card.innerHTML = `<h3 class="dynamic-title">${title}</h3>`
  document.body.appendChild(card)
  
  // ✅ Применяем к новым элементам
  fitTextAll('.dynamic-title')
}

addNewCard('New Product')
addNewCard('Another Product')

🎯 createFitText()

Создаёт переиспользуемую функцию с предустановками.

Пример 1: Базовое использование

import { createFitText } from 'snugtext'

// Создаём функцию с предустановками
const fitCardTitle = createFitText({
  fontFamily: 'Roboto',
  fontWeight: 'bold',
  minFontSize: 14,
  maxFontSize: 28,
  unit: 'rem'
})

// Теперь используем просто так
const r1 = fitCardTitle('Title 1', { maxWidth: 250, maxHeight: 60 })
const r2 = fitCardTitle('Title 2', { maxWidth: 250, maxHeight: 60 })
const r3 = fitCardTitle('Title 3', { maxWidth: 300, maxHeight: 80 })

console.log(r1.fontSize) // в rem
console.log(r2.fontSize) // в rem

Пример 2: Множественные конфигурации

// Конфигурация для заголовков
const fitHeading = createFitText({
  fontFamily: 'Montserrat',
  fontWeight: 'bold',
  minFontSize: 24,
  maxFontSize: 72,
  lineHeight: 1.2
})

// Конфигурация для основного текста
const fitBody = createFitText({
  fontFamily: 'Inter',
  fontWeight: 'normal',
  minFontSize: 12,
  maxFontSize: 20,
  lineHeight: 1.6
})

// Конфигурация для мелкого текста
const fitSmall = createFitText({
  fontFamily: 'Arial',
  minFontSize: 8,
  maxFontSize: 14
})

// Используем везде
const h1 = fitHeading('Main Title', { maxWidth: 800, maxHeight: 150 })
const p = fitBody('Body text...', { maxWidth: 600, maxHeight: 300 })
const small = fitSmall('Note', { maxWidth: 200, maxHeight: 50 })

Пример 3: Переопределение параметров

const standardFit = createFitText({
  minFontSize: 12,
  maxFontSize: 48,
  fontFamily: 'Arial'
})

// Используем с дефолтными параметрами
const result1 = standardFit('Text 1', { maxWidth: 300, maxHeight: 100 })

// Переопределяем maxFontSize для этого случая
const result2 = standardFit('Special Text', {
  maxWidth: 500,
  maxHeight: 200,
  maxFontSize: 100  // ✅ Переопределили!
})

console.log(result1.fontSize) // Макс 48
console.log(result2.fontSize) // Макс 100

Пример 4: С единицами

// В rem
const remFit = createFitText({
  unit: 'rem',
  minFontSize: 0.75,
  maxFontSize: 3
})

const remResult = remFit('Text', { maxWidth: 400, maxHeight: 100 })
console.log(remResult.fontSize) // Например: 2.5 (rem)

// В em
const emFit = createFitText({
  unit: 'em',
  minFontSize: 0.5,
  maxFontSize: 2
})

const emResult = emFit('Text', { maxWidth: 400, maxHeight: 100 })
console.log(emResult.fontSize) // Например: 1.5 (em)

Пример 5: Для React компонентов

import { createFitText } from 'snugtext'
import { useState, useEffect } from 'react'

const fitProductTitle = createFitText({
  fontFamily: 'Roboto',
  minFontSize: 14,
  maxFontSize: 24
})

function ProductCard({ title, width = 250, height = 60 }) {
  const [fontSize, setFontSize] = useState(16)

  useEffect(() => {
    const result = fitProductTitle(title, { maxWidth: width, maxHeight: height })
    setFontSize(result.fontSize)
  }, [title, width, height])

  return (
    <h3 style={{ fontSize: `${fontSize}px` }}>
      {title}
    </h3>
  )
}

⚛️ React компоненты

AutoFitText Component

Готовый компонент для React.

Пример 1: Базовое использование

import { AutoFitText } from 'snugtext/react'

function MyPage() {
  return (
    <div style={{ width: 300, height: 100, border: '1px solid' }}>
      <AutoFitText minFontSize={12} maxFontSize={48}>
        Adaptive Title
      </AutoFitText>
    </div>
  )
}

Пример 2: С явными размерами

<AutoFitText
  style={{ 
    width: 400, 
    height: 120,
    border: '1px solid #ccc'
  }}
  minFontSize={16}
  maxFontSize={56}
  fontWeight="bold"
>
  Product Title
</AutoFitText>

Пример 3: WatchResize для адаптивности

function ResponsiveTitle() {
  return (
    <AutoFitText
      style={{ 
        width: '100%',
        height: 120
      }}
      watchResize={true}      // ✅ Следит за resize
      resizeDebounce={150}    // Debounce 150ms
      minFontSize={20}
      maxFontSize={80}
    >
      Responsive Text
    </AutoFitText>
  )
}

Пример 4: Multiline текст

<AutoFitText
  multiline={true}
  lineHeight={1.6}
  style={{ 
    width: 500, 
    height: 300
  }}
  minFontSize={14}
  maxFontSize={28}
>
  Длинный текст который автоматически переносится на несколько строк
  и подстраивает размер шрифта так чтобы всё уместилось в контейнер
</AutoFitText>

Пример 5: С разными тегами

// h1
<AutoFitText 
  as="h1" 
  style={{ width: '100%', height: 100 }}
  minFontSize={32}
  maxFontSize={96}
  fontWeight="900"
>
  Main Title
</AutoFitText>

// p
<AutoFitText 
  as="p" 
  multiline
  style={{ width: '100%', height: 300 }}
  minFontSize={14}
  maxFontSize={20}
>
  Body paragraph text...
</AutoFitText>

// span
<AutoFitText 
  as="span" 
  style={{ width: 200, height: 40 }}
>
  Inline text
</AutoFitText>

Пример 6: С callback

<AutoFitText
  style={{ width: 300, height: 100 }}
  onResize={(fontSize) => {
    console.log(`Font size changed to: ${fontSize}px`)
  }}
  watchResize
>
  Text
</AutoFitText>

Пример 7: Lazy loading

function LongList({ items }) {
  return (
    <div style={{ maxHeight: 600, overflow: 'auto' }}>
      {items.map(item => (
        <div key={item.id} style={{ height: 100 }}>
          <AutoFitText
            lazy={true}  // ✅ Вычисляется только когда виден
            style={{ width: 300, height: 80 }}
          >
            {item.title}
          </AutoFitText>
        </div>
      ))}
    </div>
  )
}

FitTextProvider

Глобальный провайдер для настроек по умолчанию.

Пример 1: Базовое использование

import { FitTextProvider, AutoFitText } from 'snugtext/react'

function App() {
  return (
    <FitTextProvider 
      defaultOptions={{
        minFontSize: 12,
        maxFontSize: 48,
        fontFamily: 'Inter',
        unit: 'rem'
      }}
    >
      {/* Все AutoFitText наследуют эти настройки */}
      <AutoFitText style={{ width: 300, height: 80 }}>
        Text 1 (использует default options)
      </AutoFitText>
      
      <AutoFitText style={{ width: 400, height: 100 }}>
        Text 2 (использует default options)
      </AutoFitText>
      
      {/* Можно переопределить */}
      <AutoFitText 
        style={{ width: 500, height: 120 }}
        maxFontSize={72}  // ✅ Переопределение
      >
        Text 3 (custom max size)
      </AutoFitText>
    </FitTextProvider>
  )
}

Пример 2: Вложенные провайдеры

<FitTextProvider defaultOptions={{ fontFamily: 'Roboto' }}>
  
  {/* Секция заголовков */}
  <FitTextProvider defaultOptions={{ 
    minFontSize: 24, 
    maxFontSize: 72,
    fontWeight: 'bold'
  }}>
    <AutoFitText style={{ width: 600, height: 100 }}>
      Title 1
    </AutoFitText>
    <AutoFitText style={{ width: 600, height: 100 }}>
      Title 2
    </AutoFitText>
  </FitTextProvider>
  
  {/* Секция описаний */}
  <FitTextProvider defaultOptions={{ 
    minFontSize: 12, 
    maxFontSize: 18,
    lineHeight: 1.5
  }}>
    <AutoFitText multiline style={{ width: 600, height: 300 }}>
      Description text...
    </AutoFitText>
  </FitTextProvider>
  
</FitTextProvider>

useFitText Hook

Хук для полного контроля.

Пример 1: Базовый хук

import { useFitText } from 'snugtext/react'

function MyText({ text }) {
  const { ref, fontSize, dimensions } = useFitText(text, {
    maxWidth: 300,
    maxHeight: 100,
    minFontSize: 12,
    maxFontSize: 48
  })

  return (
    <div 
      ref={ref}
      style={{ 
        fontSize: `${fontSize}px`,
        width: 300,
        height: 100
      }}
    >
      {text}
    </div>
  )
}

Пример 2: С информацией о размерах

function InfoComponent({ text }) {
  const { ref, fontSize, unit, dimensions } = useFitText(text, {
    maxWidth: 400,
    maxHeight: 150,
    unit: 'rem'
  })

  return (
    <div>
      <div 
        ref={ref}
        style={{ fontSize: `${fontSize}rem` }}
      >
        {text}
      </div>
      
      <div className="debug">
        <p>Font size: {fontSize}{unit}</p>
        <p>Width: {dimensions.width}px</p>
        <p>Height: {dimensions.height}px</p>
      </div>
    </div>
  )
}

Пример 3: С watchResize

function ResponsiveWithHook({ text }) {
  const { ref, fontSize } = useFitText(text, {
    maxWidth: 400,
    maxHeight: 150,
    watchResize: true,
    debounce: 100
  })

  return (
    <div 
      ref={ref}
      style={{ fontSize: `${fontSize}px`, width: '100%' }}
    >
      {text}
    </div>
  )
}

Пример 4: С мемоизацией

import { useMemo } from 'react'
import { useFitText } from 'snugtext/react'

function OptimizedComponent({ text, width, height }) {
  // Мемоизируем options чтобы избежать бесконечных пересчётов
  const options = useMemo(() => ({
    maxWidth: width,
    maxHeight: height,
    minFontSize: 14,
    maxFontSize: 56
  }), [width, height])

  const { ref, fontSize } = useFitText(text, options)

  return (
    <div ref={ref} style={{ fontSize: `${fontSize}px` }}>
      {text}
    </div>
  )
}

💡 Примеры использования

Пример 1: Система карточек товаров

import { fitTextAll } from 'snugtext'

function ProductCatalog() {
  useEffect(() => {
    // Применяем ко всем карточкам
    fitTextAll('.product-title', {
      fontWeight: 'bold',
      minFontSize: 14,
      maxFontSize: 28
    })

    fitTextAll('.product-price', {
      fontWeight: '900',
      minFontSize: 16,
      maxFontSize: 32
    })

    fitTextAll('.product-description', {
      minFontSize: 11,
      maxFontSize: 16,
      lineHeight: 1.4
    })
  }, [])

  return (
    <div className="catalog">
      <div className="product-card">
        <h3 className="product-title">Amazing Product</h3>
        <p className="product-price">$99.99</p>
        <p className="product-description">High quality product</p>
      </div>
      {/* More cards */}
    </div>
  )
}

Пример 2: Адаптивный баннер

import { AutoFitText } from 'snugtext/react'

function Banner({ title, subtitle }) {
  return (
    <div className="banner" style={{ 
      background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
      padding: 40,
      minHeight: 300
    }}>
      <AutoFitText
        style={{ 
          width: '100%', 
          height: 120,
          color: 'white'
        }}
        watchResize
        resizeDebounce={200}
        fontWeight="900"
        minFontSize={32}
        maxFontSize={96}
      >
        {title}
      </AutoFitText>

      <AutoFitText
        style={{ 
          width: '100%', 
          height: 60,
          color: 'rgba(255,255,255,0.8)',
          marginTop: 20
        }}
        watchResize
        fontWeight="500"
        minFontSize={16}
        maxFontSize={32}
      >
        {subtitle}
      </AutoFitText>
    </div>
  )
}

Пример 3: Форма с валидацией

import { AutoFitText } from 'snugtext/react'

function ContactForm() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const isValid = name.length >= 3 && email.includes('@')

  return (
    <form>
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input 
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />

      {isValid && (
        <AutoFitText
          style={{ 
            width: 300, 
            height: 60,
            color: 'green',
            padding: 20,
            textAlign: 'center'
          }}
          minFontSize={14}
          maxFontSize={32}
          fontWeight="bold"
        >
          ✅ Form is valid!
        </AutoFitText>
      )}
    </form>
  )
}

Пример 4: Таблица с адаптивным текстом

import { AutoFitText } from 'snugtext/react'

function DataTable({ data }) {
  return (
    <table>
      <thead>
        <tr>
          <th style={{ width: 150, height: 40 }}>
            <AutoFitText minFontSize={10} maxFontSize={14}>
              Product Name
            </AutoFitText>
          </th>
          <th style={{ width: 100, height: 40 }}>
            <AutoFitText minFontSize={10} maxFontSize={14}>
              Price
            </AutoFitText>
          </th>
          <th style={{ width: 120, height: 40 }}>
            <AutoFitText minFontSize={10} maxFontSize={14}>
              Quantity
            </AutoFitText>
          </th>
        </tr>
      </thead>
      <tbody>
        {data.map(row => (
          <tr key={row.id}>
            <td style={{ width: 150, height: 40 }}>
              <AutoFitText minFontSize={9} maxFontSize={12}>
                {row.name}
              </AutoFitText>
            </td>
            <td style={{ width: 100, height: 40 }}>
              <AutoFitText minFontSize={9} maxFontSize={12}>
                ${row.price}
              </AutoFitText>
            </td>
            <td style={{ width: 120, height: 40 }}>
              <AutoFitText minFontSize={9} maxFontSize={12}>
                {row.quantity}
              </AutoFitText>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  )
}

Пример 5: Длинный список с lazy loading

import { AutoFitText } from 'snugtext/react'

function ProductList({ products }) {
  return (
    <div className="products-container" style={{ maxHeight: 800, overflow: 'auto' }}>
      {products.map(product => (
        <div key={product.id} className="product-item" style={{ height: 100 }}>
          <AutoFitText
            lazy={true}  // ✅ Вычисляется только когда виден!
            style={{ width: 300, height: 80 }}
            minFontSize={14}
            maxFontSize={24}
          >
            {product.title}
          </AutoFitText>
        </div>
      ))}
    </div>
  )
}

Пример 6: Тёмный режим

import { AutoFitText, FitTextProvider } from 'snugtext/react'

function App() {
  const [isDark, setIsDark] = useState(false)

  return (
    <div style={{ 
      background: isDark ? '#1a1a1a' : '#fff',
      color: isDark ? '#fff' : '#000'
    }}>
      <button onClick={() => setIsDark(!isDark)}>
        Toggle Dark Mode
      </button>

      <FitTextProvider defaultOptions={{
        fontFamily: isDark ? 'Courier' : 'Roboto',
        minFontSize: 14,
        maxFontSize: 48
      }}>
        <AutoFitText style={{ width: 300, height: 100 }}>
          {isDark ? 'Dark Mode' : 'Light Mode'}
        </AutoFitText>
      </FitTextProvider>
    </div>
  )
}

📖 API Reference

fitText(text, options)

import { fitText } from 'snugtext'

const result = fitText(text: string, options: FitTextOptions): FitTextResult

// options
interface FitTextOptions {
  maxWidth: number                    // Обязательно - ширина контейнера
  maxHeight: number                   // Обязательно - высота контейнера
  minFontSize?: number               // Мин размер (default: 1)
  maxFontSize?: number               // Макс размер (default: 1000)
  unit?: 'px' | 'rem' | 'em'        // Единицы (default: 'px')
  fontFamily?: string                // Шрифт (default: 'Arial')
  fontStyle?: string                 // Стиль (default: 'normal')
  fontWeight?: string | number       // Толщина (default: 'normal')
  lineHeight?: string | number       // Интервал (default: 'normal')
  additionalStyles?: Record<string, string>
}

// result
interface FitTextResult {
  fontSize: number  // Оптимальный размер
  width: number     // Реальная ширина (px)
  height: number    // Реальная высота (px)
}

fitTextMultiline(text, options)

import { fitTextMultiline } from 'snugtext'

const result = fitTextMultiline(
  text: string, 
  options: FitTextOptions
): FitTextResult

// Используются те же options что и fitText

autoFitText(element, options?)

import { autoFitText } from 'snugtext'

autoFitText(element: HTMLElement, options?: Partial<FitTextOptions>): void

// Автоматически использует стили из CSS элемента

fitTextAll(selector, options)

import { fitTextAll } from 'snugtext'

fitTextAll(selector: string, options: FitTextOptions): void

// Применяет ко всем найденным элементам

createFitText(options)

import { createFitText } from 'snugtext'

const myFit = createFitText(options: Partial<FitTextOptions>)
const result = myFit(text: string, options: FitTextOptions): FitTextResult

// Возвращает функцию с предустановками

Утилиты кэша

import { 
  clearMeasurementCache, 
  getCacheStats 
} from 'snugtext'

// Очистить кэш
clearMeasurementCache(): void

// Получить статистику
getCacheStats(): { size: number; maxSize: number }

⚡ Производительность

Оптимизации

SnugText включает все современные оптимизации:

1. Кэширование с TTL

// Первый вызов - 3ms
fitText('Hello', { maxWidth: 200, maxHeight: 50 })

// Повторный - 0.1ms (из кэша)
fitText('Hello', { maxWidth: 200, maxHeight: 50 })

2. Бинарный поиск

Алгоритм O(log n) находит оптимальный размер за 6-8 итераций вместо 20-30.

// Быстро находит размер между minFontSize и maxFontSize
fitText('Text', {
  maxWidth: 300,
  maxHeight: 100,
  minFontSize: 10,
  maxFontSize: 100  // Поиск в диапазоне 10-100
})

3. Ранний выход (early exit)

// Если найден "достаточно хороший" размер - алгоритм останавливается
// Экономит 1-2 вычисления
const result = fitText('Text', { maxWidth: 300, maxHeight: 100 })

4. Batch DOM операции

import { batchDOMOperation } from 'snugtext/utils'

// ❌ 10 операций = 10 reflow
elements.forEach(el => el.style.fontSize = '20px')

// ✅ 1 операция = 1 reflow
batchDOMOperation(() => {
  elements.forEach(el => el.style.fontSize = '20px')
})

5. Lazy loading

// Вычисляется только когда элемент виден в viewport
<AutoFitText lazy>
  Heavy calculation
</AutoFitText>

Бенчмарки

Сценарий                      | Время    | vs baseline
--------------------------------------------------
Одно измерение (fresh)        | 2-3ms    | 1x
Одно измерение (cached)       | 0.1ms    | 30x faster!
100 элементов (fitTextAll)    | 150ms    | 
100 элементов (lazy)          | 50ms     | 3x faster!
React render (memoized)       | 1ms      | 
React render (no memo)        | 5ms      | 5x slower

Советы оптимизации

// ✅ 1. Используйте createFitText для переиспользования
const fit = createFitText({ fontFamily: 'Arial', minFontSize: 12 })
const r1 = fit('Text1', { maxWidth: 200, maxHeight: 50 })  // Быстро
const r2 = fit('Text2', { maxWidth: 200, maxHeight: 50 })  // Очень быстро

// ✅ 2. Используйте lazy для длинных списков
<AutoFitText lazy>Heavy Text</AutoFitText>

// ✅ 3. Используйте debounce для resize
const debouncedResize = debounce(fitTextAll, 150)
window.addEventListener('resize', debouncedResize)

// ✅ 4. Используйте watchResize с debounce
<AutoFitText watchResize resizeDebounce={200}>
  Text
</AutoFitText>

// ✅ 5. Очищайте кэш при смене шрифтов
function changeFont(newFont) {
  applyFont(newFont)
  clearMeasurementCache()  // Очистить кэш
  fitTextAll('.text')      // Пересчитать
}

🧪 Обработка ошибок

Некорректные параметры

try {
  fitText('Text', {
    maxWidth: -100,   // ❌ Отрицательное
    maxHeight: 50
  })
} catch (error) {
  if (error instanceof RangeError) {
    console.error('Invalid parameters:', error.message)
  }
}

Проверка перед использованием

function safeFitText(text, options) {
  // Проверяем параметры
  if (!text || typeof text !== 'string') {
    throw new TypeError('Text must be a non-empty string')
  }
  
  if (options.maxWidth <= 0 || options.maxHeight <= 0) {
    throw new RangeError('maxWidth and maxHeight must be positive')
  }

  return fitText(text, options)
}

Обработка в React

function SafeAutoFitText({ text, ...props }) {
  const [error, setError] = useState(null)

  if (!text) {
    return <div>No text provided</div>
  }

  return (
    <div>
      {error && <div className="error">Error: {error}</div>}
      <AutoFitText {...props}>
        {text}
      </AutoFitText>
    </div>
  )
}

🔗 Ссылки


📄 Лицензия

Apache-2.0 © Bilal


💬 FAQ

Q: Почему текст не помещается? A: Убедитесь что maxWidth и maxHeight достаточно большие, или уменьшите minFontSize.

Q: Как использовать с Next.js? A: Просто добавьте 'use client' в начало файла компонента при использовании App Router.

Q: Работает ли с SSR? A: Да, но пересчёт происходит после гидрации на клиенте.

Q: Как очистить кэш? A: Используйте clearMeasurementCache() при смене шрифтов или тем.