text size presets

This commit is contained in:
Michael Freno
2025-10-11 22:26:10 -04:00
parent 1f1add77a0
commit 89767230d5
2 changed files with 354 additions and 101 deletions

View File

@@ -102,9 +102,34 @@ local enums = {
}, },
---@enum FlexWrap ---@enum FlexWrap
FlexWrap = { NOWRAP = "nowrap", WRAP = "wrap", WRAP_REVERSE = "wrap-reverse" }, FlexWrap = { NOWRAP = "nowrap", WRAP = "wrap", WRAP_REVERSE = "wrap-reverse" },
---@enum TextSize
TextSize = {
XXS = "xxs",
XS = "xs",
SM = "sm",
MD = "md",
LG = "lg",
XL = "xl",
XXL = "xxl",
XL3 = "3xl",
XL4 = "4xl",
},
} }
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap = -- Text size preset mappings (in vh units for auto-scaling)
local TEXT_SIZE_PRESETS = {
xxs = 0.75, -- 0.75vh
xs = 1.25, -- 1.25vh
sm = 1.75, -- 1.75vh
md = 2.25, -- 2.25vh (default)
lg = 2.75, -- 2.75vh
xl = 3.5, -- 3.5vh
xxl = 4.5, -- 4.5vh
["3xl"] = 5.0, -- 5vh
["4xl"] = 7.0, -- 7vh
}
local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, TextAlign, AlignSelf, JustifySelf, FlexWrap, TextSize =
enums.Positioning, enums.Positioning,
enums.FlexDirection, enums.FlexDirection,
enums.JustifyContent, enums.JustifyContent,
@@ -113,7 +138,8 @@ local Positioning, FlexDirection, JustifyContent, AlignContent, AlignItems, Text
enums.TextAlign, enums.TextAlign,
enums.AlignSelf, enums.AlignSelf,
enums.JustifySelf, enums.JustifySelf,
enums.FlexWrap enums.FlexWrap,
enums.TextSize
-- ==================== -- ====================
-- Units System -- Units System
@@ -332,10 +358,18 @@ function Grid.layoutGridItems(element)
local childTotalHeight = child.height + child.padding.top + child.padding.bottom local childTotalHeight = child.height + child.padding.top + child.padding.bottom
child.x = cellX + (cellWidth - childTotalWidth) / 2 child.x = cellX + (cellWidth - childTotalWidth) / 2
child.y = cellY + (cellHeight - childTotalHeight) / 2 child.y = cellY + (cellHeight - childTotalHeight) / 2
elseif effectiveAlignItems == AlignItems.FLEX_START or effectiveAlignItems == "flex-start" or effectiveAlignItems == "start" then elseif
effectiveAlignItems == AlignItems.FLEX_START
or effectiveAlignItems == "flex-start"
or effectiveAlignItems == "start"
then
child.x = cellX child.x = cellX
child.y = cellY child.y = cellY
elseif effectiveAlignItems == AlignItems.FLEX_END or effectiveAlignItems == "flex-end" or effectiveAlignItems == "end" then elseif
effectiveAlignItems == AlignItems.FLEX_END
or effectiveAlignItems == "flex-end"
or effectiveAlignItems == "end"
then
local childTotalWidth = child.width + child.padding.left + child.padding.right local childTotalWidth = child.width + child.padding.left + child.padding.right
local childTotalHeight = child.height + child.padding.top + child.padding.bottom local childTotalHeight = child.height + child.padding.top + child.padding.bottom
child.x = cellX + cellWidth - childTotalWidth child.x = cellX + cellWidth - childTotalWidth
@@ -433,6 +467,9 @@ function Gui.destroy()
win:destroy() win:destroy()
end end
Gui.topElements = {} Gui.topElements = {}
-- Reset base scale and scale factors
Gui.baseScale = nil
Gui.scaleFactors = { x = 1.0, y = 1.0 }
end end
-- Simple GUI library for LOVE2D -- Simple GUI library for LOVE2D
@@ -583,6 +620,25 @@ function FONT_CACHE.getFont(textSize)
end end
end end
-- ====================
-- Text Size Utilities
-- ====================
--- Resolve text size preset to viewport units
---@param sizeValue string|number
---@return number, string -- Returns value and unit ("vh" for presets, original unit otherwise)
local function resolveTextSizePreset(sizeValue)
if type(sizeValue) == "string" then
-- Check if it's a preset
local preset = TEXT_SIZE_PRESETS[sizeValue]
if preset then
return preset, "vh"
end
end
-- Not a preset, return nil to indicate normal parsing should occur
return nil, nil
end
---@class Border ---@class Border
---@field top boolean? ---@field top boolean?
---@field right boolean? ---@field right boolean?
@@ -625,7 +681,7 @@ end
---@field flexWrap FlexWrap -- Whether children wrap to multiple lines (default: NOWRAP) ---@field flexWrap FlexWrap -- Whether children wrap to multiple lines (default: NOWRAP)
---@field justifySelf JustifySelf -- Alignment of the item itself along main axis (default: AUTO) ---@field justifySelf JustifySelf -- Alignment of the item itself along main axis (default: AUTO)
---@field alignSelf AlignSelf -- Alignment of the item itself along cross axis (default: AUTO) ---@field alignSelf AlignSelf -- Alignment of the item itself along cross axis (default: AUTO)
---@field textSize number? -- Font size for text content ---@field textSize number? -- Resolved font size for text content in pixels
---@field autoScaleText boolean -- Whether text should auto-scale with window size (default: true) ---@field autoScaleText boolean -- Whether text should auto-scale with window size (default: true)
---@field transform TransformProps -- Transform properties for animations and styling ---@field transform TransformProps -- Transform properties for animations and styling
---@field transition TransitionProps -- Transition settings for animations ---@field transition TransitionProps -- Transition settings for animations
@@ -661,7 +717,7 @@ Element.__index = Element
---@field titleColor Color? -- Color of the text content (default: black) ---@field titleColor Color? -- Color of the text content (default: black)
---@field textAlign TextAlign? -- Alignment of the text content (default: START) ---@field textAlign TextAlign? -- Alignment of the text content (default: START)
---@field textColor Color? -- Color of the text content (default: black) ---@field textColor Color? -- Color of the text content (default: black)
---@field textSize number|string? -- Font size for text content (default: auto-scaled) ---@field textSize number|string? -- Font size: number (px), string with units ("2vh", "10%"), or preset ("xxs"|"xs"|"sm"|"md"|"lg"|"xl"|"xxl"|"3xl"|"4xl") (default: "md")
---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true) ---@field autoScaleText boolean? -- Whether text should auto-scale with window size (default: true)
---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE) ---@field positioning Positioning? -- Layout positioning mode (default: ABSOLUTE)
---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL) ---@field flexDirection FlexDirection? -- Direction of flex layout (default: HORIZONTAL)
@@ -711,7 +767,6 @@ function Element.new(props)
self.opacity = props.opacity or 1 self.opacity = props.opacity or 1
self.text = props.text self.text = props.text
self.textSize = props.textSize or 12
self.textAlign = props.textAlign or TextAlign.START self.textAlign = props.textAlign or TextAlign.START
--- self positioning --- --- self positioning ---
@@ -747,6 +802,95 @@ function Element.new(props)
-- Get scale factors from Gui (will be used later) -- Get scale factors from Gui (will be used later)
local scaleX, scaleY = Gui.getScaleFactors() local scaleX, scaleY = Gui.getScaleFactors()
-- Store original textSize units and constraints
self.minTextSize = props.minTextSize
self.maxTextSize = props.maxTextSize
-- Set autoScaleText BEFORE textSize processing (needed for correct initialization)
if props.autoScaleText == nil then
self.autoScaleText = true
else
self.autoScaleText = props.autoScaleText
end
-- Handle textSize BEFORE width/height calculation (needed for auto-sizing)
if props.textSize then
if type(props.textSize) == "string" then
-- Check if it's a preset first
local presetValue, presetUnit = resolveTextSizePreset(props.textSize)
local value, unit
if presetValue then
-- It's a preset, use the preset value and unit
value, unit = presetValue, presetUnit
self.units.textSize = { value = value, unit = unit }
else
-- Not a preset, parse normally
value, unit = Units.parse(props.textSize)
self.units.textSize = { value = value, unit = unit }
end
-- Resolve textSize based on unit type
if unit == "%" or unit == "vh" then
-- Percentage and vh are relative to viewport height
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
elseif unit == "vw" then
-- vw is relative to viewport width
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
elseif unit == "ew" then
-- ew is relative to element width (use viewport width as fallback during initialization)
-- Will be re-resolved after width is set
self.textSize = (value / 100) * viewportWidth
elseif unit == "eh" then
-- eh is relative to element height (use viewport height as fallback during initialization)
-- Will be re-resolved after height is set
self.textSize = (value / 100) * viewportHeight
elseif unit == "px" then
-- Pixel units
self.textSize = value
else
error("Unknown textSize unit: " .. unit)
end
else
-- Validate pixel textSize value
if props.textSize <= 0 then
error("textSize must be greater than 0, got: " .. tostring(props.textSize))
end
-- Pixel textSize value
if self.autoScaleText and Gui.baseScale then
-- With base scaling: store original pixel value and scale relative to base resolution
self.units.textSize = { value = props.textSize, unit = "px" }
self.textSize = props.textSize * scaleY
elseif self.autoScaleText then
-- Without base scaling: convert to viewport units for auto-scaling
-- Calculate what percentage of viewport height this represents
local vhValue = (props.textSize / viewportHeight) * 100
self.units.textSize = { value = vhValue, unit = "vh" }
self.textSize = props.textSize -- Initial size is the specified pixel value
else
-- No auto-scaling: apply base scaling if set, otherwise use raw value
self.textSize = Gui.baseScale and (props.textSize * scaleY) or props.textSize
self.units.textSize = { value = props.textSize, unit = "px" }
end
end
else
-- No textSize specified - use auto-scaling default
if self.autoScaleText and Gui.baseScale then
-- With base scaling: use 12px as default and scale
self.units.textSize = { value = 12, unit = "px" }
self.textSize = 12 * scaleY
elseif self.autoScaleText then
-- Without base scaling: default to 1.5vh (1.5% of viewport height)
self.units.textSize = { value = 1.5, unit = "vh" }
self.textSize = (1.5 / 100) * viewportHeight
else
-- No auto-scaling: use 12px with optional base scaling
self.textSize = Gui.baseScale and (12 * scaleY) or 12
self.units.textSize = { value = nil, unit = "px" }
end
end
-- Handle width (both w and width properties, prefer w if both exist) -- Handle width (both w and width properties, prefer w if both exist)
local widthProp = props.width local widthProp = props.width
if widthProp then if widthProp then
@@ -808,67 +952,15 @@ function Element.new(props)
self.padding = Units.resolveSpacing(props.padding, self.width, self.height) self.padding = Units.resolveSpacing(props.padding, self.width, self.height)
self.margin = Units.resolveSpacing(props.margin, self.width, self.height) self.margin = Units.resolveSpacing(props.margin, self.width, self.height)
-- Store original textSize units and constraints -- Re-resolve ew/eh textSize units now that width/height are set
self.minTextSize = props.minTextSize if props.textSize and type(props.textSize) == "string" then
self.maxTextSize = props.maxTextSize
-- Auto-scale text by default (can be disabled with autoScaleText = false)
if props.autoScaleText == nil then
self.autoScaleText = true
else
self.autoScaleText = props.autoScaleText
end
if props.textSize then
if type(props.textSize) == "string" then
local value, unit = Units.parse(props.textSize) local value, unit = Units.parse(props.textSize)
self.units.textSize = { value = value, unit = unit } if unit == "ew" then
-- Element width relative (now that width is set)
-- Resolve textSize based on unit type
if unit == "%" or unit == "vh" then
-- Percentage and vh are relative to viewport height
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
elseif unit == "vw" then
-- vw is relative to viewport width
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
elseif unit == "ew" then
-- Element width relative (will be resolved after width is set)
self.textSize = (value / 100) * self.width self.textSize = (value / 100) * self.width
elseif unit == "eh" then elseif unit == "eh" then
-- Element height relative (will be resolved after height is set) -- Element height relative (now that height is set)
self.textSize = (value / 100) * self.height self.textSize = (value / 100) * self.height
else
self.textSize = Units.resolve(value, unit, viewportWidth, viewportHeight, nil)
end
else
-- Validate pixel textSize value
if props.textSize <= 0 then
error("textSize must be greater than 0, got: " .. tostring(props.textSize))
end
-- Pixel textSize value
if self.autoScaleText then
-- Convert pixel value to viewport units for auto-scaling
-- Calculate what percentage of viewport height this represents
local vhValue = (props.textSize / viewportHeight) * 100
self.units.textSize = { value = vhValue, unit = "vh" }
self.textSize = props.textSize -- Initial size is the specified pixel value
else
-- Apply base scaling to pixel text sizes (no auto-scaling)
self.textSize = Gui.baseScale and (props.textSize * scaleY) or props.textSize
self.units.textSize = { value = props.textSize, unit = "px" }
end
end
else
-- No textSize specified - use auto-scaling default
if self.autoScaleText then
-- Default to 1.5vh (1.5% of viewport height) for auto-scaling
self.textSize = (1.5 / 100) * viewportHeight
self.units.textSize = { value = 1.5, unit = "vh" }
else
-- Fixed 12px when auto-scaling is disabled (with base scaling if set)
self.textSize = Gui.baseScale and (12 * scaleY) or 12
self.units.textSize = { value = nil, unit = "px" }
end end
end end
@@ -1505,10 +1597,7 @@ function Element:layoutChildren()
child.y = self.y + self.padding.top + currentCrossPos child.y = self.y + self.padding.top + currentCrossPos
elseif effectiveAlign == AlignItems.CENTER then elseif effectiveAlign == AlignItems.CENTER then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
child.y = self.y child.y = self.y + self.padding.top + currentCrossPos + ((lineHeight - childTotalHeight) / 2)
+ self.padding.top
+ currentCrossPos
+ ((lineHeight - childTotalHeight) / 2)
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom local childTotalHeight = (child.height or 0) + child.padding.top + child.padding.bottom
child.y = self.y + self.padding.top + currentCrossPos + lineHeight - childTotalHeight child.y = self.y + self.padding.top + currentCrossPos + lineHeight - childTotalHeight
@@ -1543,10 +1632,7 @@ function Element:layoutChildren()
child.x = self.x + self.padding.left + currentCrossPos child.x = self.x + self.padding.left + currentCrossPos
elseif effectiveAlign == AlignItems.CENTER then elseif effectiveAlign == AlignItems.CENTER then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
child.x = self.x child.x = self.x + self.padding.left + currentCrossPos + ((lineHeight - childTotalWidth) / 2)
+ self.padding.left
+ currentCrossPos
+ ((lineHeight - childTotalWidth) / 2)
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right local childTotalWidth = (child.width or 0) + child.padding.left + child.padding.right
child.x = self.x + self.padding.left + currentCrossPos + lineHeight - childTotalWidth child.x = self.x + self.padding.left + currentCrossPos + lineHeight - childTotalWidth
@@ -1640,12 +1726,7 @@ function Element:draw()
Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity) Color.new(self.borderColor.r, self.borderColor.g, self.borderColor.b, self.borderColor.a * self.opacity)
love.graphics.setColor(borderColorWithOpacity:toRGBA()) love.graphics.setColor(borderColorWithOpacity:toRGBA())
if self.border.top then if self.border.top then
love.graphics.line( love.graphics.line(self.x, self.y, self.x + self.width + self.padding.left + self.padding.right, self.y)
self.x,
self.y,
self.x + self.width + self.padding.left + self.padding.right,
self.y
)
end end
if self.border.bottom then if self.border.bottom then
love.graphics.line( love.graphics.line(
@@ -1656,12 +1737,7 @@ function Element:draw()
) )
end end
if self.border.left then if self.border.left then
love.graphics.line( love.graphics.line(self.x, self.y, self.x, self.y + self.height + self.padding.top + self.padding.bottom)
self.x,
self.y,
self.x,
self.y + self.height + self.padding.top + self.padding.bottom
)
end end
if self.border.right then if self.border.right then
love.graphics.line( love.graphics.line(
@@ -1851,11 +1927,17 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight)
end end
-- Recalculate textSize if auto-scaling is enabled or using viewport/element-relative units -- Recalculate textSize if auto-scaling is enabled or using viewport/element-relative units
if self.autoScaleText and self.units.textSize.value and self.units.textSize.unit ~= "px" then if self.autoScaleText and self.units.textSize.value then
local unit = self.units.textSize.unit local unit = self.units.textSize.unit
local value = self.units.textSize.value local value = self.units.textSize.value
if unit == "%" or unit == "vh" then if unit == "px" and Gui.baseScale then
-- With base scaling: scale pixel values relative to base resolution
self.textSize = value * scaleY
elseif unit == "px" then
-- Without base scaling but auto-scaling enabled: text doesn't scale
self.textSize = value
elseif unit == "%" or unit == "vh" then
-- Percentage and vh are relative to viewport height -- Percentage and vh are relative to viewport height
self.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight) self.textSize = Units.resolve(value, unit, newViewportWidth, newViewportHeight, newViewportHeight)
elseif unit == "vw" then elseif unit == "vw" then
@@ -1887,7 +1969,7 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight)
self.textSize = 1 -- Minimum 1px self.textSize = 1 -- Minimum 1px
end end
elseif self.units.textSize.unit == "px" and self.units.textSize.value and Gui.baseScale then elseif self.units.textSize.unit == "px" and self.units.textSize.value and Gui.baseScale then
-- Reapply base scaling to pixel text sizes -- No auto-scaling but base scaling is set: reapply base scaling to pixel text sizes
self.textSize = self.units.textSize.value * scaleY self.textSize = self.units.textSize.value * scaleY
-- Protect against too-small text sizes (minimum 1px) -- Protect against too-small text sizes (minimum 1px)
@@ -1983,6 +2065,10 @@ end
---@return number ---@return number
function Element:calculateTextHeight() function Element:calculateTextHeight()
if self.text == nil then
return 0
end
if self.textSize then if self.textSize then
local tempFont = FONT_CACHE.get(self.textSize) local tempFont = FONT_CACHE.get(self.textSize)
local height = tempFont:getHeight() local height = tempFont:getHeight()

View File

@@ -0,0 +1,167 @@
-- Example demonstrating text size presets
-- FlexLove provides convenient size presets that automatically scale with viewport
package.path = package.path .. ";?.lua"
require("testing/loveStub")
local FlexLove = require("FlexLove")
local Gui = FlexLove.GUI
local Color = FlexLove.Color
print("=== Text Size Presets Examples ===\n")
-- Example 1: All size presets
print("1. All Text Size Presets")
print(" Demonstrating all available size presets\n")
local presets = {
{ name = "xxs", vh = 0.75 },
{ name = "xs", vh = 1.0 },
{ name = "sm", vh = 1.25 },
{ name = "md", vh = 1.5 },
{ name = "lg", vh = 2.0 },
{ name = "xl", vh = 2.5 },
{ name = "xxl", vh = 3.0 },
{ name = "3xl", vh = 4.0 },
{ name = "4xl", vh = 5.0 },
}
print("At viewport height 600px:")
for _, preset in ipairs(presets) do
local element = Gui.new({
text = "Sample Text (" .. preset.name .. ")",
textSize = preset.name,
textColor = Color.new(1, 1, 1),
})
local expectedSize = (preset.vh / 100) * 600
print(string.format(" %4s: textSize = %.2fpx (expected: %.2fpx = %.2fvh)",
preset.name, element.textSize, expectedSize, preset.vh))
-- Verify it matches expected size
assert(math.abs(element.textSize - expectedSize) < 0.01,
string.format("Size mismatch for %s: got %.2f, expected %.2f", preset.name, element.textSize, expectedSize))
element:destroy()
end
print("\n2. Auto-Scaling Behavior")
print(" Text size presets automatically scale with viewport\n")
Gui.destroy()
local mdElement = Gui.new({
text = "Medium Text",
textSize = "md",
textColor = Color.new(1, 1, 1),
})
print(" 'md' preset at 600px viewport: " .. mdElement.textSize .. "px")
mdElement:resize(1200, 1200)
print(" 'md' preset at 1200px viewport: " .. mdElement.textSize .. "px")
print(" Scaling factor: " .. (mdElement.textSize / 9.0) .. "x\n")
-- Example 3: Combining presets with other properties
print("3. Presets with Flex Layout")
print(" Using presets in a practical layout\n")
Gui.destroy()
local container = Gui.new({
x = 10,
y = 10,
width = 400,
height = 300,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
padding = { horizontal = 20, vertical = 20 },
background = Color.new(0.1, 0.1, 0.1),
})
local title = Gui.new({
parent = container,
text = "Title (xl)",
textSize = "xl",
textColor = Color.new(1, 1, 1),
})
local subtitle = Gui.new({
parent = container,
text = "Subtitle (lg)",
textSize = "lg",
textColor = Color.new(0.8, 0.8, 0.8),
})
local body = Gui.new({
parent = container,
text = "Body text (md)",
textSize = "md",
textColor = Color.new(0.7, 0.7, 0.7),
})
local caption = Gui.new({
parent = container,
text = "Caption (sm)",
textSize = "sm",
textColor = Color.new(0.5, 0.5, 0.5),
})
print(" Title: " .. title.textSize .. "px")
print(" Subtitle: " .. subtitle.textSize .. "px")
print(" Body: " .. body.textSize .. "px")
print(" Caption: " .. caption.textSize .. "px\n")
-- Example 4: Presets vs Custom Units
print("4. Presets vs Custom Units")
print(" Comparing preset convenience with custom units\n")
Gui.destroy()
local preset = Gui.new({
text = "Using preset 'lg'",
textSize = "lg",
textColor = Color.new(1, 1, 1),
})
local custom = Gui.new({
text = "Using custom '2vh'",
textSize = "2vh",
textColor = Color.new(1, 1, 1),
})
print(" Preset 'lg': " .. preset.textSize .. "px (2vh)")
print(" Custom '2vh': " .. custom.textSize .. "px")
print(" Both are equivalent!\n")
-- Example 5: Responsive Typography
print("5. Responsive Typography Scale")
print(" Building a complete type scale with presets\n")
Gui.destroy()
local typeScale = {
{ label = "Display", preset = "4xl" },
{ label = "Heading 1", preset = "3xl" },
{ label = "Heading 2", preset = "xxl" },
{ label = "Heading 3", preset = "xl" },
{ label = "Heading 4", preset = "lg" },
{ label = "Body Large", preset = "md" },
{ label = "Body", preset = "sm" },
{ label = "Caption", preset = "xs" },
{ label = "Fine Print", preset = "xxs" },
}
print(" Typography Scale at 600px viewport:")
for _, item in ipairs(typeScale) do
local element = Gui.new({
text = item.label,
textSize = item.preset,
textColor = Color.new(1, 1, 1),
})
print(string.format(" %-15s (%4s): %.2fpx", item.label, item.preset, element.textSize))
element:destroy()
end
print("\n=== Summary ===")
print("• Text size presets: xxs, xs, sm, md, lg, xl, xxl, 3xl, 4xl")
print("• All presets use viewport-relative units (vh)")
print("• Automatically scale with window size")
print("• Provide consistent typography scales")
print("• Can be mixed with custom units (px, vh, vw, %, ew, eh)")
print("• Default preset when no textSize specified: md (1.5vh)")