Migrate core JavaScript to TypeScript (#2582)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Paweł Kuna
2026-01-09 21:49:22 +01:00
committed by GitHub
parent a24d5cab13
commit 857988dd44
51 changed files with 753 additions and 500 deletions

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env node
'use strict'
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'
import { sync } from 'glob';
import * as prettier from "prettier";
const __dirname = dirname(fileURLToPath(import.meta.url))
const docs = sync(join(__dirname, '..', 'docs', '**', '*.md'))
async function formatHTML(htmlString) {
try {
const formattedHtml = await prettier.format(htmlString, {
parser: "html",
printWidth: 100,
});
return formattedHtml;
} catch (error) {
console.error("Error formatting HTML:", error);
return htmlString; // Return original in case of an error
}
}
async function replaceAsync(str, regex, asyncFn) {
const matches = [...str.matchAll(regex)];
const replacements = await Promise.all(
matches.map(async (match) => asyncFn(...match))
);
let result = str;
matches.forEach((match, i) => {
result = result.replace(match[0], replacements[i]);
});
return result;
}
for (const file of docs) {
const oldContent = readFileSync(file, 'utf8')
// get codeblocks from markdown
const content = await replaceAsync(oldContent, /(```([a-z0-9]+).*?\n)(.*?)(```)/gs, async (m, m1, m2, m3, m4) => {
if (m2 === 'html') {
m3 = await formatHTML(m3);
// remove empty lines
m3 = m3.replace(/^\s*[\r\n]/gm, '');
return m1 + m3.trim() + "\n" + m4;
}
return m.trim();
})
if (content !== oldContent) {
writeFileSync(file, content, 'utf8')
console.log(`Reformatted ${file}`)
}
}

79
.build/reformat-mdx.ts Normal file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { sync } from 'glob'
import * as prettier from 'prettier'
const __dirname = dirname(fileURLToPath(import.meta.url))
const docs: string[] = sync(join(__dirname, '..', 'docs', '**', '*.md'))
async function formatHTML(htmlString: string): Promise<string> {
try {
const formattedHtml = await prettier.format(htmlString, {
parser: 'html',
printWidth: 100,
})
return formattedHtml
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error('Error formatting HTML:', errorMessage)
return htmlString // Return original in case of an error
}
}
async function replaceAsync(
str: string,
regex: RegExp,
asyncFn: (...args: string[]) => Promise<string>
): Promise<string> {
const matches = [...str.matchAll(regex)]
const replacements = await Promise.all(
matches.map(async (match: RegExpMatchArray) => asyncFn(...match))
)
let result = str
matches.forEach((match: RegExpMatchArray, i: number) => {
result = result.replace(match[0], replacements[i])
})
return result
}
async function processFiles(): Promise<void> {
for (const file of docs) {
const oldContent = readFileSync(file, 'utf8')
// get codeblocks from markdown
const content = await replaceAsync(
oldContent,
/(```([a-z0-9]+).*?\n)(.*?)(```)/gs,
async (m: string, m1: string, m2: string, m3: string, m4: string) => {
if (m2 === 'html') {
let formattedHtml = await formatHTML(m3)
// remove empty lines
formattedHtml = formattedHtml.replace(/^\s*[\r\n]/gm, '')
return m1 + formattedHtml.trim() + '\n' + m4
}
return m.trim()
}
)
if (content !== oldContent) {
writeFileSync(file, content, 'utf8')
console.log(`Reformatted ${file}`)
}
}
}
processFiles().catch((error) => {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error('Error processing files:', errorMessage)
process.exit(1)
})

View File

@@ -1,60 +0,0 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
/**
* Creates a Vite configuration for building libraries
* @param {Object} options - Configuration options
* @param {string} options.entry - Path to the entry file
* @param {string|undefined} options.name - Library name (undefined for ESM)
* @param {string|Function} options.fileName - Output file name or function returning it
* @param {string[]} options.formats - Output formats (e.g., ['es'], ['umd'], ['es', 'umd'])
* @param {string} options.outDir - Output directory path
* @param {string|undefined} options.banner - Optional banner text to add to output
* @returns {import('vite').UserConfig} Vite configuration
*/
export function createViteConfig({
entry,
name,
fileName,
formats,
outDir,
banner
}) {
const rollupOutput = {
generatedCode: {
constBindings: true
}
}
// Add banner if provided
if (banner) {
rollupOutput.banner = banner
}
return defineConfig({
build: {
lib: {
entry: path.resolve(entry),
name: name,
fileName: typeof fileName === 'function' ? fileName : () => fileName,
formats: formats
},
outDir: path.resolve(outDir),
emptyOutDir: false,
sourcemap: true,
rollupOptions: {
output: rollupOutput
},
target: 'es2015',
minify: false // Minification is done by terser in a separate step
},
define: {
'process.env.NODE_ENV': '"production"'
},
esbuild: {
target: 'es2015'
}
})
}

View File

@@ -0,0 +1,76 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig, type UserConfig } from 'vite'
interface CreateViteConfigOptions {
entry: string
name?: string
fileName: string | ((format: string) => string)
formats: ('es' | 'umd' | 'iife' | 'cjs')[]
outDir: string
banner?: string
minify?: boolean | 'esbuild'
}
/**
* Creates a Vite configuration for building libraries
*/
export function createViteConfig({
entry,
name,
fileName,
formats,
outDir,
banner,
minify = false
}: CreateViteConfigOptions): UserConfig {
const rollupOutput: {
generatedCode: {
constBindings: boolean
}
banner?: string
} = {
generatedCode: {
constBindings: true
}
}
// Add banner if provided
if (banner) {
rollupOutput.banner = banner
}
const config: UserConfig = {
build: {
lib: {
entry: path.resolve(entry),
name: name,
fileName: typeof fileName === 'function' ? fileName : () => fileName,
formats: formats
},
outDir: path.resolve(outDir),
emptyOutDir: false,
sourcemap: true,
rollupOptions: {
output: rollupOutput
},
target: 'es2015',
minify: minify
},
define: {
'process.env.NODE_ENV': '"production"'
},
esbuild: {
target: 'es2015',
tsconfigRaw: {
compilerOptions: {
module: 'ES2020',
target: 'ES2015'
}
}
}
}
return defineConfig(config)
}

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env node
import AdmZip from 'adm-zip';
import path from 'path';
import { fileURLToPath } from 'url';
import { readFileSync } from 'fs';
// Get __dirname in ESM
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkg = JSON.parse(
readFileSync(path.join(__dirname, '../core', 'package.json'), 'utf8')
)
// Create zip instance and add folder
const zip = new AdmZip();
zip.addLocalFolder(path.join(__dirname, '../preview/dist'), 'dashboard');
zip.addLocalFile(path.join(__dirname, '../preview/static', 'og.png'), '.', 'preview.png');
zip.addFile("documentation.url", Buffer.from("[InternetShortcut]\nURL = https://tabler.io/docs"));
// Folder to zip and output path
const outputZipPath = path.join(__dirname, '../packages-zip', `tabler-${pkg.version}.zip`);
// Write the zip file
zip.writeZip(outputZipPath);
console.log(`Zipped folder to ${outputZipPath}`);

46
.build/zip-package.ts Normal file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
import AdmZip from 'adm-zip'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
// Get __dirname in ESM
const __dirname = path.dirname(fileURLToPath(import.meta.url))
interface PackageJson {
version: string
[key: string]: unknown
}
const pkg: PackageJson = JSON.parse(
readFileSync(path.join(__dirname, '../core', 'package.json'), 'utf8')
)
// Create zip instance and add folder
const zip = new AdmZip()
zip.addLocalFolder(path.join(__dirname, '../preview/dist'), 'dashboard')
zip.addLocalFile(
path.join(__dirname, '../preview/static', 'og.png'),
'.',
'preview.png'
)
zip.addFile(
'documentation.url',
Buffer.from('[InternetShortcut]\nURL = https://tabler.io/docs')
)
// Folder to zip and output path
const outputZipPath = path.join(
__dirname,
'../packages-zip',
`tabler-${pkg.version}.zip`
)
// Write the zip file
zip.writeZip(outputZipPath)
console.log(`Zipped folder to ${outputZipPath}`)

45
.github/workflows/type-check.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Type Check
on:
pull_request: null
push:
branches:
- main
- dev
env:
NODE: 20
permissions:
contents: read
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v6
- name: Cache turbo build setup
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Install PNPM
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "${{ env.NODE }}"
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm install
- name: Run type-check
run: pnpm run type-check

4
.gitignore vendored
View File

@@ -38,3 +38,7 @@ dist/
packages-zip/
.env
sri.json
# TypeScript
*.tsbuildinfo
.tsbuildinfo

View File

@@ -1,18 +1,20 @@
#!/usr/bin/env node
'use strict'
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { readFileSync, writeFileSync } from 'node:fs'
import { join, dirname, basename } from 'node:path'
import { fileURLToPath } from 'node:url'
import { sync } from 'glob';
import banner from '../../shared/banner/index.mjs';
import { sync } from 'glob'
import banner from '../../shared/banner/index.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const styles = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
const styles: string[] = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
const plugins = {
interface Plugins {
[key: string]: string
}
const plugins: Plugins = {
'tabler-flags': 'Flags',
'tabler-flags.rtl': 'Flags RTL',
'tabler-marketing': 'Marketing',
@@ -25,22 +27,24 @@ const plugins = {
'tabler-vendors.rtl': 'Vendors RTL',
}
styles.forEach((file, i) => {
styles.forEach((file: string) => {
const content = readFileSync(file, 'utf8')
const filename = basename(file)
const pluginKey = Object.keys(plugins).find(plugin => filename.includes(plugin))
const plugin = plugins[pluginKey]
const pluginKey = Object.keys(plugins).find((plugin: string) => filename.includes(plugin))
const plugin = pluginKey ? plugins[pluginKey] : undefined
const regex = /^(@charset ['"][a-zA-Z0-9-]+['"];?)\n?/i
let newContent = ''
const bannerText = banner(plugin)
if (content.match(regex)) {
newContent = content.replace(regex, (m, m1) => {
return `${m1}\n${banner(plugin)}\n`
newContent = content.replace(regex, (m: string, m1: string) => {
return `${m1}\n${bannerText}\n`
})
} else {
newContent = `${banner(plugin)}\n${content}`
newContent = `${bannerText}\n${content}`
}
writeFileSync(file, newContent, 'utf8')
})

View File

@@ -1,82 +0,0 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Get __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// File paths (relative to core/.build directory)
const bootstrapPath = path.join(__dirname, '../node_modules/bootstrap/scss/_variables.scss');
const tablerPath = path.join(__dirname, '../scss/_variables.scss');
// Function to extract variable names from SCSS file
function extractVariables(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const variables = new Set();
// Regex to find SCSS variables
// Looks for patterns like: $variable-name: value
// Includes variables in maps and lists
const variableRegex = /\$([a-zA-Z0-9_-]+)\s*[:=]/g;
let match;
while ((match = variableRegex.exec(content)) !== null) {
const varName = match[1];
variables.add(varName);
}
return variables;
}
// Main function
function compareVariables() {
console.log('Analyzing Bootstrap variables...');
const bootstrapVars = extractVariables(bootstrapPath);
console.log(`Found ${bootstrapVars.size} variables in Bootstrap\n`);
console.log('Analyzing Tabler variables...');
const tablerVars = extractVariables(tablerPath);
console.log(`Found ${tablerVars.size} variables in Tabler\n`);
// Find variables that are in Bootstrap but not in Tabler
const missingInTabler = [];
for (const varName of bootstrapVars) {
if (!tablerVars.has(varName)) {
missingInTabler.push(varName);
}
}
// Sort alphabetically
missingInTabler.sort();
console.log('='.repeat(60));
console.log(`Variables in Bootstrap that are missing in Tabler: ${missingInTabler.length}`);
console.log('='.repeat(60));
if (missingInTabler.length === 0) {
console.log('All Bootstrap variables are present in Tabler!');
} else {
console.log('\nList of missing variables:\n');
missingInTabler.forEach((varName, index) => {
console.log(`${(index + 1).toString().padStart(4)}. $${varName}`);
});
}
// Optionally: show statistics
console.log('\n' + '='.repeat(60));
console.log('Statistics:');
console.log(` Bootstrap: ${bootstrapVars.size} variables`);
console.log(` Tabler: ${tablerVars.size} variables`);
console.log(` Missing: ${missingInTabler.length} variables`);
console.log(` Coverage: ${((1 - missingInTabler.length / bootstrapVars.size) * 100).toFixed(1)}%`);
console.log('='.repeat(60));
}
// Run analysis
try {
compareVariables();
} catch (error) {
console.error('Error during analysis:', error.message);
process.exit(1);
}

View File

@@ -0,0 +1,84 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// Get __dirname in ES modules
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// File paths (relative to core/.build directory)
const bootstrapPath = path.join(__dirname, '../node_modules/bootstrap/scss/_variables.scss')
const tablerPath = path.join(__dirname, '../scss/_variables.scss')
// Function to extract variable names from SCSS file
function extractVariables(filePath: string): Set<string> {
const content = readFileSync(filePath, 'utf8')
const variables = new Set<string>()
// Regex to find SCSS variables
// Looks for patterns like: $variable-name: value
// Includes variables in maps and lists
const variableRegex = /\$([a-zA-Z0-9_-]+)\s*[:=]/g
let match: RegExpExecArray | null
while ((match = variableRegex.exec(content)) !== null) {
const varName = match[1]
variables.add(varName)
}
return variables
}
// Main function
function compareVariables(): void {
console.log('Analyzing Bootstrap variables...')
const bootstrapVars = extractVariables(bootstrapPath)
console.log(`Found ${bootstrapVars.size} variables in Bootstrap\n`)
console.log('Analyzing Tabler variables...')
const tablerVars = extractVariables(tablerPath)
console.log(`Found ${tablerVars.size} variables in Tabler\n`)
// Find variables that are in Bootstrap but not in Tabler
const missingInTabler: string[] = []
for (const varName of bootstrapVars) {
if (!tablerVars.has(varName)) {
missingInTabler.push(varName)
}
}
// Sort alphabetically
missingInTabler.sort()
console.log('='.repeat(60))
console.log(`Variables in Bootstrap that are missing in Tabler: ${missingInTabler.length}`)
console.log('='.repeat(60))
if (missingInTabler.length === 0) {
console.log('All Bootstrap variables are present in Tabler!')
} else {
console.log('\nList of missing variables:\n')
missingInTabler.forEach((varName: string, index: number) => {
console.log(`${(index + 1).toString().padStart(4)}. $${varName}`)
})
}
// Optionally: show statistics
console.log('\n' + '='.repeat(60))
console.log('Statistics:')
console.log(` Bootstrap: ${bootstrapVars.size} variables`)
console.log(` Tabler: ${tablerVars.size} variables`)
console.log(` Missing: ${missingInTabler.length} variables`)
console.log(` Coverage: ${((1 - missingInTabler.length / bootstrapVars.size) * 100).toFixed(1)}%`)
console.log('='.repeat(60))
}
// Run analysis
try {
compareVariables()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error('Error during analysis:', errorMessage)
process.exit(1)
}

View File

@@ -1,19 +1,30 @@
#!/usr/bin/env node
'use strict'
import { existsSync, mkdirSync, lstatSync } from 'fs'
import { existsSync, mkdirSync } from 'node:fs'
import { emptyDirSync, copySync } from 'fs-extra/esm'
import libs from '../libs.json' with { type: 'json' }
import { fileURLToPath } from 'url'
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'
import { join, dirname } from 'node:path'
const __dirname = dirname(fileURLToPath(import.meta.url))
interface LibConfig {
npm?: string
js?: string[]
css?: string[]
head?: boolean
}
interface Libs {
[key: string]: LibConfig
}
const libsData = libs as Libs
emptyDirSync(join(__dirname, '..', 'dist/libs'))
for(const name in libs) {
const { npm } = libs[name]
for (const name in libsData) {
const { npm } = libsData[name]
if (npm) {
const from = join(__dirname, '..', `node_modules/${npm}`)
@@ -31,3 +42,4 @@ for(const name in libs) {
console.log(`Successfully copied ${npm}`)
}
}

View File

@@ -1,10 +1,18 @@
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
import * as crypto from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const configFile = path.join(__dirname, '../../shared/data/sri.json')
const files = [
interface FileConfig {
file: string
configPropertyName: string
}
const files: FileConfig[] = [
{
file: 'dist/css/tabler.min.css',
configPropertyName: 'css'
@@ -79,13 +87,13 @@ const files = [
},
]
function generateSRI() {
const sriData = {}
function generateSRI(): void {
const sriData: Record<string, string> = {}
for (const { file, configPropertyName } of files) {
try {
const filePath = path.join(__dirname, '..', file)
const data = fs.readFileSync(filePath, 'utf8')
const data = readFileSync(filePath, 'utf8')
const algorithm = 'sha384'
const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64')
@@ -95,12 +103,13 @@ function generateSRI() {
sriData[configPropertyName] = integrity
} catch (error) {
console.error(`Error processing ${file}:`, error.message)
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error processing ${file}:`, errorMessage)
throw error
}
}
fs.writeFileSync(configFile, JSON.stringify(sriData, null, 2) + '\n', 'utf8')
writeFileSync(configFile, JSON.stringify(sriData, null, 2) + '\n', 'utf8')
}
try {
@@ -109,3 +118,4 @@ try {
console.error('Failed to generate SRI:', error)
process.exit(1)
}

View File

@@ -1,10 +1,8 @@
#!/usr/bin/env node
'use strict'
import { existsSync, mkdirSync } from 'fs'
import { existsSync, mkdirSync } from 'node:fs'
import { copySync } from 'fs-extra/esm'
import { fileURLToPath } from 'url'
import { fileURLToPath } from 'node:url'
import { join, dirname } from 'node:path'
const __dirname = dirname(fileURLToPath(import.meta.url))

View File

@@ -1,7 +1,8 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { createViteConfig } from '../../.build/vite.config.helper.mjs'
import { existsSync } from 'node:fs'
import { createViteConfig } from '../../.build/vite.config.helper'
import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -9,18 +10,24 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const ESM = process.env.ESM === 'true'
const THEME = process.env.THEME === 'true'
const MINIFY = process.env.MINIFY === 'true'
const destinationFile = `tabler${THEME ? '-theme' : ''}${ESM ? '.esm' : ''}`
const entryFile = `tabler${THEME ? '-theme' : ''}`
const libraryName = `tabler${THEME ? '-theme' : ''}`
const bannerText = getBanner()
// Try .ts first, fallback to .js for gradual migration
const entryPath = path.resolve(__dirname, `../js/${entryFile}`)
const entry = existsSync(`${entryPath}.ts`) ? `${entryPath}.ts` : `${entryPath}.js`
export default createViteConfig({
entry: path.resolve(__dirname, `../js/${entryFile}.js`),
entry: entry,
name: ESM ? undefined : libraryName,
fileName: () => `${destinationFile}.js`,
fileName: () => MINIFY ? `${destinationFile}.min.js` : `${destinationFile}.js`,
formats: [ESM ? 'es' : 'umd'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: bannerText
banner: bannerText,
minify: MINIFY ? true : false
})

View File

@@ -1,8 +0,0 @@
// Autosize plugin
const elements = document.querySelectorAll('[data-bs-toggle="autosize"]')
if (elements.length) {
elements.forEach(function (element) {
window.autosize && window.autosize(element)
})
}

10
core/js/src/autosize.ts Normal file
View File

@@ -0,0 +1,10 @@
// Autosize plugin
const autosizeElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-bs-toggle="autosize"]')
if (autosizeElements.length) {
autosizeElements.forEach(function (element: HTMLElement) {
if (window.autosize) {
window.autosize(element)
}
})
}

View File

@@ -1,17 +1,19 @@
const elements = document.querySelectorAll('[data-countup]')
const countupElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-countup]')
if (elements.length) {
elements.forEach(function (element) {
let options = {}
if (countupElements.length) {
countupElements.forEach(function (element: HTMLElement) {
let options: Record<string, any> = {}
try {
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')) : {}
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')!) : {}
options = Object.assign(
{
enableScrollSpy: true,
},
dataOptions,
)
} catch (error) {}
} catch (error) {
// ignore invalid JSON
}
const value = parseInt(element.innerHTML, 10)

View File

@@ -3,9 +3,9 @@ import { Dropdown } from './bootstrap'
/*
Core dropdowns
*/
let dropdownTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="dropdown"]'))
dropdownTriggerList.map(function (dropdownTriggerEl) {
let options = {
const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
const options = {
boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.querySelector('.btn') : 'clippingParents',
}
return new Dropdown(dropdownTriggerEl, options)

14
core/js/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
// Global type declarations for window properties
interface Window {
autosize?: (element: HTMLElement | HTMLTextAreaElement) => void
countUp?: {
CountUp: new (target: HTMLElement, endVal: number, options?: any) => {
error: boolean
start: () => void
}
}
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => any
Sortable?: new (element: HTMLElement, options?: any) => any
}

View File

@@ -1,7 +1,7 @@
// Input mask plugin
var maskElementList = [].slice.call(document.querySelectorAll('[data-mask]'))
maskElementList.map(function (maskEl) {
const maskElementList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-mask]'))
maskElementList.map(function (maskEl: HTMLElement) {
window.IMask &&
new window.IMask(maskEl, {
mask: maskEl.dataset.mask,

View File

@@ -3,9 +3,9 @@ import { Popover } from './bootstrap'
/*
Core popovers
*/
let popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
popoverTriggerList.map(function (popoverTriggerEl) {
let options = {
const popoverTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="popover"]'))
popoverTriggerList.map(function (popoverTriggerEl: HTMLElement) {
const options = {
delay: { show: 50, hide: 50 },
html: popoverTriggerEl.getAttribute('data-bs-html') === 'true',
placement: popoverTriggerEl.getAttribute('data-bs-placement') ?? 'auto',

View File

@@ -2,11 +2,11 @@
// Initializes Sortable on elements marked with [data-sortable]
// Allows options via JSON in data attribute: data-sortable='{"animation":150}'
const sortableElements = document.querySelectorAll('[data-sortable]')
const sortableElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-sortable]')
if (sortableElements.length) {
sortableElements.forEach(function (element) {
let options = {}
sortableElements.forEach(function (element: HTMLElement) {
let options: Record<string, any> = {}
try {
const rawOptions = element.getAttribute('data-sortable')

View File

@@ -1,11 +0,0 @@
/*
Switch icons
*/
let switchesTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="switch-icon"]'))
switchesTriggerList.map(function (switchTriggerEl) {
switchTriggerEl.addEventListener('click', (e) => {
e.stopPropagation()
switchTriggerEl.classList.toggle('active')
})
})

View File

@@ -0,0 +1,11 @@
/*
Switch icons
*/
const switchesTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="switch-icon"]'))
switchesTriggerList.map(function (switchTriggerEl: HTMLElement) {
switchTriggerEl.addEventListener('click', (e: MouseEvent) => {
e.stopPropagation()
switchTriggerEl.classList.toggle('active')
})
})

View File

@@ -1,16 +0,0 @@
import { Tab } from './bootstrap'
export const EnableActivationTabsFromLocationHash = () => {
const locationHash = window.location.hash
if (locationHash) {
const tabsList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tab"]'))
const matchedTabs = tabsList.filter((tab) => tab.hash === locationHash)
matchedTabs.map((tab) => {
new Tab(tab).show()
})
}
}
EnableActivationTabsFromLocationHash()

16
core/js/src/tab.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Tab } from './bootstrap'
export const EnableActivationTabsFromLocationHash = (): void => {
const locationHash: string = window.location.hash
if (locationHash) {
const tabsList: HTMLAnchorElement[] = [].slice.call(document.querySelectorAll<HTMLAnchorElement>('[data-bs-toggle="tab"]'))
const matchedTabs = tabsList.filter((tab: HTMLAnchorElement) => tab.hash === locationHash)
matchedTabs.map((tab: HTMLAnchorElement) => {
new Tab(tab).show()
})
}
}
EnableActivationTabsFromLocationHash()

View File

@@ -1,12 +1,12 @@
export const prefix = 'tblr-'
export const prefix: string = 'tblr-'
export const hexToRgba = (hex, opacity) => {
export const hexToRgba = (hex: string, opacity: number): string | null => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})` : null
}
export const getColor = (color, opacity = 1) => {
export const getColor = (color: string, opacity: number = 1): string | null => {
const c = getComputedStyle(document.body).getPropertyValue(`--${prefix}${color}`).trim()
if (opacity !== 1) {

View File

@@ -1,17 +0,0 @@
import { Toast } from './bootstrap'
/*
Toasts
*/
let toastsTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="toast"]'))
toastsTriggerList.map(function (toastTriggerEl) {
if (!toastTriggerEl.hasAttribute('data-bs-target')) {
return
}
const toastEl = new Toast(toastTriggerEl.getAttribute('data-bs-target'))
toastTriggerEl.addEventListener('click', () => {
toastEl.show()
})
})

18
core/js/src/toast.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Toast } from './bootstrap'
/*
Toasts
*/
const toastsTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="toast"]'))
toastsTriggerList.map(function (toastTriggerEl: HTMLElement) {
const target = toastTriggerEl.getAttribute('data-bs-target')
if (target === null) {
return
}
const toastEl = new Toast(target)
toastTriggerEl.addEventListener('click', () => {
toastEl.show()
})
})

View File

@@ -1,8 +1,8 @@
import { Tooltip } from './bootstrap'
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
let options = {
const tooltipTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl: HTMLElement) {
const options = {
delay: { show: 50, hide: 50 },
html: tooltipTriggerEl.getAttribute('data-bs-html') === 'true',
placement: tooltipTriggerEl.getAttribute('data-bs-placement') ?? 'auto',

View File

@@ -3,7 +3,15 @@
* to ensure we switch to the chosen dark/light theme as fast as possible.
* This will prevent any flashes of the light theme (default) before switching.
*/
const themeConfig = {
interface ThemeConfig {
'theme': string
'theme-base': string
'theme-font': string
'theme-primary': string
'theme-radius': string
}
const themeConfig: ThemeConfig = {
'theme': 'light',
'theme-base': 'gray',
'theme-font': 'sans-serif',
@@ -12,22 +20,22 @@ const themeConfig = {
}
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
})
for (const key in themeConfig) {
const param = params[key]
let selectedValue
let selectedValue: string
if (!!param) {
localStorage.setItem('tabler-' + key, param)
selectedValue = param
} else {
const storedTheme = localStorage.getItem('tabler-' + key)
selectedValue = storedTheme ? storedTheme : themeConfig[key]
selectedValue = storedTheme ? storedTheme : themeConfig[key as keyof ThemeConfig]
}
if (selectedValue !== themeConfig[key]) {
if (selectedValue !== themeConfig[key as keyof ThemeConfig]) {
document.documentElement.setAttribute('data-bs-' + key, selectedValue)
} else {
document.documentElement.removeAttribute('data-bs-' + key)

View File

@@ -9,7 +9,7 @@ import './src/tab'
import './src/toast'
import './src/sortable'
// Re-export everything from bootstrap.js (single source of truth)
// Re-export everything from bootstrap.ts (single source of truth)
export * from './src/bootstrap'
// Re-export tabler namespace

View File

@@ -8,9 +8,9 @@
"build": "pnpm run clean && pnpm run build-assets && pnpm run copy && pnpm run generate-sri",
"build-assets": "concurrently \"pnpm run css\" \"pnpm run js\"",
"clean": "shx rm -rf dist demo",
"css": "pnpm run css-compile && pnpm run css-prefix && pnpm run css-rtl && pnpm run css-minify && pnpm run css-banner",
"css-compile": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/css/",
"css-banner": "node .build/add-banner.mjs",
"css": "pnpm run css-build && pnpm run css-prefix && pnpm run css-rtl && pnpm run css-minify && pnpm run css-banner",
"css-build": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/css/",
"css-banner": "tsx .build/add-banner.ts",
"css-prefix": "postcss --config .build/postcss.config.mjs --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"",
"css-rtl": "cross-env NODE_ENV=RTL postcss --config .build/postcss.config.mjs --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"",
"css-minify": "concurrently \"pnpm run css-minify-main\" \"pnpm run css-minify-rtl\"",
@@ -18,29 +18,30 @@
"css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"",
"css-lint": "pnpm run css-lint-variables",
"css-lint-variables": "find-unused-sass-variables scss/ node_modules/bootstrap/scss/",
"js": "pnpm run js-compile && pnpm run js-minify",
"js-compile": "concurrently \"pnpm run js-compile-standalone\" \"pnpm run js-compile-standalone-esm\" \"pnpm run js-compile-theme\" \"pnpm run js-compile-theme-esm\"",
"js-compile-theme-esm": "cross-env THEME=true ESM=true vite build --config .build/vite.config.mjs",
"js-compile-theme": "cross-env THEME=true vite build --config .build/vite.config.mjs",
"js-compile-standalone": "vite build --config .build/vite.config.mjs",
"js-compile-standalone-esm": "cross-env ESM=true vite build --config .build/vite.config.mjs",
"js-minify": "concurrently \"pnpm run js-minify-standalone\" \"pnpm run js-minify-standalone-esm\" \"pnpm run js-minify-theme\" \"pnpm run js-minify-theme-esm\"",
"js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.js.map,includeSources,url=tabler.min.js.map\" --output dist/js/tabler.min.js dist/js/tabler.js",
"js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.esm.js.map,includeSources,url=tabler.esm.min.js.map\" --output dist/js/tabler.esm.min.js dist/js/tabler.esm.js",
"js-minify-theme": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.js.map,includeSources,url=tabler-theme.min.js.map\" --output dist/js/tabler-theme.min.js dist/js/tabler-theme.js",
"js-minify-theme-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.esm.js.map,includeSources,url=tabler-theme.esm.min.js.map\" --output dist/js/tabler-theme.esm.min.js dist/js/tabler-theme.esm.js",
"js": "pnpm run js-build && pnpm run js-build-min",
"js-build": "concurrently \"pnpm run js-build-standalone\" \"pnpm run js-build-standalone-esm\" \"pnpm run js-build-theme\" \"pnpm run js-build-theme-esm\"",
"js-build-theme-esm": "cross-env THEME=true ESM=true vite build --config .build/vite.config.ts",
"js-build-theme": "cross-env THEME=true vite build --config .build/vite.config.ts",
"js-build-standalone": "vite build --config .build/vite.config.ts",
"js-build-standalone-esm": "cross-env ESM=true vite build --config .build/vite.config.ts",
"js-build-min": "concurrently \"pnpm run js-build-min-standalone\" \"pnpm run js-build-min-standalone-esm\" \"pnpm run js-build-min-theme\" \"pnpm run js-build-min-theme-esm\"",
"js-build-min-standalone": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
"js-build-min-standalone-esm": "cross-env MINIFY=true ESM=true vite build --config .build/vite.config.ts",
"js-build-min-theme": "cross-env MINIFY=true THEME=true vite build --config .build/vite.config.ts",
"js-build-min-theme-esm": "cross-env MINIFY=true THEME=true ESM=true vite build --config .build/vite.config.ts",
"copy": "concurrently \"pnpm run copy-img\" \"pnpm run copy-libs\" \"pnpm run copy-fonts\"",
"copy-img": "shx mkdir -p dist/img && shx cp -rf img/* dist/img",
"copy-libs": "node .build/copy-libs.mjs",
"copy-libs": "tsx .build/copy-libs.ts",
"copy-fonts": "shx mkdir -p dist/fonts && shx cp -rf fonts/* dist/fonts",
"import-fonts": "node .build/import-fonts.mjs",
"import-fonts": "tsx .build/import-fonts.ts",
"watch": "concurrently \"pnpm run watch-css\" \"pnpm run watch-js\"",
"watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css-compile && pnpm run css-prefix\"",
"watch-js": "nodemon --watch js/ --ext js --exec \"pnpm run js-compile\"",
"watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css-build && pnpm run css-prefix\"",
"watch-js": "nodemon --watch js/ --ext ts,js --exec \"pnpm run js-build\"",
"bundlewatch": "bundlewatch",
"generate-sri": "node .build/generate-sri.js",
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.js\" --cache",
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.js\" --cache"
"generate-sri": "tsx .build/generate-sri.ts",
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
"type-check": "tsc --noEmit"
},
"repository": {
"type": "git",
@@ -70,7 +71,7 @@
"files": [
"docs/**/*",
"dist/**/*",
"js/**/*.{js,map}",
"js/**/*.{ts,js,map}",
"img/**/*.{svg}",
"scss/**/*.scss",
"libs.json"
@@ -156,6 +157,7 @@
"devDependencies": {
"@hotwired/turbo": "^8.0.18",
"@melloware/coloris": "^0.25.0",
"@types/node": "^22.0.0",
"apexcharts": "^5.3.6",
"autosize": "^6.0.1",
"choices.js": "^11.1.0",
@@ -179,6 +181,7 @@
"star-rating.js": "^4.3.1",
"tom-select": "^2.4.3",
"typed.js": "^2.1.0",
"typescript": "^5.9.3",
"driver.js": "^1.0.0"
},
"directories": {

30
core/tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2015",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"allowJs": true
},
"include": [
"js/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}

View File

@@ -1,15 +0,0 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createViteConfig } from '../../.build/vite.config.helper.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default createViteConfig({
entry: path.resolve(__dirname, '../js/docs.js'),
name: 'docs',
fileName: () => 'docs.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: undefined
})

View File

@@ -0,0 +1,25 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { existsSync } from 'node:fs'
import { createViteConfig } from '../../.build/vite.config.helper'
import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const MINIFY = process.env.MINIFY === 'true'
// Try .ts first, fallback to .js for gradual migration
const entryPath = path.resolve(__dirname, '../js/docs')
const entry = existsSync(`${entryPath}.ts`) ? `${entryPath}.ts` : `${entryPath}.js`
export default createViteConfig({
entry: entry,
name: 'docs',
fileName: () => MINIFY ? 'docs.min.js' : 'docs.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: undefined,
minify: MINIFY
})

View File

@@ -1,8 +1,9 @@
import docsearch from '@docsearch/js';
import docsearch from '@docsearch/js'
docsearch({
container: '#docsearch',
appId: "NE1EGTYLS9",
indexName: "tabler",
apiKey: "016353235ef1dd32a6c392be0e939058",
});
})

View File

@@ -7,12 +7,12 @@
"build": "pnpm run clean && pnpm run build-assets && pnpm run html",
"build-assets": "concurrently \"pnpm run js\" \"pnpm run css\"",
"html": "eleventy",
"js": "pnpm run js-compile && pnpm run js-minify",
"js-compile": "vite build --config .build/vite.config.mjs",
"js-minify": "pnpm run js-minify-docs",
"js-minify-docs": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/docs.js.map,includeSources,url=docs.min.js.map\" --output dist/js/docs.min.js dist/js/docs.js",
"css": "pnpm run css-compile && pnpm run css-prefix && pnpm run css-minify",
"css-compile": "sass scss/:dist/css/ --no-source-map --load-path=./node_modules",
"js": "pnpm run js-build && pnpm run js-build-min",
"js-build": "vite build --config .build/vite.config.ts",
"js-build-min": "pnpm run js-build-min-docs",
"js-build-min-docs": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
"css": "pnpm run css-build && pnpm run css-prefix && pnpm run css-minify",
"css-build": "sass scss/:dist/css/ --no-source-map --load-path=./node_modules",
"css-prefix": "postcss --config .build/postcss.config.mjs --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"",
"css-minify": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"",
"dev": "pnpm run clean && pnpm run watch",

View File

@@ -7,13 +7,14 @@
"dev": "turbo dev",
"clean": "turbo clean",
"bundlewatch": "turbo bundlewatch",
"type-check": "turbo type-check",
"version": "changeset version",
"publish": "changeset publish",
"playwright": "pnpm run build && pnpm run vt",
"reformat-md": "node .build/reformat-md.mjs",
"reformat-md": "tsx .build/reformat-mdx.ts",
"lint": "pnpm run lint-md",
"lint-md": "markdownlint docs/content/**/*.md",
"zip-package": "node .build/zip-package.mjs",
"zip-package": "tsx .build/zip-package.ts",
"start": "pnpm dev"
},
"packageManager": "pnpm@10.27.0",
@@ -45,6 +46,7 @@
"shelljs": "^0.10.0",
"terser": "^5.44.1",
"turbo": "^2.7.3",
"tsx": "^4.21.0",
"vite": "^7.3.0"
}
}

77
pnpm-lock.yaml generated
View File

@@ -20,7 +20,7 @@ importers:
version: 0.5.2
'@changesets/cli':
specifier: ^2.29.8
version: 2.29.8
version: 2.29.8(@types/node@22.19.3)
'@playwright/test':
specifier: ^1.57.0
version: 1.57.0
@@ -65,7 +65,7 @@ importers:
version: 8.5.6
postcss-cli:
specifier: ^11.0.1
version: 11.0.1(postcss@8.5.6)
version: 11.0.1(postcss@8.5.6)(tsx@4.21.0)
prettier:
specifier: ^3.7.4
version: 3.7.4
@@ -81,12 +81,15 @@ importers:
terser:
specifier: ^5.44.1
version: 5.44.1
tsx:
specifier: ^4.21.0
version: 4.21.0
turbo:
specifier: ^2.7.3
version: 2.7.3
vite:
specifier: ^7.3.0
version: 7.3.0(sass@1.97.2)(terser@5.44.1)(yaml@2.7.1)
version: 7.3.0(@types/node@22.19.3)(sass@1.97.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.7.1)
core:
dependencies:
@@ -103,6 +106,9 @@ importers:
'@melloware/coloris':
specifier: ^0.25.0
version: 0.25.0
'@types/node':
specifier: ^22.0.0
version: 22.19.3
apexcharts:
specifier: ^5.3.6
version: 5.3.6
@@ -175,6 +181,9 @@ importers:
typed.js:
specifier: ^2.1.0
version: 2.1.0
typescript:
specifier: ^5.9.3
version: 5.9.3
docs:
dependencies:
@@ -1207,6 +1216,9 @@ packages:
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
'@types/node@22.19.3':
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -1961,6 +1973,9 @@ packages:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
getpass@0.1.7:
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
@@ -2943,6 +2958,9 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@@ -3289,6 +3307,11 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -3340,12 +3363,20 @@ packages:
typed.js@2.1.0:
resolution: {integrity: sha512-bDuXEf7YcaKN4g08NMTUM6G90XU25CK3bh6U0THC/Mod/QPKlEt9g/EjvbYB8x2Qwr2p6J6I3NrsoYaVnY6wsQ==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
@@ -3813,7 +3844,7 @@ snapshots:
transitivePeerDependencies:
- encoding
'@changesets/cli@2.29.8':
'@changesets/cli@2.29.8(@types/node@22.19.3)':
dependencies:
'@changesets/apply-release-plan': 7.0.14
'@changesets/assemble-release-plan': 6.0.9
@@ -3829,7 +3860,7 @@ snapshots:
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@changesets/write': 0.4.0
'@inquirer/external-editor': 1.0.2
'@inquirer/external-editor': 1.0.2(@types/node@22.19.3)
'@manypkg/get-packages': 1.1.3
ansi-colors: 4.1.3
ci-info: 3.9.0
@@ -4170,10 +4201,12 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
'@inquirer/external-editor@1.0.2':
'@inquirer/external-editor@1.0.2(@types/node@22.19.3)':
dependencies:
chardet: 2.1.0
iconv-lite: 0.7.0
optionalDependencies:
'@types/node': 22.19.3
'@isaacs/balanced-match@4.0.1': {}
@@ -4508,6 +4541,10 @@ snapshots:
'@types/node@12.20.55': {}
'@types/node@22.19.3':
dependencies:
undici-types: 6.21.0
'@types/normalize-package-data@2.4.4': {}
'@types/unist@2.0.11': {}
@@ -5265,6 +5302,10 @@ snapshots:
get-stream@6.0.1: {}
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
getpass@0.1.7:
dependencies:
assert-plus: 1.0.0
@@ -6124,14 +6165,14 @@ snapshots:
pnpm@10.6.5: {}
postcss-cli@11.0.1(postcss@8.5.6):
postcss-cli@11.0.1(postcss@8.5.6)(tsx@4.21.0):
dependencies:
chokidar: 3.6.0
dependency-graph: 1.0.0
fs-extra: 11.3.3
picocolors: 1.1.1
postcss: 8.5.6
postcss-load-config: 5.1.0(postcss@8.5.6)
postcss-load-config: 5.1.0(postcss@8.5.6)(tsx@4.21.0)
postcss-reporter: 7.1.0(postcss@8.5.6)
pretty-hrtime: 1.0.3
read-cache: 1.0.0
@@ -6142,12 +6183,13 @@ snapshots:
- jiti
- tsx
postcss-load-config@5.1.0(postcss@8.5.6):
postcss-load-config@5.1.0(postcss@8.5.6)(tsx@4.21.0):
dependencies:
lilconfig: 3.1.3
yaml: 2.7.1
optionalDependencies:
postcss: 8.5.6
tsx: 4.21.0
postcss-reporter@7.1.0(postcss@8.5.6):
dependencies:
@@ -6317,6 +6359,8 @@ snapshots:
resolve-from@5.0.0: {}
resolve-pkg-maps@1.0.0: {}
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@@ -6692,6 +6736,13 @@ snapshots:
tslib@2.8.1: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.2
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
@@ -6731,10 +6782,14 @@ snapshots:
typed.js@2.1.0: {}
typescript@5.9.3: {}
uc.micro@2.1.0: {}
undefsafe@2.0.5: {}
undici-types@6.21.0: {}
unist-util-is@6.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -6801,7 +6856,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.2
vite@7.3.0(sass@1.97.2)(terser@5.44.1)(yaml@2.7.1):
vite@7.3.0(@types/node@22.19.3)(sass@1.97.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.7.1):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -6810,9 +6865,11 @@ snapshots:
rollup: 4.52.5
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.3
fsevents: 2.3.3
sass: 1.97.2
terser: 5.44.1
tsx: 4.21.0
yaml: 2.7.1
webidl-conversions@3.0.1: {}

View File

@@ -1,21 +0,0 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkgJson = path.join(__dirname, '../package.json')
const pkg = JSON.parse(await fs.readFile(pkgJson, 'utf8'))
const year = new Date().getFullYear()
function getBanner(pluginFilename) {
return `/*!
* Tabler${pluginFilename ? ` ${pluginFilename}` : ''} v${pkg.version} (${pkg.homepage})
* Copyright 2018-${year} The Tabler Authors
* Copyright 2018-${year} codecalm.net Paweł Kuna
* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
*/`
}
export default getBanner

View File

@@ -1,18 +0,0 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createViteConfig } from '../../.build/vite.config.helper.mjs'
import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const bannerText = getBanner('Demo')
export default createViteConfig({
entry: path.resolve(__dirname, '../js/demo.js'),
name: 'demo',
fileName: () => 'demo.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/preview/js'),
banner: bannerText
})

View File

@@ -0,0 +1,26 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { existsSync } from 'node:fs'
import { createViteConfig } from '../../.build/vite.config.helper'
import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const MINIFY = process.env.MINIFY === 'true'
const bannerText = getBanner('Demo')
// Try .ts first, fallback to .js for gradual migration
const entryPath = path.resolve(__dirname, '../js/demo')
const entry = existsSync(`${entryPath}.ts`) ? `${entryPath}.ts` : `${entryPath}.js`
export default createViteConfig({
entry: entry,
name: 'demo',
fileName: () => MINIFY ? 'demo.min.js' : 'demo.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/preview/js'),
banner: bannerText,
minify: MINIFY
})

View File

@@ -1,5 +1,14 @@
// Setting items
const items = {
interface SettingItem {
localStorage: string
default: string
}
interface SettingsItems {
[key: string]: SettingItem
}
const items: SettingsItems = {
"menu-position": { localStorage: "tablerMenuPosition", default: "top" },
"menu-behavior": { localStorage: "tablerMenuBehavior", default: "sticky" },
"container-layout": {
@@ -9,14 +18,14 @@ const items = {
}
// Theme config
const config = {}
const config: Record<string, string> = {}
for (const [key, params] of Object.entries(items)) {
const lsParams = localStorage.getItem(params.localStorage)
config[key] = lsParams ? lsParams : params.default
}
// Parse url params
const parseUrl = () => {
const parseUrl = (): void => {
const search = window.location.search.substring(1)
const params = search.split("&")
@@ -36,11 +45,11 @@ const parseUrl = () => {
}
// Toggle form controls
const toggleFormControls = (form) => {
const toggleFormControls = (form: HTMLFormElement): void => {
for (const [key, params] of Object.entries(items)) {
const elem = form.querySelector(
`[name="settings-${key}"][value="${config[key]}"]`,
)
) as HTMLInputElement | null
if (elem) {
elem.checked = true
@@ -49,27 +58,34 @@ const toggleFormControls = (form) => {
}
// Submit form
const submitForm = (form) => {
const submitForm = (form: HTMLFormElement): void => {
// Save data to localStorage
for (const [key, params] of Object.entries(items)) {
// Save to localStorage
const value = form.querySelector(`[name="settings-${key}"]:checked`).value
const checkedInput = form.querySelector(`[name="settings-${key}"]:checked`) as HTMLInputElement
if (checkedInput) {
const value = checkedInput.value
localStorage.setItem(params.localStorage, value)
// Update local variables
config[key] = value
}
}
window.dispatchEvent(new Event("resize"))
// Bootstrap is available globally
const bootstrap = (window as any).bootstrap
if (bootstrap) {
new bootstrap.Offcanvas(form).hide()
}
}
// Parse url
parseUrl()
// Elements
const form = document.querySelector("#offcanvas-settings")
const form = document.querySelector("#offcanvas-settings") as HTMLFormElement | null
// Toggle form controls
if (form) {
@@ -81,3 +97,4 @@ if (form) {
toggleFormControls(form)
}

View File

@@ -10,14 +10,14 @@
"watch-html": "cross-env NODE_ENV=development eleventy --serve --port=3000 --watch --incremental",
"watch-js": "nodemon --watch js/ --ext js --exec \"pnpm run js\"",
"watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css\"",
"css": "pnpm run css-compile && pnpm run css-prefix && pnpm run css-minify",
"css-compile": "sass scss/:dist/preview/css/ --no-source-map --load-path=node_modules",
"css": "pnpm run css-build && pnpm run css-prefix && pnpm run css-minify",
"css-build": "sass scss/:dist/preview/css/ --no-source-map --load-path=node_modules",
"css-prefix": "postcss --config .build/postcss.config.mjs --replace \"dist/preview/css/*.css\" \"!dist/preview/css/*.rtl*.css\" \"!dist/preview/css/*.min.css\"",
"css-minify": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/preview/css/ --batch --batch-suffix \".min\" \"dist/preview/css/*.css\" \"!dist/preview/css/*.min.css\" \"!dist/preview/css/*rtl*.css\"",
"js": "pnpm run js-compile && pnpm run js-minify",
"js-compile": "vite build --config .build/vite.config.mjs",
"js-minify": "pnpm run js-minify-demo",
"js-minify-demo": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/preview/js/demo.js.map,includeSources,url=demo.min.js.map\" --output dist/preview/js/demo.min.js dist/preview/js/demo.js",
"js": "pnpm run js-build && pnpm run js-build-min",
"js-build": "vite build --config .build/vite.config.ts",
"js-build-min": "pnpm run js-build-min-demo",
"js-build-min-demo": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
"clean": "shx rm -rf dist demo",
"html": "pnpm run html-build && pnpm run html-prettify && pnpm run html-remove-prettier-ignore",
"html-build": "eleventy",

View File

@@ -1,11 +1,11 @@
import fs from 'node:fs/promises'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkgJson = path.join(__dirname, '../../core/package.json')
const pkg = JSON.parse(await fs.readFile(pkgJson, 'utf8'))
const pkg = JSON.parse(readFileSync(pkgJson, 'utf8'))
const year = new Date().getFullYear()

View File

@@ -1,17 +0,0 @@
const elements = document.querySelectorAll('[data-countup]');
if (elements.length) {
elements.forEach(function (element) {
let options = {};
try {
options = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')) : {};
} catch (error) {}
const value = parseInt(element.innerHTML, 10);
const countUp = new window.countUp.CountUp(element, value, options);
if (!countUp.error) {
countUp.start();
}
});
}

View File

@@ -1,11 +1,11 @@
import test, { expect } from '@playwright/test';
import { argosScreenshot } from "@argos-ci/playwright"
import fs from "fs"
import { readdirSync } from "fs"
import path from "path"
const previewDir = path.join(__dirname, "../preview/dist")
const htmlFiles = fs.readdirSync(previewDir).filter((file) => file.endsWith(".html"))
const htmlFiles = readdirSync(previewDir).filter((file) => file.endsWith(".html"))
for (const file of htmlFiles) {
test(`Compare ${file}`, async ({ page }) => {

View File

@@ -31,6 +31,12 @@
"build"
],
"cache": true
},
"type-check": {
"dependsOn": [
"^type-check"
],
"cache": true
}
}
}