var MAX_NAME_LENGTH = 40; var SHIPPED = [ { id: "moon", name: "Moon", scheme: "dark", accent: "#82aaff", secondary: "#b172b0", shipped: true }, { id: "moon-rose", name: "Moon Rose", scheme: "dark", accent: "#ff757f", secondary: "#c099ff", shipped: true }, { id: "day", name: "Day", scheme: "light", accent: "#2e7de9", secondary: "#9854f1", shipped: true } ]; // The curated accents. `gnome` is the nearest member of GNOME's own // accent-color enum, which is a fixed list of nine we do not get to extend. It // is what libadwaita applications -- Files, Papers, Loupe -- are told to use, so // choosing an accent here recolors them too instead of leaving them in GNOME // blue. Nearest by hue, not by name: "rose" maps to red rather than pink // because it is the red role in this palette. adwaita-accent-contract reads // this table and fails when a member is missing or is not in GNOME's enum. var CURATED = { blue: { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue", gnome: "blue" }, orchid: { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid", gnome: "purple" }, teal: { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal", gnome: "teal" }, green: { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green", gnome: "green" }, amber: { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber", gnome: "yellow" }, orange: { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange", gnome: "orange" }, rose: { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose", gnome: "red" }, slate: { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate", gnome: "slate" } }; function copyProfile(profile) { return { id: profile.id, name: profile.name, scheme: profile.scheme, accent: profile.accent, secondary: profile.secondary, shipped: profile.shipped === true }; } function shippedProfiles() { return SHIPPED.map(copyProfile); } function curatedAccents() { var result = {}; Object.keys(CURATED).forEach(function(name) { result[name] = Object.assign({}, CURATED[name]); }); return result; } function isScheme(value) { return value === "dark" || value === "light"; } function normalizedColor(value) { var color = String(value || "").trim().toLowerCase(); return /^#[0-9a-f]{6}$/.test(color) ? color : null; } function normalizeStoredProfile(value) { if (!value || typeof value !== "object" || value.shipped === true) return null; var id = String(value.id || "").trim(); var name = String(value.name || "").trim(); var accent = normalizedColor(value.accent); var secondary = normalizedColor(value.secondary); if (!/^custom-[a-z0-9][a-z0-9-]{0,56}$/.test(id) || name.length === 0 || name.length > MAX_NAME_LENGTH || !isScheme(value.scheme) || !accent || !secondary) return null; return { id: id, name: name, scheme: value.scheme, accent: accent, secondary: secondary, shipped: false }; } function validCustomProfiles(values) { if (!Array.isArray(values)) return []; var ids = {}; var names = {}; SHIPPED.forEach(function(profile) { ids[profile.id] = true; names[profile.name.toLowerCase()] = true; }); var result = []; values.forEach(function(value) { var profile = normalizeStoredProfile(value); if (!profile) return; var foldedName = profile.name.toLowerCase(); if (ids[profile.id] || names[foldedName]) return; ids[profile.id] = true; names[foldedName] = true; result.push(profile); }); return result; } function profileCatalog(values) { return shippedProfiles().concat(validCustomProfiles(values)); } function boundedName(value) { var name = String(value || "").trim(); if (!name) name = "Custom theme"; return name.slice(0, MAX_NAME_LENGTH); } function uniqueName(value, profiles) { var requested = boundedName(value); var names = {}; profiles.forEach(function(profile) { names[profile.name.toLowerCase()] = true; }); if (!names[requested.toLowerCase()]) return requested; for (var suffix = 2; suffix < 10000; suffix++) { var ending = " " + suffix; var candidate = requested.slice(0, MAX_NAME_LENGTH - ending.length) + ending; if (!names[candidate.toLowerCase()]) return candidate; } return requested.slice(0, MAX_NAME_LENGTH - 6) + " 10000"; } function slug(value) { var result = String(value || "").toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 48) .replace(/-+$/g, ""); return result || "theme"; } function uniqueId(name, profiles) { var base = "custom-" + slug(name); var ids = {}; profiles.forEach(function(profile) { ids[profile.id] = true; }); if (!ids[base]) return base; for (var suffix = 2; suffix < 10000; suffix++) { var candidate = base.slice(0, 57 - String(suffix).length) + "-" + suffix; if (!ids[candidate]) return candidate; } return base.slice(0, 52) + "-10000"; } function createCustomProfile(values, input) { var customs = validCustomProfiles(values); var catalog = shippedProfiles().concat(customs); var scheme = input && input.scheme; var accent = normalizedColor(input && input.accent); var secondary = normalizedColor(input && input.secondary); if (!isScheme(scheme) || !accent || !secondary) return { profiles: customs, profile: null }; var name = uniqueName(input && input.name, catalog); var profile = { id: uniqueId(name, catalog), name: name, scheme: scheme, accent: accent, secondary: secondary, shipped: false }; return { profiles: customs.concat([profile]), profile: profile }; } function findProfile(values, id) { var catalog = profileCatalog(values); for (var index = 0; index < catalog.length; index++) { if (catalog[index].id === id) return catalog[index]; } return null; } function editProfile(values, selected, changes) { var customs = validCustomProfiles(values); if (!selected || typeof selected !== "object") return { profiles: customs, profile: null }; var accent = normalizedColor(changes && changes.accent !== undefined ? changes.accent : selected.accent); var secondary = normalizedColor(changes && changes.secondary !== undefined ? changes.secondary : selected.secondary); var scheme = changes && changes.scheme !== undefined ? changes.scheme : selected.scheme; if (!isScheme(scheme) || !accent || !secondary) return { profiles: customs, profile: null }; if (selected.shipped === true) { return createCustomProfile(customs, { name: selected.name + " custom", scheme: scheme, accent: accent, secondary: secondary }); } var stored = normalizeStoredProfile(selected); if (!stored) return { profiles: customs, profile: null }; var updated = Object.assign({}, stored, { scheme: scheme, accent: accent, secondary: secondary }); var found = false; var next = customs.map(function(profile) { if (profile.id !== updated.id) return profile; found = true; return updated; }); if (!found) next.push(updated); return { profiles: next, profile: updated }; } function deleteProfile(values, id) { var customs = validCustomProfiles(values); for (var shippedIndex = 0; shippedIndex < SHIPPED.length; shippedIndex++) { if (SHIPPED[shippedIndex].id === id) return { profiles: customs, removed: false }; } var next = customs.filter(function(profile) { return profile.id !== id; }); return { profiles: next, removed: next.length !== customs.length }; } function curatedPair(name, scheme) { var entry = CURATED[name] || CURATED.blue; if (scheme === "light") return { accent: entry.light, secondary: entry.lightSecondary }; return { accent: entry.dark, secondary: entry.darkSecondary }; } function matchingShippedProfile(scheme, accent, secondary) { var first = normalizedColor(accent); var second = normalizedColor(secondary); for (var index = 0; index < SHIPPED.length; index++) { var profile = SHIPPED[index]; if (profile.scheme === scheme && profile.accent === first && profile.secondary === second) return copyProfile(profile); } return null; } function curatedNameForProfile(profile) { if (!profile || !isScheme(profile.scheme)) return ""; var accent = normalizedColor(profile.accent); var secondary = normalizedColor(profile.secondary); var names = Object.keys(CURATED); for (var index = 0; index < names.length; index++) { var name = names[index]; var pair = curatedPair(name, profile.scheme); if (pair.accent === accent && pair.secondary === secondary) return name; } return ""; } function clamp(value, minimum, maximum) { return Math.max(minimum, Math.min(maximum, Number(value))); } function channelHex(value) { var text = Math.round(value).toString(16); return text.length < 2 ? "0" + text : text; } function hsvToHex(hue, saturation, value) { var h = Number(hue); var s = Number(saturation); var v = Number(value); if (!isFinite(h) || !isFinite(s) || !isFinite(v)) return null; h = ((h % 360) + 360) % 360; s = clamp(s, 0, 100) / 100; v = clamp(v, 0, 100) / 100; var chroma = v * s; var section = h / 60; var x = chroma * (1 - Math.abs(section % 2 - 1)); var red = 0; var green = 0; var blue = 0; if (section < 1) { red = chroma; green = x; } else if (section < 2) { red = x; green = chroma; } else if (section < 3) { green = chroma; blue = x; } else if (section < 4) { green = x; blue = chroma; } else if (section < 5) { red = x; blue = chroma; } else { red = chroma; blue = x; } var match = v - chroma; return "#" + channelHex((red + match) * 255) + channelHex((green + match) * 255) + channelHex((blue + match) * 255); } function hexToHsv(value) { var color = normalizedColor(value); if (!color) return null; var red = parseInt(color.slice(1, 3), 16) / 255; var green = parseInt(color.slice(3, 5), 16) / 255; var blue = parseInt(color.slice(5, 7), 16) / 255; var maximum = Math.max(red, green, blue); var minimum = Math.min(red, green, blue); var delta = maximum - minimum; var hue = 0; if (delta !== 0) { if (maximum === red) hue = 60 * (((green - blue) / delta) % 6); else if (maximum === green) hue = 60 * ((blue - red) / delta + 2); else hue = 60 * ((red - green) / delta + 4); } if (hue < 0) hue += 360; return { h: Math.round(hue), s: Math.round((maximum === 0 ? 0 : delta / maximum) * 100), v: Math.round(maximum * 100) }; } if (typeof module !== "undefined") { module.exports = { MAX_NAME_LENGTH: MAX_NAME_LENGTH, shippedProfiles: shippedProfiles, curatedAccents: curatedAccents, validCustomProfiles: validCustomProfiles, profileCatalog: profileCatalog, createCustomProfile: createCustomProfile, findProfile: findProfile, editProfile: editProfile, deleteProfile: deleteProfile, curatedPair: curatedPair, matchingShippedProfile: matchingShippedProfile, curatedNameForProfile: curatedNameForProfile, hsvToHex: hsvToHex, hexToHsv: hexToHsv }; }