chore(theme): refresh vendored cms theme copies for font size overrides
Vendors cms backend/internal/theme css.go, defaults.go and the new fontsize.go so ThemeTypography carries FontSizeOverrides and preset validation with DisallowUnknownFields keeps accepting current presets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f69e824f78
commit
6979a87d92
@ -12,6 +12,7 @@ type FontFile struct {
|
||||
Style string
|
||||
URL string
|
||||
Format string // "woff2", "woff", "truetype"
|
||||
UnicodeRange string // optional; preserves Google's subset splitting for self-hosted fonts
|
||||
}
|
||||
|
||||
// GenerateCSS generates CSS variable declarations from theme settings
|
||||
@ -31,8 +32,11 @@ func GenerateCSS(theme *Theme, googleFontURLs []string, fontFaces []FontFile) st
|
||||
font-weight: %s;
|
||||
font-style: %s;
|
||||
font-display: swap;
|
||||
}
|
||||
`, ff.Family, ff.URL, ff.Format, ff.Weight, ff.Style)
|
||||
if ff.UnicodeRange != "" {
|
||||
fmt.Fprintf(&css, " unicode-range: %s;\n", ff.UnicodeRange)
|
||||
}
|
||||
css.WriteString("}\n")
|
||||
}
|
||||
|
||||
if len(googleFontURLs) > 0 || len(fontFaces) > 0 {
|
||||
@ -63,6 +67,8 @@ func GenerateCSS(theme *Theme, googleFontURLs []string, fontFaces []FontFile) st
|
||||
// Button class definitions
|
||||
WriteButtonClasses(&css, theme.Buttons)
|
||||
|
||||
writeFontSizeOverrideRules(&css, theme.Typography)
|
||||
|
||||
return css.String()
|
||||
}
|
||||
|
||||
@ -124,15 +130,65 @@ func writeTypographyVars(css *strings.Builder, typography *ThemeTypography) {
|
||||
writeVar(css, "font-sans", bodyStack)
|
||||
writeVar(css, "font-mono", monoStack)
|
||||
|
||||
// Font size base as rem
|
||||
if typography.FontSizeBase != "" {
|
||||
writeVar(css, "font-size-base", typography.FontSizeBase+"px")
|
||||
if v := NormalizeFontSizeBase(typography.FontSizeBase); v != "" {
|
||||
writeVar(css, "font-size-base", v)
|
||||
}
|
||||
if typography.LineHeightBase != "" {
|
||||
writeVar(css, "line-height-base", typography.LineHeightBase)
|
||||
}
|
||||
// Font weight base
|
||||
writeVar(css, "font-weight-base", getFontWeight(typography.FontWeightBase))
|
||||
for _, k := range FontSizeOverrideKeys() {
|
||||
v := typography.FontSizeOverrides[k]
|
||||
if v == "" || ValidateFontSize(v) != nil {
|
||||
continue
|
||||
}
|
||||
writeVar(css, "fs-"+k, v)
|
||||
}
|
||||
}
|
||||
|
||||
var fontSizeOverrideSelectors = map[string]string{
|
||||
"h1": "h1", "h2": "h2", "h3": "h3", "h4": "h4", "h5": "h5", "h6": "h6",
|
||||
"nav-link": ".bn-navbar a",
|
||||
"hero-title": ".bn-hero-title",
|
||||
"hero-subtitle": ".bn-hero-subtitle",
|
||||
"index-card-title": ".bn-post-card-title",
|
||||
}
|
||||
|
||||
var headingOverrideKeys = map[string]bool{"h1": true, "h2": true, "h3": true, "h4": true, "h5": true, "h6": true}
|
||||
|
||||
// writeFontSizeOverrideRules emits element rules for overrides that cannot be
|
||||
// consumed via var() fallbacks. Headings go in @layer base so Tailwind
|
||||
// utilities on blocks still win; chrome rules stay unlayered so they beat the
|
||||
// utility classes baked into builtin templates. Emitted last so they win
|
||||
// same-specificity cascade inside this sheet.
|
||||
func writeFontSizeOverrideRules(css *strings.Builder, typography *ThemeTypography) {
|
||||
if typography == nil || len(typography.FontSizeOverrides) == 0 {
|
||||
return
|
||||
}
|
||||
var headings, chrome []string
|
||||
for _, k := range FontSizeOverrideKeys() {
|
||||
v := typography.FontSizeOverrides[k]
|
||||
sel, ok := fontSizeOverrideSelectors[k]
|
||||
if !ok || v == "" || ValidateFontSize(v) != nil {
|
||||
continue
|
||||
}
|
||||
rule := fmt.Sprintf("%s { font-size: var(--fs-%s); }", sel, k)
|
||||
if headingOverrideKeys[k] {
|
||||
headings = append(headings, " "+rule)
|
||||
} else {
|
||||
chrome = append(chrome, rule)
|
||||
}
|
||||
}
|
||||
if len(headings) > 0 {
|
||||
css.WriteString("\n@layer base {\n")
|
||||
css.WriteString(strings.Join(headings, "\n"))
|
||||
css.WriteString("\n}\n")
|
||||
}
|
||||
if len(chrome) > 0 {
|
||||
css.WriteString("\n")
|
||||
css.WriteString(strings.Join(chrome, "\n"))
|
||||
css.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func writeSpacingVars(css *strings.Builder, spacing *ThemeSpacing) {
|
||||
@ -361,7 +417,7 @@ func WriteButtonClasses(css *strings.Builder, buttons *ThemeButtons) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-size: var(--fs-button, 1rem);
|
||||
font-weight: 500;
|
||||
border-radius: var(--button-radius, 0.375rem);
|
||||
transition: all 150ms ease;
|
||||
@ -370,9 +426,9 @@ func WriteButtonClasses(css *strings.Builder, buttons *ThemeButtons) {
|
||||
}
|
||||
|
||||
/* Button sizes */
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.875rem; }
|
||||
.btn-md { padding: 0.5rem 1rem; font-size: 1rem; }
|
||||
.btn-lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; }
|
||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: calc(var(--fs-button, 1rem) * 0.875); }
|
||||
.btn-md { padding: 0.5rem 1rem; font-size: var(--fs-button, 1rem); }
|
||||
.btn-lg { padding: 0.75rem 1.5rem; font-size: calc(var(--fs-button, 1rem) * 1.125); }
|
||||
|
||||
/* Theme-aware button types */
|
||||
.btn-primary {
|
||||
|
||||
@ -235,6 +235,7 @@ type ThemeTypography struct {
|
||||
FontSizeBase string `json:"fontSizeBase"`
|
||||
LineHeightBase string `json:"lineHeightBase"`
|
||||
FontWeightBase string `json:"fontWeightBase"`
|
||||
FontSizeOverrides map[string]string `json:"fontSizeOverrides,omitempty"`
|
||||
}
|
||||
|
||||
// ThemeSpacing represents spacing settings
|
||||
|
||||
96
internal/theme/fontsize.go
Normal file
96
internal/theme/fontsize.go
Normal file
@ -0,0 +1,96 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var fontSizeOverrideKeys = map[string]bool{
|
||||
"h1": true, "h2": true, "h3": true, "h4": true, "h5": true, "h6": true,
|
||||
"post-title": true, "post-lede": true, "post-body": true,
|
||||
"post-h2": true, "post-h3": true, "post-meta": true,
|
||||
"page-title": true, "page-lede": true, "index-card-title": true,
|
||||
"hero-title": true, "hero-subtitle": true,
|
||||
"button": true, "nav-link": true,
|
||||
}
|
||||
|
||||
var fontSizePattern = regexp.MustCompile(`^(\d+(?:\.\d+)?|\.\d+)(rem|px)$`)
|
||||
|
||||
// FontSizeOverrideKeys returns the canonical override keys, sorted.
|
||||
func FontSizeOverrideKeys() []string {
|
||||
keys := make([]string, 0, len(fontSizeOverrideKeys))
|
||||
for k := range fontSizeOverrideKeys {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// ValidateFontSize accepts unit-suffixed CSS lengths within sane bounds.
|
||||
// Values are emitted into a stylesheet, so this is a security gate.
|
||||
func ValidateFontSize(v string) error {
|
||||
m := fontSizePattern.FindStringSubmatch(v)
|
||||
if m == nil {
|
||||
return fmt.Errorf("font size %q must be a number with rem or px unit", v)
|
||||
}
|
||||
n, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("font size %q is not a number", v)
|
||||
}
|
||||
switch m[2] {
|
||||
case "rem":
|
||||
if n < 0.25 || n > 10 {
|
||||
return fmt.Errorf("font size %q out of range (0.25rem to 10rem)", v)
|
||||
}
|
||||
case "px":
|
||||
if n < 4 || n > 160 {
|
||||
return fmt.Errorf("font size %q out of range (4px to 160px)", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NormalizeFontSizeBase upgrades legacy bare px numbers ("16") to "16px" and
|
||||
// validates unit-suffixed values. Returns "" for anything invalid so bad
|
||||
// stored data degrades to the CSS fallback instead of breaking the sheet.
|
||||
func NormalizeFontSizeBase(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
if _, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
v += "px"
|
||||
}
|
||||
if err := ValidateFontSize(v); err != nil {
|
||||
return ""
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// SanitizeFontSizeOverrides drops empty values, rejects unknown keys and
|
||||
// invalid values, and returns a fresh map safe to persist and emit.
|
||||
func SanitizeFontSizeOverrides(m map[string]string) (map[string]string, error) {
|
||||
if len(m) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if !fontSizeOverrideKeys[k] {
|
||||
return nil, fmt.Errorf("unknown font size element %q", k)
|
||||
}
|
||||
if err := ValidateFontSize(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user