Files
tabler/screenshots/layouts/ScreenshotLayout.astro
T

167 lines
11 KiB
Plaintext

---
// Chrome-free layout for component screenshots: just @tabler/core CSS/JS
// around a single component, no navbar/sidebar/demo styling. Modeled 1:1 on
// preview/pages/screenshot.astro (the marketing hero-shot page): a fixed
// 1024x768 .screenshot canvas with the Tabler logo above an elevated
// .screenshot-card, on a soft gradient backdrop. Dark/light is driven by the
// same ?theme= query param + dist/js/tabler-theme(.min).js + the
// hide-theme-light/hide-theme-dark nav-segmented toggle as that page, so a
// screenshot tool can request /button.html?theme=dark without depending on
// stored/localStorage state.
import Logo from '@shared/components/navbar/Logo.astro'
import PageScripts from '@shared/components/PageScripts.astro'
import libs from '@tabler/core/libs.json'
interface Props {
title: string
/** dir="rtl" + the .rtl.css variant of tabler.css */
rtl?: boolean
/** the Tabler logo above the card (+ balancing spacer below it) */
showLogo?: boolean
/** caps the [data-screenshot-target] width at columns * 320px */
columns?: number
/** CSS zoom applied to [data-screenshot-target] */
zoom?: number
/** third-party libs from libs.json the component needs, e.g. ['apexcharts'] */
pageLibs?: string[]
/** core CSS plugins (site.cssPlugins), e.g. ['socials'] for tabler-socials.css */
cssPlugins?: string[]
/** extra classes on the component wrapper */
class?: string
}
const { title, rtl = false, showLogo = true, columns, zoom = 1, pageLibs = [], cssPlugins = [], class: className } = Astro.props
const targetStyle = [columns && `max-width: ${columns * 310}px`, `zoom: ${zoom}`].filter(Boolean).join('; ')
const rtlSuffix = rtl ? '.rtl' : ''
// core/dist only has .min. files after a full `pnpm build`; `astro dev` (this
// package's own dev-prepare-less dev script) runs against whatever core's own
// dev watcher last emitted, which is unminified — so match the served core
// build instead of hardcoding one variant.
const min = import.meta.env.DEV ? '' : '.min'
type Lib = { npm?: string; js?: string[]; css?: string[] }
const pageLibEntries = Object.entries(libs as Record<string, Lib>).filter(([name]) => pageLibs.includes(name))
const libHref = (lib: Lib, file: string) => `/dist/libs/${lib.npm}/${file}`
const libCssFiles = pageLibEntries.flatMap(([, lib]) => (lib.css ?? []).map((file) => libHref(lib, file)))
const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file) => libHref(lib, file)))
---
<!doctype html>
<html lang="en" dir={rtl ? 'rtl' : undefined}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>{title} - Tabler screenshots</title>
<link rel="icon" href="./favicon-dev.ico" type="image/x-icon" />
<link href={`./dist/css/tabler${rtlSuffix}${min}.css`} rel="stylesheet" />
{cssPlugins.map((plugin) => <link href={`./dist/css/tabler-${plugin}${rtlSuffix}${min}.css`} rel="stylesheet" />)}
{libCssFiles.map((href) => <link href={href} rel="stylesheet" />)}
<style is:inline>
/* Deterministic screenshots: no mid-capture animation/transition frames, no blinking caret. */
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
caret-color: transparent !important;
}
.screenshot {
width: 1024px;
height: 768px;
background: var(--tblr-bg-surface-tertiary) linear-gradient(to top left, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0));
position: relative;
padding: 0 4rem;
}
.screenshot-wrapper {
width: 100%;
}
.screenshot-wrapper > .card {
box-shadow:
0 1px 1px 0 rgba(0, 0, 0, 0.02),
0 8px 24px -4px rgba(0, 0, 0, 0.04),
0 24px 40px -8px rgba(0, 0, 0, 0.1);
}
.screenshot-logo {
display: flex;
align-items: center;
justify-content: center;
padding: 0 5rem;
flex: 1;
}
/* fake mouse pointer, to make the static screenshot read as a live interaction */
.cursor {
position: absolute;
width: 26px;
height: 37px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 26 37'%3E%3Cg filter='url(%23a)'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M8.56 13.902c-.284-.358-.63-1.092-1.243-1.983-.348-.505-1.211-1.454-1.468-1.935-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z' clip-rule='evenodd'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' d='M16.794 14.257v-3.459m-2.015 3.47-.016-3.472m-1.98.031.02 3.426m-4.243-.35c-.284-.36-.63-1.094-1.243-1.985-.348-.503-1.211-1.452-1.468-1.934-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='a' width='30' height='40' x='-2' y='-1' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.05 0'/%3E%3CfeBlend in2='BackgroundImageFix' result='effect1_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='3'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.04 0'/%3E%3CfeBlend in2='effect1_dropShadow_55_1571' result='effect2_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='8'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.03 0'/%3E%3CfeBlend in2='effect2_dropShadow_55_1571' result='effect3_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='14'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.01 0'/%3E%3CfeBlend in2='effect3_dropShadow_55_1571' result='effect4_dropShadow_55_1571'/%3E%3CfeBlend in='SourceGraphic' in2='effect4_dropShadow_55_1571' result='shape'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E");
top: calc(100% - 10px);
left: calc(50% - 10px);
}
</style>
<!-- loaded early, before paint, so ?theme=dark applies before first render -->
<script is:inline src={`/dist/js/tabler-theme${min}.js`}></script>
</head>
<body>
<div class="page page-center align-items-center">
<div class="screenshot d-flex flex-column justify-content-center align-items-center px-12" id="screenshot">
{showLogo && <div class="screenshot-logo" />}
<div class:list={['screenshot-wrapper', className]} style={targetStyle} data-screenshot-target>
<slot />
</div>
{
showLogo && (
<div class="screenshot-logo">
<Logo gray />
</div>
)
}
</div>
<!-- outside .screenshot on purpose: a screenshot tool crops to [data-screenshot-target], so this toggle never ends up in the capture -->
<nav class="nav-segmented mt-4">
<a href="?theme=light" class="nav-link hide-theme-light">Light</a>
<a href="?theme=light" class="nav-link active hide-theme-dark">Light</a>
<a href="?theme=dark" class="nav-link hide-theme-dark">Dark</a>
<a href="?theme=dark" class="nav-link active hide-theme-light">Dark</a>
</nav>
</div>
{libJsFiles.map((src) => <script is:inline src={src} defer />)}
<script is:inline src={`/dist/js/tabler${min}.js`} defer></script>
<!-- components that render via CaptureScript (e.g. Chart) queue their init script
into shared/lib/page-scripts instead of inlining it where used; this drains it. -->
<PageScripts />
<script>
// Signals to the screenshot tool that fonts and JS-driven component init
// (dropdowns, tooltips, charts via PageScripts, ...) are settled.
// DOMContentLoaded — not window "load" — on purpose: deferred scripts
// (tabler(.min).js above, and everything PageScripts drained) already run
// before DOMContentLoaded per spec, and unlike "load" it doesn't block on
// slow/unreachable subresources (e.g. an external chat GIF).
const domReady = new Promise((resolve) => {
if (document.readyState !== 'loading') resolve(undefined)
else document.addEventListener('DOMContentLoaded', () => resolve(undefined), { once: true })
})
Promise.all([document.fonts.ready, domReady]).then(() => {
requestAnimationFrame(() => requestAnimationFrame(() => (document.documentElement.dataset.screenshotReady = 'true')))
})
</script>
</body>
</html>