1
0
mirror of https://github.com/tabler/tabler.git synced 2026-08-03 17:04:39 +04:00

Render component scripts as real JS instead of built strings (#2731)

This commit is contained in:
Paweł Kuna
2026-07-31 00:23:25 +02:00
committed by GitHub
parent ad2ed849dd
commit f72cb5cedf
41 changed files with 899 additions and 1141 deletions
+5
View File
@@ -34,6 +34,11 @@ export function createViteConfig({
}
const config: UserConfig = {
// These are library (JS bundle) builds, not app builds — Vite's default
// behavior of copying <root>/public into outDir on every build is not wanted
// here (and in @tabler/preview, where public/ is itself generated from this
// same outDir, caused unbounded growth across repeated builds).
publicDir: false,
build: {
lib: {
entry: path.resolve(entry),
+5 -2
View File
@@ -21,10 +21,13 @@ const copies = [
requiredFile: join(repo, 'core', 'dist', 'css', 'tabler.css'),
},
{
from: join(repo, 'preview', 'dist', 'preview'),
// Sourced from preview's isolated tmp-assets/ (not dist/) — dist/ is Astro's own
// build output there, and reading demo assets from it caused unbounded growth
// across repeated builds. See preview/.build/copy-assets.mjs.
from: join(repo, 'preview', 'tmp-assets'),
to: join(publicDir, 'preview'),
packageName: '@tabler/preview',
requiredFile: join(repo, 'preview', 'dist', 'preview', 'css', 'demo.css'),
requiredFile: join(repo, 'preview', 'tmp-assets', 'css', 'demo.css'),
},
{
from: join(repo, 'shared', 'static'),
-5
View File
@@ -17,11 +17,6 @@ export default defineConfig({
host: true,
},
vite: {
// InlineScript.astro emits scripts inline at the component site —
// docs pages have no <PageScripts /> drain
define: {
'import.meta.env.INLINE_PAGE_SCRIPTS': 'true',
},
resolve: {
alias: {
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
+15 -2
View File
@@ -6,6 +6,15 @@ import { fileURLToPath } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
const repo = join(root, '..')
const publicDir = join(root, 'public')
// Always start from a clean public/ (mirrors @tabler/docs' and Bootstrap's own docs
// integration — see astro:config:done in bootstrap/site/src/libs/astro.ts): running
// this script twice in a row, or running it without a prior `pnpm run clean`, must
// never be able to accumulate stale/nested content. Without this, a previous run's
// output could get copied into a source directory and re-copied on the next run,
// growing without bound (this happened for real — see preview/.build/vite.config.mts).
rmSync(publicDir, { recursive: true, force: true })
const copies = [
// @tabler/core dist (css/js/fonts/img/libs) — same as the Eleventy passthrough
@@ -15,8 +24,12 @@ const copies = [
required: true,
allowDestinationFallback: true,
},
// demo css/js built by this package's sass/vite pipeline
{ from: join(root, 'dist', 'preview'), to: join(root, 'public', 'preview'), required: true },
// demo css/js built by this package's sass/vite pipeline. Source is tmp-assets/
// (NOT dist/) on purpose — dist/ is Astro's own build output, and copying from a
// path Astro also writes to caused unbounded growth across repeated builds (each
// `astro build` re-seeds dist/ from public/, which the next `pnpm run assets`
// would then copy right back in).
{ from: join(root, 'tmp-assets'), to: join(root, 'public', 'preview'), required: true },
// docs.css built by the @tabler/docs sass pipeline (used by docs pages)
{ from: join(repo, 'docs', 'dist', 'css'), to: join(root, 'public', 'css'), required: false },
// static assets (photos, avatars, tracks, brand svgs...).
+18 -1
View File
@@ -6,12 +6,29 @@ import getBanner from '../../shared/banner/index.mjs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const entry = path.resolve(__dirname, '../js/demo.ts')
// Build mode (default): tmp-assets/js, copied into public/preview by copy-assets.mjs.
// Outside dist/ (Astro's build output) on purpose: dist/ is copied from public/ on
// every `astro build`, so an outDir inside dist/ would get re-seeded with whatever
// public/ already contained and re-copied back on the next `pnpm run assets`,
// growing without bound across repeated builds (see copy-assets.mjs). Not
// dot-prefixed either — terser's CLI --source-map option parser chokes on a
// leading-dot path segment (e.g. ".build/out/...") with a spurious "not a supported
// option" error; a plain relative dir avoids that entirely.
//
// Watch mode (pnpm run watch-js, via PREVIEW_JS_OUT_DIR): writes straight to
// public/preview/js. `astro dev` never touches public/, so there's no dist/
// collision to avoid there, and this lets edits show up on refresh without an
// extra copy-assets.mjs pass.
const outDir = process.env.PREVIEW_JS_OUT_DIR
? path.resolve(__dirname, '..', process.env.PREVIEW_JS_OUT_DIR)
: path.resolve(__dirname, '../tmp-assets/js')
export default createViteConfig({
entry,
name: 'demo',
fileName: () => 'demo.js',
formats: ['es'],
outDir: path.resolve(__dirname, '../dist/preview/js'),
outDir,
banner: getBanner('Demo'),
minify: false,
})
+1
View File
@@ -1,6 +1,7 @@
# build output
dist/
.astro/
tmp-assets/
# public/ is fully generated by .build/copy-assets.mjs
public/
-5
View File
@@ -69,11 +69,6 @@ export default defineConfig({
host: true,
},
vite: {
// InlineScript.astro renders nothing here — scripts go through the
// addPageScript() registry and the <PageScripts /> drain (Eleventy model)
define: {
'import.meta.env.INLINE_PAGE_SCRIPTS': 'false',
},
resolve: {
alias: {
// demo data lives in the monorepo's shared/data — single source of
+8 -5
View File
@@ -7,13 +7,16 @@
"node": ">=22.12.0"
},
"scripts": {
"assets": "pnpm run css && pnpm run js && node .build/copy-assets.mjs",
"css": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/preview/css/ && postcss --config ../core/.build/postcss.config.mjs --replace \"dist/preview/css/*.css\" \"!dist/preview/css/*.min.css\" && 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\"",
"js": "vite build --config .build/vite.config.mts && 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",
"dev": "pnpm run assets && astro dev",
"assets": "shx rm -rf tmp-assets && pnpm run css && pnpm run js && node .build/copy-assets.mjs",
"css": "sass --no-source-map --load-path=node_modules --style expanded scss/:tmp-assets/css/ && postcss --config ../core/.build/postcss.config.mjs --replace \"tmp-assets/css/*.css\" \"!tmp-assets/css/*.min.css\" && cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output tmp-assets/css/ --batch --batch-suffix \".min\" \"tmp-assets/css/*.css\" \"!tmp-assets/css/*.min.css\"",
"js": "vite build --config .build/vite.config.mts && terser tmp-assets/js/demo.js --module --compress --mangle --comments '/@license|@preserve|^!/' --source-map \"content=tmp-assets/js/demo.js.map,filename=tmp-assets/js/demo.min.js.map,url=demo.min.js.map\" -o tmp-assets/js/demo.min.js",
"watch-css": "nodemon --watch scss/ --ext scss --exec \"sass --no-source-map --load-path=node_modules --style expanded scss/:public/preview/css/ && postcss --config ../core/.build/postcss.config.mjs --replace 'public/preview/css/*.css' '!public/preview/css/*.min.css'\"",
"watch-js": "cross-env PREVIEW_JS_OUT_DIR=public/preview/js vite build --watch --config .build/vite.config.mts",
"watch": "concurrently \"pnpm run watch-css\" \"pnpm run watch-js\"",
"dev": "pnpm run assets && concurrently -n astro,css,js -c blue,magenta,cyan \"astro dev\" \"pnpm run watch-css\" \"pnpm run watch-js\"",
"build": "pnpm run assets && astro build",
"preview": "astro preview",
"clean": "shx rm -rf dist public/dist public/preview public/css .astro",
"clean": "shx rm -rf dist public/dist public/preview public/css .astro tmp-assets",
"import-icons": "pnpm up @tabler/icons@latest && node .build/import-icons.mjs",
"import-illustrations": "node .build/import-illustrations.mjs"
},
+21 -24
View File
@@ -3,30 +3,6 @@ import FormFooter from '@ui/FormFooter.astro';
import SingleLayout from '@shared/layouts/SingleLayout.astro';
import Button from '@ui/Button.astro';
import ButtonList from '@ui/ButtonList.astro';
import { addPageScript } from '@shared/lib/page-scripts';
addPageScript(`<script>
document.addEventListener("DOMContentLoaded", function() {
var inputs = document.querySelectorAll('[data-code-input]');
// Attach an event listener to each input element
for(let i = 0; i < inputs.length; i++) {
inputs[i].addEventListener('input', function(e) {
// If the input field has a character, and there is a next input field, focus it
if(e.target.value.length === e.target.maxLength && i + 1 < inputs.length) {
inputs[i + 1].focus();
}
});
inputs[i].addEventListener('keydown', function(e) {
// If the input field is empty and the keyCode for Backspace (8) is detected, and there is a previous input field, focus it
if(e.target.value.length === 0 && e.keyCode === 8 && i > 0) {
inputs[i - 1].focus();
}
});
}
});
</script>`);
---
<SingleLayout title="2-Step Verification">
@@ -81,6 +57,27 @@ document.addEventListener("DOMContentLoaded", function() {
</div>
</form>
<script>
const inputs = document.querySelectorAll('[data-code-input]');
// Attach an event listener to each input element
inputs.forEach((input, i) => {
input.addEventListener('input', (e) => {
// If the input field has a character, and there is a next input field, focus it
if (e.target.value.length === e.target.maxLength && i + 1 < inputs.length) {
inputs[i + 1].focus();
}
});
input.addEventListener('keydown', (e) => {
// If the input field is empty and the keyCode for Backspace (8) is detected, and there is a previous input field, focus it
if (e.target.value.length === 0 && e.keyCode === 8 && i > 0) {
inputs[i - 1].focus();
}
});
});
</script>
<div class="text-center text-secondary mt-3">
It may take a minute to receive your code. Haven't received it? <a href="./"
>Resend a new code.</a
+10 -3
View File
@@ -23,8 +23,15 @@ const rows = (rollercoasters as Record<string, any>[]).map((rc, i) => {
};
});
const parameters =
"[ 'sort-name', 'sort-type', 'sort-city', 'sort-score', { attr: 'data-date', name: 'sort-date' }, { attr: 'data-progress', name: 'sort-progress' }, 'sort-quantity' ]";
const valueNames = [
'sort-name',
'sort-type',
'sort-city',
'sort-score',
{ attr: 'data-date', name: 'sort-date' },
{ attr: 'data-progress', name: 'sort-progress' },
'sort-quantity',
];
---
<DefaultLayout title="Datatables" pageHeader="Datatables" pageMenu="plugins.datatables" pageLibs={['lists']}>
@@ -66,5 +73,5 @@ const parameters =
</CardBody>
</Card>
<TablerList id={id} parameters={parameters} />
<TablerList id={id} valueNames={valueNames} />
</DefaultLayout>
+19 -22
View File
@@ -1,35 +1,13 @@
---
// Masonry auto-inits from the data-masonry attribute (page-lib, no init script,
// like cards-masonry.astro); fslightbox is a pure page-lib (no init script).
// The show.bs.modal handler is captured via {% capture_script %} → addPageScript.
import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import CardStamp from '@ui/CardStamp.astro';
import Prose from '@ui/Prose.astro';
import { site } from '@shared/lib/site';
import { addPageScript } from '@shared/lib/page-scripts';
import emails from '@data/emails.json';
const emailEntries = Object.entries(emails);
addPageScript(`<script>
const emailModal = document.getElementById("email-modal")
if (emailModal) {
emailModal.addEventListener("show.bs.modal", function (e) {
const button = e.relatedTarget
const image = button.getAttribute("data-bs-image"),
title = button.getAttribute("data-bs-title"),
description = button.getAttribute("data-bs-description")
emailModal.querySelector("[data-email-title]").textContent = title
emailModal.querySelector("[data-email-description]").textContent = description
emailModal.querySelector("[data-email-image]").src = image
modalTitle.textContent = \`New message to \${recipient}\`
modalBodyInput.value = recipient
})
}
</script>`);
---
<DefaultLayout
@@ -104,4 +82,23 @@ addPageScript(`<script>
</div>
</div>
</div>
<script>
const emailModal = document.getElementById('email-modal');
if (emailModal) {
emailModal.addEventListener('show.bs.modal', function (e) {
const button = e.relatedTarget;
const image = button.getAttribute('data-bs-image'),
title = button.getAttribute('data-bs-title'),
description = button.getAttribute('data-bs-description');
emailModal.querySelector('[data-email-title]').textContent = title;
emailModal.querySelector('[data-email-description]').textContent = description;
emailModal.querySelector('[data-email-image]').src = image;
modalTitle.textContent = `New message to ${recipient}`;
modalBodyInput.value = recipient;
});
}
</script>
</DefaultLayout>
+31 -43
View File
@@ -8,7 +8,6 @@ import Prose from '@ui/Prose.astro';
import freeIllustrations from '@data/free-illustrations.json';
import illustrationsList from '@data/illustrations.json';
import siteData from '@data/site.json';
import { addPageScript } from '@shared/lib/page-scripts';
const autodark = (freeIllustrations as { autodark: Record<string, string> }).autodark;
const autodarkEntries = Object.entries(autodark);
@@ -35,48 +34,9 @@ const moreCount = illustrationsList.length - 4;
// {% capture_script %} — build the illustrations JS map from the same data.
// skin_color / color[1].prop resolve to empty in Liquid → literal "var()".
const illustrationsJs = autodarkEntries
.map(([key, svg]) => ` "${key}": {\n svg: '${withClass(svg)}',\n },`)
.join('\n \n');
addPageScript(`<script>
let skinColor = "var()",
primaryColor = "var()";
const illustrations = {
${illustrationsJs}
}
const selectIllustrations = document.querySelectorAll(".js-select-illustration"),
currentIllustration = document.getElementById("current-illustration"),
currentIllustrationCode = document.getElementById("current-illustration-code");
document.querySelectorAll(".js-select-illustration").forEach((elem) => {
elem.addEventListener("change", (e) => {
const selectedId = e.target.value,
selectedIllustration = illustrations[selectedId]
currentIllustration.innerHTML = selectedIllustration.svg
})
})
document.querySelectorAll(".js-select-color").forEach((elem) => {
elem.addEventListener("change", (e) => {
primaryColor = e.target.value
document.getElementById("current-illustration-style").style.setProperty("--tblr-illustrations-primary", primaryColor)
})
})
document.querySelectorAll(".js-select-skin-color").forEach((elem) => {
elem.addEventListener("change", (e) => {
skinColor = e.target.value
document.getElementById("current-illustration-style").style.setProperty("--tblr-illustrations-skin", skinColor)
})
})
</script>`);
const illustrationsData = Object.fromEntries(
autodarkEntries.map(([key, svg]) => [key, { svg: withClass(svg) }]),
);
---
<DefaultLayout
@@ -214,4 +174,32 @@ ${illustrationsJs}
</div>
</div>
</div>
<script define:vars={{ illustrationsData }}>
let skinColor = 'var()',
primaryColor = 'var()';
const illustrations = illustrationsData;
const currentIllustration = document.getElementById('current-illustration');
document.querySelectorAll('.js-select-illustration').forEach((elem) => {
elem.addEventListener('change', (e) => {
const selectedIllustration = illustrations[e.target.value];
currentIllustration.innerHTML = selectedIllustration.svg;
});
});
document.querySelectorAll('.js-select-color').forEach((elem) => {
elem.addEventListener('change', (e) => {
primaryColor = e.target.value;
document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-primary', primaryColor);
});
});
document.querySelectorAll('.js-select-skin-color').forEach((elem) => {
elem.addEventListener('change', (e) => {
skinColor = e.target.value;
document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-skin', skinColor);
});
});
</script>
</DefaultLayout>
+19 -15
View File
@@ -12,21 +12,6 @@
// class + slot rendered WITHOUT the .container-xl wrapper. Not implemented in
// the shared DefaultLayout yet — flagged rather than modified here.
import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import { addPageScript } from '@shared/lib/page-scripts';
addPageScript(`<script>
let map;
window.tabler_map = window.tabler_map || {};
document.addEventListener("DOMContentLoaded", function() {
map = new google.maps.Map(document.getElementById("map-google"), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
window.tabler_map["map-google"] = map;
});
</script>`);
---
<DefaultLayout
@@ -37,4 +22,23 @@ addPageScript(`<script>
wrapperFull
>
<div class="map flex-fill" id="map-google"></div>
<!--
is:inline: google-maps loads via a deferred page-lib <script>; see
FormElements1.astro for why a processed (module) script here would run too
early and find window.google undefined.
-->
<script is:inline>
window.tabler_map ??= {};
function initMap() {
const map = new google.maps.Map(document.getElementById('map-google'), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
window.tabler_map['map-google'] = map;
}
document.readyState !== 'loading' ? initMap() : document.addEventListener('DOMContentLoaded', initMap, { once: true });
</script>
</DefaultLayout>
+30 -61
View File
@@ -8,67 +8,6 @@ import Progress from '@ui/Progress.astro';
import ProgressSteps from '@ui/ProgressSteps.astro';
import ProgressBg from '@ui/ProgressBg.astro';
import ProgressDescription from '@ui/ProgressDescription.astro';
import { addPageScript } from '@shared/lib/page-scripts';
addPageScript(`<!-- BEGIN SCRIPT OF ANIMATION -->
<script>
/*
This script is for animation of the last progress bar
It increases the progress bar value by a random amount every 2 seconds until it reaches 100%
When it reaches 100%, it changes the color to green and stops the animation
This is just a demo script to show how to animate the progress bar. You can modify it as needed.
*/
document.addEventListener("DOMContentLoaded", function () {
var width = 0;
var setWidth = function(w) {
width = Math.min(Math.max(w, 0), 100);
progress.querySelector('.progress-bar').style.width = width + '%';
progress.querySelector('.progress-bar').setAttribute('aria-valuenow', width);
document.getElementById('progress-animated-value').innerText = width + '%';
if (width >= 100) {
progress.querySelector('.progress-bar').classList.add('bg-green');
} else {
progress.querySelector('.progress-bar').classList.remove('bg-green');
}
}
var progress = document.getElementById('progress-animated');
var increment = 0;
var interval = setInterval(function () {
increment = Math.ceil(Math.random() * 10);
setWidth(width + increment);
if (width >= 100) {
clearInterval(interval);
}
}, 2000);
document.getElementById('progress-animated-0').addEventListener('click', function() {
setWidth(0);
});
document.getElementById('progress-animated-add-10').addEventListener('click', function() {
setWidth(width + 10);
});
document.getElementById('progress-animated-minus-10').addEventListener('click', function() {
setWidth(width - 10);
});
document.getElementById('progress-animated-100').addEventListener('click', function() {
setWidth(100);
});
});
</script>
<!-- END SCRIPT OF ANIMATION -->`);
---
<DefaultLayout title="Progress" pageHeader="Progress" pageMenu="base.progress">
@@ -193,6 +132,36 @@ addPageScript(`<!-- BEGIN SCRIPT OF ANIMATION -->
<button class="btn btn-sm ms-3" id="progress-animated-minus-10">-10%</button>
<button class="btn btn-sm" id="progress-animated-add-10">+10%</button>
</ButtonList>
<script>
/*
This script is for animation of the last progress bar.
It increases the progress bar value by a random amount every 2 seconds until it reaches 100%.
When it reaches 100%, it changes the color to green and stops the animation.
This is just a demo script to show how to animate the progress bar. You can modify it as needed.
*/
const progress = document.getElementById('progress-animated');
let width = 0;
const setWidth = (w) => {
width = Math.min(Math.max(w, 0), 100);
progress.querySelector('.progress-bar').style.width = `${width}%`;
progress.querySelector('.progress-bar').setAttribute('aria-valuenow', width);
document.getElementById('progress-animated-value').innerText = `${width}%`;
progress.querySelector('.progress-bar').classList.toggle('bg-green', width >= 100);
};
const interval = setInterval(() => {
setWidth(width + Math.ceil(Math.random() * 10));
if (width >= 100) clearInterval(interval);
}, 2000);
document.getElementById('progress-animated-0').addEventListener('click', () => setWidth(0));
document.getElementById('progress-animated-add-10').addEventListener('click', () => setWidth(width + 10));
document.getElementById('progress-animated-minus-10').addEventListener('click', () => setWidth(width - 10));
document.getElementById('progress-animated-100').addEventListener('click', () => setWidth(100));
</script>
</CardBody>
</Card>
</div>
-17
View File
@@ -1,17 +0,0 @@
---
// Single shared implementation of the script emission point. The consuming
// package selects the behavior via the INLINE_PAGE_SCRIPTS vite define in its
// astro.config:
// - preview (false): renders nothing — scripts are registered via
// addPageScript() and drained by <PageScripts /> at the end of <body>,
// mirroring Eleventy's {% capture_script %} model (parity gate).
// - docs (true): emits the script inline at the component site — the docs
// layout has no drain; example markup carries its own scripts.
interface Props {
code: string;
}
const { code } = Astro.props;
---
{import.meta.env.INLINE_PAGE_SCRIPTS && <Fragment set:html={code} />}
-9
View File
@@ -1,9 +0,0 @@
---
// Separate component so drainPageScripts() executes only at this point of the
// render stream (after the page content has been rendered).
import { drainPageScripts } from '@shared/lib/page-scripts';
const scripts = drainPageScripts();
---
<Fragment set:html={scripts.join('\n')} />
+35 -39
View File
@@ -1,49 +1,13 @@
---
// Static inputs, plain selects, a TomSelect "states" multi-select, input groups
// with dropdowns, icon/loader/separated inputs, and a help-icon input.
// Registers the TomSelect init for #select-states via addPageScript (synchronous,
// before any await) — mirrors the include's {% capture_script %}. This is the
// FIRST page script, matching the reference order (select-states precedes the
// FormElements6 scripts).
// TomSelect init for #select-states — FIRST page script, matching the reference
// order (select-states precedes the FormElements6 scripts).
import Icon from '@ui/Icon.astro';
import FormGroup from '@ui/FormGroup.astro';
import selects from '@data/selects.json';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
const states = (selects as { states: { options: Record<string, { name: string; selected?: boolean }> } }).states.options;
const selectStatesScript = `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_select = window.tabler_select || {};
var el;
window.TomSelect && (window.tabler_select["select-states"] = new TomSelect(el = document.getElementById('select-states'), {
copyClassesToDropdown: false,
dropdownParent: 'body',
controlInput: '<input>',
render:{
item: function(data,escape) {
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
option: function(data,escape){
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
},
}));
});
</script>`;
addPageScript(selectStatesScript);
---
<FormGroup label="Static">
@@ -188,4 +152,36 @@ addPageScript(selectStatesScript);
</div>
</div>
</FormGroup>
<InlineScript code={selectStatesScript} />
<!--
is:inline: TomSelect loads via a deferred page-lib <script>. A processed (module)
script runs in document order alongside other deferred/module scripts — since this
tag sits before the library's <script defer>, it would run first and find
window.TomSelect undefined. is:inline keeps this a classic script that runs
synchronously at parse time, so the readyState/DOMContentLoaded guard below
actually delays until after the library has loaded.
-->
<script is:inline>
function initSelectStates() {
window.tabler_select ??= {};
const renderOption = (data, escape) => {
if (data.customProperties) {
return `<div><span class="dropdown-item-indicator">${data.customProperties}</span>${escape(data.text)}</div>`;
}
return `<div>${escape(data.text)}</div>`;
};
window.TomSelect &&
(window.tabler_select['select-states'] = new TomSelect(document.getElementById('select-states'), {
copyClassesToDropdown: false,
dropdownParent: 'body',
controlInput: '<input>',
render: {
item: renderOption,
option: renderOption,
},
}));
}
document.readyState !== 'loading' ? initSelectStates() : document.addEventListener('DOMContentLoaded', initSelectStates, { once: true });
</script>
+16 -14
View File
@@ -3,21 +3,23 @@
// which keeps the `window.tabler_list` registry assignment).
interface Props {
id?: string;
parameters: string;
/** List.js `valueNames` entries — plain strings or `{ attr, name }` sort descriptors. */
valueNames: (string | { attr: string; name: string })[];
}
const { id = 'default', parameters } = Astro.props;
const script = `
window.tabler_list = window.tabler_list || {};
document.addEventListener("DOMContentLoaded", function() {
const list = window.tabler_list["table-${id}"] = new List('table-${id}', {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames: ${parameters}
});
})
`;
const { id = 'default', valueNames } = Astro.props;
const listId = `table-${id}`;
---
<script is:inline set:html={script} />
<script define:vars={{ listId, valueNames }}>
function initTablerList() {
window.tabler_list ??= {};
window.tabler_list[listId] = new List(listId, {
sortClass: 'table-sort',
listClass: 'table-tbody',
valueNames,
});
}
document.readyState !== 'loading' ? initTablerList() : document.addEventListener('DOMContentLoaded', initTablerList, { once: true });
</script>
+20 -21
View File
@@ -1,30 +1,13 @@
---
// Inlines ui/typed.html: renders the seed span (first string) and registers the
// Typed.js init via addPageScript (page-libs: [typed.js] loads the library).
// Port of marketing/hero/side.html.
// Inlines ui/typed.html: renders the seed span (first string) and the Typed.js init
// (page-libs: [typed.js] loads the library), rendered directly here.
import Icon from '@ui/Icon.astro';
import Illustration from '@ui/Illustration.astro';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
// ui/typed.html: strings = include.strings | split: '|'; id defaults to "typed".
const strings = ['more effective', 'more efficient', 'more productive'];
const typedId = 'typed';
// {% capture_script %} → registered synchronously (before any await).
const script = `<script>
document.addEventListener("DOMContentLoaded", function() {
var typed = new Typed('#${typedId}', {
strings: [${strings.map((s) => `'${s}'`).join(', ')}],
typeSpeed: 100,
backSpeed: 50,
backDelay: 1000,
startDelay: 1000,
loop: true,
fade: true
});
});
</script>`;
addPageScript(script);
---
<header class="hero">
@@ -72,4 +55,20 @@ addPageScript(script);
</div>
</div>
</header>
<InlineScript code={script} />
<script define:vars={{ typedId, strings }}>
function initTyped() {
const typed = new Typed(`#${typedId}`, {
strings,
typeSpeed: 100,
backSpeed: 50,
backDelay: 1000,
startDelay: 1000,
loop: true,
fade: true,
});
}
// Typed.js loads via a deferred page-lib <script>, which only runs once parsing
// finishes — this inline script (positioned mid-body) would otherwise run first.
document.readyState !== 'loading' ? initTyped() : document.addEventListener('DOMContentLoaded', initTyped, { once: true });
</script>
@@ -1,110 +1,9 @@
---
// The trailing {% capture_script %} is registered via addPageScript synchronously.
// Port of parts/modals/change-password.html.
import FormHint from '@ui/FormHint.astro';
import Button from '@ui/Button.astro';
import InputGroup from '@ui/InputGroup.astro';
import FormGroup from '@ui/FormGroup.astro';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
const script = `<script>
document.addEventListener("DOMContentLoaded", function () {
function setupPasswordToggle(inputId) {
const input = document.getElementById(inputId);
if (!input) return;
const inputGroup = input.closest('.input-group');
if (!inputGroup) return;
const toggleLink = inputGroup.querySelector('a.link-secondary');
if (!toggleLink) return;
toggleLink.addEventListener('click', function(e) {
e.preventDefault();
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
// Update tooltip text
const tooltipText = isPassword ? 'Hide password' : 'Show password';
this.setAttribute('title', tooltipText);
this.setAttribute('data-bs-original-title', tooltipText);
// Update icon (simple approach - toggle classes if needed)
const svg = this.querySelector('svg');
if (svg) {
const use = svg.querySelector('use');
if (use) {
use.setAttribute('href', isPassword ? '#icon-eye-off' : '#icon-eye');
}
}
});
}
setupPasswordToggle('password-current');
setupPasswordToggle('password-new');
setupPasswordToggle('password-confirm');
const newPasswordInput = document.getElementById('password-new');
const strengthBar = document.getElementById('password-strength');
const strengthText = document.getElementById('password-strength-text');
if (newPasswordInput && strengthBar && strengthText) {
newPasswordInput.addEventListener('input', function() {
const password = this.value;
let strength = 0;
let strengthLabel = '';
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
if (/\\d/.test(password)) strength++;
if (/[^a-zA-Z0-9]/.test(password)) strength++;
const percentage = (strength / 5) * 100;
strengthBar.style.width = percentage + '%';
if (strength <= 2) {
strengthBar.className = 'progress-bar bg-danger';
strengthLabel = 'Weak';
} else if (strength <= 3) {
strengthBar.className = 'progress-bar bg-warning';
strengthLabel = 'Fair';
} else if (strength <= 4) {
strengthBar.className = 'progress-bar bg-info';
strengthLabel = 'Good';
} else {
strengthBar.className = 'progress-bar bg-success';
strengthLabel = 'Strong';
}
strengthText.textContent = password ? strengthLabel : '';
});
}
const confirmPasswordInput = document.getElementById('password-confirm');
const matchError = document.getElementById('password-match-error');
if (newPasswordInput && confirmPasswordInput && matchError) {
function validateMatch() {
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (confirmPassword && newPassword !== confirmPassword) {
confirmPasswordInput.classList.add('is-invalid');
matchError.classList.remove('d-none');
} else {
confirmPasswordInput.classList.remove('is-invalid');
matchError.classList.add('d-none');
}
}
newPasswordInput.addEventListener('input', validateMatch);
confirmPasswordInput.addEventListener('input', validateMatch);
}
});
</script>`;
addPageScript(script);
---
<div class="modal-header">
@@ -142,4 +41,81 @@ addPageScript(script);
<Button type="submit" text="Update password" color="primary" block class="mt-4" />
</form>
</div>
<InlineScript code={script} />
<script>
function setupPasswordToggle(inputId) {
const input = document.getElementById(inputId);
if (!input) return;
const inputGroup = input.closest('.input-group');
if (!inputGroup) return;
const toggleLink = inputGroup.querySelector('a.link-secondary');
if (!toggleLink) return;
toggleLink.addEventListener('click', function (e) {
e.preventDefault();
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
// Update tooltip text
const tooltipText = isPassword ? 'Hide password' : 'Show password';
this.setAttribute('title', tooltipText);
this.setAttribute('data-bs-original-title', tooltipText);
// Update icon (simple approach - toggle classes if needed)
const svg = this.querySelector('svg');
const use = svg?.querySelector('use');
use?.setAttribute('href', isPassword ? '#icon-eye-off' : '#icon-eye');
});
}
setupPasswordToggle('password-current');
setupPasswordToggle('password-new');
setupPasswordToggle('password-confirm');
const newPasswordInput = document.getElementById('password-new');
const strengthBar = document.getElementById('password-strength');
const strengthText = document.getElementById('password-strength-text');
if (newPasswordInput && strengthBar && strengthText) {
newPasswordInput.addEventListener('input', function () {
const password = this.value;
let strength = 0;
if (password.length >= 8) strength++;
if (password.length >= 12) strength++;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
if (/\d/.test(password)) strength++;
if (/[^a-zA-Z0-9]/.test(password)) strength++;
const percentage = (strength / 5) * 100;
strengthBar.style.width = `${percentage}%`;
const strengthLevels = ['bg-danger', 'bg-danger', 'bg-danger', 'bg-warning', 'bg-info', 'bg-success'];
const strengthLabels = ['Weak', 'Weak', 'Weak', 'Fair', 'Good', 'Strong'];
strengthBar.className = `progress-bar ${strengthLevels[strength]}`;
strengthText.textContent = password ? strengthLabels[strength] : '';
});
}
const confirmPasswordInput = document.getElementById('password-confirm');
const matchError = document.getElementById('password-match-error');
if (newPasswordInput && confirmPasswordInput && matchError) {
const validateMatch = () => {
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
if (confirmPassword && newPassword !== confirmPassword) {
confirmPasswordInput.classList.add('is-invalid');
matchError.classList.remove('d-none');
} else {
confirmPasswordInput.classList.remove('is-invalid');
matchError.classList.add('d-none');
}
};
newPasswordInput.addEventListener('input', validateMatch);
confirmPasswordInput.addEventListener('input', validateMatch);
}
</script>
@@ -1,24 +1,8 @@
---
// The trailing {% capture_script %} is registered via addPageScript synchronously.
// Port of parts/modals/confirm-delete.html.
import Icon from '@ui/Icon.astro';
import Button from '@ui/Button.astro';
import ModalClose from './ModalClose.astro';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
const script = `<script>
document.addEventListener("DOMContentLoaded", function () {
const checkbox = document.getElementById("confirm-delete-checkbox");
const deleteButton = document.getElementById("confirm-delete-button");
if (checkbox && deleteButton) {
checkbox.addEventListener("change", function() {
deleteButton.disabled = !this.checked;
});
}
});
</script>`;
addPageScript(script);
---
<ModalClose />
@@ -61,4 +45,13 @@ addPageScript(script);
</div>
</div>
</div>
<InlineScript code={script} />
<script>
const checkbox = document.getElementById('confirm-delete-checkbox');
const deleteButton = document.getElementById('confirm-delete-button');
if (checkbox && deleteButton) {
checkbox.addEventListener('change', function () {
deleteButton.disabled = !this.checked;
});
}
</script>
-3
View File
@@ -2,7 +2,6 @@
import ThemeSettings from '@ui/ThemeSettings.astro';
import { site } from '@shared/lib/site';
import PageModals from '@shared/components/PageModals.astro';
import PageScripts from '@shared/components/PageScripts.astro';
import libs from '@tabler/core/libs.json';
interface Props {
@@ -135,8 +134,6 @@ const libJsFiles = (head: boolean) =>
<!-- BEGIN DEMO SCRIPTS -->
<script is:inline src={`${base}/preview/js/demo${min}.js`} defer></script>
<!-- END DEMO SCRIPTS -->
{/* equivalent of {% scripts %}: snippets registered by the page components */}
<PageScripts />
<!-- BEGIN PAGE SCRIPTS -->
<script is:inline>
/*
+3 -1
View File
@@ -35,5 +35,7 @@ const target = isAbsolute ? url : `${base}${url}`;
<meta http-equiv="refresh" content={`0; url=${target}`} />
<meta name="robots" content="noindex" />
<noscript><a href={target}>Click here if you are not redirected.</a></noscript>
<script is:inline set:html={`location=${JSON.stringify(target)};`}></script>
<script define:vars={{ target }}>
location = target;
</script>
</html>
+34 -65
View File
@@ -1,7 +1,8 @@
// The config is built as a plain object (field order mirrors the Liquid template)
// and serialized to formatted JS at the end. The generated code is JS-token
// equivalent (not byte-identical) to the Eleventy output — the parity comparator
// normalizes script content at the token level (quotes, trailing commas, spacing).
// Port of ui/chart.html (Liquid) — generator of the ApexCharts <style> block and
// config object. chartStyle() returns plain CSS text (rendered via set:html — inert,
// not executable, so there's no injection surface to harden). chartConfig() returns a
// real, JSON-serializable config object, rendered by Chart.astro via
// <script define:vars> — no string-built script here at all.
type Serie = {
'name'?: string
@@ -69,46 +70,6 @@ export type ChartData = {
const escapeHtml = (value: string) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
/** Verbatim JS code embedded in the config (e.g. formatter functions). */
class RawJs {
readonly code: string
constructor(code: string) {
this.code = code
}
}
const raw = (code: string) => new RawJs(code)
/**
* Serialize a config object to formatted JS: unquoted keys, tab indentation,
* `undefined` entries omitted, small objects and primitive arrays kept inline.
*/
function formatJs(value: unknown, indent: number): string {
if (value instanceof RawJs) return value.code
if (typeof value === 'string') return JSON.stringify(value)
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
const tab = '\t'.repeat(indent)
const isLeaf = (v: unknown) => typeof v !== 'object' || v === null || v instanceof RawJs
if (Array.isArray(value)) {
if (value.length === 0) return '[]'
const items = value.map((item) => formatJs(item, indent + 1))
if (value.every(isLeaf)) {
return `[${items.join(', ')}]`
}
return `[\n${items.map((item) => `${tab}\t${item}`).join(',\n')}\n${tab}]`
}
const entries = Object.entries(value as Record<string, unknown>).filter(([, v]) => v !== undefined)
if (entries.length === 0) return '{}'
// a `//N` key suffix emits a duplicate key (mirrors Liquid emitting e.g. two
// `tooltip:` entries where the later one shadows the earlier — last wins in JS)
const parts = entries.map(([key, v]) => `${key.replace(/\/\/\d+$/, '')}: ${formatJs(v, indent + 1)}`)
const inline = `{ ${parts.join(', ')} }`
// inline only "flat" objects (primitives, raw code, primitive arrays) that fit on one line
const flat = entries.every(([, v]) => isLeaf(v) || (Array.isArray(v) && v.every(isLeaf)))
if (flat && !inline.includes('\n') && inline.length <= 100) return inline
return `{\n${parts.map((part) => `${tab}\t${part}`).join(',\n')}\n${tab}}`
}
/** Equivalent of the datetime loop: consecutive days from start-date (YYYY-MM-DD). */
function datetimeLabels(startDate: string, count: number): string[] {
const start = new Date(`${startDate}T00:00:00Z`)
@@ -118,13 +79,12 @@ function datetimeLabels(startDate: string, count: number): string[] {
})
}
export function chartSnippet(opts: { id: string; chartId: string; data: ChartData; height: number }): string {
const { id, chartId, data, height } = opts
/** The :root custom-property block (chart series colors, area fill gradient). */
export function chartStyle(opts: { id: string; data: ChartData }): string {
const { id, data } = opts
const type = data.type ?? 'bar'
const series = data.series ?? []
const isRound = type === 'pie' || type === 'donut' || type === 'radialBar'
// --- <style> ---
let css = ''
for (const [i, serie] of series.entries()) {
const color = serie.color ?? data.color ?? 'primary'
@@ -135,9 +95,25 @@ export function chartSnippet(opts: { id: string; chartId: string; data: ChartDat
css += ` --chart-${id}-fill-0: color-mix(in srgb, transparent, var(--tblr-primary) 16%);\n`
css += ` --chart-${id}-fill-1: color-mix(in srgb, transparent, var(--tblr-primary) 16%);\n`
}
const style = `<style>\n :root {\n${css} }\n</style>`
return `<style>\n :root {\n${css} }\n</style>`
}
/**
* The ApexCharts config, as a real JSON-serializable object (rendered via
* <script define:vars> — no string-built script). The one exception is
* `x-formatter`: a handful of charts.json entries carry a raw JS expression for
* the x-axis label formatter (e.g. `val + "K"`), which can't be represented as
* data. It comes back as `xFormatterExpr` instead of being embedded in `config`,
* for the caller to turn into a real function with `new Function` — see
* Chart.astro. This is data-driven (from our own charts.json, not user input),
* same trust level as everything else here.
*/
export function chartConfig(opts: { id: string; data: ChartData; height: number }): { config: Record<string, unknown>; xFormatterExpr?: string } {
const { id, data, height } = opts
const type = data.type ?? 'bar'
const series = data.series ?? []
const isRound = type === 'pie' || type === 'donut' || type === 'radialBar'
// --- config ---
const config: Record<string, unknown> = {}
config.chart = {
@@ -253,15 +229,14 @@ export function chartSnippet(opts: { id: string; chartId: string; data: ChartDat
}
if (data['show-data-labels']) {
config['dataLabels//2'] = { enabled: true }
// Liquid emits a second `dataLabels:` here, shadowing the earlier one — a
// plain reassignment (not a merge) reproduces that.
config.dataLabels = { enabled: true }
}
if (data.categories || data.datetime) {
config.xaxis = {
labels: {
padding: 0,
formatter: data['x-formatter'] ? raw(`function (val) { return ${data['x-formatter']} }`) : undefined,
},
labels: { padding: 0 },
tooltip: { enabled: false },
axisBorder: type === 'area' || type === 'bar' ? { show: false } : undefined,
categories: data.categories?.map(String),
@@ -300,7 +275,9 @@ export function chartSnippet(opts: { id: string; chartId: string; data: ChartDat
: { show: false }
if (data['hide-tooltip'] || type === 'pie' || type === 'donut') {
config['tooltip//2'] = {
// Same reassignment-not-merge behavior as dataLabels above — the earlier
// `theme: 'dark'` is intentionally dropped, matching the Liquid source.
config.tooltip = {
enabled: data['hide-tooltip'] ? false : undefined,
fillSeriesColor: type === 'pie' || type === 'donut' ? false : undefined,
}
@@ -314,13 +291,5 @@ export function chartSnippet(opts: { id: string; chartId: string; data: ChartDat
config.markers = { size: 2 }
}
// environment === 'development' → window.tabler_chart registry
const script = `<script>
\tdocument.addEventListener("DOMContentLoaded", function () {
\t\twindow.tabler_chart = window.tabler_chart || {};
\t\twindow.ApexCharts && (window.tabler_chart["chart-${chartId}"] = new ApexCharts(document.getElementById('chart-${id}'), ${formatJs(config, 2)})).render();
\t});
</script>`
return `${style}\n${script}`
return { config, xFormatterExpr: data['x-formatter'] }
}
-18
View File
@@ -1,18 +0,0 @@
// Equivalent of eleventy {% capture_script %} / {% scripts %}: components in the
// page content register snippets (<style>/<script>) during rendering, and BaseLayout
// emits them at the end of <body>. This works because Astro streams the render
// sequentially: the <slot /> content renders before the tail of the layout.
const scripts: string[] = []
export function addPageScript(html: string): void {
// No dedup: Liquid's {% capture_script %} emits every snippet, even byte-identical
// ones (e.g. two identical star-rating inits on stars-rating.html).
scripts.push(html)
}
/** Returns the collected snippets and clears the buffer (called once per page, in BaseLayout). */
export function drainPageScripts(): string[] {
const out = [...scripts]
scripts.length = 0
return out
}
+33 -38
View File
@@ -37,44 +37,8 @@ const rows = (people as Record<string, any>[]).map((person, i) => {
});
// Inline <script> from advanced-table.html (renders in the page body).
const advancedJson = JSON.stringify((tableProperties as Record<string, any>)['advanced-table']);
const advancedTable = (tableProperties as Record<string, any>)['advanced-table'];
const perPageDefault = perPage[1];
const script = `
const advancedTable = ${advancedJson}
const setPageListItems = e => {
window.tabler_list["${tableId}"].page = parseInt(e.target.dataset.value)
window.tabler_list["${tableId}"].update()
document.querySelector("#page-count").innerHTML = e.target.dataset.value
}
window.tabler_list = window.tabler_list || {}
document.addEventListener("DOMContentLoaded", function() {
const list = window.tabler_list["${tableId}"] = new List('${tableId}', {
sortClass: 'table-sort',
listClass: 'table-tbody',
page: parseInt("${perPageDefault}"),
pagination: {
item: value => {
return \`<li class="page-item"><a class="page-link cursor-pointer">\${value.page}</a></li>\`
},
innerWindow: 1,
outerWindow: 1,
left: 0,
right: 0,
},
valueNames: advancedTable.headers.map(header => header['data-sort'])
});
const searchInput = document.querySelector('#${tableId}-search');
if (searchInput) {
searchInput.addEventListener('input', () => {
list.search(searchInput.value)
})
}
})
`;
---
{/*
@@ -207,4 +171,35 @@ const script = `
</div>
</div>
<script is:inline set:html={script} />
<script define:vars={{ tableId, advancedTable, perPageDefault }}>
// Astro wraps define:vars scripts in an IIFE, so this needs an explicit window
// assignment to stay reachable from the onclick="setPageListItems(event)" markup below.
window.setPageListItems = function setPageListItems(e) {
window.tabler_list[tableId].page = parseInt(e.target.dataset.value);
window.tabler_list[tableId].update();
document.querySelector('#page-count').innerHTML = e.target.dataset.value;
};
function initAdvancedTable() {
window.tabler_list ??= {};
const list = (window.tabler_list[tableId] = new List(tableId, {
sortClass: 'table-sort',
listClass: 'table-tbody',
page: parseInt(perPageDefault),
pagination: {
item: (value) => `<li class="page-item"><a class="page-link cursor-pointer">${value.page}</a></li>`,
innerWindow: 1,
outerWindow: 1,
left: 0,
right: 0,
},
valueNames: advancedTable.headers.map((header) => header['data-sort']),
}));
const searchInput = document.querySelector(`#${tableId}-search`);
searchInput?.addEventListener('input', () => list.search(searchInput.value));
}
document.readyState !== 'loading' ? initAdvancedTable() : document.addEventListener('DOMContentLoaded', initAdvancedTable, { once: true });
</script>
+26 -9
View File
@@ -1,8 +1,6 @@
---
import charts from '@data/charts.json';
import { addPageScript } from '@shared/lib/page-scripts';
import { chartSnippet, type ChartData } from '@shared/lib/chart-script';
import InlineScript from '@shared/components/InlineScript.astro';
import { chartStyle, chartConfig, type ChartData } from '@shared/lib/chart-script';
interface Props {
chartId: string;
@@ -32,17 +30,36 @@ if (data?.extend) {
data = { ...chartsData[data.extend], ...chartsData[chartId] };
}
const script = data ? chartSnippet({ id, chartId, data, height }) : '';
if (data) {
addPageScript(script);
}
// The :root custom-property block stays a plain string rendered via set:html — it's
// inert CSS text (no injection surface), not executable code.
const style = data ? chartStyle({ id, data }) : '';
const { config, xFormatterExpr } = data ? chartConfig({ id, data, height }) : { config: undefined, xFormatterExpr: undefined };
const chartKey = `chart-${chartId}`;
const elementId = `chart-${id}`;
---
{
data && (
<Fragment>
<div id={`chart-${id}`} class:list={['position-relative', className]} />
<InlineScript code={script} />
<div id={elementId} class:list={['position-relative', className]} />
<Fragment set:html={style} />
<script define:vars={{ chartKey, elementId, config, xFormatterExpr }}>
window.tabler_chart ??= {};
function initChart() {
// x-formatter is a raw JS expression from charts.json (e.g. `val + "K"`) — the
// only piece of this config that can't be plain data. Reconstructed here
// rather than baked into `config` by chartConfig(), which returns it separately.
if (xFormatterExpr && config.xaxis?.labels) {
config.xaxis.labels.formatter = new Function('val', `return (${xFormatterExpr})`);
}
window.ApexCharts &&
(window.tabler_chart[chartKey] = new ApexCharts(document.getElementById(elementId), config)).render();
}
document.readyState !== 'loading' ? initChart() : document.addEventListener('DOMContentLoaded', initChart, { once: true });
</script>
</Fragment>
)
}
+50 -54
View File
@@ -13,60 +13,56 @@ const { id = "active-users-4", height = 350, class: className = "", title = "Act
const chartId = `chart-${id}`;
const fixedTitle = title.toUpperCase();
const script = `<script>
document.addEventListener("DOMContentLoaded", () => {
window.tabler_chart = window.tabler_chart || {};
window.ApexCharts &&
(window.tabler_chart[${JSON.stringify(chartId)}] = new ApexCharts(
document.getElementById(${JSON.stringify(chartId)}),
{
series: [${value}],
chart: {
height: ${height},
type: "radialBar",
},
plotOptions: {
radialBar: {
startAngle: -120,
endAngle: 120,
hollow: {
margin: 15,
size: "70%",
},
dataLabels: {
name: {
show: true,
offsetY: -20,
fontSize: "14px",
fontFamily: "inherit",
fontWeight: 400,
color: "var(--tblr-secondary)",
},
value: {
show: true,
fontSize: "30px",
fontWeight: 700,
fontFamily: "inherit",
offsetY: 0,
color: "var(--tblr-body-color)",
formatter: function (val) {
return val + "%";
},
},
},
},
},
colors: ["var(--tblr-primary)"],
stroke: {
lineCap: "round",
},
labels: ["${fixedTitle}"],
}
)).render();
});</script>`;
---
<div id={chartId} class={`position-relative ${className}`}></div>
<Fragment set:html={script} />
<script define:vars={{ chartId, height, value, fixedTitle }}>
function initRadialChart() {
window.tabler_chart ??= {};
window.ApexCharts &&
(window.tabler_chart[chartId] = new ApexCharts(document.getElementById(chartId), {
series: [value],
chart: {
height,
type: 'radialBar',
},
plotOptions: {
radialBar: {
startAngle: -120,
endAngle: 120,
hollow: {
margin: 15,
size: '70%',
},
dataLabels: {
name: {
show: true,
offsetY: -20,
fontSize: '14px',
fontFamily: 'inherit',
fontWeight: 400,
color: 'var(--tblr-secondary)',
},
value: {
show: true,
fontSize: '30px',
fontWeight: 700,
fontFamily: 'inherit',
offsetY: 0,
color: 'var(--tblr-body-color)',
formatter: (val) => `${val}%`,
},
},
},
},
colors: ['var(--tblr-primary)'],
stroke: {
lineCap: 'round',
},
labels: [fixedTitle],
})).render();
}
document.readyState !== 'loading' ? initRadialChart() : document.addEventListener('DOMContentLoaded', initRadialChart, { once: true });
</script>
+40 -72
View File
@@ -1,8 +1,5 @@
---
// Equivalent of ui/chart-sparkline.html
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
type?: string;
@@ -40,79 +37,50 @@ const classes = [
small && 'chart-sparkline-sm',
]
let script = '';
if (id) {
script = `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_chart = window.tabler_chart || {};
const seriesData = String(data ?? '').split(',').map(Number);
const chartKey = `sparkline-${id}`;
window.ApexCharts && (window.tabler_chart["sparkline-${id}"] = new ApexCharts(document.getElementById('sparkline-${id}'), {
const chartOptions = id
? {
chart: {
type: "${chartType}",
type: chartType,
fontFamily: 'inherit',
height: ${heightPx},
${isSquare ? ` width: ${heightPx},\n` : ''} animations: {
enabled: false
},
sparkline: {
enabled: true
},
height: heightPx,
...(isSquare ? { width: heightPx } : {}),
animations: { enabled: false },
sparkline: { enabled: true },
},
tooltip: {
enabled: false,
},
${
type === 'donut'
? ` plotOptions: {
radialBar: {
hollow: {
margin: 0,
size: '75%'
},
track: {
margin: 0
},
dataLabels: {
show: false
tooltip: { enabled: false },
...(type === 'donut'
? {
plotOptions: {
radialBar: {
hollow: { margin: 0, size: '75%' },
track: { margin: 0 },
dataLabels: { show: false },
},
},
}
}
},
`
: ''
}${
type === 'area'
? ` fill: {
gradient: {
opacityFrom: [.1, .1]
}
},
`
: ''
}${
type === 'area' || type === 'line'
? ` stroke: {
width: 2,
lineCap: "round",
},
`
: ''
}${
type === 'donut'
? ` colors: ['var(--tblr-${color})'],
series: [${data}],
`
: ` series: [{
color: 'var(--tblr-${color})',
data: [${data}]
}],
`
} })).render();
});
</script>`;
addPageScript(script);
}
: {}),
...(type === 'area' ? { fill: { gradient: { opacityFrom: [0.1, 0.1] } } } : {}),
...(type === 'area' || type === 'line' ? { stroke: { width: 2, lineCap: 'round' } } : {}),
...(type === 'donut'
? { colors: [`var(--tblr-${color})`], series: seriesData }
: { series: [{ color: `var(--tblr-${color})`, data: seriesData }] }),
}
: undefined;
---
{id && <div class:list={classes} id={`sparkline-${id}`} />}
{id && <InlineScript code={script} />}
{id && <div class:list={classes} id={chartKey} />}
{
id && (
<script define:vars={{ chartKey, chartOptions }}>
function initSparkline() {
window.tabler_chart ??= {};
window.ApexCharts && (window.tabler_chart[chartKey] = new ApexCharts(document.getElementById(chartKey), chartOptions)).render();
}
document.readyState !== 'loading' ? initSparkline() : document.addEventListener('DOMContentLoaded', initSparkline, { once: true });
</script>
)
}
+26 -28
View File
@@ -1,9 +1,7 @@
---
// The Coloris init <script> is captured via {% capture_script %} in Liquid, so it
// is registered with addPageScript synchronously here. We mirror the development
// build, which wraps the instance in window.tabler_colorpicker.
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
// Port of ui/colorpicker.html.
// The Coloris init <script> renders directly here (no more page-scripts registry),
// wrapping the instance in window.tabler_colorpicker.
import site from '@data/site.json';
interface Props {
@@ -26,34 +24,34 @@ const {
} = Astro.props;
const colors = site.colors as Record<string, { prop: string }>;
const swatchProps = Object.values(colors).map((color) => color.prop);
const swatches = Object.values(colors)
.map((color) => `\n\t\t\t\twindow.getComputedStyle(document.body).getPropertyValue('${color.prop}'),`)
.join('');
const script = `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_colorpicker = window.tabler_colorpicker || {};
window.Coloris && (window.tabler_colorpicker["colorpicker-${id}"] = Coloris({
el: "#colorpicker-${id}",
selectInput: false,
alpha: ${alpha ? 'true' : 'false'},
${format ? `format: "${format}",` : ''}
${swatchesOnly ? 'swatchesOnly: true,' : ''}
swatches: [${swatches}
],
}))
})
</script>`;
addPageScript(script);
const colorpickerId = `colorpicker-${id}`;
const colorpickerSelector = `#colorpicker-${id}`;
---
<input
type="text"
class={`form-control d-block${className ? ` ${className}` : ''}`}
id={`colorpicker-${id}`}
id={colorpickerId}
value={value}
/>
<InlineScript code={script} />
<script define:vars={{ colorpickerId, colorpickerSelector, alpha, format, swatchesOnly, swatchProps }}>
function initColorpicker() {
window.tabler_colorpicker ??= {};
const swatches = swatchProps.map((prop) => window.getComputedStyle(document.body).getPropertyValue(prop));
window.Coloris &&
(window.tabler_colorpicker[colorpickerId] = Coloris({
el: colorpickerSelector,
selectInput: false,
alpha,
...(format ? { format } : {}),
...(swatchesOnly ? { swatchesOnly: true } : {}),
swatches,
}));
}
document.readyState !== 'loading' ? initColorpicker() : document.addEventListener('DOMContentLoaded', initColorpicker, { once: true });
</script>
+25 -32
View File
@@ -1,12 +1,8 @@
---
// Renders the datepicker input (icon / icon-prepend / inline / plain layouts)
// and registers its Litepicker init <script> via addPageScript (the include's
// {% capture_script %}). Registration is synchronous in the frontmatter — see
// src/lib/page-scripts.ts. The reference build is `environment == 'development'`,
// so the init assigns into window.tabler_datepicker[...].
// Port of shared/includes/ui/datepicker.html.
// Renders the datepicker input (icon / icon-prepend / inline / plain layouts) and its
// Litepicker init <script>, rendered directly here (no more page-scripts registry).
import Icon from './Icon.astro';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
@@ -16,8 +12,6 @@ interface Props {
/** 'icon' | 'icon-prepend' | undefined (plain) */
layout?: string;
inline?: boolean;
/** show-scripts: emit the init inline instead of via capture_script (unused here) */
showScripts?: boolean;
}
const {
@@ -27,7 +21,6 @@ const {
class: className,
layout,
inline,
showScripts,
} = Astro.props;
// The Litepicker chevron buttons. In the reference the icons carry the junk
@@ -37,26 +30,7 @@ const chevronLeft =
const chevronRight =
'<!-- Download SVG icon from http://tabler.io/icons/icon/chevron-right -->\n\t<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="icon"><path d="M9 6l6 6l-6 6" /></svg>';
const script = id
? `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_datepicker = window.tabler_datepicker || {};
window.Litepicker && (window.tabler_datepicker["datepicker-${id}"] = new Litepicker({
element: document.getElementById('datepicker-${id}'),
buttonText: {
previousMonth: \`${chevronLeft}\`,
nextMonth: \`${chevronRight}\`,
},
${inline ? 'inlineMode: true,' : ''}
}));
});
</script>`
: '';
if (id && script && !showScripts) {
addPageScript(script);
}
const datepickerId = id ? `datepicker-${id}` : undefined;
---
{
@@ -92,5 +66,24 @@ if (id && script && !showScripts) {
/>
))
}
{showScripts && id && <Fragment set:html={script} />}
{!showScripts && id && script && <InlineScript code={script} />}
{
id && (
<script define:vars={{ datepickerId, chevronLeft, chevronRight, inline }}>
function initDatepicker() {
window.tabler_datepicker ??= {};
window.Litepicker &&
(window.tabler_datepicker[datepickerId] = new Litepicker({
element: document.getElementById(datepickerId),
buttonText: {
previousMonth: chevronLeft,
nextMonth: chevronRight,
},
...(inline ? { inlineMode: true } : {}),
}));
}
document.readyState !== 'loading' ? initDatepicker() : document.addEventListener('DOMContentLoaded', initDatepicker, { once: true });
</script>
)
}
+15 -17
View File
@@ -1,11 +1,8 @@
---
// The Dropzone init <script> is captured via {% capture_script %} in Liquid, so it
// is registered with addPageScript synchronously here. We mirror the development
// build, which wraps the instance in window.tabler_dropzone.
// Port of shared/includes/ui/dropzone.html.
// The Dropzone init <script> renders directly here (no more page-scripts registry),
// wrapping the instance in window.tabler_dropzone.
// Liquid action="{{ page | relative }}/" resolves to "./" for root-level pages.
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
multiple?: boolean;
@@ -16,18 +13,11 @@ interface Props {
const { id, multiple, custom, text = 'Text', description = 'Description' } = Astro.props;
const script = `<script>
window.tabler_dropzone = window.tabler_dropzone || {};
document.addEventListener("DOMContentLoaded", function() {
window.tabler_dropzone["dropzone-${id}"] = new Dropzone("#dropzone-${id}")
})
</script>`;
addPageScript(script);
const dropzoneId = `dropzone-${id}`;
const dropzoneSelector = `#dropzone-${id}`;
---
<form class="dropzone" id={`dropzone-${id}`} action="./" autocomplete="off" novalidate>
<form class="dropzone" id={dropzoneId} action="./" autocomplete="off" novalidate>
<div class="fallback">
{/* emit a bare boolean attribute (empty string) to match Liquid's {% if %}multiple */}
<input name="file" type="file" multiple={multiple ? '' : undefined} />
@@ -41,4 +31,12 @@ addPageScript(script);
)
}
</form>
<InlineScript code={script} />
<script define:vars={{ dropzoneId, dropzoneSelector }}>
window.tabler_dropzone ??= {};
function initDropzone() {
window.tabler_dropzone[dropzoneId] = new Dropzone(dropzoneSelector);
}
document.readyState !== 'loading' ? initDropzone() : document.addEventListener('DOMContentLoaded', initDropzone, { once: true });
</script>
+46 -83
View File
@@ -1,9 +1,8 @@
---
// The init <script> is captured via {% capture_script %} in the Liquid source,
// so it is registered with addPageScript synchronously here (before any await).
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
// Port of ui/fullcalendar.html.
// The init <script> renders directly here (no more page-scripts registry). Sample
// events carry [day, hour, minute] tuples instead of new Date(...) expressions — the
// actual Date objects are built at runtime, relative to currentYear/currentMonth.
interface Props {
id?: string;
/** sample-events param in ui/fullcalendar.html — injects the demo events array */
@@ -11,84 +10,48 @@ interface Props {
}
const { id = 'default', sampleEvents } = Astro.props;
const calendarId = `calendar-${id}`;
const events = `events: [
{
title: "Offsite Retreat",
start: new Date(currentYear, currentMonth, 2, 9, 0),
end: new Date(currentYear, currentMonth, 4, 17, 0),
color: 'var(--tblr-red)',
backgroundColor: 'var(--tblr-red-lt)',
borderColor: 'var(--tblr-red-200)',
},
{
title: "Monthly Planning",
start: new Date(currentYear, currentMonth, 1, 10, 0),
end: new Date(currentYear, currentMonth, 1, 11, 30),
},
{
title: "Marketing Strategy Call",
start: new Date(currentYear, currentMonth, 4, 14, 0),
end: new Date(currentYear, currentMonth, 4, 15, 0)
},
{
title: "Design Sprint",
start: new Date(currentYear, currentMonth, 7, 9, 0),
end: new Date(currentYear, currentMonth, 7, 12, 0)
},
{
title: "Dev Team Check-in",
start: new Date(currentYear, currentMonth, 10, 11, 0),
end: new Date(currentYear, currentMonth, 10, 11, 30)
},
{
title: "Customer Feedback Review",
start: new Date(currentYear, currentMonth, 13, 13, 0),
end: new Date(currentYear, currentMonth, 13, 14, 0)
},
{
title: "Mid-Month Review",
start: new Date(currentYear, currentMonth, 15, 10, 30),
end: new Date(currentYear, currentMonth, 15, 11, 30)
},
{
title: "Webinar: Product Update",
start: new Date(currentYear, currentMonth, 18, 16, 0),
end: new Date(currentYear, currentMonth, 18, 17, 0)
},
{
title: "Sales Training",
start: new Date(currentYear, currentMonth, 21, 9, 30),
end: new Date(currentYear, currentMonth, 21, 11, 0)
},
{
title: "Company All-Hands",
start: new Date(currentYear, currentMonth, 25, 15, 0),
end: new Date(currentYear, currentMonth, 25, 16, 0)
},
{
title: "End-of-Month Wrap-up",
start: new Date(currentYear, currentMonth, 31, 10, 0),
end: new Date(currentYear, currentMonth, 31, 11, 0)
}
],`;
const script = `<script>
document.addEventListener('DOMContentLoaded', function () {
var calendarEl = document.getElementById('calendar-${id}');
var currentYear = new Date().getFullYear();
var currentMonth = new Date().getMonth();
var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
${sampleEvents ? events : ''}
});
calendar.render();
});
</script>`;
addPageScript(script);
const sampleEventData = sampleEvents
? [
{
title: 'Offsite Retreat',
start: [2, 9, 0],
end: [4, 17, 0],
color: 'var(--tblr-red)',
backgroundColor: 'var(--tblr-red-lt)',
borderColor: 'var(--tblr-red-200)',
},
{ title: 'Monthly Planning', start: [1, 10, 0], end: [1, 11, 30] },
{ title: 'Marketing Strategy Call', start: [4, 14, 0], end: [4, 15, 0] },
{ title: 'Design Sprint', start: [7, 9, 0], end: [7, 12, 0] },
{ title: 'Dev Team Check-in', start: [10, 11, 0], end: [10, 11, 30] },
{ title: 'Customer Feedback Review', start: [13, 13, 0], end: [13, 14, 0] },
{ title: 'Mid-Month Review', start: [15, 10, 30], end: [15, 11, 30] },
{ title: 'Webinar: Product Update', start: [18, 16, 0], end: [18, 17, 0] },
{ title: 'Sales Training', start: [21, 9, 30], end: [21, 11, 0] },
{ title: 'Company All-Hands', start: [25, 15, 0], end: [25, 16, 0] },
{ title: 'End-of-Month Wrap-up', start: [31, 10, 0], end: [31, 11, 0] },
]
: undefined;
---
<div id={`calendar-${id}`}></div>
<InlineScript code={script} />
<div id={calendarId}></div>
<script define:vars={{ calendarId, sampleEventData }}>
function initCalendar() {
const calendarEl = document.getElementById(calendarId);
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth();
const toDate = ([day, hour, minute]) => new Date(currentYear, currentMonth, day, hour, minute);
const calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
...(sampleEventData
? { events: sampleEventData.map((event) => ({ ...event, start: toDate(event.start), end: toDate(event.end) })) }
: {}),
});
calendar.render();
}
document.readyState !== 'loading' ? initCalendar() : document.addEventListener('DOMContentLoaded', initCalendar, { once: true });
</script>
+19 -19
View File
@@ -1,11 +1,8 @@
---
// The Plyr init <script> is captured via {% capture_script %} in Liquid, so it is
// registered with addPageScript synchronously here. We mirror the development
// build, which wraps the instance in window.tabler_player.
// Port of shared/includes/ui/inline-player.html.
// The Plyr init <script> renders directly here (no more page-scripts registry),
// wrapping the instance in window.tabler_player.
// Renders nothing unless both id and embedId are provided (Liquid: {% if id and include.embed-id %}).
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
type?: string;
@@ -16,22 +13,25 @@ interface Props {
const { id, type = 'youtube', embedId } = Astro.props;
const enabled = Boolean(id && embedId);
let script = '';
if (enabled) {
script = `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_player = window.tabler_player || {};
window.Plyr && (window.tabler_player["player-${id}"] = new Plyr('#player-${id}'));
});
</script>`;
addPageScript(script);
}
const playerId = `player-${id}`;
const playerSelector = `#player-${id}`;
---
{
enabled && (
<div id={`player-${id}`} data-plyr-provider={type} data-plyr-embed-id={embedId} />
<div id={playerId} data-plyr-provider={type} data-plyr-embed-id={embedId} />
)
}
{
enabled && (
<script define:vars={{ playerId, playerSelector }}>
window.tabler_player ??= {};
function initPlayer() {
window.Plyr && (window.tabler_player[playerId] = new Plyr(playerSelector));
}
document.readyState !== 'loading' ? initPlayer() : document.addEventListener('DOMContentLoaded', initPlayer, { once: true });
</script>
)
}
{enabled && <InlineScript code={script} />}
+34 -39
View File
@@ -1,11 +1,9 @@
---
// The Mapbox GL init <script> is captured via {% capture_script %} in Liquid, so it
// is registered with addPageScript synchronously here. We mirror the development
// build, which wraps the instance in window.tabler_map.
// Port of shared/includes/ui/map.html.
// The Mapbox GL init <script> renders directly here (no more page-scripts registry),
// wrapping the instance in window.tabler_map.
// The access token comes from site data (site.mapboxKey).
// Renders nothing unless maps[mapId] exists (Liquid: {% if data %}).
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
import maps from '@data/maps.json';
import site from '@data/site.json';
@@ -29,38 +27,11 @@ type MapData = {
const data = (maps as Record<string, MapData>)[mapId];
let script = '';
if (data) {
const style = data.style ?? 'streets-v11';
const zoom = data.zoom ?? 13;
const centerJs = data.center
? `center: [${data.center[1]}, ${data.center[0]}],`
: `center: [13.404900, 52.518827],`;
const markersJs = (data.markers ?? [])
.map(
(marker) =>
`\n\t\t\tnew mapboxgl.Marker({ color: "var(--tblr-primary)" }).setLngLat([${marker.center[1]}, ${marker.center[0]}]).addTo(map);\n`,
)
.join('');
script = `<script>
window.tabler_map = window.tabler_map || {};
document.addEventListener("DOMContentLoaded", function() {
mapboxgl.accessToken = '${site.mapboxKey}';
var map = new mapboxgl.Map({
container: 'map-${mapId}',
style: 'mapbox://styles/mapbox/${style}',
zoom: ${zoom},
${centerJs}
});
${markersJs}
window.tabler_map["map-${mapId}"] = map;
});
</script>`;
addPageScript(script);
}
const mapContainerId = `map-${mapId}`;
const style = data ? (data.style ?? 'streets-v11') : undefined;
const zoom = data?.zoom ?? 13;
const center = data?.center ? [data.center[1], data.center[0]] : [13.4049, 52.518827];
const markers = (data?.markers ?? []).map((marker) => [marker.center[1], marker.center[0]]);
const ratioClass = data?.ratio ?? ratio ?? '16x9';
---
@@ -69,9 +40,33 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9';
data && (
<div class={`ratio ratio-${ratioClass}`}>
<div>
<div id={`map-${mapId}`} class={`w-100 h-100${data.card ? ' rounded' : ''}`} />
<div id={mapContainerId} class={`w-100 h-100${data.card ? ' rounded' : ''}`} />
</div>
</div>
)
}
{data && <InlineScript code={script} />}
{
data && (
<script define:vars={{ mapboxKey: site.mapboxKey, mapContainerId, style, zoom, center, markers }}>
window.tabler_map ??= {};
function initMap() {
mapboxgl.accessToken = mapboxKey;
const map = new mapboxgl.Map({
container: mapContainerId,
style: `mapbox://styles/mapbox/${style}`,
zoom,
center,
});
markers.forEach((coords) => {
new mapboxgl.Marker({ color: 'var(--tblr-primary)' }).setLngLat(coords).addTo(map);
});
window.tabler_map[mapContainerId] = map;
}
document.readyState !== 'loading' ? initMap() : document.addEventListener('DOMContentLoaded', initMap, { once: true });
</script>
)
}
+75 -109
View File
@@ -1,8 +1,6 @@
---
// Equivalent of ui/map-vector.html
import { addPageScript } from '@shared/lib/page-scripts';
import mapsVector from '@data/maps-vector.json';
import InlineScript from '@shared/components/InlineScript.astro';
interface MapData {
title?: string;
@@ -26,118 +24,67 @@ const { mapId, color: colorProp, ratio = '4x3' } = Astro.props;
const data = (mapsVector as Record<string, MapData>)[mapId];
const color = colorProp ?? data?.color ?? 'primary';
let script = '';
if (data) {
const scaleLines = Array.from({ length: 10 }, (_, n) => {
const i = n + 1;
return ` scale${i}: 'color-mix(in srgb, transparent, var(--tblr-primary) ${i * 10}%)',`;
}).join('\n');
// scale1..scale10 — a 10-step color-mix ramp against var(--tblr-primary).
const scale = Object.fromEntries(
Array.from({ length: 10 }, (_, n) => [
`scale${n + 1}`,
`color-mix(in srgb, transparent, var(--tblr-primary) ${(n + 1) * 10}%)`,
]),
);
const valuesBlock = data.values
? ` series: {
regions: [{
attribute: "fill",
scale: {
${scaleLines}
},
values: ${JSON.stringify(data.values)},
}]
}
`
: '';
const markers = data?.markers?.map((marker) => ({
coords: marker.coords.split(',').map((n) => Number(n.trim())),
name: marker.name,
}));
const markersBlock = data.markers
? ` markers: [
${data.markers
.map(
(marker) => ` {
coords: [${marker.coords}],
name: "${marker.name}",
},`
)
.join('\n')}
],
markerStyle: {
initial: {
r: 4,
stroke: '#fff',
opacity: 1,
strokeWidth: 3,
stokeOpacity: .5,
fill: 'var(--tblr-${color})'
},
hover: {
fill: 'var(--tblr-${color})',
stroke: 'var(--tblr-${color})'
}
},
markerLabelStyle: {
initial: {
fontSize: 10
},
},
labels: {
markers: {
render: function(marker) {
return marker.name
},
},
},
`
: '';
const mapKey = `map-${mapId}`;
const mapSelector = `#map-${mapId}`;
const linesBlock = data.lines
? ` lines: [
${data.lines
.map(
(line) => ` {
from: "${line.from}",
to: "${line.to}"
},`
)
.join('\n')}
],
lineStyle: {
strokeDasharray:"4 4",
animation: true,
stroke: "rgba(98, 105, 118, .75)",
strokeWidth: .5,
},
`
: '';
const regionStyleInitial = !data.filled
? ` fill: 'var(--tblr-bg-surface-secondary)',
stroke: 'var(--tblr-border-color)',
strokeWidth: 2,`
: ` fill: 'var(--tblr-bg-surface-secondary)',
stroke: '#fff',
strokeWidth: 1,`;
script = `<script>
window.tabler_map_vector = window.tabler_map_vector || {};
document.addEventListener("DOMContentLoaded", function() {
const map = window.tabler_map_vector["map-${mapId}"] = new jsVectorMap({
selector: '#map-${mapId}',
map: '${data.map}',
const mapOptions = data
? {
backgroundColor: 'transparent',
regionStyle: {
initial: {
${regionStyleInitial}
}
fill: 'var(--tblr-bg-surface-secondary)',
stroke: data.filled ? '#fff' : 'var(--tblr-border-color)',
strokeWidth: data.filled ? 1 : 2,
},
},
zoomOnScroll: ${data.zoom ? 'true' : 'false'},
zoomButtons: ${data.zoom ? 'true' : 'false'},
${valuesBlock}${markersBlock}${linesBlock} });
window.addEventListener("resize", () => {
map.updateSize();
});
});
</script>`;
addPageScript(script);
}
zoomOnScroll: Boolean(data.zoom),
zoomButtons: Boolean(data.zoom),
...(data.values
? { series: { regions: [{ attribute: 'fill', scale, values: data.values }] } }
: {}),
...(markers
? {
markers,
markerStyle: {
initial: {
r: 4,
stroke: '#fff',
opacity: 1,
strokeWidth: 3,
stokeOpacity: 0.5,
fill: `var(--tblr-${color})`,
},
hover: { fill: `var(--tblr-${color})`, stroke: `var(--tblr-${color})` },
},
markerLabelStyle: { initial: { fontSize: 10 } },
}
: {}),
...(data.lines
? {
lines: data.lines,
lineStyle: {
strokeDasharray: '4 4',
animation: true,
stroke: 'rgba(98, 105, 118, .75)',
strokeWidth: 0.5,
},
}
: {}),
}
: undefined;
---
{
@@ -145,10 +92,29 @@ ${valuesBlock}${markersBlock}${linesBlock} });
<Fragment>
<div class={`ratio ratio-${ratio}`}>
<div>
<div id={`map-${mapId}`} class="w-100 h-100" />
<div id={mapKey} class="w-100 h-100" />
</div>
</div>
<InlineScript code={script} />
<script define:vars={{ mapKey, mapSelector, mapName: data.map, mapOptions }}>
window.tabler_map_vector ??= {};
function initMapVector() {
const map = (window.tabler_map_vector[mapKey] = new jsVectorMap({
selector: mapSelector,
map: mapName,
...mapOptions,
// labels.markers.render is a function, not JSON-serializable — added here
// rather than baked into mapOptions.
...(mapOptions.markers ? { labels: { markers: { render: (marker) => marker.name } } } : {}),
}));
window.addEventListener('resize', () => {
map.updateSize();
});
}
document.readyState !== 'loading' ? initMapVector() : document.addEventListener('DOMContentLoaded', initMapVector, { once: true });
</script>
</Fragment>
)
}
+30 -29
View File
@@ -1,8 +1,6 @@
---
// (registered via addPageScript); without an id → a native <input type=range>.
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
// Port of ui/range.html. With an id → a noUiSlider div + init script, rendered
// directly here; without an id → a native <input type=range>.
interface Props {
min?: number;
max?: number;
@@ -17,38 +15,41 @@ const { min = 0, max = 100, step = 10, value = 50, id, connect = false, class: c
const values = String(value).split(',');
const size = values.length;
const numericValues = values.map(Number);
const startValue = size > 1 ? numericValues : numericValues[0];
let script = '';
if (id) {
// connect array: {% for i in (2..size) %}cycle false,true{% endfor %} then true, false
let connectStr = '';
if (size > 1 || connect) {
const parts: string[] = [];
for (let i = 2; i <= size; i++) parts.push((i - 2) % 2 === 0 ? 'false' : 'true');
connectStr = `\t\t\t\t\t connect: [${[...parts, 'true', 'false'].join(', ')}],\n`;
}
const start = size > 1 ? `[${values.join(', ')}]` : values[0];
script = `<script>
document.addEventListener("DOMContentLoaded", function () {
window.noUiSlider && (noUiSlider.create(document.getElementById('range-${id}'), {
start: ${start},
${connectStr} step: ${step},
range: {
min: ${min},
max: ${max}
}
}));
});
</script>`;
addPageScript(script);
// connect array: {% for i in (2..size) %}cycle false,true{% endfor %} then true, false
let connectValue;
if (size > 1 || connect) {
const parts: boolean[] = [];
for (let i = 2; i <= size; i++) parts.push((i - 2) % 2 !== 0);
connectValue = [...parts, true, false];
}
const rangeId = `range-${id}`;
---
{
id ? (
<div class:list={['form-range mb-2', className]} id={`range-${id}`} />
<div class:list={['form-range mb-2', className]} id={rangeId} />
) : (
<input type="range" class:list={['form-range mb-2', className]} value={value} min={min} max={max} step={step} />
)
}
{id && <InlineScript code={script} />}
{
id && (
<script define:vars={{ rangeId, startValue, connectValue, step, min, max }}>
function initRange() {
window.noUiSlider &&
noUiSlider.create(document.getElementById(rangeId), {
start: startValue,
...(connectValue ? { connect: connectValue } : {}),
step,
range: { min, max },
});
}
document.readyState !== 'loading' ? initRange() : document.addEventListener('DOMContentLoaded', initRange, { once: true });
</script>
)
}
+19 -21
View File
@@ -1,7 +1,5 @@
---
import icons from '@data/icons.json';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
@@ -34,24 +32,8 @@ let star = iconData?.svg?.filled ?? '';
star = star.replace('<path stroke="none" d="M0 0h24v24H0z" fill="none"/>', '');
star = star.replace(/class="[^"]+"/, `aria-hidden="true" focusable="false" class="${starClasses}"`);
// {% capture_script %} → registered synchronously (before any await).
const script = `<script>
window.tabler_rating = window.tabler_rating || {};
document.addEventListener("DOMContentLoaded", function () {
const rating = new StarRating('#rating-${id}', {
tooltip: false,
clearable: false,
stars: function (el, item, index) {
el.innerHTML = \`${star}\`;
},
classNames: {
}
})
window.tabler_rating["rating-${id}"] = rating;
})
</script>`;
addPageScript(script);
const ratingSelector = `#rating-${id}`;
const ratingKey = `rating-${id}`;
---
<select id={`rating-${id}`}>
@@ -62,4 +44,20 @@ addPageScript(script);
<option value="2" selected={value === 2}>Poor</option>
<option value="1" selected={value === 1}>Terrible</option>
</select>
<InlineScript code={script} />
<script define:vars={{ ratingSelector, ratingKey, star }}>
window.tabler_rating ??= {};
function initRating() {
const rating = new StarRating(ratingSelector, {
tooltip: false,
clearable: false,
stars: (el) => {
el.innerHTML = star;
},
classNames: {},
});
window.tabler_rating[ratingKey] = rating;
}
document.readyState !== 'loading' ? initRating() : document.addEventListener('DOMContentLoaded', initRating, { once: true });
</script>
+33 -46
View File
@@ -1,15 +1,9 @@
---
// Renders a <select id="select-{id}"> populated from @data/selects.json (+ people)
// and registers a TomSelect init <script> via addPageScript (the include's
// {% capture_script %}). Registration is synchronous — see src/lib/page-scripts.ts.
// The reference build is `environment == 'development'`, so the init assigns into
// window.tabler_select[...]. Note: tom-select is NOT in this page's libs, but the
// capture_script is still emitted (mirrors Liquid).
// and its TomSelect init <script>, rendered directly here (no more page-scripts registry).
// TODO: optgroup / flag / label indicator branches (unused on the modals page).
import selects from '@data/selects.json';
import people from '@data/people.json';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
import { firstLetters } from '@shared/lib/string-format';
interface Props {
@@ -23,7 +17,6 @@ interface Props {
multiple?: boolean;
values?: string[];
showSearch?: boolean;
showScripts?: boolean;
}
const {
@@ -37,7 +30,6 @@ const {
multiple,
values,
showSearch,
showScripts,
} = Astro.props;
const id = idProp ?? keyProp;
@@ -52,6 +44,8 @@ const selectClass = [
const isMultiple = Boolean(multiple || data.multiple);
const firstLetters = (s: string) => (s || '').split(' ').map((w) => w.charAt(0)).join('');
interface Person {
id: string;
full_name: string;
@@ -93,41 +87,7 @@ if (values) {
});
}
const script = id
? `<script>
document.addEventListener("DOMContentLoaded", function () {
window.tabler_select = window.tabler_select || {};
var el;
window.TomSelect && (window.tabler_select["select-${id}"] = new TomSelect(el = document.getElementById('select-${id}'), {
copyClassesToDropdown: false,
dropdownParent: 'body',
${showSearch ? '' : "controlInput: '<input>',"}
render:{
item: function(data,escape) {
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
option: function(data,escape){
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
},
}));
});
</script>`
: '';
if (id && script && !showScripts) {
addPageScript(script);
}
const selectId = id ? `select-${id}` : undefined;
---
{
@@ -162,5 +122,32 @@ if (id && script && !showScripts) {
</select>
)
}
{showScripts && id && <Fragment set:html={script} />}
{!showScripts && id && script && <InlineScript code={script} />}
{
id && (
<script define:vars={{ selectId, showSearch }}>
function initSelect() {
window.tabler_select ??= {};
const renderOption = (data, escape) => {
if (data.customProperties) {
return `<div><span class="dropdown-item-indicator">${data.customProperties}</span>${escape(data.text)}</div>`;
}
return `<div>${escape(data.text)}</div>`;
};
window.TomSelect &&
(window.tabler_select[selectId] = new TomSelect(document.getElementById(selectId), {
copyClassesToDropdown: false,
dropdownParent: 'body',
...(showSearch ? {} : { controlInput: '<input>' }),
render: {
item: renderOption,
option: renderOption,
},
}));
}
document.readyState !== 'loading' ? initSelect() : document.addEventListener('DOMContentLoaded', initSelect, { once: true });
</script>
)
}
+12 -10
View File
@@ -1,10 +1,7 @@
---
// Renders the signature canvas markup and registers its SignaturePad init
// script via addPageScript (the include's {% capture_script %}). Registration
// is synchronous in the frontmatter — see src/lib/page-scripts.ts.
// Port of ui/signature.html.
// Renders the signature canvas markup and its SignaturePad init script.
import Icon from './Icon.astro';
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
interface Props {
id?: string;
@@ -30,19 +27,25 @@ const {
const signatureClass = ['signature position-relative', className];
const clearSnippet = clear
? ` document.querySelector("#signature-${id}-clear").addEventListener("click", function () {
? ` document.querySelector(${JSON.stringify(`#signature-${id}-clear`)}).addEventListener("click", function () {
signaturePad.clear();
});
`
: '';
// extraJs is a documented escape hatch for raw JS (see Props) — not user data, so it
// stays interpolated verbatim rather than JSON-serialized.
const extraSnippet = extraJs ? `${extraJs}
` : '';
// set:html (below) is genuinely required here: extraJs is a documented raw-JS escape
// hatch that must land *inside* the init callback body (it references canvas /
// signaturePad), so the whole script has to be assembled as text — <script define:vars>
// can only inject variables, not splice caller-supplied code into the function body.
const script = `<!-- BEGIN SIGNATURE PAD -->
<script>
document.addEventListener("${event}", function () {
const canvas = document.getElementById("signature-${id}");
document.addEventListener(${JSON.stringify(event)}, function () {
const canvas = document.getElementById(${JSON.stringify(`signature-${id}`)});
if (canvas) {
const signaturePad = new SignaturePad(canvas, {
@@ -69,7 +72,6 @@ ${extraSnippet}
}
});
</script>`;
addPageScript(script);
---
<div class:list={signatureClass}>
@@ -84,4 +86,4 @@ addPageScript(script);
}
<canvas id={`signature-${id}`} width={width} height={height} class="signature-canvas"></canvas>
</div>
<InlineScript code={script} />
<Fragment set:html={script} />
+36 -39
View File
@@ -1,50 +1,47 @@
---
// The HugeRTE init <script> is captured via {% capture script %} then
// {% capture_script %} in Liquid, so it is registered with addPageScript
// synchronously here.
import { addPageScript } from '@shared/lib/page-scripts';
import InlineScript from '@shared/components/InlineScript.astro';
// Port of shared/includes/ui/wysiwyg.html.
// The HugeRTE init <script> renders directly here (no more page-scripts registry).
interface Props {
id?: string;
}
const { id = 'mytextarea' } = Astro.props;
const script = `<script>
document.addEventListener("DOMContentLoaded", function () {
let options = {
selector: '#hugerte-${id}',
height: 300,
menubar: false,
statusbar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', 'anchor',
'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat',
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif; font-size: 14px; -webkit-font-smoothing: antialiased; }'
}
// check current theme is light or dark
const theme = document.documentElement.getAttribute('data-bs-theme');
if (theme === 'dark') {
options.skin = 'oxide-dark';
options.content_css = 'dark';
}
hugeRTE.init(options);
})
</script>`;
addPageScript(script);
const selector = `#hugerte-${id}`;
---
<form method="post">
<textarea id={`hugerte-${id}`}>Hello, <b>Tabler</b>!</textarea>
</form>
<InlineScript code={script} />
<script define:vars={{ selector }}>
function initWysiwyg() {
const options = {
selector,
height: 300,
menubar: false,
statusbar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', 'anchor',
'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount',
],
toolbar:
'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat',
content_style:
'body { font-family: -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif; font-size: 14px; -webkit-font-smoothing: antialiased; }',
};
// check current theme is light or dark
const theme = document.documentElement.getAttribute('data-bs-theme');
if (theme === 'dark') {
options.skin = 'oxide-dark';
options.content_css = 'dark';
}
hugeRTE.init(options);
}
document.readyState !== 'loading' ? initWysiwyg() : document.addEventListener('DOMContentLoaded', initWysiwyg, { once: true });
</script>