This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541// Palette snapshot: @bsky.app/alf 0.1.15. See docs/theme-color-audit.md.(() => { 'use strict';
const STORAGE_KEY = 'bskyplusThemes'; const SOURCE = { "light": { "contrast_0": "#FFFFFF", "contrast_25": "#F9FAFB", "contrast_50": "#EFF2F6", "contrast_100": "#DCE2EA", "contrast_200": "#C0CAD8", "contrast_300": "#A5B2C5", "contrast_400": "#8798B0", "contrast_500": "#667B99", "contrast_600": "#526580", "contrast_700": "#405168", "contrast_800": "#313F54", "contrast_900": "#232E3E", "contrast_950": "#19222E", "contrast_975": "#111822", "contrast_1000": "#000000", "primary_25": "#F5F9FF", "primary_50": "#E5F0FF", "primary_100": "#CCE1FF", "primary_200": "#A8CCFF", "primary_300": "#75AFFF", "primary_400": "#4291FF", "primary_500": "#006AFF", "primary_600": "#0059D6", "primary_700": "#0048AD", "primary_800": "#00398A", "primary_900": "#002861", "primary_950": "#001E47", "primary_975": "#001533", "positive_25": "#ECFEF5", "positive_50": "#D3FDE8", "positive_100": "#A3FACF", "positive_200": "#6AF6B0", "positive_300": "#2CF28F", "positive_400": "#0DD370", "positive_500": "#09B35E", "positive_600": "#04904A", "positive_700": "#036D38", "positive_800": "#04522B", "positive_900": "#033F21", "positive_950": "#032A17", "positive_975": "#021D0F", "negative_25": "#FFF5F7", "negative_50": "#FEE7EC", "negative_100": "#FDD3DD", "negative_200": "#FBBBCA", "negative_300": "#F891A9", "negative_400": "#F65A7F", "negative_500": "#E91646", "negative_600": "#CA123D", "negative_700": "#A71134", "negative_800": "#7F0B26", "negative_900": "#5F071C", "negative_950": "#430413", "negative_975": "#30030D" }, "subdued": { "contrast_0": "#FFFFFF", "contrast_25": "#F9FAFB", "contrast_50": "#F2F4F8", "contrast_100": "#E2E7EE", "contrast_200": "#C3CDDA", "contrast_300": "#ABB8C9", "contrast_400": "#8D9DB4", "contrast_500": "#6F839F", "contrast_600": "#586C89", "contrast_700": "#485B75", "contrast_800": "#394960", "contrast_900": "#2C3A4E", "contrast_950": "#222E3F", "contrast_975": "#1C2736", "contrast_1000": "#151D28", "primary_25": "#F5F9FF", "primary_50": "#EBF3FF", "primary_100": "#D6E7FF", "primary_200": "#ADCFFF", "primary_300": "#80B5FF", "primary_400": "#4D97FF", "primary_500": "#0F73FF", "primary_600": "#0661E0", "primary_700": "#0A52B8", "primary_800": "#0E4490", "primary_900": "#123464", "primary_950": "#122949", "primary_975": "#122136", "positive_25": "#ECFEF5", "positive_50": "#D8FDEB", "positive_100": "#A8FAD1", "positive_200": "#6FF6B3", "positive_300": "#31F291", "positive_400": "#0EDD75", "positive_500": "#0AC266", "positive_600": "#049F52", "positive_700": "#038142", "positive_800": "#056636", "positive_900": "#04522B", "positive_950": "#053D21", "positive_975": "#052917", "negative_25": "#FFF5F7", "negative_50": "#FEEBEF", "negative_100": "#FDD8E1", "negative_200": "#FCC0CE", "negative_300": "#F99AB0", "negative_400": "#F76486", "negative_500": "#EB2452", "negative_600": "#D81341", "negative_700": "#BA1239", "negative_800": "#910D2C", "negative_900": "#6F0B22", "negative_950": "#500B1C", "negative_975": "#3E0915" }}; const GROUPS = [ { name: 'Surfaces', fields: [ ['background', 'Page background', 'Feed, navigation, and page canvas'], ['surface', 'Subtle surface', 'Cards and grouped settings'], ['surfaceRaised', 'Raised surface', 'Secondary buttons, menus, and inputs'], ['hover', 'Hover and selection', 'Neutral hovered and selected controls'], ] }, { name: 'Text and icons', fields: [ ['text', 'Main text', 'Post text, headings, and primary icons'], ['textMuted', 'Secondary text', 'Handles, timestamps, and descriptions'], ['textSubtle', 'Subtle text', 'Placeholders and disabled icons'], ['textInverse', 'Inverse text', 'Text on inverted neutral surfaces'], ] }, { name: 'Borders and focus', fields: [ ['border', 'Subtle border', 'Post dividers and input outlines'], ['borderStrong', 'Strong border', 'Emphasized borders'], ['focus', 'Focus ring', 'Keyboard focus outlines'], ] }, { name: 'Links and primary controls', fields: [ ['primary', 'Primary control', 'Compose, follow, and selected controls'], ['primaryHover', 'Primary hover', 'Hovered primary controls'], ['primaryMuted', 'Primary soft surface', 'Subtle primary and disabled backgrounds'], ['link', 'Link text', 'Links and mentions'], ['onPrimary', 'Text on colored controls', 'Text and icons on solid colored buttons'], ] }, { name: 'Status and reactions', fields: [ ['positive', 'Success and reposts', 'Positive feedback and repost icons'], ['positiveMuted', 'Success soft surface', 'Positive notices and subtle controls'], ['negative', 'Danger and errors', 'Destructive actions and error messages'], ['negativeMuted', 'Danger soft surface', 'Error notices and subtle controls'], ['like', 'Likes', 'Liked hearts and pink indicators'], ['warning', 'Warnings', 'Yellow warnings and highlights'], ] }, { name: 'Overlays and shadows', fields: [ ['overlay', 'Overlay tint', 'Modal backdrops; original opacity is preserved'], ['shadow', 'Shadow tint', 'UI shadows; original opacity is preserved'], ] }, ].map(group => ({ ...group, fields: group.fields.map(([key, label, description]) => ({ key, label, description })) })); const FIELD_LABELS = Object.fromEntries(GROUPS.flatMap(group => group.fields.map(field => [field.key, field.label]))); const KEYS = Object.keys(FIELD_LABELS); const CONTRAST_PAIRS = [ ...['text', 'textMuted', 'link'].flatMap(foreground => ['background', 'surface', 'surfaceRaised', 'hover'].map(background => [foreground, background])), ['onPrimary', 'primary'], ['onPrimary', 'primaryHover'], ['textInverse', 'text'], ...['primaryMuted', 'positiveMuted', 'negativeMuted'].map(background => ['text', background]), ...['positive', 'negative', 'like', 'warning'].map(foreground => [foreground, 'background']), ];
function palette(mode, subdued = mode === 'dim') { const base = SOURCE[subdued ? 'subdued' : 'light']; if (mode === 'light') return { ...base }; return Object.fromEntries(Object.entries(base).map(([key]) => { const [family, step] = key.split('_'); return [key, base[family + '_' + (1000 - Number(step))]]; })); }
function preset(mode) { const p = palette(mode); return { background: p.contrast_0, surface: p.contrast_25, surfaceRaised: p.contrast_50, hover: p.contrast_100, text: p.contrast_1000, textMuted: p.contrast_700, textSubtle: p.contrast_400, textInverse: p.contrast_0, border: p.contrast_100, borderStrong: p.contrast_200, primary: p.primary_500, primaryHover: p.primary_600, primaryMuted: p.primary_50, link: mode === 'light' ? p.primary_500 : p.primary_600, onPrimary: '#FFFFFF', positive: p.positive_500, positiveMuted: p.positive_50, negative: p.negative_500, negativeMuted: p.negative_50, like: '#EC4899', warning: '#FFC404', overlay: '#000000', shadow: '#000000', focus: mode === 'light' ? p.primary_500 : p.primary_600, }; }
const PRESETS = Object.fromEntries(['light', 'dim', 'dark'].map(mode => [mode, preset(mode)])); const IMPORTED_BUILTINS = [ { schemaVersion: 1, id: 'bskyplus-coffee-espresso-light', name: 'Coffee / Espresso — Light', base: 'light', colors: { background: '#F7F2E9', surface: '#EFE3D0', surfaceRaised: '#FDF8F0', hover: '#7B3D11', text: '#1C1310', textMuted: '#1C1310', textSubtle: '#1C1310', textInverse: '#F7F2E9', border: '#2E2018', borderStrong: '#2E2018', focus: '#A55A1F', primary: '#A55A1F', primaryHover: '#A55A1F', primaryMuted: '#7B3D11', link: '#A55A1F', onPrimary: '#F7F2E9', positive: '#4CAF7D', positiveMuted: '#D3FDE8', negative: '#E06C6C', negativeMuted: '#FEE7EC', like: '#E06C6C', warning: '#E2B76F', overlay: '#000000', shadow: '#000000', }, }, { schemaVersion: 1, id: 'bskyplus-floras-moat-floras-moat', name: "Flora's Moat — Flora's Moat", base: 'dark', colors: { background: '#333333', surface: '#222222', surfaceRaised: '#333333', hover: '#574C56', text: '#EEEEEE', textMuted: '#EEEEEE', textSubtle: '#EEEEEE', textInverse: '#333333', border: '#444444', borderStrong: '#444444', focus: '#E0759C', primary: '#E0759C', primaryHover: '#E0759C', primaryMuted: '#574C56', link: '#E0759C', onPrimary: '#333333', positive: '#9DBF8A', positiveMuted: '#032A17', negative: '#C97A7A', negativeMuted: '#430413', like: '#E0759C', warning: '#D9B27C', overlay: '#000000', shadow: '#000000', }, }, { schemaVersion: 1, id: 'bskyplus-witchsky-mu-pink-midnight', name: 'Witchsky + mu Pink — Midnight', base: 'dark', colors: { background: '#150B08', surface: '#2A1A15', surfaceRaised: '#3E2B21', hover: '#75295E', text: '#FEFBFB', textMuted: '#C0BBB9', textSubtle: '#C0BBB9', textInverse: '#FEFBFB', border: '#624C43', borderStrong: '#624C43', focus: '#DB4AA6', primary: '#DB4AA6', primaryHover: '#DB4AA6', primaryMuted: '#75295E', link: '#DB4AA6', onPrimary: '#FEFBFB', positive: '#09B35E', positiveMuted: '#032A17', negative: '#E91646', negativeMuted: '#430413', like: '#EC4899', warning: '#FFC404', overlay: '#000000', shadow: '#000000', }, }, { schemaVersion: 1, id: 'bskyplus-snuffbait-dark-coven', name: 'snuffbait dark — Coven', base: 'dark', colors: { background: '#231825', surface: '#2A1E2B', surfaceRaised: '#393140', hover: '#514453', text: '#FFE0FF', textMuted: '#C0BBB9', textSubtle: '#C0BBB9', textInverse: '#FEFBFB', border: '#624C43', borderStrong: '#624C43', focus: '#B09FB4', primary: '#B09FB4', primaryHover: '#B09FB4', primaryMuted: '#514453', link: '#B09FB4', onPrimary: '#FEFBFB', positive: '#0AC266', positiveMuted: '#032A17', negative: '#EB2452', negativeMuted: '#430413', like: '#DD5E8F', warning: '#FFC404', overlay: '#000000', shadow: '#000000', }, }, { schemaVersion: 1, id: 'bskyplus-latte-mauve', name: 'Latte — Mauve', base: 'light', colors: { background: '#EFF1F5', surface: '#E6E9EF', surfaceRaised: '#DCE0E8', hover: '#E6E9EF', text: '#4C4F69', textMuted: '#6C6F85', textSubtle: '#6C6F85', textInverse: '#EFF1F5', border: '#9CA0B0', borderStrong: '#9CA0B0', focus: '#8839EF', primary: '#8839EF', primaryHover: '#8839EF', primaryMuted: '#E6E9EF', link: '#8839EF', onPrimary: '#EFF1F5', positive: '#40A02B', positiveMuted: '#D3FDE8', negative: '#D20F39', negativeMuted: '#FEE7EC', like: '#EA76CB', warning: '#DF8E1D', overlay: '#000000', shadow: '#000000', }, }, ]; const BUILTIN_THEMES = Object.freeze({ ...Object.fromEntries(Object.entries(PRESETS).map(([base, colors]) => [ 'bluesky-' + base, Object.freeze({ schemaVersion: 1, id: 'bluesky-' + base, name: 'Bluesky ' + base[0].toUpperCase() + base.slice(1), base, colors: Object.freeze({ ...colors }) }), ])), ...Object.fromEntries(IMPORTED_BUILTINS.map(candidate => { const theme = validateTheme(candidate); return [theme.id, Object.freeze({ ...theme, colors: Object.freeze({ ...theme.colors }) })]; })), }); const isBuiltinTheme = id => Object.hasOwn(BUILTIN_THEMES, id); const availableBuiltinThemes = library => { const deleted = new Set(library?.deletedBuiltinIds || []); return Object.values(BUILTIN_THEMES).filter(theme => !deleted.has(theme.id)); };
function validateTheme(value) { if (!value || value.schemaVersion !== 1) throw new Error('Unsupported theme format.'); if (typeof value.id !== 'string' || !/^[a-zA-Z0-9_-]{1,80}$/.test(value.id)) throw new Error('Invalid theme ID.'); if (typeof value.name !== 'string' || !value.name.trim() || value.name.trim().length > 80) throw new Error('Use a theme name between 1 and 80 characters.'); if (!Object.hasOwn(PRESETS, value.base)) throw new Error('Choose a light, dim, or dark base.'); const colors = {}; for (const key of KEYS) { const color = value.colors?.[key]; if (typeof color !== 'string' || !/^#[0-9a-f]{6}$/i.test(color)) throw new Error('Use a six-digit hex color for ' + key + '.'); colors[key] = color.toUpperCase(); } return { schemaVersion: 1, id: value.id, name: value.name.trim(), base: value.base, colors }; }
function createTheme(name = 'My theme', base = 'dim') { if (!Object.hasOwn(PRESETS, base)) base = 'dim'; return { schemaVersion: 1, id: crypto.randomUUID(), name, base, colors: { ...PRESETS[base] } }; }
function normalizeLibrary(value) { const themes = []; const deletedBuiltinIds = []; if (value?.schemaVersion === 1 && Array.isArray(value.themes)) { for (const candidate of value.themes.slice(0, 100)) { try { const theme = validateTheme(candidate); if (isBuiltinTheme(theme.id)) continue; if (!themes.some(saved => saved.id === theme.id)) themes.push(theme); } catch { /* A malformed entry cannot disable other saved themes. */ } } } if (value?.schemaVersion === 1 && Array.isArray(value.deletedBuiltinIds)) { for (const id of value.deletedBuiltinIds) { if (typeof id === 'string' && isBuiltinTheme(id) && !deletedBuiltinIds.includes(id)) deletedBuiltinIds.push(id); } } const activeId = themes.some(theme => theme.id === value?.activeId) || (value?.schemaVersion === 1 && isBuiltinTheme(value?.activeId) && !deletedBuiltinIds.includes(value.activeId)) ? value.activeId : null; const library = { schemaVersion: 1, activeId, themes }; if (deletedBuiltinIds.length) library.deletedBuiltinIds = deletedBuiltinIds; return library; }
function parseWitchSkyUri(value) { let text = typeof value === 'string' ? value.trim() : ''; const explorer = 'https://pds.ls/'; if (text.startsWith(explorer)) { try { text = decodeURIComponent(text.slice(explorer.length)).split('#')[0]; } catch { throw new Error('Enter a complete WitchSky theme at:// URI using a did:plc or did:web repository.'); } } const match = text.match(/^at:\/\/([^/]+)\/app\.witchsky\.theme\.colors\/([a-zA-Z0-9._~:-]{1,512})$/); if (!match) throw new Error('Enter a complete WitchSky theme at:// URI using a did:plc or did:web repository.'); const repo = match[1]; if (/^did:plc:[a-z2-7]{24}$/.test(repo)) return { uri: text, repo, rkey: match[2] }; const didWebDocumentUrl = resolveDidWebDocumentUrl(repo); if (!didWebDocumentUrl) throw new Error('Enter a complete WitchSky theme at:// URI using a did:plc or did:web repository.'); return { uri: text, repo, rkey: match[2], didDocumentUrl: didWebDocumentUrl }; }
function resolveDidWebDocumentUrl(repo) { const parts = repo.split(':'); if (parts.length < 3 || parts[0] !== 'did' || parts[1] !== 'web' || parts.length > 20) return null; let host; try { host = decodeURIComponent(parts[2]); } catch { return null; } if (!host || /[\\/?#@%\u0000-\u0020\u007f]/.test(host)) return null;
let origin; try { const url = new URL('https://' + host + '/'); if (!url.hostname || url.username || url.password || url.pathname !== '/' || url.search || url.hash) return null; origin = url.origin; } catch { return null; }
const path = []; for (const encodedPart of parts.slice(3)) { let segment; try { segment = decodeURIComponent(encodedPart); } catch { return null; } if (!segment || segment === '.' || segment === '..' || /[\\/?#@\u0000-\u001f\u007f]/.test(segment)) return null; path.push(encodeURIComponent(segment)); } return origin + (path.length ? '/' + path.join('/') + '/did.json' : '/.well-known/did.json'); }
function witchSkyThemes(value) { if (!value || value.$type !== 'app.witchsky.theme.colors' || typeof value.name !== 'string' || !value.name.trim() || !value.base || typeof value.base !== 'object') { throw new Error('The AT record is not a WitchSky color theme.'); }
const normalizeColors = colors => { if (!colors || typeof colors !== 'object') throw new Error('The WitchSky theme has no color palette.'); const output = {}; for (const [key, color] of Object.entries(colors)) { const parsed = typeof color === 'string' ? parseColor(color) : null; if (!parsed || parsed.alpha !== 1) throw new Error('WitchSky color “' + key + '” must be opaque.'); output[key] = parsed.hex; } return output; }; const baseColors = normalizeColors(value.base.colors); for (const key of ['canvas', 'surface', 'surfaceRaised', 'text', 'textMuted', 'border', 'accent', 'accentSoft', 'onAccent', 'positive', 'warning', 'critical', 'favorite']) { if (!baseColors[key]) throw new Error('The WitchSky theme is missing the “' + key + '” color.'); } const sets = [{ name: value.base.name || 'Default', colors: baseColors }]; if (value.variants !== undefined && !Array.isArray(value.variants)) throw new Error('The WitchSky theme variants are invalid.'); for (const variant of (value.variants || []).slice(0, 11)) { if (!variant || typeof variant.name !== 'string' || !variant.name.trim()) throw new Error('A WitchSky theme variant has no name.'); sets.push({ name: variant.name, colors: { ...baseColors, ...normalizeColors(variant.colors || {}) } }); }
return sets.map(set => { const wc = set.colors; const base = contrastRatio(wc.canvas, '#000000') > contrastRatio(wc.canvas, '#FFFFFF') ? 'light' : 'dark'; const name = (value.name.trim() + ' — ' + set.name.trim()).slice(0, 80); return validateTheme({ ...createTheme(name, base), colors: { ...PRESETS[base], background: wc.canvas, surface: wc.surface, surfaceRaised: wc.surfaceRaised, hover: wc.accentSoft, text: wc.text, textMuted: wc.textMuted, textSubtle: wc.textMuted, textInverse: wc.onAccent, border: wc.border, borderStrong: wc.border, focus: wc.accent, primary: wc.accent, primaryHover: wc.accent, primaryMuted: wc.accentSoft, link: wc.accent, onPrimary: wc.onAccent, positive: wc.positive, negative: wc.critical, like: wc.favorite, warning: wc.warning, }, }); }); }
// Rotate HSL hue while preserving each color's saturation and lightness. function shiftHue(colors, degrees) { if (!Number.isFinite(degrees)) throw new Error('Use a finite hue shift.'); const rotation = ((degrees % 360) + 360) % 360; return Object.fromEntries(Object.entries(colors).map(([key, color]) => { const [r, g, b] = color.slice(1).match(/../g).map(channel => parseInt(channel, 16)); const min = Math.min(r, g, b); const max = Math.max(r, g, b); const chroma = max - min; if (!chroma || !rotation) return [key, color.toUpperCase()]; const hue = max === r ? (g - b) / chroma : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4; const sector = ((hue + rotation / 60) % 6 + 6) % 6; const x = chroma * (1 - Math.abs(sector % 2 - 1)); const rgb = sector < 1 ? [chroma, x, 0] : sector < 2 ? [x, chroma, 0] : sector < 3 ? [0, chroma, x] : sector < 4 ? [0, x, chroma] : sector < 5 ? [x, 0, chroma] : [chroma, 0, x]; return [key, '#' + rgb.map(channel => Math.round(channel + min).toString(16).padStart(2, '0')).join('').toUpperCase()]; })); }
function contrastRatio(a, b) { const luminance = hex => { const channels = hex.slice(1).match(/../g).map(value => parseInt(value, 16) / 255) .map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4); return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; }; const first = luminance(a), second = luminance(b); return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05); }
function contrastIssues(colors) { return CONTRAST_PAIRS.map(([foreground, background]) => ({ foreground, background, label: FIELD_LABELS[foreground] + ' on ' + FIELD_LABELS[background], ratio: contrastRatio(colors[foreground], colors[background]), })).filter(pair => pair.ratio < 3).sort((a, b) => a.ratio - b.ratio); }
function mix(a, b, amount) { return '#' + [1, 3, 5].map(offset => Math.round( parseInt(a.slice(offset, offset + 2), 16) * (1 - amount) + parseInt(b.slice(offset, offset + 2), 16) * amount ).toString(16).padStart(2, '0')).join('').toUpperCase(); }
function ramp(step, anchors) { const entries = Object.entries(anchors).map(([point, color]) => [Number(point), color]).sort((a, b) => a[0] - b[0]); for (let i = 1; i < entries.length; i++) { if (step <= entries[i][0]) { const [low, a] = entries[i - 1], [high, b] = entries[i]; return mix(a, b, (step - low) / (high - low)); } } return entries.at(-1)[1]; }
// Property-aware mapping keeps borders distinct from surfaces with the same source color. function tokenColor(token, property, colors) { const [family, rawStep] = token.split('_'); const step = Number(rawStep); if (family === 'contrast') { if (property.includes('border') || property === 'outline-color') { return step <= 100 ? colors.border : colors.borderStrong; } if (property === 'color' || property === 'fill' || property === 'stroke' || property === 'caret-color' || property === 'text-decoration-color') { return ramp(step, { 0: colors.textInverse, 400: colors.textSubtle, 700: colors.textMuted, 900: colors.text, 1000: colors.text }); } return ramp(step, { 0: colors.background, 25: colors.surface, 50: colors.surfaceRaised, 100: colors.hover, 400: colors.textSubtle, 700: colors.textMuted, 1000: colors.text }); } const base = colors[family]; const soft = colors[family + 'Muted']; if (!base || !soft) return null; if (family === 'primary' && ['color', 'fill', 'stroke', 'text-decoration-color'].includes(property) && (step === 500 || step === 600)) return colors.link; return ramp(step, { 0: colors.background, 50: soft, 500: base, 600: family === 'primary' ? colors.primaryHover : mix(base, colors.text, 0.12), 1000: colors.text }); }
function parseColor(value) { let text = value.toLowerCase().trim(); if (text === 'white') text = '#ffffff'; if (text === 'black') text = '#000000'; if (/^#[\da-f]{3,4}$/i.test(text)) text = '#' + [...text.slice(1)].map(c => c + c).join(''); if (/^#[\da-f]{6}([\da-f]{2})?$/i.test(text)) { return { hex: text.slice(0, 7).toUpperCase(), alpha: text.length === 9 ? parseInt(text.slice(7), 16) / 255 : 1 }; } const match = text.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/); if (!match) return null; const channels = match.slice(1, 4).map(Number); if (channels.some(c => c < 0 || c > 255)) return null; return { hex: '#' + channels.map(c => Math.round(c).toString(16).padStart(2, '0')).join('').toUpperCase(), alpha: match[4] ? parseFloat(match[4]) / (match[4].endsWith('%') ? 100 : 1) : 1, }; }
function mapColor(value, property, theme, mode) { const parsed = parseColor(value); if (!parsed || parsed.alpha === 0) return null; const { hex, alpha } = parsed; const c = theme.colors; let target; if (property.includes('shadow')) target = c.shadow; else if (hex === '#000000' && alpha < 1 && property.startsWith('background')) target = c.overlay; else if (property === 'outline-color') target = c.focus; else if (hex === '#EC4899') target = c.like; else if (hex === '#FFC404') target = c.warning; else { // Check the native palette first; subdued aliases cover older dim deployments. for (const p of [palette(mode), palette(mode, mode !== 'dim')]) { const entry = Object.entries(p).find(([, color]) => color === hex); if (entry) { target = tokenColor(entry[0], property, c); break; } } if (!target && ['#1083FE', '#0085FF', '#007AFF', '#0A7AFF', '#208BFE', '#2E93FE', '#127EF1'].includes(hex)) target = ['color', 'fill', 'stroke'].includes(property) ? c.link : c.primary; if (!target && ['#20BC07', '#27F406'].includes(hex)) target = c.positive; if (!target && ['#EC4868', '#D11043'].includes(hex)) target = c.negative; } if (!target) return null; if (alpha >= 1) return target; const rgb = target.slice(1).match(/../g).map(channel => parseInt(channel, 16)); return 'rgba(' + rgb.join(', ') + ', ' + alpha + ')'; }
globalThis.BskyPlusThemes = { STORAGE_KEY, GROUPS, KEYS, PRESETS, BUILTIN_THEMES, isBuiltinTheme, availableBuiltinThemes, SOURCE, palette, parseColor, mapColor, createTheme, validateTheme, normalizeLibrary, parseWitchSkyUri, witchSkyThemes, contrastRatio, contrastIssues, shiftHue, exportTheme: theme => JSON.stringify(validateTheme(theme), null, 2) + '\n', };})();