File size: 5,178 Bytes
1f908a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Theme & color system with "undefined" tokens and safe fallbacks
(() => {
  const DEFAULT_PRIMARY = [59, 130, 246];   // blue-500
  const DEFAULT_SECONDARY = [236, 72, 153]; // pink-500

  const docEl = document.documentElement;

  function toRgbArray(input) {
    // Accepts: "59 130 246", "59,130,246", "rgb(59 130 246)", "#3b82f6"
    if (!input) return null;

    const trimmed = String(input).trim();

    // Hex (#RRGGBB or #RGB)
    if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(trimmed)) {
      let hex = trimmed.replace('#', '');
      if (hex.length === 3) {
        hex = hex.split('').map(c => c + c).join('');
      }
      const int = parseInt(hex, 16);
      return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
    }

    // rgb(...) or space-separated
    const rgbMatch =
      trimmed.match(/^rgb\(\s*([0-9]{1,3})\s*[, ]\s*([0-9]{1,3})\s*[, ]\s*([0-9]{1,3})\s*\)$/i) ||
      trimmed.match(/^([0-9]{1,3})\s+([0-9]{1,3})\s+([0-9]{1,3})$/);

    if (rgbMatch) {
      const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1], 10)));
      const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2], 10)));
      const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3], 10)));
      return [r, g, b];
    }

    // Comma-separated "R,G,B"
    const csv = trimmed.split(',').map(n => parseInt(n, 10));
    if (csv.length === 3 && csv.every(n => Number.isFinite(n))) {
      return csv.map(n => Math.max(0, Math.min(255, n)));
    }

    return null;
  }

  function setColorVar(name, rgbArr) {
    const val = rgbArr ? `${rgbArr[0]} ${rgbArr[1]} ${rgbArr[2]}` : '';
    docEl.style.setProperty(name, val);
  }

  function applyPrimaryColor(input) {
    const arr = toRgbArray(input) || DEFAULT_PRIMARY;
    setColorVar('--color-primary', arr);
    try {
      localStorage.setItem('primary', arr.join(' '));
    } catch {}
    return arr;
  }

  function applySecondaryColor(input) {
    const arr = toRgbArray(input) || DEFAULT_SECONDARY;
    setColorVar('--color-secondary', arr);
    try {
      localStorage.setItem('secondary', arr.join(' '));
    } catch {}
    return arr;
  }

  function applyThemeMode(mode) {
    // Accepts "light" or "dark"
    const m = mode === 'dark' ? 'dark' : 'light';
    docEl.setAttribute('data-theme', m);
    try {
      localStorage.setItem('theme', m);
    } catch {}
    return m;
  }

  function currentTheme() {
    const m = docEl.getAttribute('data-theme') || 'light';
    return m;
  }

  function toggleTheme() {
    const next = currentTheme() === 'dark' ? 'light' : 'dark';
    applyThemeMode(next);
  }

  function boot() {
    // Apply theme preference from URL or localStorage or default to light ("undefined mode")
    const params = new URLSearchParams(location.search);
    const themeFromUrl = params.get('mode') || params.get('theme');
    const theme = themeFromUrl === 'dark' ? 'dark' : 'light';
    applyThemeMode(theme);

    // Apply colors from URL -> localStorage -> defaults
    const primaryFromUrl = params.get('primary') || params.get('primaryColor') || params.get('p');
    const secondaryFromUrl = params.get('secondary') || params.get('secondaryColor') || params.get('s');

    let primary, secondary;

    if (primaryFromUrl) {
      primary = applyPrimaryColor(primaryFromUrl);
    } else {
      try {
        const stored = localStorage.getItem('primary');
        if (stored) primary = applyPrimaryColor(stored);
      } catch {}
      if (!primary) primary = applyPrimaryColor(null);
    }

    if (secondaryFromUrl) {
      secondary = applySecondaryColor(secondaryFromUrl);
    } else {
      try {
        const stored = localStorage.getItem('secondary');
        if (stored) secondary = applySecondaryColor(stored);
      } catch {}
      if (!secondary) secondary = applySecondaryColor(null);
    }

    // UI hooks
    const themeToggle = document.getElementById('themeToggle');
    if (themeToggle) {
      themeToggle.addEventListener('click', () => {
        toggleTheme();
      });
    }

    const applyBtn = document.getElementById('applyColorsBtn');
    const primaryInput = document.getElementById('primaryColor');
    const secondaryInput = document.getElementById('secondaryColor');

    if (applyBtn && primaryInput && secondaryInput) {
      // Initialize inputs with current values
      try {
        const sp = getComputedStyle(docEl).getPropertyValue('--color-primary').trim() || DEFAULT_PRIMARY.join(' ');
        const ss = getComputedStyle(docEl).getPropertyValue('--color-secondary').trim() || DEFAULT_SECONDARY.join(' ');
        primaryInput.value = sp;
        secondaryInput.value = ss;
      } catch {}

      applyBtn.addEventListener('click', () => {
        applyPrimaryColor(primaryInput.value);
        applySecondaryColor(secondaryInput.value);
      });
    }

    // Expose a tiny API for integration
    window.ExcailTheme = {
      setPrimary: applyPrimaryColor,
      setSecondary: applySecondaryColor,
      setMode: applyThemeMode,
      toggleTheme,
      getMode: currentTheme,
    };
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot, { once: true });
  } else {
    boot();
  }
})();