(in-package :cl-tui.box) ;; ── Theme Engine ────────────────────────────────────────────── (defclass theme () ((mode :initform :dark :initarg :mode :accessor theme-mode) (roles :initform (make-hash-table) :accessor theme-roles))) (defun make-theme (&key (mode :dark)) (make-instance 'theme :mode mode)) (defun theme-color (theme role) "Resolve a semantic ROLE to a hex color string in THEME." (gethash role (theme-roles theme))) (defun (setf theme-color) (hex theme role) "Set the hex color for a semantic ROLE in THEME." (setf (gethash role (theme-roles theme)) hex)) (defparameter *presets* (make-hash-table :test #'eq)) (defmacro define-preset (name &key dark light) "Define a theme preset with DARK and LIGHT variants. NAME should be a keyword (e.g., :default, :nord)." (check-type name keyword) `(setf (gethash ,name *presets*) '(:dark ,dark :light ,light))) (defun load-preset (theme preset-name) "Load PRESET-NAME (a keyword) into THEME, overwriting role mappings." (let ((preset (gethash preset-name *presets*))) (if preset (let* ((variant (if (eql (theme-mode theme) :dark) (getf preset :dark) (getf preset :light))) (roles (theme-roles theme))) (clrhash roles) (loop for (role hex) on variant by #'cddr do (setf (gethash role roles) hex))) (warn "Unknown preset: ~S" preset-name)))) (define-preset :default :dark (:primary "#FFD700" :secondary "#B8860B" :accent "#FFA500" :error "#FF4444" :warning "#FF8800" :success "#44BB44" :info "#4488FF" :text "#FFFFFF" :text-muted "#888888" :background "#1A1A2E" :background-panel "#16213E" :background-element "#0F3460" :border "#334155" :border-active "#FFD700" :diff-added "#164B16" :diff-removed "#4B1616" :diff-context "#1A1A2E" :markdown-heading "#FFD700" :markdown-code "#334155" :markdown-link "#4488FF" :markdown-quote "#888888" :syntax-keyword "#FF79C6" :syntax-function "#50FA7B" :syntax-string "#F1FA8C" :syntax-number "#BD93F9" :syntax-comment "#6272A4" :syntax-type "#8BE9FD") :light (:primary "#B8860B" :secondary "#8B6914" :accent "#FF8C00" :error "#CC0000" :warning "#CC6600" :success "#228B22" :info "#0055CC" :text "#1A1A2E" :text-muted "#888888" :background "#F8F9FA" :background-panel "#FFFFFF" :background-element "#E9ECEF" :border "#DEE2E6" :border-active "#B8860B" :diff-added "#DFD" :diff-removed "#FDD" :diff-context "#F8F9FA" :markdown-heading "#B8860B" :markdown-code "#E9ECEF" :markdown-link "#0055CC" :markdown-quote "#888888" :syntax-keyword "#D63384" :syntax-function "#198754" :syntax-string "#FFC107" :syntax-number "#6F42C1" :syntax-comment "#6C757D" :syntax-type "#0DCAF0")) (define-preset :nord :dark (:primary "#88C0D0" :secondary "#81A1C1" :accent "#5E81AC" :error "#BF616A" :warning "#D08770" :success "#A3BE8C" :info "#B48EAD" :text "#ECEFF4" :text-muted "#616E88" :background "#2E3440" :background-panel "#3B4252" :background-element "#434C5E" :border "#4C566A" :border-active "#88C0D0" :diff-added "#164B16" :diff-removed "#4B1616" :diff-context "#2E3440" :markdown-heading "#88C0D0" :markdown-code "#3B4252" :markdown-link "#81A1C1" :markdown-quote "#616E88" :syntax-keyword "#81A1C1" :syntax-function "#A3BE8C" :syntax-string "#EBCB8B" :syntax-number "#B48EAD" :syntax-comment "#616E88" :syntax-type "#88C0D0") :light (:primary "#5E81AC" :secondary "#81A1C1" :accent "#88C0D0" :error "#BF616A" :warning "#D08770" :success "#A3BE8C" :info "#B48EAD" :text "#2E3440" :text-muted "#8F9BB3" :background "#ECEFF4" :background-panel "#FFFFFF" :background-element "#E5E9F0" :border "#D8DEE9" :border-active "#5E81AC" :diff-added "#DFD" :diff-removed "#FDD" :diff-context "#ECEFF4" :markdown-heading "#5E81AC" :markdown-code "#E5E9F0" :markdown-link "#81A1C1" :markdown-quote "#8F9BB3" :syntax-keyword "#81A1C1" :syntax-function "#A3BE8C" :syntax-string "#D08770" :syntax-number "#B48EAD" :syntax-comment "#8F9BB3" :syntax-type "#88C0D0"))