diff --git a/components/Custom/Colors/ColorPicker.vue b/components/Custom/Colors/ColorPicker.vue new file mode 100644 index 0000000..7a7f10c --- /dev/null +++ b/components/Custom/Colors/ColorPicker.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/components/Custom/Colors/Index.vue b/components/Custom/Colors/Index.vue index 7e672de..fae02e4 100644 --- a/components/Custom/Colors/Index.vue +++ b/components/Custom/Colors/Index.vue @@ -1,18 +1,14 @@ - diff --git a/composables/useAccentColors.js b/composables/useAccentColors.js new file mode 100644 index 0000000..80ec5ab --- /dev/null +++ b/composables/useAccentColors.js @@ -0,0 +1,61 @@ +// composables/useAccentColors.js +import { ref, computed } from 'vue' +import { accentLightItem, accentDarkItem } from '@/utils/storage' +import { hexToHSL } from '@/composables/useColorConversion' + +export function useAccentColors() { + const lightHex = ref(accentLightItem.fallback) + const darkHex = ref(accentDarkItem.fallback) + + // Load stored values + const load = async () => { + try { + const [storedLight, storedDark] = await Promise.all([accentLightItem.getValue(), accentDarkItem.getValue()]) + if (storedLight) lightHex.value = storedLight + if (storedDark) darkHex.value = storedDark + } catch (err) { + console.error('Failed to load accent colors', err) + } + } + + // Save on change + const saveLight = () => accentLightItem.setValue(lightHex.value) + const saveDark = () => accentDarkItem.setValue(darkHex.value) + + const reset = () => { + lightHex.value = accentLightItem.fallback + darkHex.value = accentDarkItem.fallback + saveLight() + saveDark() + } + + // Compute HSL for CSS injection + const lightHSL = computed(() => hexToHSL(lightHex.value)) + const darkHSL = computed(() => hexToHSL(darkHex.value)) + + // // CSS string + const cssString = computed( + () => ` + body.light { + --accent-h: ${lightHSL.value[0]} !important; + --accent-s: ${lightHSL.value[1]}% !important; + --accent-l: ${lightHSL.value[2]}% !important; + } + body.dark { + --accent-h: ${darkHSL.value[0]} !important; + --accent-s: ${darkHSL.value[1]}% !important; + --accent-l: ${darkHSL.value[2]}% !important; + } + ` + ) + + return { + lightHex, + darkHex, + load, + saveLight, + saveDark, + reset, + cssString, + } +} diff --git a/composables/useColorConversion.js b/composables/useColorConversion.js index 2d26b73..bf47f1c 100644 --- a/composables/useColorConversion.js +++ b/composables/useColorConversion.js @@ -1,89 +1,68 @@ -// src/composables/useColorConversion.js - -/** - * Converts a hex color string (e.g. "#ff9800") into an HSL array: [h, s, l] - */ export function hexToHSL(hex) { + console.log(hex) + + // if (typeof hex !== 'string') throw new Error('Invalid HEX value') hex = hex.replace(/^#/, '') + // if (!/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)) throw new Error('Invalid HEX value') + if (hex.length === 3) { hex = hex .split('') .map((c) => c + c) .join('') } - const r = parseInt(hex.substr(0, 2), 16) / 255 - const g = parseInt(hex.substr(2, 2), 16) / 255 - const b = parseInt(hex.substr(4, 2), 16) / 255 + + const r = parseInt(hex.slice(0, 2), 16) / 255 + const g = parseInt(hex.slice(2, 4), 16) / 255 + const b = parseInt(hex.slice(4, 6), 16) / 255 const max = Math.max(r, g, b) const 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 = h * 60 + const diff = max - min + + let h = 0 + let s = 0 + const l = (max + min) * 0.5 + + if (diff !== 0) { + s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min) + + if (max === r) h = (g - b) / diff + (g < b ? 6 : 0) + else if (max === g) h = (b - r) / diff + 2 + else h = (r - g) / diff + 4 + + h *= 60 } - return [Math.round(h), Math.round(s * 100), Math.round(l * 100)] + + return [h, s * 100, l * 100] // keep floats internally for better round-trip } -/** - * Converts an HSL array [h, s, l] into a hex color string. - * h should be between 0 and 360, s and l between 0 and 100. - */ export function hslToHex([h, s, l]) { - s /= 100 - l /= 100 + if (!Array.isArray([h, s, l]) || [h, s, l].some((v) => typeof v !== 'number')) throw new Error('Invalid HSL array') + + s *= 0.01 + l *= 0.01 const c = (1 - Math.abs(2 * l - 1)) * s const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) - const m = l - c / 2 + const m = l - c * 0.5 + let r = 0, g = 0, b = 0 + const h60 = h / 60 - if (h >= 0 && h < 60) { - r = c - g = x - b = 0 - } else if (h >= 60 && h < 120) { - r = x - g = c - b = 0 - } else if (h >= 120 && h < 180) { - r = 0 - g = c - b = x - } else if (h >= 180 && h < 240) { - r = 0 - g = x - b = c - } else if (h >= 240 && h < 300) { - r = x - g = 0 - b = c - } else if (h >= 300 && h < 360) { - r = c - g = 0 - b = x - } + if (h60 < 1) [r, g, b] = [c, x, 0] + else if (h60 < 2) [r, g, b] = [x, c, 0] + else if (h60 < 3) [r, g, b] = [0, c, x] + else if (h60 < 4) [r, g, b] = [0, x, c] + else if (h60 < 5) [r, g, b] = [x, 0, c] + else [r, g, b] = [c, 0, x] const toHex = (n) => { - const hex = Math.round((n + m) * 255).toString(16) - return hex.length === 1 ? '0' + hex : hex + const val = Math.round((n + m) * 255) + const clamped = Math.min(255, Math.max(0, val)) // clamp for safety + return clamped.toString(16).padStart(2, '0') } return `#${toHex(r)}${toHex(g)}${toHex(b)}` diff --git a/utils/storage.js b/utils/storage.js index 406d50e..2d78f50 100644 --- a/utils/storage.js +++ b/utils/storage.js @@ -14,8 +14,8 @@ export const THEMES = Object.freeze({ }) // Pre-compute hex values instead of doing it every time -const DEFAULT_ACCENT_LIGHT_HEX = hslToHex(DEFAULT_ACCENT_LIGHT_HSL) -const DEFAULT_ACCENT_DARK_HEX = hslToHex(DEFAULT_ACCENT_DARK_HSL) +export const DEFAULT_ACCENT_LIGHT_HEX = hslToHex(DEFAULT_ACCENT_LIGHT_HSL) +export const DEFAULT_ACCENT_DARK_HEX = hslToHex(DEFAULT_ACCENT_DARK_HSL) // ---------- Helper ---------- export async function getAllStorageItems() {