c + c).join('');
const num = parseInt(clean, 16);
return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
},
rgbToHex(r, g, b) {
const clamp = v => Math.max(0, Math.min(255, Math.round(v)));
return '#' + [clamp(r), clamp(g), clamp(b)].map(x => x.toString(16).padStart(2, '0')).join('');
},
rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
case g: h = ((b - r) / d + 2); break;
case b: h = ((r - g) / d + 4); break;
}
h *= 60;
}
return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
},
hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360;
s /= 100; l /= 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return this.rgbToHex(Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255));
},
init() {
this.darkMode = document.documentElement.classList.contains('dark');
window.addEventListener('dark-mode-toggled', (e) => {
if (e.detail && typeof e.detail.darkMode !== 'undefined') {
this.darkMode = e.detail.darkMode;
}
});
const userCustomized = localStorage.getItem('theme_user_customized') === 'true';
const savedOrgCode = localStorage.getItem('theme_org_code');
if (userCustomized && savedOrgCode === this.orgCode) {
const savedWheel = localStorage.getItem('theme_color_wheel_palette');
if (savedWheel) {
try {
const parsed = JSON.parse(savedWheel);
if (parsed.primaryHex && /^#[0-9A-Fa-f]{3,8}$/.test(parsed.primaryHex)) this.primaryHex = parsed.primaryHex;
if (parsed.secondaryHex && /^#[0-9A-Fa-f]{3,8}$/.test(parsed.secondaryHex)) this.secondaryHex = parsed.secondaryHex;
if (parsed.harmonyType) this.harmonyType = parsed.harmonyType;
if (parsed.secondaryMode) this.secondaryMode = parsed.secondaryMode;
if (parsed.currentTheme) this.currentTheme = parsed.currentTheme;
} catch (e) {}
} else if (localStorage.getItem('customAccentColor')) {
this.primaryHex = localStorage.getItem('customAccentColor');
this.currentTheme = localStorage.getItem('currentTheme') || 'theme-custom';
}
} else {
this.primaryHex = this.defaultColor;
this.harmonyType = 'complementary';
this.secondaryMode = 'harmony';
this.currentTheme = 'theme-custom';
}
const rgb = this.hexToRgb(this.primaryHex);
const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
this.primaryHue = hsl.h;
this.primarySat = hsl.s;
this.primaryLit = hsl.l;
this.applyAll(userCustomized && savedOrgCode === this.orgCode);
window.addEventListener('open-theme-customizer', () => {
this.open = true;
this.$nextTick(() => this.drawWheel());
});
this.$watch('open', value => {
if (value) {
this.$nextTick(() => this.drawWheel());
}
});
},
toggleDarkMode() {
this.darkMode = !this.darkMode;
if (this.darkMode) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
localStorage.setItem('theme_user_customized', 'true');
localStorage.setItem('theme_org_code', this.orgCode);
window.dispatchEvent(new CustomEvent('dark-mode-toggled', { detail: { darkMode: this.darkMode } }));
},
// Canva Color Wheel Canvas Drawing
drawWheel() {
const canvas = document.getElementById('canvaColorWheel');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const radius = canvas.width / 2 - 10;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. Draw Hue Spectrum (Conic Gradient)
for (let angle = 0; angle < 360; angle += 1) {
const startAngle = (angle - 1.5) * Math.PI / 180;
const endAngle = (angle + 1.5) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.closePath();
ctx.fillStyle = `hsl(${angle}, 100%, 50%)`;
ctx.fill();
}
// 2. Center Radial Gradient (White in center to full saturation on edge)
const radGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
radGrad.addColorStop(0, 'rgba(255, 255, 255, 0.85)');
radGrad.addColorStop(0.65, 'rgba(255, 255, 255, 0.15)');
radGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = radGrad;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
// 3. Wheel Outline Border
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
const dist = radius * 0.76;
// 4. Draw Harmony Points (Circles on wheel)
const harmonyHues = this.getHarmonyHues();
harmonyHues.forEach(h => {
const hRad = h * Math.PI / 180;
const hx = cx + dist * Math.cos(hRad);
const hy = cy + dist * Math.sin(hRad);
ctx.beginPath();
ctx.arc(hx, hy, 8, 0, Math.PI * 2);
ctx.fillStyle = this.hslToHex(h, 90, 50);
ctx.fill();
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3;
ctx.stroke();
});
// 5. Draw Primary Pointer Marker (Large ring with white stroke, like Canva)
const primaryRad = this.primaryHue * Math.PI / 180;
const px = cx + dist * Math.cos(primaryRad);
const py = cy + dist * Math.sin(primaryRad);
ctx.beginPath();
ctx.arc(px, py, 11, 0, Math.PI * 2);
ctx.fillStyle = this.primaryHex;
ctx.fill();
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 4;
ctx.stroke();
// Shadow for primary marker
ctx.beginPath();
ctx.arc(px, py, 13, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)';
ctx.lineWidth = 1.5;
ctx.stroke();
},
getHarmonyHues() {
const h = this.primaryHue;
switch (this.harmonyType) {
case 'complementary':
return [(h + 180) % 360];
case 'analogous':
return [(h - 35 + 360) % 360, (h + 35) % 360];
case 'triadic':
return [(h + 120) % 360, (h + 240) % 360];
case 'tetradic':
return [(h + 60) % 360, (h + 180) % 360, (h + 240) % 360];
case 'monochromatic':
return [h];
default:
return [];
}
},
handleWheelEvent(e) {
const canvas = document.getElementById('canvaColorWheel');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const x = clientX - rect.left - canvas.width / 2;
const y = clientY - rect.top - canvas.height / 2;
let angle = Math.atan2(y, x) * (180 / Math.PI);
if (angle < 0) angle += 360;
this.primaryHue = Math.round(angle);
this.primaryHex = this.hslToHex(this.primaryHue, 88, 48);
this.currentTheme = 'theme-custom';
this.applyAll(true);
this.drawWheel();
},
onWheelMouseDown(e) {
this.isDraggingWheel = true;
this.handleWheelEvent(e);
},
onWheelMouseMove(e) {
if (!this.isDraggingWheel) return;
this.handleWheelEvent(e);
},
onWheelMouseUp() {
this.isDraggingWheel = false;
},
setPrimaryFromHex(hex) {
if (!/^#[0-9A-Fa-f]{6}$/.test(hex)) return;
this.primaryHex = hex;
const rgb = this.hexToRgb(hex);
const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
this.primaryHue = hsl.h;
this.primarySat = hsl.s;
this.primaryLit = hsl.l;
this.currentTheme = 'theme-custom';
this.applyAll(true);
this.drawWheel();
},
setHarmony(type) {
this.harmonyType = type;
this.applyAll(true);
this.drawWheel();
},
setSecondaryMode(mode) {
this.secondaryMode = mode;
this.applyAll(true);
this.drawWheel();
},
selectPreset(p) {
this.primaryHex = p.hex;
this.currentTheme = p.theme;
const rgb = this.hexToRgb(p.hex);
const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
this.primaryHue = hsl.h;
this.primarySat = hsl.s;
this.primaryLit = hsl.l;
const isDefaultPreset = (p.hex.toLowerCase() === this.defaultColor.toLowerCase());
this.applyAll(!isDefaultPreset);
if (isDefaultPreset) {
localStorage.removeItem('theme_user_customized');
localStorage.removeItem('theme_color_wheel_palette');
localStorage.removeItem('customAccentColor');
localStorage.setItem('theme_org_code', this.orgCode);
}
this.drawWheel();
},
applyAll(isUserAction = true) {
const pRgb = this.hexToRgb(this.primaryHex);
const pHsl = this.rgbToHsl(pRgb.r, pRgb.g, pRgb.b);
// Hitung warna sekunder
if (this.secondaryMode === 'harmony') {
if (this.harmonyType === 'complementary') {
this.secondaryHex = this.hslToHex((pHsl.h + 180) % 360, Math.min(Math.max(pHsl.s, 65), 90), Math.min(Math.max(pHsl.l, 46), 54));
} else if (this.harmonyType === 'analogous') {
this.secondaryHex = this.hslToHex((pHsl.h + 35) % 360, pHsl.s, pHsl.l);
} else if (this.harmonyType === 'triadic') {
this.secondaryHex = this.hslToHex((pHsl.h + 120) % 360, Math.min(Math.max(pHsl.s, 60), 90), Math.min(Math.max(pHsl.l, 46), 54));
} else if (this.harmonyType === 'tetradic') {
this.secondaryHex = this.hslToHex((pHsl.h + 180) % 360, Math.min(Math.max(pHsl.s, 60), 90), Math.min(Math.max(pHsl.l, 46), 54));
} else if (this.harmonyType === 'monochromatic') {
this.secondaryHex = this.hslToHex(pHsl.h, Math.max(30, pHsl.s - 15), pHsl.l > 50 ? Math.max(25, pHsl.l - 25) : Math.min(75, pHsl.l + 25));
} else {
this.secondaryHex = this.hslToHex((pHsl.h + 180) % 360, 85, 50);
}
} else {
this.secondaryHex = '#EA580C'; // Fixed pekat orange
}
const sRgb = this.hexToRgb(this.secondaryHex);
const pHoverHex = this.rgbToHex(pRgb.r * 0.85, pRgb.g * 0.85, pRgb.b * 0.85);
const sHoverHex = this.rgbToHex(sRgb.r * 0.85, sRgb.g * 0.85, sRgb.b * 0.85);
const sDarkHex = this.rgbToHex(sRgb.r * 0.65, sRgb.g * 0.65, sRgb.b * 0.65);
// Dynamic Themed Topbar & Footer Tokens
const isGrayScale = pHsl.s < 20;
let surfaceDark, cardDark, cardAltDark, borderDark;
let topbarBg, footerBg, footerCard, footerCardAlt, footerBorder, footerInput;
if (isGrayScale) {
surfaceDark = '#0B0F17';
cardDark = '#111827';
cardAltDark = '#1E293B';
borderDark = '#1E293B';
topbarBg = '#1E293B';
footerBg = '#0F172A';
footerCard = '#1E293B';
footerCardAlt = '#334155';
footerBorder = 'rgba(255, 255, 255, 0.12)';
footerInput = '#1E293B';
} else {
surfaceDark = this.hslToHex(pHsl.h, Math.min(25, Math.round(pHsl.s * 0.25)), 6);
cardDark = this.hslToHex(pHsl.h, Math.min(22, Math.round(pHsl.s * 0.2)), 10);
cardAltDark = this.hslToHex(pHsl.h, Math.min(22, Math.round(pHsl.s * 0.2)), 15);
borderDark = this.hslToHex(pHsl.h, Math.min(22, Math.round(pHsl.s * 0.2)), 20);
// Dynamic rich tones for topbar and footer reflecting theme
topbarBg = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s, 45), 75), 18);
footerBg = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s * 0.5, 30), 55), 10);
footerCard = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s * 0.45, 25), 45), 15);
footerCardAlt = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s * 0.4, 25), 45), 20);
footerBorder = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s * 0.5, 35), 60), 26);
footerInput = this.hslToHex(pHsl.h, Math.min(Math.max(pHsl.s * 0.45, 25), 45), 13);
}
const topbarRgb = this.hexToRgb(topbarBg);
const footerRgb = this.hexToRgb(footerBg);
const footerCardRgb = this.hexToRgb(footerCard);
const footerCardAltRgb = this.hexToRgb(footerCardAlt);
const footerBorderRgb = this.hexToRgb(footerBorder.startsWith('#') ? footerBorder : '#1E293B');
const footerInputRgb = this.hexToRgb(footerInput);
const surfaceDarkRgb = this.hexToRgb(surfaceDark);
const cardDarkRgb = this.hexToRgb(cardDark);
const cardAltDarkRgb = this.hexToRgb(cardAltDark);
const borderDarkRgb = this.hexToRgb(borderDark);
// Terapkan ke DOM (:root dan document.body)
const targets = [document.documentElement, document.body].filter(Boolean);
targets.forEach(target => {
target.style.setProperty('--color-primary', this.primaryHex);
target.style.setProperty('--color-primary-rgb', `${pRgb.r} ${pRgb.g} ${pRgb.b}`);
target.style.setProperty('--color-primary-hover', pHoverHex);
target.style.setProperty('--color-primary-hover-rgb', `${Math.round(pRgb.r * 0.85)} ${Math.round(pRgb.g * 0.85)} ${Math.round(pRgb.b * 0.85)}`);
target.style.setProperty('--color-primary-glow', `rgba(${pRgb.r}, ${pRgb.g}, ${pRgb.b}, 0.38)`);
target.style.setProperty('--color-primary-subtle', `rgba(${pRgb.r}, ${pRgb.g}, ${pRgb.b}, 0.12)`);
target.style.setProperty('--color-secondary', this.secondaryHex);
target.style.setProperty('--color-secondary-rgb', `${sRgb.r} ${sRgb.g} ${sRgb.b}`);
target.style.setProperty('--color-secondary-hover', sHoverHex);
target.style.setProperty('--color-secondary-hover-rgb', `${Math.round(sRgb.r * 0.85)} ${Math.round(sRgb.g * 0.85)} ${Math.round(sRgb.b * 0.85)}`);
target.style.setProperty('--color-secondary-dark', sDarkHex);
target.style.setProperty('--color-secondary-dark-rgb', `${Math.round(sRgb.r * 0.65)} ${Math.round(sRgb.g * 0.65)} ${Math.round(sRgb.b * 0.65)}`);
target.style.setProperty('--color-secondary-glow', `rgba(${sRgb.r}, ${sRgb.g}, ${sRgb.b}, 0.35)`);
target.style.setProperty('--color-secondary-subtle', `rgba(${sRgb.r}, ${sRgb.g}, ${sRgb.b}, 0.12)`);
target.style.setProperty('--color-accent', this.primaryHex);
target.style.setProperty('--color-accent-rgb', `${pRgb.r} ${pRgb.g} ${pRgb.b}`);
target.style.setProperty('--color-accent-hover', pHoverHex);
target.style.setProperty('--color-accent-hover-rgb', `${Math.round(pRgb.r * 0.85)} ${Math.round(pRgb.g * 0.85)} ${Math.round(pRgb.b * 0.85)}`);
target.style.setProperty('--color-accent-glow', `rgba(${pRgb.r}, ${pRgb.g}, ${pRgb.b}, 0.38)`);
target.style.setProperty('--color-accent-subtle', `rgba(${pRgb.r}, ${pRgb.g}, ${pRgb.b}, 0.12)`);
target.style.setProperty('--color-surface-dark', surfaceDark);
target.style.setProperty('--color-surface-dark-rgb', `${surfaceDarkRgb.r} ${surfaceDarkRgb.g} ${surfaceDarkRgb.b}`);
target.style.setProperty('--color-card-dark', cardDark);
target.style.setProperty('--color-card-dark-rgb', `${cardDarkRgb.r} ${cardDarkRgb.g} ${cardDarkRgb.b}`);
target.style.setProperty('--color-card-alt-dark', cardAltDark);
target.style.setProperty('--color-card-alt-dark-rgb', `${cardAltDarkRgb.r} ${cardAltDarkRgb.g} ${cardAltDarkRgb.b}`);
target.style.setProperty('--color-border-dark', borderDark);
target.style.setProperty('--color-border-dark-rgb', `${borderDarkRgb.r} ${borderDarkRgb.g} ${borderDarkRgb.b}`);
target.style.setProperty('--color-topbar-bg', topbarBg);
target.style.setProperty('--color-topbar-bg-rgb', `${topbarRgb.r} ${topbarRgb.g} ${topbarRgb.b}`);
target.style.setProperty('--color-footer-bg', footerBg);
target.style.setProperty('--color-footer-bg-rgb', `${footerRgb.r} ${footerRgb.g} ${footerRgb.b}`);
target.style.setProperty('--color-footer-card', footerCard);
target.style.setProperty('--color-footer-card-rgb', `${footerCardRgb.r} ${footerCardRgb.g} ${footerCardRgb.b}`);
target.style.setProperty('--color-footer-card-alt', footerCardAlt);
target.style.setProperty('--color-footer-card-alt-rgb', `${footerCardAltRgb.r} ${footerCardAltRgb.g} ${footerCardAltRgb.b}`);
target.style.setProperty('--color-footer-border', footerBorder);
target.style.setProperty('--color-footer-border-rgb', `${footerBorderRgb.r} ${footerBorderRgb.g} ${footerBorderRgb.b}`);
target.style.setProperty('--color-footer-input', footerInput);
target.style.setProperty('--color-footer-input-rgb', `${footerInputRgb.r} ${footerInputRgb.g} ${footerInputRgb.b}`);
});
// Update tema class pada document.documentElement dan body
const themeClassList = ['theme-eduma', 'theme-emerald', 'theme-neon', 'theme-blue', 'theme-sapphire', 'theme-purple', 'theme-orange', 'theme-crimson', 'theme-rose', 'theme-gold', 'theme-teal', 'theme-gray', 'theme-dark', 'theme-custom'];
themeClassList.forEach(cls => {
document.documentElement.classList.remove(cls);
if (document.body) document.body.classList.remove(cls);
});
if (this.currentTheme) {
document.documentElement.classList.add(this.currentTheme);
if (document.body) document.body.classList.add(this.currentTheme);
}
// Simpan ke localStorage jika user mengubah tema secara manual
if (isUserAction) {
localStorage.setItem('theme_user_customized', 'true');
localStorage.setItem('theme_org_code', this.orgCode);
localStorage.setItem('theme_color_wheel_palette', JSON.stringify({
primaryHex: this.primaryHex,
secondaryHex: this.secondaryHex,
harmonyType: this.harmonyType,
secondaryMode: this.secondaryMode,
currentTheme: this.currentTheme
}));
localStorage.setItem('customAccentColor', this.primaryHex);
localStorage.setItem('currentTheme', this.currentTheme);
}
// Event global
window.dispatchEvent(new CustomEvent('theme-changed', {
detail: {
primaryHex: this.primaryHex,
secondaryHex: this.secondaryHex,
harmonyType: this.harmonyType,
themeName: this.currentTheme
}
}));
},
resetToDefault() {
this.primaryHex = this.defaultColor;
this.secondaryHex = '#EA580C';
this.harmonyType = 'complementary';
this.secondaryMode = 'harmony';
this.currentTheme = 'theme-custom';
const rgb = this.hexToRgb(this.primaryHex);
const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
this.primaryHue = hsl.h;
this.primarySat = hsl.s;
this.primaryLit = hsl.l;
const targetDark = (this.defaultMode === 'dark');
this.darkMode = targetDark;
if (targetDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
window.dispatchEvent(new CustomEvent('dark-mode-toggled', { detail: { darkMode: targetDark } }));
localStorage.removeItem('theme_user_customized');
localStorage.removeItem('theme_color_wheel_palette');
localStorage.removeItem('customAccentColor');
localStorage.setItem('theme_org_code', this.orgCode);
this.applyAll(false);
this.drawWheel();
},
copyHex(hex) {
navigator.clipboard.writeText(hex).then(() => {
this.copied = true;
this.copiedText = hex;
setTimeout(() => { this.copied = false; }, 2000);
});
}
}"
@keydown.escape.window="open = false"
@keydown.alt.t.window.prevent="open = !open">
Kombinasi Palette Yang Diterapkan