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:
@@ -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),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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...).
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
# build output
|
||||
dist/
|
||||
.astro/
|
||||
tmp-assets/
|
||||
|
||||
# public/ is fully generated by .build/copy-assets.mjs
|
||||
public/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />}
|
||||
@@ -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')} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
/*
|
||||
|
||||
@@ -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
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
|
||||
/** 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'] }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user