Skip to content

Instantly share code, notes, and snippets.

@TomIsLoading
Created April 9, 2026 01:16
Show Gist options
  • Select an option

  • Save TomIsLoading/52ad4fac75342a71f94878a67d74c8a3 to your computer and use it in GitHub Desktop.

Select an option

Save TomIsLoading/52ad4fac75342a71f94878a67d74c8a3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pretext API Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
<body style="font-family: Inter, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px;">
<h1>Pretext API Demo</h1>
<textarea id="text" rows="2" style="width: 100%; font: inherit; padding: 8px; box-sizing: border-box;">The web has always forced you to render text into the DOM just to learn how tall it is. Every call to offsetHeight triggers a layout reflow. Pretext changes everything.</textarea>
<p>
<label>Width <input id="width" type="range" min="100" max="600" value="360"> <code id="widthVal">360px</code></label><br>
<label>Line height <input id="lh" type="range" min="16" max="40" value="24"> <code id="lhVal">24px</code></label>
</p>
<p>
<code>prepare()</code> time: <strong id="prepTime">—</strong><br>
<code>layout()</code> result: <strong id="result">—</strong> in <strong id="layTime">—</strong>
</p>
<div id="wrap" style="border: 1px solid #ccc; padding: 8px; display: inline-block; width: 360px;">
<div id="box" style="font: 16px Inter; line-height: 24px; overflow-wrap: break-word;"></div>
</div>
<script type="module">
import { prepare, layout } from 'https://esm.sh/@chenglou/pretext@0.0.5'
const textInput = document.getElementById('text')
const widthSlider = document.getElementById('width')
const lhSlider = document.getElementById('lh')
const widthVal = document.getElementById('widthVal')
const lhVal = document.getElementById('lhVal')
const prepTimeEl = document.getElementById('prepTime')
const resultEl = document.getElementById('result')
const layTimeEl = document.getElementById('layTime')
const box = document.getElementById('box')
const wrap = document.getElementById('wrap')
let prepared = null
function onTextChange() {
const t0 = performance.now()
prepared = prepare(textInput.value, '16px Inter')
const ms = performance.now() - t0
prepTimeEl.textContent = ms.toFixed(2) + 'ms'
onLayout()
}
function onLayout() {
const w = +widthSlider.value
const lh = +lhSlider.value
widthVal.textContent = w + 'px'
lhVal.textContent = lh + 'px'
const t0 = performance.now()
const { height, lineCount } = layout(prepared, w, lh)
const ms = performance.now() - t0
resultEl.textContent = height + 'px, ' + lineCount + ' lines'
layTimeEl.textContent = ms.toFixed(4) + 'ms'
// We know the height and line count before we actually render the text!
box.textContent = textInput.value
box.style.lineHeight = lh + 'px'
wrap.style.width = w + 'px'
}
textInput.addEventListener('input', onTextChange)
widthSlider.addEventListener('input', onLayout)
lhSlider.addEventListener('input', onLayout)
document.fonts.ready.then(onTextChange)
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pretext Advanced API Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
</head>
<body style="font-family: Inter, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px;">
<h1>Pretext Advanced API</h1>
<p style="color:#666;">Use-case 2: lay out paragraph lines yourself. Render to canvas, shrink-wrap bubbles, flow text around obstacles.</p>
<textarea id="text" rows="3" style="width: 100%; font: inherit; padding: 8px; box-sizing: border-box;">Pretext lets you lay out text line by line. You get the actual line strings, their widths, and cursor positions. This means you can render to canvas, SVG, or WebGL — not just the DOM. You can also vary the available width per line, flowing text around images or other obstacles.</textarea>
<!-- ============================================================
1. layoutWithLines — render to canvas
Get individual line strings + widths, draw them anywhere.
============================================================ -->
<h2 style="margin-top:40px; border-top:1px solid #ddd; padding-top:20px;">1. layoutWithLines()</h2>
<p style="color:#666; font-size:14px;">Returns actual line strings. Here we draw them to a <code>&lt;canvas&gt;</code> — no DOM text at all.</p>
<p>
<label>Width <input id="canvas-width" type="range" min="150" max="600" value="400"> <code id="canvas-width-val">400px</code></label>
</p>
<p style="font-size:13px; color:#999;" id="canvas-stats"></p>
<canvas id="canvas" style="border: 1px solid #ccc; display: block;"></canvas>
<!-- ============================================================
2. walkLineRanges + measureLineStats — shrink-wrap
Binary search for the tightest width that keeps the same
line count. The "tight bubble" problem.
============================================================ -->
<h2 style="margin-top:40px; border-top:1px solid #ddd; padding-top:20px;">2. measureLineStats() — shrink-wrap</h2>
<p style="color:#666; font-size:14px;"><code>measureLineStats(prepared, width)</code> returns <code>{ lineCount, maxLineWidth }</code> without building line strings. We use it to binary search for the tightest bubble width. There's also <code>walkLineRanges(prepared, width, cb)</code> which calls a callback per line with its width and start/end cursors — useful when you need per-line data rather than just the summary stats.</p>
<p>
<label>Max width <input id="bubble-max" type="range" min="150" max="600" value="350"> <code id="bubble-max-val">350px</code></label>
</p>
<p style="font-size:13px; color:#999;" id="bubble-stats"></p>
<div style="display: flex; gap: 20px; align-items: start;">
<div>
<div style="font-size:12px; color:#999; margin-bottom:4px;">Normal (max-width)</div>
<div id="bubble-normal" style="font: 16px/24px Inter; background: #e3f2fd; border-radius: 12px; padding: 8px 12px; overflow-wrap: break-word;"></div>
</div>
<div>
<div style="font-size:12px; color:#999; margin-bottom:4px;">Shrink-wrapped</div>
<div id="bubble-tight" style="font: 16px/24px Inter; background: #e8f5e9; border-radius: 12px; padding: 8px 12px; overflow-wrap: break-word;"></div>
</div>
</div>
<!-- ============================================================
3. layoutNextLineRange — variable width per line
Flow text around an obstacle, changing available width
on each line.
============================================================ -->
<h2 style="margin-top:40px; border-top:1px solid #ddd; padding-top:20px;">3. layoutNextLine() — text around obstacles</h2>
<p style="color:#666; font-size:14px;">Drag the obstacle around. Text reflows in real time — each line gets a different available width via <code>layoutNextLine()</code>.</p>
<div id="flow-container" style="position: relative; border: 1px solid #ccc; min-height: 200px; overflow: hidden; cursor: default;">
<div id="obstacle" style="position: absolute; right: 20px; top: 10px; width: 130px; height: 90px; background: #fee2e2; border: 1px solid #fca5a5; border-radius: 4px; cursor: grab; display: flex; align-items: center; justify-content: center; font-size: 13px; color: #b91c1c; user-select: none; z-index: 1;">drag me</div>
<div id="flow-lines" style="position: relative;"></div>
</div>
<script type="module">
import {
prepareWithSegments,
layoutWithLines,
measureLineStats,
layoutNextLine
} from 'https://esm.sh/@chenglou/pretext@0.0.5'
const FONT = '16px Inter'
const LINE_HEIGHT = 24
await document.fonts.ready
const textInput = document.getElementById('text')
let prepared = null
function onTextChange() {
prepared = prepareWithSegments(textInput.value, FONT)
updateCanvas()
updateBubble()
updateFlow()
}
textInput.addEventListener('input', onTextChange)
// ==========================================================
// 1. layoutWithLines — render to canvas
// ==========================================================
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const canvasWidthSlider = document.getElementById('canvas-width')
const canvasWidthVal = document.getElementById('canvas-width-val')
const canvasStats = document.getElementById('canvas-stats')
function updateCanvas() {
const w = +canvasWidthSlider.value
canvasWidthVal.textContent = w + 'px'
const { lines, height, lineCount } = layoutWithLines(prepared, w, LINE_HEIGHT)
// Resize canvas and draw each line
canvas.width = w * devicePixelRatio
canvas.height = height * devicePixelRatio
canvas.style.width = w + 'px'
canvas.style.height = height + 'px'
ctx.scale(devicePixelRatio, devicePixelRatio)
ctx.font = FONT
ctx.fillStyle = '#111'
ctx.textBaseline = 'top'
// Each line has .text and .width — draw them one by one
for (let i = 0; i < lines.length; i++) {
// Offset by ~4px to approximate the baseline within line-height
ctx.fillText(lines[i].text, 0, i * LINE_HEIGHT + 4)
}
canvasStats.textContent = lineCount + ' lines — each line is a { text, width, start, end } object'
}
canvasWidthSlider.addEventListener('input', updateCanvas)
// ==========================================================
// 2. measureLineStats — shrink-wrap binary search
// ==========================================================
const bubbleMaxSlider = document.getElementById('bubble-max')
const bubbleMaxVal = document.getElementById('bubble-max-val')
const bubbleStats = document.getElementById('bubble-stats')
const bubbleNormal = document.getElementById('bubble-normal')
const bubbleTight = document.getElementById('bubble-tight')
function updateBubble() {
const maxW = +bubbleMaxSlider.value
bubbleMaxVal.textContent = maxW + 'px'
// measureLineStats returns { lineCount, maxLineWidth } without building strings.
// walkLineRanges does the same but calls a callback per line with { width, start, end },
// useful when you need per-line widths or cursors rather than just the aggregate stats.
// e.g.: walkLineRanges(prepared, maxW, line => { console.log(line.width) })
const { lineCount } = measureLineStats(prepared, maxW)
// Binary search: find the narrowest width that still produces the same lineCount.
// lo = too narrow (causes more lines), hi = wide enough (keeps same line count).
// Each iteration tests the midpoint: if it still fits in the same number of
// lines, we can go narrower (move hi down); if it wraps to more lines,
// we've gone too narrow (move lo up). We stop when lo and hi are within 0.5px.
let lo = 50
let hi = maxW
while (hi - lo > 0.5) {
const mid = (lo + hi) / 2
if (measureLineStats(prepared, mid).lineCount <= lineCount) {
hi = mid // still fits — try even narrower
} else {
lo = mid // too narrow — back off
}
}
// Get the actual widest line at that tight width for a pixel-perfect fit
const shrunkWidth = Math.ceil(measureLineStats(prepared, Math.ceil(hi)).maxLineWidth)
// Show both bubbles
bubbleNormal.textContent = textInput.value
bubbleNormal.style.width = maxW + 'px'
bubbleTight.textContent = textInput.value
bubbleTight.style.width = shrunkWidth + 'px'
bubbleStats.textContent =
'Normal: ' + maxW + 'px wide. ' +
'Shrink-wrapped: ' + shrunkWidth + 'px (' +
(maxW - shrunkWidth) + 'px saved, same ' + lineCount + ' lines)'
}
bubbleMaxSlider.addEventListener('input', updateBubble)
// ==========================================================
// 3. layoutNextLine — text around a draggable obstacle
// ==========================================================
const flowContainer = document.getElementById('flow-container')
const obstacle = document.getElementById('obstacle')
const flowLinesEl = document.getElementById('flow-lines')
function updateFlow() {
var containerWidth = flowContainer.clientWidth
var obsRect = obstacle.getBoundingClientRect()
var contRect = flowContainer.getBoundingClientRect()
// Obstacle position relative to container
var obsLeft = obsRect.left - contRect.left
var obsRight = obsLeft + obsRect.width
var obsTop = obsRect.top - contRect.top
var obsBottom = obsTop + obsRect.height
var gap = 16
// Available space on each side of the obstacle
var leftWidth = obsLeft - gap
var rightWidth = containerWidth - obsRight - gap
var rightX = obsRight + gap
var cursor = { segmentIndex: 0, graphemeIndex: 0 }
var y = 0
var spans = []
while (true) {
var overlaps = y < obsBottom && y + LINE_HEIGHT > obsTop
if (!overlaps) {
// Full-width line, no obstacle in the way
var line = layoutNextLine(prepared, cursor, containerWidth)
if (line === null) break
spans.push({ text: line.text, y: y, x: 0 })
cursor = line.end
y += LINE_HEIGHT
} else {
// Flow into left side, then right side on the same row
var filled = false
if (leftWidth > 30) {
var leftLine = layoutNextLine(prepared, cursor, leftWidth)
if (leftLine === null) break
spans.push({ text: leftLine.text, y: y, x: 0 })
cursor = leftLine.end
filled = true
}
if (rightWidth > 30) {
var rightLine = layoutNextLine(prepared, cursor, rightWidth)
if (rightLine === null) {
if (!filled) break
} else {
spans.push({ text: rightLine.text, y: y, x: rightX })
cursor = rightLine.end
filled = true
}
}
if (!filled) break
y += LINE_HEIGHT
}
}
// Render as absolutely positioned spans
var html = ''
for (var i = 0; i < spans.length; i++) {
var escaped = spans[i].text.replace(/&/g, '&amp;').replace(/</g, '&lt;')
html += '<div style="position:absolute; left:' + spans[i].x + 'px; top:' + spans[i].y + 'px; height:' + LINE_HEIGHT + 'px; font:' + FONT + '; line-height:' + LINE_HEIGHT + 'px; white-space:nowrap;">' + escaped + '</div>'
}
flowLinesEl.innerHTML = html
var totalHeight = Math.max(y, obsBottom) + 10
flowContainer.style.height = totalHeight + 'px'
}
// Drag logic
var dragging = false
var dragOffsetX = 0
var dragOffsetY = 0
obstacle.addEventListener('mousedown', function(e) {
dragging = true
dragOffsetX = e.clientX - obstacle.getBoundingClientRect().left
dragOffsetY = e.clientY - obstacle.getBoundingClientRect().top
obstacle.style.cursor = 'grabbing'
e.preventDefault()
})
document.addEventListener('mousemove', function(e) {
if (!dragging) return
var contRect = flowContainer.getBoundingClientRect()
var x = e.clientX - contRect.left - dragOffsetX
var y = e.clientY - contRect.top - dragOffsetY
// Clamp to container bounds
x = Math.max(0, Math.min(x, contRect.width - obstacle.offsetWidth))
y = Math.max(0, y)
obstacle.style.left = x + 'px'
obstacle.style.top = y + 'px'
obstacle.style.right = 'auto'
updateFlow()
})
document.addEventListener('mouseup', function() {
dragging = false
obstacle.style.cursor = 'grab'
})
// Initial render
onTextChange()
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pretext in Action</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
body { font-family: Inter, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
h1 { margin-bottom: 4px; }
h2 { margin-top: 48px; border-top: 1px solid #ddd; padding-top: 24px; }
code { background: #f3f3f3; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Pretext in Action</h1>
<p style="color:#666;">Three examples using just <code>prepare()</code> + <code>layout()</code>. Each section is independent.</p>
<!-- ============================================================
1. ACCORDION
Animate open/close to a height computed by Pretext.
No hidden rendering, no max-height hacks.
============================================================ -->
<h2>1. Accordion</h2>
<p style="color:#666; font-size:14px;">Click a section. Pretext computes the open height — the CSS transition animates to it.</p>
<div id="accordion"></div>
<style>
.acc-item { border: 1px solid #ddd; border-radius: 6px; margin-bottom: 6px; overflow: hidden; }
.acc-header { padding: 10px 14px; cursor: pointer; font-weight: 500; background: #fafafa; user-select: none; }
.acc-header:hover { background: #f0f0f0; }
.acc-body { overflow: hidden; height: 0; transition: height 0.3s ease; }
.acc-content { padding: 10px 14px; font: 16px/24px Inter; }
</style>
<!-- ============================================================
2. VIRTUALIZED LIST
10,000 items, only ~visible ones rendered.
Pretext computes all heights upfront — no guessing.
============================================================ -->
<h2>2. Virtualized List</h2>
<p style="color:#666; font-size:14px;">10,000 items. Heights computed by Pretext before any DOM insertion. Scroll to see only visible items rendered.</p>
<div id="virtual-container" style="height: 300px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px; position: relative;">
<div id="virtual-spacer" style="position: relative;"></div>
</div>
<p style="font-size:13px; color:#999;" id="virtual-stats"></p>
<style>
.virtual-item { position: absolute; left: 0; right: 0; padding: 8px 14px; border-bottom: 1px solid #eee; font: 15px/22px Inter; overflow-wrap: break-word; }
</style>
<!-- ============================================================
3. "SHOW MORE" / LINE CLAMPING
Check if text exceeds N lines. If so, clamp and show
a toggle. Uses lineCount from layout().
============================================================ -->
<h2>3. "Show More" Line Clamping</h2>
<p style="color:#666; font-size:14px;"><code>layout()</code> returns <code>lineCount</code>. If it exceeds the limit, we clamp and show a toggle.</p>
<div id="clamp-demos"></div>
<style>
.clamp-card { border: 1px solid #ddd; border-radius: 6px; padding: 14px; margin-bottom: 10px; }
.clamp-text { font: 16px/24px Inter; overflow: hidden; overflow-wrap: break-word; }
.clamp-toggle { color: #2563eb; cursor: pointer; font-size: 14px; margin-top: 4px; background: none; border: none; padding: 0; font-family: inherit; }
.clamp-toggle:hover { text-decoration: underline; }
</style>
<!-- ============================================================
SCRIPTS — one per demo, sharing the same import
============================================================ -->
<script type="module">
import { prepare, layout } from 'https://esm.sh/@chenglou/pretext@0.0.5';
// Wait for Inter to load so measurements are accurate
await document.fonts.ready;
// ==========================================================
// 1. ACCORDION
// ==========================================================
(() => {
const FONT = '16px Inter'
const LINE_HEIGHT = 24
const sections = [
{ title: 'What is Pretext?', body: 'A pure JS library for text measurement and layout. It computes how tall text will be at a given width without touching the DOM. No hidden elements, no reflow, no guesswork.' },
{ title: 'How does it work?', body: 'prepare() segments and measures text once using canvas measureText(). layout() then does pure arithmetic over cached widths — walking segments, accumulating until a line overflows, counting lines, multiplying by line-height. The result matches what the browser would produce.' },
{ title: 'Why does this matter?', body: 'Layout reflow (triggered by reading offsetHeight, getBoundingClientRect, etc.) is one of the most expensive browser operations. It blocks the main thread and cascades through the entire page. Pretext eliminates this for text measurement — unlocking smooth animations, virtualization, masonry layouts, and more.' },
]
const container = document.getElementById('accordion')
// Build the DOM first so we can read the actual content width
const items = sections.map(section => {
const item = document.createElement('div')
item.className = 'acc-item'
item.innerHTML = `
<div class="acc-header">${section.title}</div>
<div class="acc-body"><div class="acc-content">${section.body}</div></div>
`
container.appendChild(item)
return item
})
// Read content width once (clientWidth includes padding, so subtract it)
const contentEl = items[0].querySelector('.acc-content')
const contentWidth = contentEl.clientWidth - parseFloat(getComputedStyle(contentEl).paddingLeft) - parseFloat(getComputedStyle(contentEl).paddingRight)
items.forEach((item, i) => {
const prepared = prepare(sections[i].body, FONT)
const { height } = layout(prepared, contentWidth, LINE_HEIGHT)
const openHeight = height + 20 // + vertical padding
let open = false
const body = item.querySelector('.acc-body')
item.querySelector('.acc-header').addEventListener('click', () => {
open = !open
body.style.height = open ? openHeight + 'px' : '0'
})
})
})();
// ==========================================================
// 2. VIRTUALIZED LIST
// ==========================================================
(() => {
const FONT = '15px Inter'
const LINE_HEIGHT = 22
const ITEM_PADDING = 16 // top + bottom padding
const containerEl = document.getElementById('virtual-container')
const style = getComputedStyle(containerEl)
const ITEM_WIDTH = containerEl.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - 28 // minus item padding
const phrases = [
'The quick brown fox jumps over the lazy dog.',
'Pack my box with five dozen liquor jugs.',
'How vexingly quick daft zebras jump!',
'Sphinx of black quartz, judge my vow.',
'Two driven jocks help fax my big quiz.',
'The five boxing wizards jump quickly at dawn.',
'Jinxed wizards pluck ivy from the big quilt.',
'Crazy Frederick bought many very exquisite opal jewels.',
'We promptly judged antique ivory buckles for the next prize.',
'A quick movement of the enemy will jeopardize six gunboats.',
]
// Generate 10,000 items with variable-length text
const items = []
for (let i = 0; i < 10000; i++) {
const repeats = 1 + (i % 5)
let text = '#' + i + ' — '
for (let r = 0; r < repeats; r++) {
if (r > 0) text += ' '
text += phrases[i % phrases.length]
}
items.push(text)
}
// Precompute all heights with Pretext
const t0 = performance.now()
const heights = items.map(text => {
const prepared = prepare(text, FONT)
const { height } = layout(prepared, ITEM_WIDTH, LINE_HEIGHT)
return height + ITEM_PADDING + 1 // +1 for border
})
const prepMs = performance.now() - t0
// Build a prefix-sum array for fast offset lookup
const offsets = [0]
for (let i = 0; i < heights.length; i++) {
offsets.push(offsets[i] + heights[i])
}
const totalHeight = offsets[offsets.length - 1]
const spacer = document.getElementById('virtual-spacer')
spacer.style.height = totalHeight + 'px'
document.getElementById('virtual-stats').textContent =
`Computed ${items.length.toLocaleString()} item heights in ${prepMs.toFixed(0)}ms. Total scroll height: ${Math.round(totalHeight).toLocaleString()}px.`
function render() {
const scrollTop = containerEl.scrollTop
const viewHeight = containerEl.clientHeight
const viewBottom = scrollTop + viewHeight
// Binary search for first visible item
let lo = 0, hi = heights.length - 1
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2)
if (offsets[mid + 1] <= scrollTop) lo = mid + 1
else hi = mid
}
let html = ''
for (let i = lo; i < heights.length && offsets[i] < viewBottom; i++) {
html += `<div class="virtual-item" style="top:${offsets[i]}px; height:${heights[i]}px;">${items[i]}</div>`
}
spacer.innerHTML = html
}
containerEl.addEventListener('scroll', render)
render()
})();
// ==========================================================
// 3. "SHOW MORE" / LINE CLAMPING
// ==========================================================
(() => {
const FONT = '16px Inter'
const LINE_HEIGHT = 24
const MAX_LINES = 3
const texts = [
'Short text that fits in three lines easily.',
'This is a medium-length paragraph that might or might not exceed three lines depending on the container width. It has a few sentences to test the boundary.',
'This is a much longer block of text that will definitely exceed three lines. It goes on and on to demonstrate how the "Show more" pattern works with Pretext. Instead of rendering the text, measuring it with the DOM, and then deciding whether to truncate, we simply call layout() and check lineCount. If it exceeds our limit, we clamp the height and show a toggle. No hidden elements, no reflow, no flash of unclamped content. The user sees the clamped version immediately.',
]
const container = document.getElementById('clamp-demos')
// Create one card to measure the actual text width
const probe = document.createElement('div')
probe.className = 'clamp-card'
const probeText = document.createElement('div')
probeText.className = 'clamp-text'
probe.appendChild(probeText)
container.appendChild(probe)
const TEXT_WIDTH = probeText.clientWidth
container.removeChild(probe)
texts.forEach(text => {
const prepared = prepare(text, FONT)
const full = layout(prepared, TEXT_WIDTH, LINE_HEIGHT)
const needsClamp = full.lineCount > MAX_LINES
const clampedHeight = MAX_LINES * LINE_HEIGHT
const card = document.createElement('div')
card.className = 'clamp-card'
const textDiv = document.createElement('div')
textDiv.className = 'clamp-text'
textDiv.textContent = text
if (needsClamp) textDiv.style.height = clampedHeight + 'px'
card.appendChild(textDiv)
if (needsClamp) {
const btn = document.createElement('button')
btn.className = 'clamp-toggle'
btn.textContent = `Show more (${full.lineCount} lines)`
let expanded = false
btn.addEventListener('click', () => {
expanded = !expanded
textDiv.style.height = expanded ? full.height + 'px' : clampedHeight + 'px'
btn.textContent = expanded ? 'Show less' : `Show more (${full.lineCount} lines)`
})
card.appendChild(btn)
} else {
const note = document.createElement('div')
note.style.cssText = 'font-size:13px; color:#999; margin-top:4px;'
note.textContent = `${full.lineCount} line${full.lineCount === 1 ? '' : 's'} — no clamping needed`
card.appendChild(note)
}
container.appendChild(card)
})
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment