example/src/theme-builder/theme-builder.ts (334 lines of code) (raw):
import * as BaseConfigLight from './base-theme-light-config.json';
import * as BaseConfigDark from './base-theme-dark-config.json';
interface ConfigItem {
type: 'measurement' | 'text' | 'color';
description?: string;
units?: string[];
unit?: string;
category?: string;
alpha?: string;
value: string;
}
const categories = [
'sizing',
'border-style',
'font-size',
'font-family',
'text-color',
'syntax-color',
'status-color',
'background-color',
'shadow',
'radius',
'transition',
];
export class ThemeBuilder {
private themeSelector: HTMLSelectElement = document.querySelector('#theme-selector') as HTMLSelectElement;
private mainWrapper: HTMLElement = document.createElement('div');
private inputsWrapper: HTMLElement = document.createElement('div');
private buttonsWrapper: HTMLElement = document.createElement('div');
private baseThemeType: 'light' | 'dark' = 'light';
private currentConfig: Record<any, ConfigItem> = structuredClone(BaseConfigLight) as any;
constructor(selector: string | HTMLElement) {
delete this.currentConfig.default;
this.themeSelector.addEventListener('change', e => {
if (this.themeSelector.value.match('base-')) {
this.baseThemeType = this.themeSelector.value.replace('base-', '') as 'light' | 'dark';
if (this.baseThemeType === 'light') {
this.currentConfig = structuredClone(BaseConfigLight) as any;
} else {
this.currentConfig = structuredClone(BaseConfigDark) as any;
}
this.inputsWrapper.innerHTML = '';
this.fillInputWrapper();
this.buildCssValues();
} else if (this.themeSelector.value.match('dark-')) {
document.querySelector('body')?.classList.add('vscode-dark');
} else {
document.querySelector('body')?.classList.remove('vscode-dark');
}
document.querySelector('html')?.setAttribute('theme', this.themeSelector.value);
});
this.mainWrapper.classList.add('mynah-ui-example-input-main-wrapper');
this.inputsWrapper.classList.add('mynah-ui-example-input-items-wrapper');
this.buttonsWrapper.classList.add('mynah-ui-example-input-buttons-wrapper');
let parentWrapper: HTMLElement;
if (typeof selector === 'string') {
parentWrapper = document.querySelector(selector) ?? (document.querySelector('body') as HTMLElement);
} else {
parentWrapper = selector;
}
this.mainWrapper.insertAdjacentElement('beforeend', this.inputsWrapper);
parentWrapper.insertAdjacentElement('beforeend', this.buttonsWrapper);
parentWrapper.insertAdjacentElement('beforeend', this.mainWrapper);
this.mainWrapper.insertAdjacentHTML(
'beforeend',
`
<p>
First, please select one of the <b>Custom Theme</b>s from the themes list on the header bar.
After that you'll see the changes whenever you adjust one of the options below.</br>
For measurement values (or anything other than colors) you can use current custom properties like the sizings.</br>
First select (No Unit) option for the unit and then you can type any string into the value field.
And you can use custom properties as usual like <b>var(--mynah-sizing-1)</b> and the sizing values goes from <b>1 to 18</b>.
</p>
`
);
this.fillInputWrapper();
const uploadThemeConfigFilePicker = document.createElement('input');
uploadThemeConfigFilePicker.setAttribute('type', 'file');
uploadThemeConfigFilePicker.setAttribute('accept', '.mynahuitc');
uploadThemeConfigFilePicker.classList.add('hidden');
uploadThemeConfigFilePicker.classList.add('config-operation');
uploadThemeConfigFilePicker.classList.add('fill-state-always');
uploadThemeConfigFilePicker.addEventListener('change', async () => {
const file = uploadThemeConfigFilePicker.files?.item(0);
if (file) {
const text = await file.text();
try {
this.currentConfig = JSON.parse(text);
this.inputsWrapper.innerHTML = '';
this.fillInputWrapper();
this.buildCssValues();
uploadThemeConfigFilePicker.value = '';
} catch (err) {
console.warn("Coudln't read the JSON content");
}
}
});
const downloadThemeConfigButton = document.createElement('button');
downloadThemeConfigButton.innerHTML = '<span>Download Config</span>';
downloadThemeConfigButton.classList.add('mynah-button');
downloadThemeConfigButton.classList.add('config-operation');
downloadThemeConfigButton.classList.add('fill-state-always');
downloadThemeConfigButton.addEventListener('click', () => {
download('mynah-ui-theme.mynahuitc', JSON.stringify(this.currentConfig));
});
const resetThemeConfigButton = document.createElement('button');
resetThemeConfigButton.innerHTML = '<span>Reset</span>';
resetThemeConfigButton.classList.add('mynah-button');
resetThemeConfigButton.classList.add('config-operation');
resetThemeConfigButton.classList.add('fill-state-always');
resetThemeConfigButton.addEventListener('click', () => {
this.currentConfig = structuredClone(this.baseThemeType === 'light' ? BaseConfigLight : BaseConfigDark) as any;
this.inputsWrapper.innerHTML = '';
this.fillInputWrapper();
this.buildCssValues();
});
const uploadThemeConfigButton = document.createElement('button');
uploadThemeConfigButton.innerHTML = '<span>Upload Config</span>';
uploadThemeConfigButton.classList.add('mynah-button');
uploadThemeConfigButton.classList.add('config-operation');
uploadThemeConfigButton.classList.add('fill-state-always');
uploadThemeConfigButton.addEventListener('click', () => {
uploadThemeConfigFilePicker.click();
});
const downloadThemeButton = document.createElement('button');
downloadThemeButton.innerHTML = '<span>Download Theme (CSS)</span>';
downloadThemeButton.classList.add('mynah-button');
downloadThemeButton.classList.add('config-operation');
downloadThemeButton.classList.add('fill-state-always');
downloadThemeButton.addEventListener('click', () => {
download(
'mynah-ui-theme.css',
`:root {
${this.getCssCustomVars()}
}`
);
});
this.buttonsWrapper.insertAdjacentElement('beforeend', uploadThemeConfigFilePicker);
this.buttonsWrapper.insertAdjacentElement('beforeend', uploadThemeConfigButton);
this.buttonsWrapper.insertAdjacentElement('beforeend', downloadThemeConfigButton);
this.buttonsWrapper.insertAdjacentElement('beforeend', downloadThemeButton);
this.buttonsWrapper.insertAdjacentElement('beforeend', resetThemeConfigButton);
this.buildCssValues();
}
private fillInputWrapper = () => {
categories.forEach(category => {
this.inputsWrapper.insertAdjacentHTML(
'beforeend',
`
<div class="mynah-ui-example-input mynah-ui-example-input-category-${category}">
<h1>${category}</h1>
</div>
`
);
});
Object.keys(this.currentConfig).forEach((themeConfigKey: string) => {
const themeConfigItem = this.currentConfig[themeConfigKey] as ConfigItem;
switch (themeConfigItem.type) {
case 'text':
this.inputsWrapper.insertAdjacentElement(
'beforeend',
themeInputText(themeConfigKey, themeConfigItem, value => {
this.currentConfig[themeConfigKey].value = value;
this.buildCssValues();
})
);
break;
case 'measurement':
this.inputsWrapper.insertAdjacentElement(
'beforeend',
themeInputMeasurement(themeConfigKey, themeConfigItem, (value, unit) => {
this.currentConfig[themeConfigKey].value = value;
this.currentConfig[themeConfigKey].unit = unit;
this.buildCssValues();
})
);
break;
case 'color':
this.inputsWrapper.insertAdjacentElement(
'beforeend',
themeInputColor(themeConfigKey, themeConfigItem, (hex, alpha) => {
this.currentConfig[themeConfigKey].value = hex;
this.currentConfig[themeConfigKey].alpha = alpha;
this.buildCssValues();
})
);
break;
}
});
};
private buildCssValues = () => {
(document.querySelector('#custom-style') as HTMLElement).innerHTML = `
html[theme="base-${this.baseThemeType}"]:root {
font-size: 13px;
${this.getCssCustomVars()}
}
`;
};
private getCssCustomVars = (): string =>
Object.keys(this.currentConfig)
.map(configKey => {
const configItem = this.currentConfig[configKey];
let value = configItem.value;
switch (configItem.type) {
case 'measurement':
value = value + configItem.unit;
break;
case 'color':
value = getColorValue(value, configItem.alpha ?? '100');
break;
}
return `${configKey}: ${value};`;
})
.join('\n');
}
const getCleanTitle = (title: string): string => {
return title.replace('--mynah-', '').split('-').join(' ');
};
const getColorValue = (hex: string, alpha: string): string => {
const realAlpha = parseInt(alpha);
if (realAlpha === 100) {
return hex;
} else {
let hexToUse = hex.length === 4 ? hex[0] + hex.slice(1, 4).repeat(2) : hex;
var r = parseInt(hexToUse.slice(1, 3), 16),
g = parseInt(hexToUse.slice(3, 5), 16),
b = parseInt(hexToUse.slice(5, 7), 16);
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + (parseInt(alpha) / 100).toString() + ')';
}
};
const themeInputText = (title: string, configItem: ConfigItem, onValueChange: (value: string) => void): HTMLDivElement => {
const element = document.createElement('div');
element.classList.add('mynah-ui-example-input');
element.classList.add(`mynah-ui-example-input-category-${configItem.category ?? 'other'}`);
element.innerHTML = `
<div class="mynah-ui-example-input-title-wrapper"><h4>${getCleanTitle(title)}</h4>
<span>${configItem.description ?? ''}</span></div>
`;
const inputElement = document.createElement('input');
inputElement.setAttribute('type', 'text');
inputElement.setAttribute('value', configItem.value);
inputElement.addEventListener('change', e => {
onValueChange(inputElement.value);
});
const inputElementWrapper = document.createElement('div');
inputElementWrapper.classList.add('mynah-ui-example-input-wrapper');
inputElementWrapper.insertAdjacentElement('beforeend', inputElement);
element.insertAdjacentElement('beforeend', inputElementWrapper);
return element as HTMLDivElement;
};
const themeInputMeasurement = (title: string, configItem: ConfigItem, onValueChange: (value: string, unit: string) => void): HTMLDivElement => {
const element = document.createElement('div');
element.classList.add('mynah-ui-example-input');
element.classList.add(`mynah-ui-example-input-category-${configItem.category ?? 'other'}`);
element.innerHTML = `
<div class="mynah-ui-example-input-title-wrapper"><h4>${getCleanTitle(title)}</h4>
<span>${configItem.description ?? ''}</span></div>
`;
const selectElement = document.createElement('select');
configItem.units?.forEach(unitKey => {
selectElement.insertAdjacentHTML(
'beforeend',
`
<option ${configItem.unit === unitKey ? 'selected' : ''} value="${unitKey}">${unitKey !== '' ? unitKey : '?'}</option>
`
);
});
selectElement.addEventListener('change', e => {
inputElement.setAttribute('type', selectElement.value === '' ? 'text' : 'number');
onValueChange(inputElement.value, selectElement.value);
});
const inputElement = document.createElement('input');
inputElement.setAttribute('type', configItem.unit === '' ? 'text' : 'number');
inputElement.setAttribute('value', configItem.value);
inputElement.addEventListener('change', e => {
onValueChange(inputElement.value, selectElement.value);
});
const inputElementWrapper = document.createElement('div');
inputElementWrapper.classList.add('mynah-ui-example-input-wrapper');
inputElementWrapper.insertAdjacentElement('beforeend', inputElement);
inputElementWrapper.insertAdjacentElement('beforeend', selectElement);
element.insertAdjacentElement('beforeend', inputElementWrapper);
return element as HTMLDivElement;
};
const themeInputColor = (title: string, configItem: ConfigItem, onValueChange: (hex: string, alpha: string) => void): HTMLDivElement => {
const element = document.createElement('div');
element.classList.add('mynah-ui-example-input');
element.classList.add(`mynah-ui-example-input-category-${configItem.category ?? 'other'}`);
const splittedValue = {
hex: configItem.value,
alpha: configItem.alpha ?? '100',
};
element.innerHTML = `
<div class="mynah-ui-example-input-title-wrapper"><h4>${getCleanTitle(title)}</h4>
<span>${configItem.description ?? ''}</span></div>
`;
const alphaSlider = document.createElement('input');
alphaSlider.setAttribute('type', 'range');
alphaSlider.setAttribute('min', '0');
alphaSlider.setAttribute('max', '100');
alphaSlider.setAttribute('value', splittedValue.alpha ?? '100');
alphaSlider.addEventListener('change', e => {
onValueChange(inputElement.value, alphaSlider.value);
(inputElementLabelWrapper.querySelector('small[type="range"] > b') as HTMLElement).innerHTML = `${alphaSlider.value}%`;
});
const inputElement = document.createElement('input');
inputElement.setAttribute('type', 'color');
inputElement.setAttribute('value', splittedValue.hex);
inputElement.addEventListener('change', e => {
onValueChange(inputElement.value, alphaSlider.value);
(inputElementLabelWrapper.querySelector('small[type="color"] > b') as HTMLElement).innerHTML = inputElement.value;
});
const inputElementLabelWrapper = document.createElement('div');
inputElementLabelWrapper.classList.add('mynah-ui-example-input-wrapper');
inputElementLabelWrapper.insertAdjacentHTML(
'beforeend',
`
<small type="color">Color: <b>${configItem.value}</b></small>
<small type="range">Alpha: <b>${configItem.alpha ?? 100}%</b></small>
`
);
const inputElementWrapper = document.createElement('div');
inputElementWrapper.classList.add('mynah-ui-example-input-wrapper');
inputElementWrapper.insertAdjacentElement('beforeend', inputElement);
inputElementWrapper.insertAdjacentElement('beforeend', alphaSlider);
element.insertAdjacentElement('beforeend', inputElementLabelWrapper);
element.insertAdjacentElement('beforeend', inputElementWrapper);
return element as HTMLDivElement;
};
const download = (filename: string, text: string) => {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};