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
@ -7,11 +7,12 @@ import (
|
|||||||
|
|
||||||
// FontFile represents a font file for CSS generation
|
// FontFile represents a font file for CSS generation
|
||||||
type FontFile struct {
|
type FontFile struct {
|
||||||
Family string
|
Family string
|
||||||
Weight string
|
Weight string
|
||||||
Style string
|
Style string
|
||||||
URL string
|
URL string
|
||||||
Format string // "woff2", "woff", "truetype"
|
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
|
// 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-weight: %s;
|
||||||
font-style: %s;
|
font-style: %s;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
|
||||||
`, ff.Family, ff.URL, ff.Format, ff.Weight, ff.Style)
|
`, 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 {
|
if len(googleFontURLs) > 0 || len(fontFaces) > 0 {
|
||||||
@ -63,6 +67,8 @@ func GenerateCSS(theme *Theme, googleFontURLs []string, fontFaces []FontFile) st
|
|||||||
// Button class definitions
|
// Button class definitions
|
||||||
WriteButtonClasses(&css, theme.Buttons)
|
WriteButtonClasses(&css, theme.Buttons)
|
||||||
|
|
||||||
|
writeFontSizeOverrideRules(&css, theme.Typography)
|
||||||
|
|
||||||
return css.String()
|
return css.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,15 +130,65 @@ func writeTypographyVars(css *strings.Builder, typography *ThemeTypography) {
|
|||||||
writeVar(css, "font-sans", bodyStack)
|
writeVar(css, "font-sans", bodyStack)
|
||||||
writeVar(css, "font-mono", monoStack)
|
writeVar(css, "font-mono", monoStack)
|
||||||
|
|
||||||
// Font size base as rem
|
if v := NormalizeFontSizeBase(typography.FontSizeBase); v != "" {
|
||||||
if typography.FontSizeBase != "" {
|
writeVar(css, "font-size-base", v)
|
||||||
writeVar(css, "font-size-base", typography.FontSizeBase+"px")
|
|
||||||
}
|
}
|
||||||
if typography.LineHeightBase != "" {
|
if typography.LineHeightBase != "" {
|
||||||
writeVar(css, "line-height-base", typography.LineHeightBase)
|
writeVar(css, "line-height-base", typography.LineHeightBase)
|
||||||
}
|
}
|
||||||
// Font weight base
|
|
||||||
writeVar(css, "font-weight-base", getFontWeight(typography.FontWeightBase))
|
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) {
|
func writeSpacingVars(css *strings.Builder, spacing *ThemeSpacing) {
|
||||||
@ -361,7 +417,7 @@ func WriteButtonClasses(css *strings.Builder, buttons *ThemeButtons) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 1rem;
|
font-size: var(--fs-button, 1rem);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: var(--button-radius, 0.375rem);
|
border-radius: var(--button-radius, 0.375rem);
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
@ -370,9 +426,9 @@ func WriteButtonClasses(css *strings.Builder, buttons *ThemeButtons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Button sizes */
|
/* Button sizes */
|
||||||
.btn-sm { padding: 0.375rem 0.75rem; font-size: 0.875rem; }
|
.btn-sm { padding: 0.375rem 0.75rem; font-size: calc(var(--fs-button, 1rem) * 0.875); }
|
||||||
.btn-md { padding: 0.5rem 1rem; font-size: 1rem; }
|
.btn-md { padding: 0.5rem 1rem; font-size: var(--fs-button, 1rem); }
|
||||||
.btn-lg { padding: 0.75rem 1.5rem; font-size: 1.125rem; }
|
.btn-lg { padding: 0.75rem 1.5rem; font-size: calc(var(--fs-button, 1rem) * 1.125); }
|
||||||
|
|
||||||
/* Theme-aware button types */
|
/* Theme-aware button types */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
|
|||||||
@ -229,12 +229,13 @@ type ColorScheme struct {
|
|||||||
|
|
||||||
// ThemeTypography represents typography settings
|
// ThemeTypography represents typography settings
|
||||||
type ThemeTypography struct {
|
type ThemeTypography struct {
|
||||||
FontHeading string `json:"fontHeading"`
|
FontHeading string `json:"fontHeading"`
|
||||||
FontBody string `json:"fontBody"`
|
FontBody string `json:"fontBody"`
|
||||||
FontMono string `json:"fontMono"`
|
FontMono string `json:"fontMono"`
|
||||||
FontSizeBase string `json:"fontSizeBase"`
|
FontSizeBase string `json:"fontSizeBase"`
|
||||||
LineHeightBase string `json:"lineHeightBase"`
|
LineHeightBase string `json:"lineHeightBase"`
|
||||||
FontWeightBase string `json:"fontWeightBase"`
|
FontWeightBase string `json:"fontWeightBase"`
|
||||||
|
FontSizeOverrides map[string]string `json:"fontSizeOverrides,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThemeSpacing represents spacing settings
|
// 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