init.go raw

   1  package branding
   2  
   3  import (
   4  	"bytes"
   5  	"embed"
   6  	"encoding/json"
   7  	"fmt"
   8  	"image"
   9  	"image/color"
  10  	"image/png"
  11  	"io/fs"
  12  	"math"
  13  	"os"
  14  	"path/filepath"
  15  )
  16  
  17  // BrandingStyle represents the type of branding kit to generate
  18  type BrandingStyle string
  19  
  20  const (
  21  	StyleORLY    BrandingStyle = "orly"    // ORLY-branded assets
  22  	StyleGeneric BrandingStyle = "generic" // Generic/white-label assets
  23  )
  24  
  25  // InitBrandingKit creates a branding directory with assets and configuration
  26  func InitBrandingKit(dir string, embeddedFS embed.FS, style BrandingStyle) error {
  27  	// Create directory structure
  28  	dirs := []string{
  29  		dir,
  30  		filepath.Join(dir, "assets"),
  31  		filepath.Join(dir, "css"),
  32  	}
  33  
  34  	for _, d := range dirs {
  35  		if err := os.MkdirAll(d, 0755); err != nil {
  36  			return fmt.Errorf("failed to create directory %s: %w", d, err)
  37  		}
  38  	}
  39  
  40  	// Write branding.json based on style
  41  	var config Config
  42  	var cssTemplate, varsTemplate string
  43  
  44  	switch style {
  45  	case StyleGeneric:
  46  		config = GenericConfig()
  47  		cssTemplate = GenericCSSTemplate
  48  		varsTemplate = GenericCSSVariablesTemplate
  49  	default:
  50  		config = DefaultConfig()
  51  		cssTemplate = CSSTemplate
  52  		varsTemplate = CSSVariablesTemplate
  53  	}
  54  
  55  	configData, err := json.MarshalIndent(config, "", "  ")
  56  	if err != nil {
  57  		return fmt.Errorf("failed to marshal config: %w", err)
  58  	}
  59  	configPath := filepath.Join(dir, "branding.json")
  60  	if err := os.WriteFile(configPath, configData, 0644); err != nil {
  61  		return fmt.Errorf("failed to write branding.json: %w", err)
  62  	}
  63  
  64  	// Generate or extract assets based on style
  65  	if style == StyleGeneric {
  66  		// Generate generic placeholder images
  67  		if err := generateGenericAssets(dir); err != nil {
  68  			return fmt.Errorf("failed to generate generic assets: %w", err)
  69  		}
  70  	} else {
  71  		// Extract ORLY embedded assets
  72  		assetMappings := map[string]string{
  73  			"web/dist/orly.png":     filepath.Join(dir, "assets", "logo.png"),
  74  			"web/dist/favicon.png":  filepath.Join(dir, "assets", "favicon.png"),
  75  			"web/dist/icon-192.png": filepath.Join(dir, "assets", "icon-192.png"),
  76  			"web/dist/icon-512.png": filepath.Join(dir, "assets", "icon-512.png"),
  77  		}
  78  
  79  		for src, dst := range assetMappings {
  80  			data, err := fs.ReadFile(embeddedFS, src)
  81  			if err != nil {
  82  				altSrc := "web/" + filepath.Base(src)
  83  				data, err = fs.ReadFile(embeddedFS, altSrc)
  84  				if err != nil {
  85  					fmt.Printf("Warning: could not extract %s: %v\n", src, err)
  86  					continue
  87  				}
  88  			}
  89  			if err := os.WriteFile(dst, data, 0644); err != nil {
  90  				return fmt.Errorf("failed to write %s: %w", dst, err)
  91  			}
  92  		}
  93  	}
  94  
  95  	// Write CSS template
  96  	cssPath := filepath.Join(dir, "css", "custom.css")
  97  	if err := os.WriteFile(cssPath, []byte(cssTemplate), 0644); err != nil {
  98  		return fmt.Errorf("failed to write custom.css: %w", err)
  99  	}
 100  
 101  	// Write variables-only CSS template
 102  	varsPath := filepath.Join(dir, "css", "variables.css")
 103  	if err := os.WriteFile(varsPath, []byte(varsTemplate), 0644); err != nil {
 104  		return fmt.Errorf("failed to write variables.css: %w", err)
 105  	}
 106  
 107  	return nil
 108  }
 109  
 110  // generateGenericAssets creates simple geometric placeholder images
 111  func generateGenericAssets(dir string) error {
 112  	// Color scheme: neutral blue-gray
 113  	primaryColor := color.RGBA{R: 64, G: 128, B: 192, A: 255}   // #4080C0 - professional blue
 114  	transparent := color.RGBA{R: 0, G: 0, B: 0, A: 0}           // Transparent background
 115  
 116  	// Generate each size
 117  	sizes := map[string]int{
 118  		"logo.png":     256,
 119  		"favicon.png":  64,
 120  		"icon-192.png": 192,
 121  		"icon-512.png": 512,
 122  	}
 123  
 124  	for filename, size := range sizes {
 125  		img := generateRoundedSquare(size, primaryColor, transparent)
 126  		path := filepath.Join(dir, "assets", filename)
 127  		if err := savePNG(path, img); err != nil {
 128  			return fmt.Errorf("failed to save %s: %w", filename, err)
 129  		}
 130  	}
 131  
 132  	return nil
 133  }
 134  
 135  // generateRoundedSquare creates a simple rounded square icon
 136  func generateRoundedSquare(size int, primary, bg color.RGBA) image.Image {
 137  	img := image.NewRGBA(image.Rect(0, 0, size, size))
 138  
 139  	// Fill background
 140  	for y := 0; y < size; y++ {
 141  		for x := 0; x < size; x++ {
 142  			img.Set(x, y, bg)
 143  		}
 144  	}
 145  
 146  	// Draw a rounded square in the center
 147  	margin := size / 8
 148  	cornerRadius := size / 6
 149  	squareSize := size - (margin * 2)
 150  
 151  	for y := margin; y < margin+squareSize; y++ {
 152  		for x := margin; x < margin+squareSize; x++ {
 153  			// Check if point is inside rounded rectangle
 154  			if isInsideRoundedRect(x-margin, y-margin, squareSize, squareSize, cornerRadius) {
 155  				img.Set(x, y, primary)
 156  			}
 157  		}
 158  	}
 159  
 160  	// Draw a simple inner circle (relay symbol)
 161  	centerX := size / 2
 162  	centerY := size / 2
 163  	innerRadius := size / 5
 164  	ringWidth := size / 20
 165  
 166  	for y := 0; y < size; y++ {
 167  		for x := 0; x < size; x++ {
 168  			dx := float64(x - centerX)
 169  			dy := float64(y - centerY)
 170  			dist := math.Sqrt(dx*dx + dy*dy)
 171  
 172  			// Ring (circle outline)
 173  			if dist >= float64(innerRadius-ringWidth) && dist <= float64(innerRadius) {
 174  				img.Set(x, y, bg)
 175  			}
 176  		}
 177  	}
 178  
 179  	return img
 180  }
 181  
 182  // isInsideRoundedRect checks if a point is inside a rounded rectangle
 183  func isInsideRoundedRect(x, y, w, h, r int) bool {
 184  	// Check corners
 185  	if x < r && y < r {
 186  		// Top-left corner
 187  		return isInsideCircle(x, y, r, r, r)
 188  	}
 189  	if x >= w-r && y < r {
 190  		// Top-right corner
 191  		return isInsideCircle(x, y, w-r-1, r, r)
 192  	}
 193  	if x < r && y >= h-r {
 194  		// Bottom-left corner
 195  		return isInsideCircle(x, y, r, h-r-1, r)
 196  	}
 197  	if x >= w-r && y >= h-r {
 198  		// Bottom-right corner
 199  		return isInsideCircle(x, y, w-r-1, h-r-1, r)
 200  	}
 201  
 202  	// Inside main rectangle
 203  	return x >= 0 && x < w && y >= 0 && y < h
 204  }
 205  
 206  // isInsideCircle checks if a point is inside a circle
 207  func isInsideCircle(x, y, cx, cy, r int) bool {
 208  	dx := x - cx
 209  	dy := y - cy
 210  	return dx*dx+dy*dy <= r*r
 211  }
 212  
 213  // savePNG saves an image as a PNG file
 214  func savePNG(path string, img image.Image) error {
 215  	var buf bytes.Buffer
 216  	if err := png.Encode(&buf, img); err != nil {
 217  		return err
 218  	}
 219  	return os.WriteFile(path, buf.Bytes(), 0644)
 220  }
 221  
 222  // GenericConfig returns a generic/white-label configuration
 223  func GenericConfig() Config {
 224  	return Config{
 225  		Version: 1,
 226  		App: AppConfig{
 227  			Name:        "Relay",
 228  			ShortName:   "Relay",
 229  			Title:       "Relay Dashboard",
 230  			Description: "Nostr relay service",
 231  		},
 232  		NIP11: NIP11Config{
 233  			Name:        "Relay",
 234  			Description: "A Nostr relay",
 235  			Icon:        "",
 236  		},
 237  		Manifest: ManifestConfig{
 238  			ThemeColor:      "#4080C0",
 239  			BackgroundColor: "#F0F4F8",
 240  		},
 241  		Assets: AssetsConfig{
 242  			Logo:    "assets/logo.png",
 243  			Favicon: "assets/favicon.png",
 244  			Icon192: "assets/icon-192.png",
 245  			Icon512: "assets/icon-512.png",
 246  		},
 247  		CSS: CSSConfig{
 248  			CustomCSS:    "css/custom.css",
 249  			VariablesCSS: "css/variables.css",
 250  		},
 251  	}
 252  }
 253  
 254  // CSSTemplate is the full CSS template with all variables and documentation
 255  const CSSTemplate = `/*
 256   * Custom Branding CSS for ORLY Relay
 257   * ==================================
 258   *
 259   * This file is loaded AFTER the default styles, so any rules here
 260   * will override the defaults. You can customize:
 261   *
 262   * 1. CSS Variables (colors, spacing, etc.)
 263   * 2. Component styles (buttons, cards, headers, etc.)
 264   * 3. Add completely custom styles
 265   *
 266   * Restart the relay to apply changes.
 267   *
 268   * For variable-only overrides, edit variables.css instead.
 269   */
 270  
 271  /* =============================================================================
 272     LIGHT THEME VARIABLES
 273     ============================================================================= */
 274  
 275  :root {
 276      /* Background colors */
 277      --bg-color: #ddd;                    /* Main page background */
 278      --header-bg: #eee;                   /* Header background */
 279      --sidebar-bg: #eee;                  /* Sidebar background */
 280      --card-bg: #f8f9fa;                  /* Card/container background */
 281      --panel-bg: #f8f9fa;                 /* Panel background */
 282  
 283      /* Border colors */
 284      --border-color: #dee2e6;             /* Default border color */
 285  
 286      /* Text colors */
 287      --text-color: #444444;               /* Primary text color */
 288      --text-muted: #6c757d;               /* Secondary/muted text */
 289  
 290      /* Input/form colors */
 291      --input-border: #ccc;                /* Input border color */
 292      --input-bg: #ffffff;                 /* Input background */
 293      --input-text-color: #495057;         /* Input text color */
 294  
 295      /* Button colors */
 296      --button-bg: #ddd;                   /* Default button background */
 297      --button-hover-bg: #eee;             /* Button hover background */
 298      --button-text: #444444;              /* Button text color */
 299      --button-hover-border: #adb5bd;      /* Button hover border */
 300  
 301      /* Theme/accent colors */
 302      --primary: #00bcd4;                  /* Primary accent (cyan) */
 303      --primary-bg: rgba(0, 188, 212, 0.1); /* Primary background tint */
 304      --secondary: #6c757d;                /* Secondary color */
 305  
 306      /* Status colors */
 307      --success: #28a745;                  /* Success/positive */
 308      --success-bg: #d4edda;               /* Success background */
 309      --success-text: #155724;             /* Success text */
 310      --info: #17a2b8;                     /* Info/neutral */
 311      --warning: #ff3e00;                  /* Warning (Svelte orange) */
 312      --warning-bg: #fff3cd;               /* Warning background */
 313      --danger: #dc3545;                   /* Danger/error */
 314      --danger-bg: #f8d7da;                /* Danger background */
 315      --danger-text: #721c24;              /* Danger text */
 316      --error-bg: #f8d7da;                 /* Error background */
 317      --error-text: #721c24;               /* Error text */
 318  
 319      /* Code block colors */
 320      --code-bg: #f8f9fa;                  /* Code block background */
 321      --code-text: #495057;                /* Code text color */
 322  
 323      /* Tab colors */
 324      --tab-inactive-bg: #bbb;             /* Inactive tab background */
 325  
 326      /* Link/accent colors */
 327      --accent-color: #007bff;             /* Link color */
 328      --accent-hover-color: #0056b3;       /* Link hover color */
 329  }
 330  
 331  /* =============================================================================
 332     DARK THEME VARIABLES
 333     ============================================================================= */
 334  
 335  body.dark-theme {
 336      /* Background colors */
 337      --bg-color: #263238;                 /* Main page background */
 338      --header-bg: #1e272c;                /* Header background */
 339      --sidebar-bg: #1e272c;               /* Sidebar background */
 340      --card-bg: #37474f;                  /* Card/container background */
 341      --panel-bg: #37474f;                 /* Panel background */
 342  
 343      /* Border colors */
 344      --border-color: #404040;             /* Default border color */
 345  
 346      /* Text colors */
 347      --text-color: #ffffff;               /* Primary text color */
 348      --text-muted: #adb5bd;               /* Secondary/muted text */
 349  
 350      /* Input/form colors */
 351      --input-border: #555;                /* Input border color */
 352      --input-bg: #37474f;                 /* Input background */
 353      --input-text-color: #ffffff;         /* Input text color */
 354  
 355      /* Button colors */
 356      --button-bg: #263238;                /* Default button background */
 357      --button-hover-bg: #1e272c;          /* Button hover background */
 358      --button-text: #ffffff;              /* Button text color */
 359      --button-hover-border: #6c757d;      /* Button hover border */
 360  
 361      /* Theme/accent colors */
 362      --primary: #00bcd4;                  /* Primary accent (cyan) */
 363      --primary-bg: rgba(0, 188, 212, 0.2); /* Primary background tint */
 364      --secondary: #6c757d;                /* Secondary color */
 365  
 366      /* Status colors */
 367      --success: #28a745;                  /* Success/positive */
 368      --success-bg: #1e4620;               /* Success background (dark) */
 369      --success-text: #d4edda;             /* Success text (light) */
 370      --info: #17a2b8;                     /* Info/neutral */
 371      --warning: #ff3e00;                  /* Warning (Svelte orange) */
 372      --warning-bg: #4d1f00;               /* Warning background (dark) */
 373      --danger: #dc3545;                   /* Danger/error */
 374      --danger-bg: #4d1319;                /* Danger background (dark) */
 375      --danger-text: #f8d7da;              /* Danger text (light) */
 376      --error-bg: #4d1319;                 /* Error background */
 377      --error-text: #f8d7da;               /* Error text */
 378  
 379      /* Code block colors */
 380      --code-bg: #1e272c;                  /* Code block background */
 381      --code-text: #ffffff;                /* Code text color */
 382  
 383      /* Tab colors */
 384      --tab-inactive-bg: #1a1a1a;          /* Inactive tab background */
 385  
 386      /* Link/accent colors */
 387      --accent-color: #007bff;             /* Link color */
 388      --accent-hover-color: #0056b3;       /* Link hover color */
 389  }
 390  
 391  /* =============================================================================
 392     CUSTOM STYLES
 393     Add your custom CSS rules below. These will override any default styles.
 394     ============================================================================= */
 395  
 396  /* Example: Custom header styling
 397  .header {
 398      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
 399  }
 400  */
 401  
 402  /* Example: Custom button styling
 403  .btn {
 404      border-radius: 8px;
 405      text-transform: uppercase;
 406      letter-spacing: 0.5px;
 407  }
 408  */
 409  
 410  /* Example: Custom card styling
 411  .card {
 412      border-radius: 12px;
 413      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 414  }
 415  */
 416  `
 417  
 418  // CSSVariablesTemplate contains only CSS variable definitions
 419  const CSSVariablesTemplate = `/*
 420   * CSS Variables Override for ORLY Relay
 421   * ======================================
 422   *
 423   * This file contains only CSS variable definitions.
 424   * Edit values here to customize colors without touching component styles.
 425   *
 426   * For full CSS customization (including component styles),
 427   * edit custom.css instead.
 428   */
 429  
 430  /* Light theme variables */
 431  :root {
 432      --bg-color: #ddd;
 433      --header-bg: #eee;
 434      --sidebar-bg: #eee;
 435      --card-bg: #f8f9fa;
 436      --panel-bg: #f8f9fa;
 437      --border-color: #dee2e6;
 438      --text-color: #444444;
 439      --text-muted: #6c757d;
 440      --input-border: #ccc;
 441      --input-bg: #ffffff;
 442      --input-text-color: #495057;
 443      --button-bg: #ddd;
 444      --button-hover-bg: #eee;
 445      --button-text: #444444;
 446      --button-hover-border: #adb5bd;
 447      --primary: #00bcd4;
 448      --primary-bg: rgba(0, 188, 212, 0.1);
 449      --secondary: #6c757d;
 450      --success: #28a745;
 451      --success-bg: #d4edda;
 452      --success-text: #155724;
 453      --info: #17a2b8;
 454      --warning: #ff3e00;
 455      --warning-bg: #fff3cd;
 456      --danger: #dc3545;
 457      --danger-bg: #f8d7da;
 458      --danger-text: #721c24;
 459      --error-bg: #f8d7da;
 460      --error-text: #721c24;
 461      --code-bg: #f8f9fa;
 462      --code-text: #495057;
 463      --tab-inactive-bg: #bbb;
 464      --accent-color: #007bff;
 465      --accent-hover-color: #0056b3;
 466  }
 467  
 468  /* Dark theme variables */
 469  body.dark-theme {
 470      --bg-color: #263238;
 471      --header-bg: #1e272c;
 472      --sidebar-bg: #1e272c;
 473      --card-bg: #37474f;
 474      --panel-bg: #37474f;
 475      --border-color: #404040;
 476      --text-color: #ffffff;
 477      --text-muted: #adb5bd;
 478      --input-border: #555;
 479      --input-bg: #37474f;
 480      --input-text-color: #ffffff;
 481      --button-bg: #263238;
 482      --button-hover-bg: #1e272c;
 483      --button-text: #ffffff;
 484      --button-hover-border: #6c757d;
 485      --primary: #00bcd4;
 486      --primary-bg: rgba(0, 188, 212, 0.2);
 487      --secondary: #6c757d;
 488      --success: #28a745;
 489      --success-bg: #1e4620;
 490      --success-text: #d4edda;
 491      --info: #17a2b8;
 492      --warning: #ff3e00;
 493      --warning-bg: #4d1f00;
 494      --danger: #dc3545;
 495      --danger-bg: #4d1319;
 496      --danger-text: #f8d7da;
 497      --error-bg: #4d1319;
 498      --error-text: #f8d7da;
 499      --code-bg: #1e272c;
 500      --code-text: #ffffff;
 501      --tab-inactive-bg: #1a1a1a;
 502      --accent-color: #007bff;
 503      --accent-hover-color: #0056b3;
 504  }
 505  `
 506  
 507  // GenericCSSTemplate is the CSS template for generic/white-label branding
 508  const GenericCSSTemplate = `/*
 509   * Custom Branding CSS - White Label Template
 510   * ==========================================
 511   *
 512   * This file is loaded AFTER the default styles, so any rules here
 513   * will override the defaults. You can customize:
 514   *
 515   * 1. CSS Variables (colors, spacing, etc.)
 516   * 2. Component styles (buttons, cards, headers, etc.)
 517   * 3. Add completely custom styles
 518   *
 519   * Restart the relay to apply changes.
 520   *
 521   * For variable-only overrides, edit variables.css instead.
 522   */
 523  
 524  /* =============================================================================
 525     LIGHT THEME VARIABLES - Professional Blue-Gray
 526     ============================================================================= */
 527  
 528  html, body {
 529      /* Background colors */
 530      --bg-color: #F0F4F8;                 /* Light gray-blue background */
 531      --header-bg: #FFFFFF;                /* Clean white header */
 532      --sidebar-bg: #FFFFFF;               /* Clean white sidebar */
 533      --card-bg: #FFFFFF;                  /* White cards */
 534      --panel-bg: #FFFFFF;                 /* White panels */
 535  
 536      /* Border colors */
 537      --border-color: #E2E8F0;             /* Subtle gray border */
 538  
 539      /* Text colors */
 540      --text-color: #334155;               /* Dark slate text */
 541      --text-muted: #64748B;               /* Muted slate */
 542  
 543      /* Input/form colors */
 544      --input-border: #CBD5E1;             /* Light slate border */
 545      --input-bg: #FFFFFF;                 /* White input */
 546      --input-text-color: #334155;         /* Dark slate text */
 547  
 548      /* Button colors */
 549      --button-bg: #F1F5F9;                /* Light slate button */
 550      --button-hover-bg: #E2E8F0;          /* Slightly darker on hover */
 551      --button-text: #334155;              /* Dark slate text */
 552      --button-hover-border: #94A3B8;      /* Medium slate border */
 553  
 554      /* Theme/accent colors - Professional Blue */
 555      --primary: #4080C0;                  /* Professional blue */
 556      --primary-bg: rgba(64, 128, 192, 0.1); /* Light blue tint */
 557      --secondary: #64748B;                /* Slate gray */
 558  
 559      /* Status colors */
 560      --success: #22C55E;                  /* Green */
 561      --success-bg: #DCFCE7;               /* Light green */
 562      --success-text: #166534;             /* Dark green */
 563      --info: #3B82F6;                     /* Blue */
 564      --warning: #F59E0B;                  /* Amber */
 565      --warning-bg: #FEF3C7;               /* Light amber */
 566      --danger: #EF4444;                   /* Red */
 567      --danger-bg: #FEE2E2;                /* Light red */
 568      --danger-text: #991B1B;              /* Dark red */
 569      --error-bg: #FEE2E2;                 /* Light red */
 570      --error-text: #991B1B;               /* Dark red */
 571  
 572      /* Code block colors */
 573      --code-bg: #F8FAFC;                  /* Very light slate */
 574      --code-text: #334155;                /* Dark slate */
 575  
 576      /* Tab colors */
 577      --tab-inactive-bg: #E2E8F0;          /* Light slate */
 578  
 579      /* Link/accent colors */
 580      --accent-color: #4080C0;             /* Professional blue */
 581      --accent-hover-color: #2563EB;       /* Darker blue */
 582  }
 583  
 584  /* =============================================================================
 585     DARK THEME VARIABLES - Professional Dark
 586     ============================================================================= */
 587  
 588  body.dark-theme {
 589      /* Background colors */
 590      --bg-color: #0F172A;                 /* Dark navy */
 591      --header-bg: #1E293B;                /* Slate gray */
 592      --sidebar-bg: #1E293B;               /* Slate gray */
 593      --card-bg: #1E293B;                  /* Slate gray */
 594      --panel-bg: #1E293B;                 /* Slate gray */
 595  
 596      /* Border colors */
 597      --border-color: #334155;             /* Medium slate */
 598  
 599      /* Text colors */
 600      --text-color: #F8FAFC;               /* Almost white */
 601      --text-muted: #94A3B8;               /* Muted slate */
 602  
 603      /* Input/form colors */
 604      --input-border: #475569;             /* Slate border */
 605      --input-bg: #1E293B;                 /* Slate background */
 606      --input-text-color: #F8FAFC;         /* Light text */
 607  
 608      /* Button colors */
 609      --button-bg: #334155;                /* Slate button */
 610      --button-hover-bg: #475569;          /* Lighter on hover */
 611      --button-text: #F8FAFC;              /* Light text */
 612      --button-hover-border: #64748B;      /* Medium slate */
 613  
 614      /* Theme/accent colors */
 615      --primary: #60A5FA;                  /* Lighter blue for dark mode */
 616      --primary-bg: rgba(96, 165, 250, 0.2); /* Blue tint */
 617      --secondary: #94A3B8;                /* Muted slate */
 618  
 619      /* Status colors */
 620      --success: #4ADE80;                  /* Bright green */
 621      --success-bg: #166534;               /* Dark green */
 622      --success-text: #DCFCE7;             /* Light green */
 623      --info: #60A5FA;                     /* Light blue */
 624      --warning: #FBBF24;                  /* Bright amber */
 625      --warning-bg: #78350F;               /* Dark amber */
 626      --danger: #F87171;                   /* Light red */
 627      --danger-bg: #7F1D1D;                /* Dark red */
 628      --danger-text: #FEE2E2;              /* Light red */
 629      --error-bg: #7F1D1D;                 /* Dark red */
 630      --error-text: #FEE2E2;               /* Light red */
 631  
 632      /* Code block colors */
 633      --code-bg: #0F172A;                  /* Dark navy */
 634      --code-text: #F8FAFC;                /* Light text */
 635  
 636      /* Tab colors */
 637      --tab-inactive-bg: #1E293B;          /* Slate */
 638  
 639      /* Link/accent colors */
 640      --accent-color: #60A5FA;             /* Light blue */
 641      --accent-hover-color: #93C5FD;       /* Lighter blue */
 642  }
 643  
 644  /* =============================================================================
 645     PRIMARY BUTTON TEXT COLOR FIX
 646     Ensures buttons with primary background have white text for contrast
 647     ============================================================================= */
 648  
 649  /* Target all common button patterns that use primary background */
 650  button[class*="-btn"],
 651  button[class*="submit"],
 652  button[class*="action"],
 653  button[class*="save"],
 654  button[class*="add"],
 655  button[class*="create"],
 656  button[class*="connect"],
 657  button[class*="refresh"],
 658  button[class*="retry"],
 659  button[class*="send"],
 660  button[class*="apply"],
 661  button[class*="execute"],
 662  button[class*="run"],
 663  .primary-action,
 664  .action-button,
 665  .permission-badge,
 666  [class*="badge"] {
 667      color: #FFFFFF !important;
 668  }
 669  
 670  /* More specific override for any button that visually appears to have primary bg */
 671  /* This uses a broad selector with low impact on non-primary buttons */
 672  html:not(.dark-theme) button:not([disabled]) {
 673      /* Default to inherit, primary buttons will be caught above */
 674  }
 675  
 676  /* =============================================================================
 677     CUSTOM STYLES
 678     Add your custom CSS rules below. These will override any default styles.
 679     ============================================================================= */
 680  
 681  /* Example: Custom header styling
 682  .header {
 683      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 684  }
 685  */
 686  
 687  /* Example: Custom button styling
 688  .btn {
 689      border-radius: 6px;
 690      font-weight: 500;
 691  }
 692  */
 693  
 694  /* Example: Custom card styling
 695  .card {
 696      border-radius: 8px;
 697      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
 698  }
 699  */
 700  `
 701  
 702  // GenericCSSVariablesTemplate contains CSS variables for generic/white-label branding
 703  const GenericCSSVariablesTemplate = `/*
 704   * CSS Variables Override - White Label Template
 705   * ==============================================
 706   *
 707   * This file contains only CSS variable definitions.
 708   * Edit values here to customize colors without touching component styles.
 709   *
 710   * For full CSS customization (including component styles),
 711   * edit custom.css instead.
 712   */
 713  
 714  /* Light theme variables - Professional Blue-Gray */
 715  /* Applied to both html and body for maximum compatibility */
 716  html, body {
 717      --bg-color: #F0F4F8;
 718      --header-bg: #FFFFFF;
 719      --sidebar-bg: #FFFFFF;
 720      --card-bg: #FFFFFF;
 721      --panel-bg: #FFFFFF;
 722      --border-color: #E2E8F0;
 723      --text-color: #334155;
 724      --text-muted: #64748B;
 725      --input-border: #CBD5E1;
 726      --input-bg: #FFFFFF;
 727      --input-text-color: #334155;
 728      --button-bg: #F1F5F9;
 729      --button-hover-bg: #E2E8F0;
 730      --button-text: #334155;
 731      --button-hover-border: #94A3B8;
 732      --primary: #4080C0;
 733      --primary-bg: rgba(64, 128, 192, 0.1);
 734      --secondary: #64748B;
 735      --success: #22C55E;
 736      --success-bg: #DCFCE7;
 737      --success-text: #166534;
 738      --info: #3B82F6;
 739      --warning: #F59E0B;
 740      --warning-bg: #FEF3C7;
 741      --danger: #EF4444;
 742      --danger-bg: #FEE2E2;
 743      --danger-text: #991B1B;
 744      --error-bg: #FEE2E2;
 745      --error-text: #991B1B;
 746      --code-bg: #F8FAFC;
 747      --code-text: #334155;
 748      --tab-inactive-bg: #E2E8F0;
 749      --accent-color: #4080C0;
 750      --accent-hover-color: #2563EB;
 751  }
 752  
 753  /* Dark theme variables - Professional Dark */
 754  body.dark-theme {
 755      --bg-color: #0F172A;
 756      --header-bg: #1E293B;
 757      --sidebar-bg: #1E293B;
 758      --card-bg: #1E293B;
 759      --panel-bg: #1E293B;
 760      --border-color: #334155;
 761      --text-color: #F8FAFC;
 762      --text-muted: #94A3B8;
 763      --input-border: #475569;
 764      --input-bg: #1E293B;
 765      --input-text-color: #F8FAFC;
 766      --button-bg: #334155;
 767      --button-hover-bg: #475569;
 768      --button-text: #F8FAFC;
 769      --button-hover-border: #64748B;
 770      --primary: #60A5FA;
 771      --primary-bg: rgba(96, 165, 250, 0.2);
 772      --secondary: #94A3B8;
 773      --success: #4ADE80;
 774      --success-bg: #166534;
 775      --success-text: #DCFCE7;
 776      --info: #60A5FA;
 777      --warning: #FBBF24;
 778      --warning-bg: #78350F;
 779      --danger: #F87171;
 780      --danger-bg: #7F1D1D;
 781      --danger-text: #FEE2E2;
 782      --error-bg: #7F1D1D;
 783      --error-text: #FEE2E2;
 784      --code-bg: #0F172A;
 785      --code-text: #F8FAFC;
 786      --tab-inactive-bg: #1E293B;
 787      --accent-color: #60A5FA;
 788      --accent-hover-color: #93C5FD;
 789  }
 790  `
 791