diff --git a/.build/build-icons.mjs b/.build/build-icons.mjs
new file mode 100644
index 000000000..91fb4501a
--- /dev/null
+++ b/.build/build-icons.mjs
@@ -0,0 +1,107 @@
+import fs from 'fs-extra'
+import path from 'path'
+import { PACKAGES_DIR, readSvgs } from './helpers.mjs'
+import { stringify } from 'svgson'
+import prettier from 'prettier'
+
+import bundleSize from '@atomico/rollup-plugin-sizes'
+import { visualizer } from 'rollup-plugin-visualizer'
+import license from 'rollup-plugin-license'
+import esbuild from 'rollup-plugin-esbuild';
+
+
+/**
+ * Build icons
+ *
+ * @param name
+ * @param componentTemplate
+ * @param indexIconTemplate
+ * @param typeDefinitionsTemplate
+ * @param indexTypeTemplate
+ * @param ext
+ * @param pretty
+ */
+export const buildIcons = ({
+ name,
+ componentTemplate,
+ indexItemTemplate,
+ typeDefinitionsTemplate,
+ indexTypeTemplate,
+ extension = 'js',
+ pretty = true,
+ key = true
+}) => {
+ const DIST_DIR = path.resolve(PACKAGES_DIR, name),
+ svgFiles = readSvgs()
+
+ let index = []
+ let typings = []
+
+ svgFiles.forEach((svgFile, i) => {
+ const children = svgFile.obj.children
+ .map(({
+ name,
+ attributes
+ }, i) => {
+ if (key) {
+ attributes.key = `svg-${i}`
+ }
+
+ return [name, attributes]
+ })
+ .filter((i) => {
+ const [name, attributes] = i
+ return !attributes.d || attributes.d !== 'M0 0h24v24H0z'
+ })
+
+ // process.stdout.write(`Building ${i}/${svgFiles.length}: ${svgFile.name.padEnd(42)}\r`)
+
+ let component = componentTemplate({
+ name: svgFile.name,
+ namePascal: svgFile.namePascal,
+ children,
+ stringify,
+ svg: svgFile
+ })
+
+ const output = pretty ? prettier.format(component, {
+ singleQuote: true,
+ trailingComma: 'all',
+ parser: 'babel'
+ }) : component
+
+ let filePath = path.resolve(DIST_DIR, 'src/icons', `${svgFile.name}.${extension}`)
+ fs.writeFileSync(filePath, output, 'utf-8')
+
+ index.push(indexItemTemplate({
+ name: svgFile.name,
+ namePascal: svgFile.namePascal
+ }))
+
+ typings.push(indexTypeTemplate({
+ name: svgFile.name,
+ namePascal: svgFile.namePascal
+ }))
+ })
+
+ fs.writeFileSync(path.resolve(DIST_DIR, `./src/icons.js`), index.join('\n'), 'utf-8')
+
+ fs.ensureDirSync(path.resolve(DIST_DIR, `./dist/`))
+ fs.writeFileSync(path.resolve(DIST_DIR, `./dist/tabler-${name}.d.ts`), typeDefinitionsTemplate() + '\n' + typings.join('\n'), 'utf-8')
+}
+
+export const getRollupPlugins = (pkg, minify) => {
+ return [
+ esbuild({
+ minify,
+ }),
+ license({
+ banner: `${pkg.name} v${pkg.version} - ${pkg.license}`
+ }),
+ bundleSize(),
+ visualizer({
+ sourcemap: false,
+ filename: `stats/${pkg.name}${minify ? '-min' : ''}.html`
+ })
+ ].filter(Boolean)
+}
diff --git a/.build/changelog-commit.mjs b/.build/changelog-commit.mjs
new file mode 100644
index 000000000..13ecf8be1
--- /dev/null
+++ b/.build/changelog-commit.mjs
@@ -0,0 +1,24 @@
+import cp from 'child_process'
+import { printChangelog } from './helpers.mjs'
+
+cp.exec('git status', function(err, ret) {
+ let newIcons = [], modifiedIcons = [], renamedIcons = []
+
+ ret.replace(/new file:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+ newIcons.push(fileName)
+ })
+
+ ret.replace(/modified:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+ modifiedIcons.push(fileName)
+ })
+
+ ret.replace(/renamed:\s+src\/_icons\/([a-z0-9-]+).svg -> src\/_icons\/([a-z0-9-]+).svg/g, function(m, fileNameBefore, fileNameAfter) {
+ renamedIcons.push([fileNameBefore, fileNameAfter])
+ })
+
+ modifiedIcons = modifiedIcons.filter(function(el) {
+ return newIcons.indexOf(el) < 0
+ })
+
+ printChangelog(newIcons, modifiedIcons, renamedIcons)
+})
diff --git a/.build/changelog-image.mjs b/.build/changelog-image.mjs
new file mode 100644
index 000000000..29e4303fb
--- /dev/null
+++ b/.build/changelog-image.mjs
@@ -0,0 +1,27 @@
+import { generateIconsPreview, getArgvs, getPackageJson, HOME_DIR } from './helpers.mjs'
+import * as fs from 'fs'
+
+const argv = getArgvs(),
+ p = getPackageJson()
+
+const version = argv['new-version'] || `${p.version}`
+
+if (version) {
+ const icons = JSON.parse(fs.readFileSync(`${HOME_DIR}/tags.json`))
+
+ const newIcons = Object
+ .entries(icons)
+ .filter(([name, value]) => {
+ return `${value.version}.0` === version
+ })
+ .map(([name, value]) => {
+ return `./icons/${name}.svg`
+ })
+
+ if (newIcons.length > 0) {
+ generateIconsPreview(newIcons, `.github/tabler-icons-${version}.svg`, {
+ columnsCount: 6,
+ paddingOuter: 24
+ })
+ }
+}
diff --git a/.build/changelog.mjs b/.build/changelog.mjs
new file mode 100644
index 000000000..b087d5f30
--- /dev/null
+++ b/.build/changelog.mjs
@@ -0,0 +1,31 @@
+import cp from 'child_process'
+import { getArgvs, getPackageJson, printChangelog } from './helpers.mjs'
+
+const p = getPackageJson(),
+ argv = getArgvs(),
+ version = argv['latest-version'] || `${p.version}`
+
+if (version) {
+ cp.exec(`git diff ${version} HEAD --name-status src/_icons`, function(err, ret) {
+
+ let newIcons = [], modifiedIcons = [], renamedIcons = []
+
+ ret.replace(/A\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+ newIcons.push(fileName)
+ })
+
+ ret.replace(/M\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+ modifiedIcons.push(fileName)
+ })
+
+ ret.replace(/R[0-9]+\s+src\/_icons\/([a-z0-9-]+)\.svg\s+src\/_icons\/([a-z0-9-]+).svg/g, function(m, fileNameBefore, fileNameAfter) {
+ renamedIcons.push([fileNameBefore, fileNameAfter])
+ })
+
+ modifiedIcons = modifiedIcons.filter(function(el) {
+ return newIcons.indexOf(el) < 0
+ })
+
+ printChangelog(newIcons, modifiedIcons, renamedIcons, true)
+ })
+}
diff --git a/.build/helpers.mjs b/.build/helpers.mjs
new file mode 100644
index 000000000..6da48adc8
--- /dev/null
+++ b/.build/helpers.mjs
@@ -0,0 +1,353 @@
+import fs from 'fs'
+import path, { resolve, basename } from 'path'
+import { fileURLToPath } from 'url'
+import svgParse from 'parse-svg-path'
+import svgpath from 'svgpath'
+import cheerio from 'cheerio';
+import { minify } from 'html-minifier';
+import { parseSync } from 'svgson'
+import { optimize } from 'svgo'
+import cp from 'child_process'
+import minimist from 'minimist'
+
+export const getCurrentDirPath = () => {
+ return path.dirname(fileURLToPath(import.meta.url));
+}
+
+export const HOME_DIR = resolve(getCurrentDirPath(), '..')
+
+export const ICONS_SRC_DIR = resolve(HOME_DIR, 'src/_icons')
+export const ICONS_DIR = resolve(HOME_DIR, 'icons')
+export const PACKAGES_DIR = resolve(HOME_DIR, 'packages')
+
+export const getArgvs = () => {
+ return minimist(process.argv.slice(2))
+}
+
+export const getPackageDir = (packageName) => {
+ return `${PACKAGES_DIR}/${packageName}`
+}
+
+/**
+ * Return project package.json
+ * @returns {any}
+ */
+export const getPackageJson = () => {
+ return JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'package.json'), 'utf-8'))
+}
+
+/**
+ * Reads SVGs from directory
+ *
+ * @param directory
+ * @returns {string[]}
+ */
+export const readSvgDirectory = (directory) => {
+ return fs.readdirSync(directory).filter((file) => path.extname(file) === '.svg')
+}
+
+export const readSvgs = () => {
+ const svgFiles = readSvgDirectory(ICONS_DIR)
+
+ return svgFiles.map(svgFile => {
+ const name = basename(svgFile, '.svg'),
+ namePascal = toPascalCase(`icon ${name}`),
+ contents = readSvg(svgFile, ICONS_DIR).trim(),
+ path = resolve(ICONS_DIR, svgFile),
+ obj = parseSync(contents.replace('', ''));
+
+ return {
+ name,
+ namePascal,
+ contents,
+ obj,
+ path
+ };
+ });
+}
+
+/**
+ * Read SVG
+ *
+ * @param fileName
+ * @param directory
+ * @returns {string}
+ */
+export const readSvg = (fileName, directory) => {
+ return fs.readFileSync(path.join(directory, fileName), 'utf-8')
+}
+
+/**
+ * Create directory if not exists
+ * @param dir
+ */
+export const createDirectory = (dir) => {
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir);
+ }
+};
+
+/**
+ * Get SVG name
+ * @param fileName
+ * @returns {string}
+ */
+export const getSvgName = (fileName) => {
+ return path.basename(fileName, '.svg')
+}
+
+/**
+ * Convert string to CamelCase
+ * @param string
+ * @returns {*}
+ */
+export const toCamelCase = (string) => {
+ return string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())
+}
+
+export const toPascalCase = (string) => {
+ const camelCase = toCamelCase(string);
+
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
+}
+
+
+
+export const addFloats = function(n1, n2) {
+ return Math.round((parseFloat(n1) + parseFloat(n2)) * 1000) / 1000
+}
+
+export const optimizePath = function(path) {
+ let transformed = svgpath(path).rel().round(3).toString()
+
+ return svgParse(transformed).map(function(a) {
+ return a.join(' ')
+ }).join(' ')
+}
+
+export const optimizeSVG = (data) => {
+ return optimize(data, {
+ js2svg: {
+ indent: 2,
+ pretty: true
+ },
+ plugins: [
+ {
+ name: 'preset-default',
+ params: {
+ overrides: {
+ mergePaths: false
+ }
+ }
+ }]
+ }).data
+}
+
+export function buildIconsObject(svgFiles, getSvg) {
+ return svgFiles
+ .map(svgFile => {
+ const name = path.basename(svgFile, '.svg');
+ const svg = getSvg(svgFile);
+ const contents = getSvgContents(svg);
+ return { name, contents };
+ })
+ .reduce((icons, icon) => {
+ icons[icon.name] = icon.contents;
+ return icons;
+ }, {});
+}
+
+function getSvgContents(svg) {
+ const $ = cheerio.load(svg);
+ return minify($('svg').html(), { collapseWhitespace: true });
+}
+
+export const asyncForEach = async (array, callback) => {
+ for (let index = 0; index < array.length; index++) {
+ await callback(array[index], index, array)
+ }
+}
+
+export const createScreenshot = async (filePath) => {
+ await cp.exec(`rsvg-convert -x 2 -y 2 ${filePath} > ${filePath.replace('.svg', '.png')}`)
+ await cp.exec(`rsvg-convert -x 4 -y 4 ${filePath} > ${filePath.replace('.svg', '@2x.png')}`)
+}
+
+export const generateIconsPreview = async function(files, destFile, {
+ columnsCount = 19,
+ paddingOuter = 7,
+ color = '#354052',
+ background = '#fff'
+} = {}) {
+
+ const padding = 20,
+ iconSize = 24
+
+ const iconsCount = files.length,
+ rowsCount = Math.ceil(iconsCount / columnsCount),
+ width = columnsCount * (iconSize + padding) + 2 * paddingOuter - padding,
+ height = rowsCount * (iconSize + padding) + 2 * paddingOuter - padding
+
+ let svgContentSymbols = '',
+ svgContentIcons = '',
+ x = paddingOuter,
+ y = paddingOuter
+
+ files.forEach(function(file, i) {
+ let name = path.basename(file, '.svg')
+
+ let svgFile = fs.readFileSync(file),
+ svgFileContent = svgFile.toString()
+
+ svgFileContent = svgFileContent.replace('