feat: inline svg icons (#24)

This commit is contained in:
uncenter 2024-04-16 18:48:10 -04:00 committed by GitHub
parent 62f4116e4c
commit b55dab16c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 164 additions and 235 deletions

4
.gitignore vendored
View File

@ -2,8 +2,8 @@ node_modules/
dist/
.wxt/
src/public/
src/vscode-icons.json
src/associations.json
src/icons.json
.DS_Store
Thumbs.db

View File

@ -4,10 +4,12 @@
"description": "Soothing pastel icons for GitHub File Explorer",
"license": "MIT",
"dependencies": {
"@catppuccin/palette": "1.1.1",
"selector-observer": "2.1.6"
},
"devDependencies": {
"@humanfs/node": "0.14.0",
"@humanfs/types": "0.13.0",
"jiti": "1.21.0",
"prettier": "3.2.5",
"wxt": "0.17.3"

15
pnpm-lock.yaml generated
View File

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@catppuccin/palette':
specifier: 1.1.1
version: 1.1.1
selector-observer:
specifier: 2.1.6
version: 2.1.6
@ -13,6 +16,9 @@ devDependencies:
'@humanfs/node':
specifier: 0.14.0
version: 0.14.0
'@humanfs/types':
specifier: 0.13.0
version: 0.13.0
jiti:
specifier: 1.21.0
version: 1.21.0
@ -70,6 +76,10 @@ packages:
regenerator-runtime: 0.14.1
dev: true
/@catppuccin/palette@1.1.1:
resolution: {integrity: sha512-wgITbl3OEIUnv8WPTdG2BGjepSMZtJW9V4XpxNDJjCvel0Q5+BxRmKH3XYN0vVjohHR+APCyZCpzSnO+GdsTLg==}
dev: false
/@devicefarmer/adbkit-logcat@2.1.3:
resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==}
engines: {node: '>= 4'}
@ -316,6 +326,11 @@ packages:
'@humanwhocodes/retry': 0.1.2
dev: true
/@humanfs/types@0.13.0:
resolution: {integrity: sha512-3QDsfgPh0XN4D5Wg89cmswDJWLtZI4iqt7N0MrRbJBfpE9LrTYMwNKHkfZupyN531hhYPsDOtCHhHhF75LWOrg==}
engines: {node: '>=18.18.0'}
dev: true
/@humanwhocodes/retry@0.1.2:
resolution: {integrity: sha512-JNWGHkYfWI0+YgRHOwkFKjOuelfypQtp0GSx0lsOP9jU1Tj4f8k0x4dcaJSEZTp61THZJ+f9PJRh1GzYlQqHOQ==}
engines: {node: '>=18.18'}

View File

@ -1,8 +1,9 @@
import type { Associations } from './types';
import { associations as json } from '@/vscode-icons.json';
import { customAssociations } from './storage';
import json from '@/associations.json';
export async function getAssociations(): Promise<Associations> {
const custom = await customAssociations.getValue();

View File

@ -1,4 +1,4 @@
export const selectors = {
export const SELECTORS = {
row: `.js-navigation-container[role=grid] > .js-navigation-item,
file-tree .ActionList-content,
a.tree-browser-result,
@ -22,3 +22,5 @@ export const selectors = {
export const icons = {
x: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>',
};
export const ATTRIBUTE_PREFIX = 'data-catppuccin-file-explorer-icons';

View File

@ -1,10 +1,10 @@
import { defineContentScript } from 'wxt/sandbox';
import './styles.css';
import { observe } from 'selector-observer';
import { replaceAllIcons, replaceIconInRow } from '@/lib/replace';
import { selectors } from '@/lib/constants';
import { flavor } from '@/lib/storage';
import { SELECTORS } from '@/constants';
import { flavor } from '@/storage';
import { replaceIconInRow, injectStyles } from './lib';
export default defineContentScript({
matches: ['*://github.com/*'],
@ -16,7 +16,7 @@ export default defineContentScript({
// Here we compromise, rushing the first n replacements to prevent blinks that will likely be "above the fold"
// and delaying the replacement of subsequent rows.
let executions = 0;
let timerID;
let timerID: NodeJS.Timeout;
const rushFirst = (rushBatch: number, callback: () => void) => {
if (executions <= rushBatch) {
callback(); // Immediately run to prevent visual "blink".
@ -32,16 +32,15 @@ export default defineContentScript({
};
// Monitor DOM elements that match a CSS selector.
observe(selectors.row, {
observe(SELECTORS.row, {
add(row) {
const callback = async () =>
await replaceIconInRow(row as HTMLElement);
rushFirst(90, callback);
},
});
// Monitor the flavor changing.
flavor.watch(replaceAllIcons);
replaceAllIcons();
flavor.watch(injectStyles);
injectStyles();
},
});

View File

@ -1,20 +1,69 @@
import type { PublicPath } from 'wxt/browser';
import type { IconName } from '@/lib/types';
import type { IconName } from '@/types';
import { selectors } from '@/lib/constants';
import { flavor, specificFolders } from '@/lib/storage';
import { getAssociations } from './associations';
import { ATTRIBUTE_PREFIX, SELECTORS } from '@/constants';
import { flavor, specificFolders } from '@/storage';
import { getAssociations } from '@/associations';
import { createStylesElement } from '@/utils';
import { flavors } from '@catppuccin/palette';
import icons from '@/icons.json';
export async function injectStyles() {
const styles = createStylesElement();
styles.textContent = /* css */ `
:root {
${flavors[await flavor.getValue()].colorEntries
.map(([name, { hex }]) => `--ctp-${name}: ${hex};`)
.join('\n ')}
}
.PRIVATE_TreeView-directory-icon svg {
display: none !important;
}
svg[${ATTRIBUTE_PREFIX}-iconname$='_open']:has(~ svg.octicon-file-directory-open-fill:not([data-catppuccin-file-explorer-icons])),
svg:not([${ATTRIBUTE_PREFIX}-iconname$='_open']):has(~ svg.octicon-file-directory-fill:not([data-catppuccin-file-explorer-icons])),
svg[${ATTRIBUTE_PREFIX}]:has(+ .octicon-file) {
display: inline-block !important;
}
`.trim();
}
function createIconElement(
iconName: IconName,
fileName: string,
originalIcon: HTMLElement,
): SVGSVGElement {
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.innerHTML = icons[iconName];
svg.setAttribute(ATTRIBUTE_PREFIX, '');
svg.setAttribute(`${ATTRIBUTE_PREFIX}-iconname`, iconName);
svg.setAttribute(`${ATTRIBUTE_PREFIX}-filename`, fileName);
for (const attribute of originalIcon.getAttributeNames()) {
if (!attribute.startsWith(ATTRIBUTE_PREFIX)) {
svg.setAttribute(
attribute,
originalIcon.getAttribute(attribute) as string,
);
}
}
return svg;
}
export async function replaceIconInRow(row: HTMLElement) {
const icon = row.querySelector(selectors.icon) as HTMLElement;
if (icon && !icon?.hasAttribute('data-catppuccin-extension'))
const icon = row.querySelector(SELECTORS.icon) as HTMLElement;
if (icon && !icon?.hasAttribute(ATTRIBUTE_PREFIX))
await replaceIcon(icon, row);
}
export async function replaceIcon(icon: HTMLElement, row: HTMLElement) {
const fileNameEl = row.querySelector(selectors.filename) as HTMLElement;
const fileNameEl = row.querySelector(SELECTORS.filename) as HTMLElement;
if (!fileNameEl) return;
const fileName = fileNameEl.textContent?.split('/')[0].trim();
const fileName = fileNameEl.textContent?.split('/').at(0).trim();
const isDir =
icon.getAttribute('aria-label') === 'Directory' ||
@ -58,27 +107,14 @@ export async function replaceElementWithIcon(
iconName: IconName,
fileName: string,
) {
const replacement = document.createElement('img');
replacement.setAttribute('data-catppuccin-extension', 'icon');
replacement.setAttribute('data-catppuccin-extension-iconname', iconName);
replacement.setAttribute('data-catppuccin-extension-filename', fileName);
replacement.src = browser.runtime.getURL(
`${await flavor.getValue()}/${iconName}.svg` as PublicPath,
);
icon.getAttributeNames().forEach(
(attr) =>
attr !== 'src' &&
!/^data-catppuccin-extension/.test(attr) &&
replacement.setAttribute(attr, icon.getAttribute(attr) as string),
);
const replacement = createIconElement(iconName, fileName, icon);
const prevEl = icon.previousElementSibling;
if (prevEl?.getAttribute('data-catppuccin-extension') === 'icon') {
if (prevEl?.hasAttribute(ATTRIBUTE_PREFIX)) {
replacement.replaceWith(prevEl);
}
// If the icon to replace is an icon from this extension, replace it with the new icon.
else if (icon.getAttribute('data-catppuccin-extension') === 'icon') {
else if (icon.hasAttribute(ATTRIBUTE_PREFIX)) {
icon.replaceWith(replacement);
}
// If neither of the above, prepend the new icon in front of the original icon.
@ -88,29 +124,19 @@ export async function replaceElementWithIcon(
icon.style.display = 'none';
icon.before(replacement);
}
if (
icon.parentElement.classList.contains('PRIVATE_TreeView-directory-icon')
) {
const button =
icon.parentElement.parentElement.parentElement
.previousElementSibling;
const row = button.parentElement;
button.addEventListener('click', () => {
if (replacement.src.includes('_open')) {
replacement.setAttribute('data-do-not-touch', 'true');
replacement.src = replacement.src.replace('_open', '');
} else {
replacement.src = replacement.src.replace('.svg', '_open.svg');
}
});
row.addEventListener('click', () => {
if (
!replacement.src.includes('_open') &&
replacement.getAttribute('data-do-not-touch') !== 'true'
) {
replacement.src = replacement.src.replace('.svg', '_open.svg');
}
});
let companion = createIconElement(
(iconName.includes('_open')
? iconName.replace('_open', '')
: iconName + '_open') as IconName,
fileName,
icon,
);
replacement.after(companion);
}
}
@ -153,18 +179,3 @@ async function findIconMatch(
return '_file';
}
}
export function replaceAllIcons() {
for (const icon of document.querySelectorAll(
'img[data-catppuccin-extension-iconname]',
) as NodeListOf<HTMLElement>) {
const iconName = icon.getAttribute(
'data-catppuccin-extension-iconname',
) as IconName;
const fileName = icon.getAttribute(
'data-catppuccin-extension-filename',
);
if (iconName && fileName)
replaceElementWithIcon(icon, iconName, fileName);
}
}

View File

@ -1,10 +0,0 @@
.catppuccin-exension-hide-pseudo::before {
display: none !important;
}
/* Hide folder open/closed icons from new code view tree when clicked by disabling
display of those icons when they immediately follow the replaced icon. */
img[data-catppuccin-extension='icon'] + svg.octicon-file-directory-open-fill,
img[data-catppuccin-extension='icon'] + svg.octicon-file-directory-fill {
display: none !important;
}

View File

@ -1,11 +1,29 @@
import './styles.css';
import type { Associations, Flavor, IconName } from '@/lib/types';
import type { Associations, Flavor, IconName } from '@/types';
import { customAssociations, flavor, specificFolders } from '@/lib/storage';
import { icons } from '@/lib/constants';
import { flavor, specificFolders, customAssociations } from '@/storage';
import { icons } from '@/constants';
import { createStylesElement } from '@/utils';
import { flavorEntries } from '@catppuccin/palette';
function injectStyles() {
const styles = createStylesElement();
styles.textContent = flavorEntries
.map(
([flavor, { colorEntries }]) =>
`:root[theme="${flavor}"] {\n${colorEntries.map(([name, { hex }]) => ` --ctp-${name}: ${hex};`).join('\n')}\n}`,
)
.join('\n');
document.documentElement.appendChild(styles);
}
async function init() {
injectStyles();
const flavorEl = document.querySelector('#flavor') as HTMLSelectElement;
flavorEl.value = await flavor.getValue();

View File

@ -1,123 +1,3 @@
:root[theme='latte'] {
color-scheme: light;
--ctp-rosewater: #dc8a78;
--ctp-flamingo: #dd7878;
--ctp-pink: #ea76cb;
--ctp-mauve: #8839ef;
--ctp-red: #d20f39;
--ctp-maroon: #e64553;
--ctp-peach: #fe640b;
--ctp-yellow: #df8e1d;
--ctp-green: #40a02b;
--ctp-teal: #179299;
--ctp-sky: #04a5e5;
--ctp-sapphire: #209fb5;
--ctp-blue: #1e66f5;
--ctp-lavender: #7287fd;
--ctp-text: #4c4f69;
--ctp-subtext1: #5c5f77;
--ctp-subtext0: #6c6f85;
--ctp-overlay2: #7c7f93;
--ctp-overlay1: #8c8fa1;
--ctp-overlay0: #9ca0b0;
--ctp-surface2: #acb0be;
--ctp-surface1: #bcc0cc;
--ctp-surface0: #ccd0da;
--ctp-base: #eff1f5;
--ctp-mantle: #e6e9ef;
--ctp-crust: #dce0e8;
}
:root[theme='frappe'] {
color-scheme: dark;
--ctp-rosewater: #f2d5cf;
--ctp-flamingo: #eebebe;
--ctp-pink: #f4b8e4;
--ctp-mauve: #ca9ee6;
--ctp-red: #e78284;
--ctp-maroon: #ea999c;
--ctp-peach: #ef9f76;
--ctp-yellow: #e5c890;
--ctp-green: #a6d189;
--ctp-teal: #81c8be;
--ctp-sky: #99d1db;
--ctp-sapphire: #85c1dc;
--ctp-blue: #8caaee;
--ctp-lavender: #babbf1;
--ctp-text: #c6d0f5;
--ctp-subtext1: #b5bfe2;
--ctp-subtext0: #a5adce;
--ctp-overlay2: #949cbb;
--ctp-overlay1: #838ba7;
--ctp-overlay0: #737994;
--ctp-surface2: #626880;
--ctp-surface1: #51576d;
--ctp-surface0: #414559;
--ctp-base: #303446;
--ctp-mantle: #292c3c;
--ctp-crust: #232634;
}
:root[theme='macchiato'] {
color-scheme: dark;
--ctp-rosewater: #f4dbd6;
--ctp-flamingo: #f0c6c6;
--ctp-pink: #f5bde6;
--ctp-mauve: #c6a0f6;
--ctp-red: #ed8796;
--ctp-maroon: #ee99a0;
--ctp-peach: #f5a97f;
--ctp-yellow: #eed49f;
--ctp-green: #a6da95;
--ctp-teal: #8bd5ca;
--ctp-sky: #91d7e3;
--ctp-sapphire: #7dc4e4;
--ctp-blue: #8aadf4;
--ctp-lavender: #b7bdf8;
--ctp-text: #cad3f5;
--ctp-subtext1: #b8c0e0;
--ctp-subtext0: #a5adcb;
--ctp-overlay2: #939ab7;
--ctp-overlay1: #8087a2;
--ctp-overlay0: #6e738d;
--ctp-surface2: #5b6078;
--ctp-surface1: #494d64;
--ctp-surface0: #363a4f;
--ctp-base: #24273a;
--ctp-mantle: #1e2030;
--ctp-crust: #181926;
}
:root[theme='mocha'] {
color-scheme: dark;
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-maroon: #eba0ac;
--ctp-peach: #fab387;
--ctp-yellow: #f9e2af;
--ctp-green: #a6e3a1;
--ctp-teal: #94e2d5;
--ctp-sky: #89dceb;
--ctp-sapphire: #74c7ec;
--ctp-blue: #89b4fa;
--ctp-lavender: #b4befe;
--ctp-text: #cdd6f4;
--ctp-subtext1: #bac2de;
--ctp-subtext0: #a6adc8;
--ctp-overlay2: #9399b2;
--ctp-overlay1: #7f849c;
--ctp-overlay0: #6c7086;
--ctp-surface2: #585b70;
--ctp-surface1: #45475a;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
--ctp-mantle: #181825;
--ctp-crust: #11111b;
}
body {
width: 600px;
height: max-content;

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,10 +1,8 @@
import type { PublicPath } from 'wxt/browser';
import icons from '@/icons.json';
export type Flavor = 'latte' | 'frappe' | 'macchiato' | 'mocha';
type RemoveIconPrefixAndSuffix<T extends string> =
T extends `/${Flavor}/${infer Rest}.svg` ? Rest : never;
export type IconName = RemoveIconPrefixAndSuffix<PublicPath>;
export type IconName = keyof typeof icons;
export type Associations = {
languageIds: Record<string, IconName>;

13
src/utils.ts Normal file
View File

@ -0,0 +1,13 @@
export function createStylesElement() {
const id = 'catppuccin-icons-css-variables';
let styles = document.querySelector(`#${id}`);
if (!styles) {
styles = document.createElement('style');
styles.setAttribute('id', id);
document.documentElement.appendChild(styles);
}
return styles;
}

View File

@ -14,41 +14,41 @@ export default defineConfig({
permissions: ['storage'],
homepage_url:
'https://github.com/catppuccin/github-file-explorer-icons',
web_accessible_resources: [
{
resources: ['*.svg'],
matches: ['*://github.com/*'],
},
],
},
hooks: {
'build:before': async () => {
const PUBLIC_DIR = join(__dirname, './src/public/');
if (await hfs.isDirectory(PUBLIC_DIR)) {
await hfs.deleteAll(PUBLIC_DIR);
await hfs.createDirectory(PUBLIC_DIR);
const ICON_DIR = join(
__dirname,
'./vscode-icons/icons/css-variables/',
);
const icons = {};
for await (const entry of hfs.list(ICON_DIR)) {
icons[entry.name.replace('.svg', '')] = await hfs
.text(join(ICON_DIR, entry.name))
.then((text) => {
const lines = text.split('\n');
return lines
.slice(1, lines.length - 2)
.join('\n')
.trim()
.replaceAll('--vscode-ctp', '--ctp')
.replaceAll('/>', '></path>');
});
}
// Copy icons:
await hfs.copyAll(
join(__dirname, './vscode-icons/icons/'),
PUBLIC_DIR,
);
await hfs.deleteAll(join(PUBLIC_DIR, 'css-variables'));
// Write associations/config file:
await hfs.write(
join(__dirname, './src/vscode-icons.json'),
join(__dirname, './src/icons.json'),
JSON.stringify(icons),
);
await hfs.write(
join(__dirname, './src/associations.json'),
JSON.stringify(
jiti(__dirname)('./vscode-icons/src/defaults/index.ts')
.defaultConfig,
.defaultConfig.associations,
),
);
await hfs.copyAll(
join(__dirname, './assets/icons'),
join(PUBLIC_DIR),
);
},
},
});