288 lines
12 KiB
EmacsLisp
288 lines
12 KiB
EmacsLisp
// el-style — The el-ui design system.
|
|
//
|
|
// Hierarchy:
|
|
// 1. Theme — single source of truth, set at experience root
|
|
// 2. Semantic tokens — Color::Primary, Spacing::Lg, Radius::Md, ...
|
|
// 3. StyleModifier — fluent builder applied to any component
|
|
// 4. StyleSheet — named rule sets, applied with .apply("card")
|
|
//
|
|
// Resolution flow:
|
|
// component declares semantic token → theme resolves to RGBA / px →
|
|
// compiler emits CSS-in-JS for the browser target / NSColor for macOS / ...
|
|
|
|
// ── Color tokens ─────────────────────────────────────────────────────────────
|
|
|
|
let COLOR_PRIMARY: String = "primary"
|
|
let COLOR_ON_PRIMARY: String = "on_primary"
|
|
let COLOR_PRIMARY_CONTAINER: String = "primary_container"
|
|
let COLOR_SECONDARY: String = "secondary"
|
|
let COLOR_ON_SECONDARY: String = "on_secondary"
|
|
let COLOR_BACKGROUND: String = "background"
|
|
let COLOR_ON_BACKGROUND: String = "on_background"
|
|
let COLOR_SURFACE: String = "surface"
|
|
let COLOR_ON_SURFACE: String = "on_surface"
|
|
let COLOR_SURFACE_VARIANT: String = "surface_variant"
|
|
let COLOR_ERROR: String = "error"
|
|
let COLOR_ON_ERROR: String = "on_error"
|
|
let COLOR_OUTLINE: String = "outline"
|
|
let COLOR_OUTLINE_VARIANT: String = "outline_variant"
|
|
|
|
// A resolved RGBA quadruple, encoded as JSON for transport across the FFI.
|
|
type Rgba {
|
|
r: Int // 0-255
|
|
g: Int // 0-255
|
|
b: Int // 0-255
|
|
a: Int // 0-255 (we store alpha *256 to keep everything integer in El today)
|
|
}
|
|
|
|
fn rgba(r: Int, g: Int, b: Int, a: Int) -> Rgba {
|
|
{ "r": r, "g": g, "b": b, "a": a }
|
|
}
|
|
|
|
fn rgba_to_css(c: Rgba) -> String {
|
|
"rgba(" + int_to_str(c.r) + "," + int_to_str(c.g) + "," + int_to_str(c.b)
|
|
+ "," + int_to_str(c.a) + ")"
|
|
}
|
|
|
|
fn parse_hex(hex: String) -> Rgba {
|
|
let s: String = hex
|
|
if str_starts_with(s, "#") { let s = str_slice(s, 1, str_len(s)) }
|
|
let r: Int = hex_to_int(str_slice(s, 0, 2))
|
|
let g: Int = hex_to_int(str_slice(s, 2, 4))
|
|
let b: Int = hex_to_int(str_slice(s, 4, 6))
|
|
let a: Int = 255
|
|
if str_len(s) > 6 { let a = hex_to_int(str_slice(s, 6, 8)) }
|
|
{ "r": r, "g": g, "b": b, "a": a }
|
|
}
|
|
|
|
// ── ColorScheme: semantic token -> Rgba ──────────────────────────────────────
|
|
|
|
type ColorScheme {
|
|
primary: String // hex
|
|
on_primary: String
|
|
primary_container: String
|
|
secondary: String
|
|
on_secondary: String
|
|
background: String
|
|
on_background: String
|
|
surface: String
|
|
on_surface: String
|
|
surface_variant: String
|
|
error: String
|
|
on_error: String
|
|
outline: String
|
|
outline_variant: String
|
|
}
|
|
|
|
fn color_scheme_light() -> ColorScheme {
|
|
{ "primary": "#3b82f6", "on_primary": "#ffffff", "primary_container": "#dbeafe",
|
|
"secondary": "#a855f7", "on_secondary": "#ffffff",
|
|
"background": "#ffffff", "on_background": "#0f172a",
|
|
"surface": "#f8fafc", "on_surface": "#0f172a", "surface_variant": "#e2e8f0",
|
|
"error": "#dc2626", "on_error": "#ffffff",
|
|
"outline": "#cbd5e1", "outline_variant": "#e2e8f0" }
|
|
}
|
|
|
|
fn color_scheme_dark() -> ColorScheme {
|
|
{ "primary": "#60a5fa", "on_primary": "#0f172a", "primary_container": "#1e3a8a",
|
|
"secondary": "#c084fc", "on_secondary": "#0f172a",
|
|
"background": "#0f172a", "on_background": "#f8fafc",
|
|
"surface": "#1e293b", "on_surface": "#f8fafc", "surface_variant": "#334155",
|
|
"error": "#f87171", "on_error": "#0f172a",
|
|
"outline": "#475569", "outline_variant": "#334155" }
|
|
}
|
|
|
|
// Resolve a semantic color token to its hex value in the active scheme.
|
|
fn resolve_color(scheme: ColorScheme, token: String) -> String {
|
|
if str_eq(token, "primary") { return scheme.primary }
|
|
if str_eq(token, "on_primary") { return scheme.on_primary }
|
|
if str_eq(token, "primary_container") { return scheme.primary_container }
|
|
if str_eq(token, "secondary") { return scheme.secondary }
|
|
if str_eq(token, "on_secondary") { return scheme.on_secondary }
|
|
if str_eq(token, "background") { return scheme.background }
|
|
if str_eq(token, "on_background") { return scheme.on_background }
|
|
if str_eq(token, "surface") { return scheme.surface }
|
|
if str_eq(token, "on_surface") { return scheme.on_surface }
|
|
if str_eq(token, "surface_variant") { return scheme.surface_variant }
|
|
if str_eq(token, "error") { return scheme.error }
|
|
if str_eq(token, "on_error") { return scheme.on_error }
|
|
if str_eq(token, "outline") { return scheme.outline }
|
|
if str_eq(token, "outline_variant") { return scheme.outline_variant }
|
|
"#000000"
|
|
}
|
|
|
|
// ── Spacing scale ────────────────────────────────────────────────────────────
|
|
|
|
type SpacingScale {
|
|
xs: Int
|
|
sm: Int
|
|
md: Int
|
|
lg: Int
|
|
xl: Int
|
|
xxl: Int
|
|
}
|
|
|
|
fn spacing_default() -> SpacingScale {
|
|
{ "xs": 4, "sm": 8, "md": 16, "lg": 24, "xl": 32, "xxl": 48 }
|
|
}
|
|
|
|
fn resolve_spacing(s: SpacingScale, token: String) -> Int {
|
|
if str_eq(token, "xs") { return s.xs }
|
|
if str_eq(token, "sm") { return s.sm }
|
|
if str_eq(token, "md") { return s.md }
|
|
if str_eq(token, "lg") { return s.lg }
|
|
if str_eq(token, "xl") { return s.xl }
|
|
if str_eq(token, "xxl") { return s.xxl }
|
|
s.md
|
|
}
|
|
|
|
// ── Radius / shadow ──────────────────────────────────────────────────────────
|
|
|
|
type RadiusScale {
|
|
none: Int
|
|
sm: Int
|
|
md: Int
|
|
lg: Int
|
|
full: Int
|
|
}
|
|
|
|
fn radius_default() -> RadiusScale {
|
|
{ "none": 0, "sm": 4, "md": 8, "lg": 16, "full": 9999 }
|
|
}
|
|
|
|
type ShadowScale {
|
|
sm: String // CSS box-shadow string
|
|
md: String
|
|
lg: String
|
|
}
|
|
|
|
fn shadow_light() -> ShadowScale {
|
|
{ "sm": "0 1px 2px rgba(0,0,0,0.05)",
|
|
"md": "0 4px 6px rgba(0,0,0,0.1)",
|
|
"lg": "0 10px 15px rgba(0,0,0,0.1)" }
|
|
}
|
|
|
|
fn shadow_dark() -> ShadowScale {
|
|
{ "sm": "0 1px 2px rgba(0,0,0,0.4)",
|
|
"md": "0 4px 6px rgba(0,0,0,0.5)",
|
|
"lg": "0 10px 15px rgba(0,0,0,0.6)" }
|
|
}
|
|
|
|
// ── Typography ───────────────────────────────────────────────────────────────
|
|
|
|
type TextStyle {
|
|
font_family: String
|
|
font_size: Int
|
|
font_weight: Int // 100..900
|
|
line_height: Int // px
|
|
letter_spacing: Int // hundredths of em
|
|
}
|
|
|
|
fn text_style_default() -> TextStyle {
|
|
{ "font_family": "system-ui, -apple-system, sans-serif",
|
|
"font_size": 16, "font_weight": 400, "line_height": 24, "letter_spacing": 0 }
|
|
}
|
|
|
|
// ── Theme ────────────────────────────────────────────────────────────────────
|
|
|
|
let THEME_MODE_LIGHT: String = "light"
|
|
let THEME_MODE_DARK: String = "dark"
|
|
let THEME_MODE_SYSTEM: String = "system"
|
|
|
|
type Theme {
|
|
mode: String // light | dark | system
|
|
colors: ColorScheme
|
|
spacing: SpacingScale
|
|
radius: RadiusScale
|
|
shadows: ShadowScale
|
|
typography: TextStyle
|
|
}
|
|
|
|
fn theme_default_light() -> Theme {
|
|
{ "mode": "light", "colors": color_scheme_light(), "spacing": spacing_default(),
|
|
"radius": radius_default(), "shadows": shadow_light(), "typography": text_style_default() }
|
|
}
|
|
|
|
fn theme_default_dark() -> Theme {
|
|
{ "mode": "dark", "colors": color_scheme_dark(), "spacing": spacing_default(),
|
|
"radius": radius_default(), "shadows": shadow_dark(), "typography": text_style_default() }
|
|
}
|
|
|
|
// ── StyleSheet — named rule sets ─────────────────────────────────────────────
|
|
//
|
|
// A StyleSheet is a JSON object mapping selector -> declarations.
|
|
// Components apply by name: element.apply("card")
|
|
|
|
fn stylesheet_new() -> String {
|
|
"{}"
|
|
}
|
|
|
|
fn stylesheet_define(sheet: String, name: String, css_decls: String) -> String {
|
|
json_set(sheet, name, css_decls)
|
|
}
|
|
|
|
fn stylesheet_resolve(sheet: String, name: String) -> String {
|
|
json_get(sheet, name)
|
|
}
|
|
|
|
// ── CSS emit ─────────────────────────────────────────────────────────────────
|
|
//
|
|
// Server-side render: theme → CSS custom properties block.
|
|
// Emit as a <style> tag or inject into an existing stylesheet string.
|
|
|
|
fn theme_to_css_vars(theme: Theme) -> String {
|
|
let c: ColorScheme = theme.colors
|
|
let s: SpacingScale = theme.spacing
|
|
let r: RadiusScale = theme.radius
|
|
let sh: ShadowScale = theme.shadows
|
|
let ty: TextStyle = theme.typography
|
|
"--el-primary:" + c.primary + ";"
|
|
+ "--el-on-primary:" + c.on_primary + ";"
|
|
+ "--el-primary-container:" + c.primary_container + ";"
|
|
+ "--el-secondary:" + c.secondary + ";"
|
|
+ "--el-on-secondary:" + c.on_secondary + ";"
|
|
+ "--el-bg:" + c.background + ";"
|
|
+ "--el-on-bg:" + c.on_background + ";"
|
|
+ "--el-surface:" + c.surface + ";"
|
|
+ "--el-on-surface:" + c.on_surface + ";"
|
|
+ "--el-surface-variant:" + c.surface_variant + ";"
|
|
+ "--el-error:" + c.error + ";"
|
|
+ "--el-outline:" + c.outline + ";"
|
|
+ "--el-outline-variant:" + c.outline_variant + ";"
|
|
+ "--el-space-xs:" + int_to_str(s.xs) + "px;"
|
|
+ "--el-space-sm:" + int_to_str(s.sm) + "px;"
|
|
+ "--el-space-md:" + int_to_str(s.md) + "px;"
|
|
+ "--el-space-lg:" + int_to_str(s.lg) + "px;"
|
|
+ "--el-space-xl:" + int_to_str(s.xl) + "px;"
|
|
+ "--el-space-xxl:" + int_to_str(s.xxl) + "px;"
|
|
+ "--el-radius-none:" + int_to_str(r.none) + "px;"
|
|
+ "--el-radius-sm:" + int_to_str(r.sm) + "px;"
|
|
+ "--el-radius-md:" + int_to_str(r.md) + "px;"
|
|
+ "--el-radius-lg:" + int_to_str(r.lg) + "px;"
|
|
+ "--el-radius-full:" + int_to_str(r.full) + "px;"
|
|
+ "--el-shadow-sm:" + sh.sm + ";"
|
|
+ "--el-shadow-md:" + sh.md + ";"
|
|
+ "--el-shadow-lg:" + sh.lg + ";"
|
|
+ "--el-font-family:" + ty.font_family + ";"
|
|
+ "--el-font-size:" + int_to_str(ty.font_size) + "px;"
|
|
+ "--el-font-weight:" + int_to_str(ty.font_weight) + ";"
|
|
+ "--el-line-height:" + int_to_str(ty.line_height) + "px"
|
|
}
|
|
|
|
fn theme_to_style_tag(theme: Theme) -> String {
|
|
"<style>:root{" + theme_to_css_vars(theme) + "}</style>"
|
|
}
|
|
|
|
fn text_style_to_css(ts: TextStyle) -> String {
|
|
"font-family:" + ts.font_family
|
|
+ ";font-size:" + int_to_str(ts.font_size) + "px"
|
|
+ ";font-weight:" + int_to_str(ts.font_weight)
|
|
+ ";line-height:" + int_to_str(ts.line_height) + "px"
|
|
}
|
|
|
|
// ── Entry — smoke test ───────────────────────────────────────────────────────
|
|
|
|
let theme: Theme = theme_default_light()
|
|
println("[el-style] light primary = " + theme.colors.primary)
|
|
println("[el-style] css vars = " + theme_to_css_vars(theme))
|