Skip to content

SnugText is a modern, high-performance library for precise text and UI element sizing in web applications. It solves a critical front-end development challenge: how to correctly adapt interfaces to dynamic content and ensure that elements occupy optimal space, regardless of text length, fonts, styles, or container dimensions.

License

Notifications You must be signed in to change notification settings

gwizxs/snugtext

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SnugText

npm version npm downloads license typescript

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

Автоматически подбирает оптимальный 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() при смене шрифтов или тем.


Made with ❤️ by Bilal

About

SnugText is a modern, high-performance library for precise text and UI element sizing in web applications. It solves a critical front-end development challenge: how to correctly adapt interfaces to dynamic content and ensure that elements occupy optimal space, regardless of text length, fonts, styles, or container dimensions.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors