Compare commits

...

3 Commits

Author SHA1 Message Date
codecalm
6126649bcd feat: add shiki syntax highlighter and update menu with new highlight section 2026-01-21 19:26:25 +01:00
BG-Software
e4e1ff40c1 Change LambdaTest image to TestMu AI in README.md 2026-01-21 17:50:35 +01:00
Paweł Kuna
938e9d35cc refactor: migrate Vite configuration from .ts to .mts (#2594) 2026-01-12 02:52:15 +01:00
21 changed files with 309 additions and 72 deletions

View File

@@ -0,0 +1,5 @@
---
"@tabler/core": minor
---
Added Shiki code highlighting for `pre code` blocks with `language-*` classes.

View File

@@ -0,0 +1,5 @@
---
"@tabler/preview": minor
---
Added Highlight page with examples using the `ui/highlight` component.

View File

@@ -28,13 +28,13 @@ A premium and open source dashboard template with a responsive and high-quality
<p align="center">Browser testing via:</p>
<p align="center">
<a href="https://www.lambdatest.com/" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/14dd2a0a-bafe-436e-a6cb-29636278c781">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/d3dede5a-d702-47c3-bb66-4d887948ed83">
<img src="https://github.com/user-attachments/assets/d3dede5a-d702-47c3-bb66-4d887948ed83" alt="labmdatest" width="296">
</picture>
</a>
<a href="https://www.testmu.ai" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/f0967860-31ad-4078-850b-40b0abc95582" />
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/55ac290a-6729-44aa-bbc3-4c5e909facbf" />
<img src="https://github.com/user-attachments/assets/86bcbe29-eb8d-4273-a381-5ce17d4ca92d" alt="TestMu AI" width="296">
</picture>
</a>
</p>
## 🔎 Preview

View File

@@ -0,0 +1,30 @@
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import { createViteConfig } from '../../.build/vite.config.helper'
import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const baseName = process.env.BASE_NAME || 'tabler'
const entryFile = baseName
const libraryName = baseName
const bannerText = getBanner()
const entryPath = path.resolve(__dirname, `../js/${entryFile}`)
const entry = `${entryPath}.ts`
export default createViteConfig({
entry: entry,
name: libraryName,
fileName: (format) => {
const esmSuffix = format === 'es' ? '.esm' : ''
return `${baseName}${esmSuffix}.js`
},
formats: ['es', 'umd'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: bannerText,
minify: false
})

View File

@@ -1,33 +0,0 @@
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 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: entry,
name: ESM ? undefined : libraryName,
fileName: () => MINIFY ? `${destinationFile}.min.js` : `${destinationFile}.js`,
formats: [ESM ? 'es' : 'umd'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: bannerText,
minify: MINIFY ? true : false
})

View File

@@ -12,3 +12,12 @@ interface Window {
Sortable?: new (element: HTMLElement, options?: any) => any
}
declare module 'shiki/bundle/web' {
export const createHighlighter: (options: {
langs: string[]
themes: string[]
}) => Promise<{
codeToHtml: (code: string, options: { lang: string; theme: string }) => string
}>
}

84
core/js/src/shiki.ts Normal file
View File

@@ -0,0 +1,84 @@
type ShikiHighlighter = {
codeToHtml: (code: string, options: { lang: string; theme: string }) => string
}
const shikiSelector = 'pre code[class*="language-"], pre code[data-language]'
const shikiCodeBlocks = Array.from(document.querySelectorAll<HTMLElement>(shikiSelector))
const getShikiLanguage = (codeBlock: HTMLElement): string => {
const dataLanguage = codeBlock.dataset.language
if (dataLanguage) {
return dataLanguage
}
const languageClass = Array.from(codeBlock.classList).find((className) => className.startsWith('language-'))
return languageClass ? languageClass.replace('language-', '') : 'text'
}
const getShikiTheme = (codeBlock: HTMLElement): string => {
return codeBlock.dataset.shikiTheme || codeBlock.closest<HTMLElement>('[data-shiki-theme]')?.dataset.shikiTheme || 'github-dark'
}
const getShikiBlocksToHighlight = (codeBlocks: HTMLElement[]): HTMLElement[] => {
return codeBlocks.filter((codeBlock) => {
const pre = codeBlock.closest('pre')
return pre && !pre.classList.contains('shiki') && pre.dataset.shikiProcessed !== 'true'
})
}
const highlightShikiBlocks = async (codeBlocks: HTMLElement[]) => {
if (!codeBlocks.length) {
return
}
const languages = new Set<string>(['text'])
codeBlocks.forEach((codeBlock) => {
languages.add(getShikiLanguage(codeBlock))
})
const { createHighlighter, createCssVariablesTheme } = await import('shiki')
const theme = createCssVariablesTheme({
name: 'css-variables',
variablePrefix: '--shiki-',
variableDefaults: {},
fontStyle: true
})
const highlighter = (await createHighlighter({
langs: Array.from(languages).filter(Boolean),
themes: [theme],
})) as ShikiHighlighter
codeBlocks.forEach((codeBlock) => {
const pre = codeBlock.closest('pre')
if (!pre || pre.dataset.shikiProcessed === 'true') {
return
}
const language = getShikiLanguage(codeBlock)
const code = codeBlock.textContent || ''
let highlightedHtml = ''
try {
highlightedHtml = highlighter.codeToHtml(code, { lang: language, theme: 'css-variables' })
} catch (error) {
highlightedHtml = highlighter.codeToHtml(code, { lang: 'text', theme: 'css-variables' })
}
const wrapper = document.createElement('div')
wrapper.innerHTML = highlightedHtml
const highlightedPre = wrapper.firstElementChild
if (highlightedPre) {
pre.dataset.shikiProcessed = 'true'
pre.replaceWith(highlightedPre)
}
})
}
const shikiBlocksToHighlight = getShikiBlocksToHighlight(shikiCodeBlocks)
if (shikiBlocksToHighlight.length) {
void highlightShikiBlocks(shikiBlocksToHighlight)
}

View File

@@ -8,6 +8,7 @@ import './src/switch-icon'
import './src/tab'
import './src/toast'
import './src/sortable'
import './src/shiki'
// Re-export everything from bootstrap.ts (single source of truth)
export * from './src/bootstrap'

View File

@@ -19,16 +19,12 @@
"css-lint": "pnpm run css-lint-variables",
"css-lint-variables": "find-unused-sass-variables scss/ node_modules/bootstrap/scss/",
"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",
"js-build": "concurrently \"pnpm run js-build-standalone\" \"pnpm run js-build-theme\"",
"js-build-theme": "cross-env BASE_NAME=tabler-theme vite build --config .build/vite.config.mts",
"js-build-standalone": "cross-env BASE_NAME=tabler vite build --config .build/vite.config.mts",
"js-build-min": "concurrently \"pnpm run js-build-min-standalone\" \"pnpm run js-build-min-theme\"",
"js-build-min-standalone": "concurrently \"terser dist/js/tabler.js --compress --mangle --comments '/@license|@preserve|^!/' --source-map \\\"content=dist/js/tabler.js.map,filename=dist/js/tabler.min.js.map,url=tabler.min.js.map\\\" -o dist/js/tabler.min.js\" \"terser dist/js/tabler.esm.js --module --compress --mangle --comments '/@license|@preserve|^!/' --source-map \\\"content=dist/js/tabler.esm.js.map,filename=dist/js/tabler.esm.min.js.map,url=tabler.esm.min.js.map\\\" -o dist/js/tabler.esm.min.js\"",
"js-build-min-theme": "concurrently \"terser dist/js/tabler-theme.js --compress --mangle --comments '/@license|@preserve|^!/' --source-map \\\"content=dist/js/tabler-theme.js.map,filename=dist/js/tabler-theme.min.js.map,url=tabler-theme.min.js.map\\\" -o dist/js/tabler-theme.min.js\" \"terser dist/js/tabler-theme.esm.js --module --compress --mangle --comments '/@license|@preserve|^!/' --source-map \\\"content=dist/js/tabler-theme.esm.js.map,filename=dist/js/tabler-theme.esm.min.js.map,url=tabler-theme.esm.min.js.map\\\" -o dist/js/tabler-theme.esm.min.js\"",
"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": "tsx .build/copy-libs.ts",
@@ -181,6 +177,7 @@
"star-rating.js": "^4.3.1",
"tom-select": "^2.4.3",
"typed.js": "^2.1.0",
"shiki": "^3.13.0",
"typescript": "^5.9.3",
"driver.js": "^1.0.0"
},

View File

@@ -32,7 +32,6 @@
/** Theme colors */
@each $name, $color in map.merge($theme-colors, $social-colors) {
@debug contrast-ratio($color, white), $name, $min-contrast-ratio;
--#{$prefix}#{$name}: #{$color};
--#{$prefix}#{$name}-rgb: #{to-rgb($color)};
--#{$prefix}#{$name}-fg: #{if(contrast-ratio($color) > $min-contrast-ratio, var(--#{$prefix}light), var(--#{$prefix}dark))};

View File

@@ -138,8 +138,6 @@
// Colors
@function to-rgb($value) {
@debug $value;
@return color.channel($value, 'red', $space: rgb), color.channel($value, 'green', $space: rgb), color.channel($value, 'blue', $space: rgb);
}

View File

@@ -14,3 +14,4 @@
@import 'vendor/typed';
@import 'vendor/turbo';
@import 'vendor/fullcalendar';
@import 'vendor/shiki';

47
core/scss/vendor/_shiki.scss vendored Normal file
View File

@@ -0,0 +1,47 @@
:root {
--shiki-foreground: var(--#{$prefix}light);
--shiki-background: var(--#{$prefix}bg-surface-dark);
--shiki-token-constant: #79b8ff;
--shiki-token-string: #9ecbff;
--shiki-token-comment: #6a737d;
--shiki-token-keyword: #f97583;
--shiki-token-parameter: #e1e4e8;
--shiki-token-function: #b392f0;
--shiki-token-string-expression: #79b8ff;
--shiki-token-punctuation: #9ecbff;
--shiki-token-link: #79b8ff;
--shiki-ansi-black: #000000;
--shiki-ansi-black-dim: #00000080;
--shiki-ansi-red: #bb0000;
--shiki-ansi-red-dim: #bb000080;
--shiki-ansi-green: #00bb00;
--shiki-ansi-green-dim: #00bb0080;
--shiki-ansi-yellow: #bbbb00;
--shiki-ansi-yellow-dim: #bbbb0080;
--shiki-ansi-blue: #0000bb;
--shiki-ansi-blue-dim: #0000bb80;
--shiki-ansi-magenta: #ff00ff;
--shiki-ansi-magenta-dim: #ff00ff80;
--shiki-ansi-cyan: #00bbbb;
--shiki-ansi-cyan-dim: #00bbbb80;
--shiki-ansi-white: #eeeeee;
--shiki-ansi-white-dim: #eeeeee80;
--shiki-ansi-bright-black: #555555;
--shiki-ansi-bright-black-dim: #55555580;
--shiki-ansi-bright-red: #ff5555;
--shiki-ansi-bright-red-dim: #ff555580;
--shiki-ansi-bright-green: #00ff00;
--shiki-ansi-bright-green-dim: #00ff0080;
--shiki-ansi-bright-yellow: #ffff55;
--shiki-ansi-bright-yellow-dim: #ffff5580;
--shiki-ansi-bright-blue: #5555ff;
--shiki-ansi-bright-blue-dim: #5555ff80;
--shiki-ansi-bright-magenta: #ff55ff;
--shiki-ansi-bright-magenta-dim: #ff55ff80;
--shiki-ansi-bright-cyan: #55ffff;
--shiki-ansi-bright-cyan-dim: #55ffff80;
--shiki-ansi-bright-white: #ffffff;
--shiki-ansi-bright-white-dim: #ffffff80;
}

View File

@@ -1,25 +1,20 @@
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`
const entry = `${entryPath}.ts`
export default createViteConfig({
entry: entry,
name: 'docs',
fileName: () => MINIFY ? 'docs.min.js' : 'docs.js',
fileName: () => 'docs.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/js'),
banner: undefined,
minify: MINIFY
minify: false
})

View File

@@ -8,9 +8,9 @@
"build-assets": "concurrently \"pnpm run js\" \"pnpm run css\"",
"html": "eleventy",
"js": "pnpm run js-build && pnpm run js-build-min",
"js-build": "vite build --config .build/vite.config.ts",
"js-build": "vite build --config .build/vite.config.mts",
"js-build-min": "pnpm run js-build-min-docs",
"js-build-min-docs": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
"js-build-min-docs": "terser dist/js/docs.js --module --compress --mangle --comments '/@license|@preserve|^!/' --source-map \"content=dist/js/docs.js.map,filename=dist/js/docs.min.js.map,url=docs.min.js.map\" -o dist/js/docs.min.js",
"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\"",

3
pnpm-lock.yaml generated
View File

@@ -160,6 +160,9 @@ importers:
plyr:
specifier: ^3.8.3
version: 3.8.3
shiki:
specifier: ^3.13.0
version: 3.13.0
signature_pad:
specifier: ^5.1.1
version: 5.1.1

View File

@@ -1,26 +1,22 @@
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`
const entry = `${entryPath}.ts`
export default createViteConfig({
entry: entry,
name: 'demo',
fileName: () => MINIFY ? 'demo.min.js' : 'demo.js',
fileName: () => 'demo.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/preview/js'),
banner: bannerText,
minify: MINIFY
minify: false
})

View File

@@ -15,9 +15,9 @@
"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-build && pnpm run js-build-min",
"js-build": "vite build --config .build/vite.config.ts",
"js-build": "vite build --config .build/vite.config.mts",
"js-build-min": "pnpm run js-build-min-demo",
"js-build-min-demo": "cross-env MINIFY=true vite build --config .build/vite.config.ts",
"js-build-min-demo": "terser dist/preview/js/demo.js --module --compress --mangle --comments '/@license|@preserve|^!/' --source-map \"content=dist/preview/js/demo.js.map,filename=dist/preview/js/demo.min.js.map,url=demo.min.js.map\" -o dist/preview/js/demo.min.js",
"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

@@ -0,0 +1,91 @@
---
title: Highlight
page-header: Highlight
page-menu: base.highlight
permalink: highlight.html
layout: default
---
{% capture example_html -%}
<div class="card">
<div class="card-body">
<h3 class="card-title">Hello, Tabler!</h3>
<p class="text-secondary">This is a simple card example.</p>
</div>
</div>
{%- endcapture %}
{% capture example_js -%}
const greet = (name) => {
console.log(`Hello, ${name}!`)
}
greet('Tabler')
{%- endcapture %}
{% capture example_json -%}
{
"name": "tabler",
"version": "1.4.0",
"private": false
}
{%- endcapture %}
{% capture example_scss -%}
$highlight-colors: (
"primary": $primary,
"secondary": $secondary
);
.highlight-demo {
color: map-get($highlight-colors, "primary");
}
{%- endcapture %}
{% capture example_shell -%}
pnpm install
pnpm -C core build
{%- endcapture %}
<div class="row row-cols-1 row-cols-lg-2 g-3">
<div class="col">
<div class="card">
<div class="card-body">
<h3 class="card-title">HTML</h3>
{% include "ui/highlight.html" lang="html" code=example_html %}
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h3 class="card-title">JavaScript</h3>
{% include "ui/highlight.html" lang="js" code=example_js %}
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h3 class="card-title">JSON</h3>
{% include "ui/highlight.html" lang="json" code=example_json %}
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h3 class="card-title">SCSS</h3>
{% include "ui/highlight.html" lang="scss" code=example_scss %}
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h3 class="card-title">Shell</h3>
{% include "ui/highlight.html" lang="shell" code=example_shell %}
</div>
</div>
</div>
</div>

View File

@@ -161,6 +161,11 @@
"title": "Markdown",
"url": "markdown.html"
},
"highlight": {
"title": "Highlight",
"url": "highlight.html",
"badge": "New"
},
"navigation": {
"url": "navigation.html",
"title": "Navigation"

View File

@@ -0,0 +1,4 @@
{% assign language = include.lang | default: 'text' %}
{% assign class = include.class | default: '' %}
{% assign code = include.code | default: '' %}
<pre{% if class != '' %} class="{{ class }}"{% endif %}><code class="language-{{ language }}">{{ code | escape }}</code></pre>