diff --git a/.agents/skills/core-js/SKILL.md b/.agents/skills/core-js/SKILL.md
index 3b25525bf..8cfd097e7 100644
--- a/.agents/skills/core-js/SKILL.md
+++ b/.agents/skills/core-js/SKILL.md
@@ -7,14 +7,17 @@ description: >-
# The framework JavaScript
-`core/js` builds two shipped bundles. Everything here is public API and is bound by a size budget.
+`core/js` builds three shipped bundles. Everything here is public API and is bound by a size budget.
-## 1. Two entry points
+## 1. Three entry points
| Entry | Ships as | Holds |
| -------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
| `js/tabler.ts` | `tabler.js` / `.esm.js` (+ `.min`) | plugin initialisers, the Bootstrap components, the `tabler` helper namespace |
| `js/tabler-theme.ts` | `tabler-theme.js` (+ variants) | the colour-mode/theme switcher only |
+| `js/tabler-map.ts` | `tabler-map.js` (+ variants) | the `MapView` component, as an add-on that extends the `tabler` global |
+
+`tabler-map.js` is loaded after `tabler.js`, and only on pages with a map: a component that few pages use goes in an add-on bundle like this one, not in `tabler.js`. Its UMD build uses `extend` (`core/.build/vite.config.mts`), so it adds its exports to the existing `tabler` global.
`tabler-theme.js` is loaded right after `
` and **not deferred**, so the chosen theme applies before the first paint. It stays tiny on purpose (bundlewatch: 1 kB raw, 800 B minified) — do not add anything to it that is not needed before paint.
diff --git a/.build/vite.config.helper.mts b/.build/vite.config.helper.mts
index f51464e79..769cd73b2 100644
--- a/.build/vite.config.helper.mts
+++ b/.build/vite.config.helper.mts
@@ -9,20 +9,26 @@ interface CreateViteConfigOptions {
outDir: string
banner?: string
minify?: boolean | 'esbuild'
+ /** UMD only: add the exports to an existing global of the same name instead of replacing it */
+ extend?: boolean
}
/**
* Creates a Vite configuration for building libraries
*/
-export function createViteConfig({ entry, name, fileName, formats, outDir, banner, minify = false }: CreateViteConfigOptions): UserConfig {
+export function createViteConfig({ entry, name, fileName, formats, outDir, banner, minify = false, extend = false }: CreateViteConfigOptions): UserConfig {
// Vite 8 (Rolldown) always emits const bindings and no longer accepts the
// Rollup-only generatedCode.constBindings option
- const rollupOutput: { banner?: string } = {}
+ const rollupOutput: { banner?: string; extend?: boolean } = {}
if (banner) {
rollupOutput.banner = banner
}
+ if (extend) {
+ rollupOutput.extend = true
+ }
+
const config: UserConfig = {
// Library builds: never copy /public into outDir (see copy-assets.ts).
publicDir: false,
diff --git a/core/.build/vite.config.mts b/core/.build/vite.config.mts
index f11c645bd..848cdfbf9 100644
--- a/core/.build/vite.config.mts
+++ b/core/.build/vite.config.mts
@@ -8,7 +8,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const baseName = process.env.BASE_NAME || 'tabler'
const entryFile = baseName
-const libraryName = baseName
+// `tabler-map` is an add-on to the main bundle: its UMD build extends the
+// `tabler` global instead of creating one of its own.
+const isAddon = baseName === 'tabler-map'
+const libraryName = isAddon ? 'tabler' : baseName
const bannerText = getBanner()
@@ -26,4 +29,5 @@ export default createViteConfig({
outDir: path.resolve(__dirname, '../dist/js'),
banner: bannerText,
minify: false,
+ extend: isAddon,
})
diff --git a/core/js/tabler-map.ts b/core/js/tabler-map.ts
new file mode 100644
index 000000000..9afaad9a7
--- /dev/null
+++ b/core/js/tabler-map.ts
@@ -0,0 +1,6 @@
+// The map component ships as its own bundle, so pages without a map do not
+// carry it. Load it after `tabler.js`: the UMD build adds `MapView` to the
+// `tabler` global.
+import './src/map'
+
+export { default as MapView } from './src/map'
diff --git a/core/js/tabler.ts b/core/js/tabler.ts
index 048b0dae7..ef8eadf58 100644
--- a/core/js/tabler.ts
+++ b/core/js/tabler.ts
@@ -4,7 +4,6 @@ import './src/confetti'
import './src/countup'
import './src/datepicker'
import './src/input-mask'
-import './src/map'
import './src/dropdown'
import './src/sidebar'
import './src/tooltip'
@@ -27,7 +26,6 @@ export { default as Confetti } from './src/confetti'
export { default as CountUp } from './src/countup'
export { default as Datepicker } from './src/datepicker'
export { default as InputMask } from './src/input-mask'
-export { default as MapView } from './src/map'
export { default as OtpInput } from './src/otp-input'
export { default as Sortable } from './src/sortable'
export { default as Sparkline } from './src/sparkline'
diff --git a/core/libs.json b/core/libs.json
index 5d0e423d9..53f65c597 100644
--- a/core/libs.json
+++ b/core/libs.json
@@ -58,6 +58,7 @@
"maplibre-gl": {
"npm": "maplibre-gl",
"module": { "file": "dist/maplibre-gl.mjs", "global": "maplibregl" },
+ "addon": "tabler-map",
"css": ["dist/maplibre-gl.css"],
"extra": ["dist/maplibre-gl.mjs", "dist/maplibre-gl-shared.mjs", "dist/maplibre-gl-worker.mjs"]
},
diff --git a/core/package.json b/core/package.json
index 8033c064a..b01592812 100644
--- a/core/package.json
+++ b/core/package.json
@@ -17,12 +17,14 @@
"lint:scss:vars": "find-unused-sass-variables scss/ --ignoreFiles scss/_variables-deprecated.scss",
"js": "pnpm run js:build && pnpm run js:min && pnpm run js:types",
"js:types": "tsc -p tsconfig.types.json",
- "js:build": "concurrently \"pnpm run js:build:standalone\" \"pnpm run js:build:theme\"",
+ "js:build": "concurrently \"pnpm run js:build:standalone\" \"pnpm run js:build:theme\" \"pnpm run js:build:map\"",
"js:build:theme": "cross-env BASE_NAME=tabler-theme vite build --config .build/vite.config.mts",
+ "js:build:map": "cross-env BASE_NAME=tabler-map vite build --config .build/vite.config.mts",
"js:build:standalone": "cross-env BASE_NAME=tabler vite build --config .build/vite.config.mts",
- "js:min": "concurrently \"pnpm run js:min:standalone\" \"pnpm run js:min:theme\"",
+ "js:min": "concurrently \"pnpm run js:min:standalone\" \"pnpm run js:min:theme\" \"pnpm run js:min:map\"",
"js:min:standalone": "concurrently \"terser dist/js/tabler.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --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 passes=2,ecma=2020 --ecma 2020 --mangle --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:min:theme": "concurrently \"terser dist/js/tabler-theme.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --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 passes=2,ecma=2020 --ecma 2020 --mangle --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\"",
+ "js:min:map": "concurrently \"terser dist/js/tabler-map.js --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-map.js.map,filename=dist/js/tabler-map.min.js.map,url=tabler-map.min.js.map\\\" -o dist/js/tabler-map.min.js\" \"terser dist/js/tabler-map.esm.js --module --compress passes=2,ecma=2020 --ecma 2020 --mangle --source-map \\\"content=dist/js/tabler-map.esm.js.map,filename=dist/js/tabler-map.esm.min.js.map,url=tabler-map.esm.min.js.map\\\" -o dist/js/tabler-map.esm.min.js\"",
"copy": "concurrently \"pnpm run copy:img\" \"pnpm run copy:libs\"",
"copy:img": "tsx .build/copy-img.ts",
"copy:libs": "tsx .build/copy-libs.ts",
@@ -212,19 +214,35 @@
},
{
"path": "./dist/js/tabler.js",
- "maxSize": "61 kB"
+ "maxSize": "59 kB"
},
{
"path": "./dist/js/tabler.min.js",
- "maxSize": "41 kB"
+ "maxSize": "40 kB"
},
{
"path": "./dist/js/tabler.esm.js",
- "maxSize": "61 kB"
+ "maxSize": "59 kB"
},
{
"path": "./dist/js/tabler.esm.min.js",
- "maxSize": "41 kB"
+ "maxSize": "40 kB"
+ },
+ {
+ "path": "./dist/js/tabler-map.js",
+ "maxSize": "10 kB"
+ },
+ {
+ "path": "./dist/js/tabler-map.esm.js",
+ "maxSize": "10 kB"
+ },
+ {
+ "path": "./dist/js/tabler-map.min.js",
+ "maxSize": "7 kB"
+ },
+ {
+ "path": "./dist/js/tabler-map.esm.min.js",
+ "maxSize": "7 kB"
},
{
"path": "./dist/js/tabler-theme.js",
diff --git a/core/tsconfig.types.json b/core/tsconfig.types.json
index b5e5e1e66..d4251d062 100644
--- a/core/tsconfig.types.json
+++ b/core/tsconfig.types.json
@@ -8,5 +8,5 @@
"rootDir": "js",
"outDir": "dist/types"
},
- "include": ["js/tabler.ts", "js/tabler-theme.ts", "js/src/**/*"]
+ "include": ["js/tabler.ts", "js/tabler-theme.ts", "js/tabler-map.ts", "js/src/**/*"]
}
diff --git a/docs/content/ui/components/map.mdx b/docs/content/ui/components/map.mdx
index c51c3803e..d117509a8 100644
--- a/docs/content/ui/components/map.mdx
+++ b/docs/content/ui/components/map.mdx
@@ -61,12 +61,15 @@ MapLibre GL JS is published as an ES module. Tabler uses version 6. Load the sty
// The map component looks for the library here
window.maplibregl = maplibregl
-`}
+
+`}
/>
+The map component is not part of `tabler.js`. It ships as a small add-on, `tabler-map.js`, so pages without a map do not carry it. Load it after `tabler.js`; it adds `MapView` to the `tabler` object.
+
The library loads two more files from the same folder, `maplibre-gl-shared.mjs` and `maplibre-gl-worker.mjs`, so keep the three together when you host them yourself.
-With a bundler, install the package and set the global before you import Tabler:
+With a bundler, install the package, set the global, and then import the add-on:
```bash
npm install maplibre-gl
@@ -77,8 +80,12 @@ import * as maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
window.maplibregl = maplibregl
+
+const { MapView } = await import('@tabler/core/dist/js/tabler-map.esm.js')
```
+The add-on is imported after the global is set, because it turns the map elements on the page into maps as soon as it loads.
+
Older versions, including all of version 5, have a known security issue in the code that cleans the attribution text.
@@ -316,7 +323,7 @@ Live versions of a route, an area, clusters, a bubble map, a globe with arcs and
### Via data attributes
-An element with `data-bs-toggle="map"` becomes a map when the Tabler script loads:
+An element with `data-bs-toggle="map"` becomes a map when the `tabler-map.js` script loads:
diff --git a/docs/layouts/DocsLayout.astro b/docs/layouts/DocsLayout.astro
index 40f403349..1c32849c1 100644
--- a/docs/layouts/DocsLayout.astro
+++ b/docs/layouts/DocsLayout.astro
@@ -142,7 +142,7 @@ const editFilePath =
// docs-libs for this page. In dev, file names stay unchanged
// (e.g. dist/plyr.min.js keeps .min).
-type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } };
+type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string };
const libEntries = Object.entries(libs as Record);
const libUrl = (lib: Lib, file: string) =>
isExternal(file)
@@ -151,6 +151,7 @@ const libUrl = (lib: Lib, file: string) =>
const docsLibCss = libEntries
.filter(([name]) => docsLibs.includes(name))
.flatMap(([, lib]) => (lib.css ?? []).map((file) => libUrl(lib, file)));
+const docsLibAddons = libEntries.filter(([name]) => docsLibs.includes(name)).flatMap(([, lib]) => (lib.addon ? [lib.addon] : []));
const docsLibModules = libEntries
.filter(([name]) => docsLibs.includes(name))
.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libUrl(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []));
@@ -525,6 +526,7 @@ const relatedPages = await Promise.all(
+ {docsLibAddons.map((addon) => )}
diff --git a/screenshots/layouts/ScreenshotAppLayout.astro b/screenshots/layouts/ScreenshotAppLayout.astro
index 8d8daa397..2a73d33b0 100644
--- a/screenshots/layouts/ScreenshotAppLayout.astro
+++ b/screenshots/layouts/ScreenshotAppLayout.astro
@@ -73,12 +73,13 @@ const themeAttributes = themeDataAttributes({ ...theme, 'navbar-position': navba
// whatever core's own watcher last emitted, which is unminified.
const min = import.meta.env.DEV ? '' : '.min'
-type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } }
+type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string }
const pageLibEntries = Object.entries(libs as Record).filter(([name]) => pageLibs.includes(name))
const libHref = (lib: Lib, file: string) => `/dist/libs/${lib.npm}/${file}`
const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((file) => libHref(lib, file)))
const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
// ES-module-only libraries: imported in a module script and exposed as a global (see BaseLayout)
+const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []))
---
@@ -182,6 +183,7 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
{libJsFiles.map((src) => )}
{libModules.map((code) => ${code}`} />)}
+ {libAddons.map((addon) => )}
diff --git a/screenshots/layouts/ScreenshotLayout.astro b/screenshots/layouts/ScreenshotLayout.astro
index a826a6669..76f4c5d42 100644
--- a/screenshots/layouts/ScreenshotLayout.astro
+++ b/screenshots/layouts/ScreenshotLayout.astro
@@ -45,12 +45,13 @@ const rtlSuffix = rtl ? '.rtl' : ''
// build instead of hardcoding one variant.
const min = import.meta.env.DEV ? '' : '.min'
-type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string } }
+type Lib = { npm?: string; js?: string[]; css?: string[]; module?: { file: string; global: string }; addon?: string }
const pageLibEntries = Object.entries(libs as Record).filter(([name]) => pageLibs.includes(name))
const libHref = (lib: Lib, file: string) => `/dist/libs/${lib.npm}/${file}`
const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((file) => libHref(lib, file)))
const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
// ES-module-only libraries: imported in a module script and exposed as a global (see BaseLayout)
+const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'; window.${lib.module.global} = lib`] : []))
---
@@ -173,6 +174,7 @@ const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import *
{libJsFiles.map((src) => )}
{libModules.map((code) => ${code}`} />)}
+ {libAddons.map((addon) => )}
diff --git a/shared/layouts/BaseLayout.astro b/shared/layouts/BaseLayout.astro
index 14992c7cf..8352c7d4e 100644
--- a/shared/layouts/BaseLayout.astro
+++ b/shared/layouts/BaseLayout.astro
@@ -39,7 +39,7 @@ const isProduction = process.env.VERCEL_ENV === 'production'
// RTL swaps tabler/plugin stylesheets for their .rtl.css variants (demo stays LTR)
const rtlSuffix = rtl ? '.rtl' : ''
-type Lib = { npm?: string; js?: string[]; css?: string[]; head?: boolean; module?: { file: string; global: string } }
+type Lib = { npm?: string; js?: string[]; css?: string[]; head?: boolean; module?: { file: string; global: string }; addon?: string }
const unknownLibs = pageLibs.filter((name) => !(name in libs))
if (unknownLibs.length > 0) {
throw new Error(`BaseLayout: unknown pageLibs ${unknownLibs.map((name) => `"${name}"`).join(', ')} — use a key from core/libs.json`)
@@ -52,6 +52,8 @@ const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((fil
// handed to the classic page scripts as a global. Module scripts are deferred,
// so the global is set before DOMContentLoaded.
const libModules = pageLibEntries.flatMap(([, lib]) => (lib.module ? [`import * as lib from '${libHref(lib, lib.module.file)}'\n window.${lib.module.global} = lib`] : []))
+// Tabler add-on bundles (dist/js) that a library needs, loaded after tabler.js
+const libAddons = pageLibEntries.flatMap(([, lib]) => (lib.addon ? [lib.addon] : []))
const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean(lib.head) === head).flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
---
@@ -137,6 +139,7 @@ const libJsFiles = (head: boolean) => pageLibEntries.filter(([, lib]) => Boolean
+ {libAddons.map((addon) => )}