mirror of
https://github.com/tabler/tabler.git
synced 2026-08-27 12:36:28 +04:00
- Example demos use bg-surface and join their code panel into one card - Larger h1/h2/h3 in .prose and a ~70-character measure for running text
488 lines
17 KiB
Plaintext
488 lines
17 KiB
Plaintext
---
|
|
// Docs layout for the @tabler/docs app.
|
|
// Standalone layout with its own <head>; does NOT use BaseLayout.astro.
|
|
// Development uses the unminified assets; production additionally emits the SEO metadata.
|
|
import { site } from '@shared/lib/site';
|
|
import libs from '@tabler/core/libs.json';
|
|
import docs from '@data/docs.json';
|
|
import Icon from '@ui/Icon.astro';
|
|
import DocsNavbar from '@components/DocsNavbar.astro';
|
|
import DocsMenu from '@components/DocsMenu.astro';
|
|
import DocsToc from '@components/DocsToc.astro';
|
|
import type { TocItem } from '@components/DocsToc.astro';
|
|
import DocsPagination from '@components/DocsPagination.astro';
|
|
import DocsCard from '@components/DocsCard.astro';
|
|
import { getDocsPage } from '@lib/docs-pages';
|
|
import Prose from '@ui/Prose.astro';
|
|
import PageScripts from '@shared/components/PageScripts.astro';
|
|
|
|
interface Props {
|
|
/** front matter: the page's h1 heading */
|
|
title: string;
|
|
/** front matter: optional SEO title override */
|
|
seoTitle?: string | undefined;
|
|
/** front matter: lead below the title (p.text-secondary.fs-3.lh-3) */
|
|
summary?: string | undefined;
|
|
/** front matter: SEO description */
|
|
description?: string | undefined;
|
|
/** front matter: optional SEO description override */
|
|
seoDescription?: string | undefined;
|
|
/** front matter `added-in`: renders the "Added in X" badge next to the h1 */
|
|
addedIn?: string | undefined;
|
|
/** Table of contents (h2/h3 from the markdown content) — see DocsToc.astro */
|
|
toc?: TocItem[];
|
|
/**
|
|
* Page URL in the docs namespace, e.g. "/ui/components/alert".
|
|
* By default derived from Astro.url.pathname by stripping the /docs prefix.
|
|
* Drives the active menu entry, pagination, and the relative favicon path.
|
|
*/
|
|
url?: string | undefined;
|
|
/** Extra libraries from libs.json (css+js) */
|
|
docsLibs?: string[];
|
|
/** front matter `css-plugins`: extra tabler-<name>.css sheets for this page's demos */
|
|
cssPlugins?: string[];
|
|
/** front matter `related`: docs URLs of related pages, rendered above the pagination */
|
|
related?: string[];
|
|
/** hides child and previous/next page navigation */
|
|
hidePagination?: boolean | undefined;
|
|
/** url of this page's markdown mirror (see pages/[...slug].md.ts); omitted for pages outside the docs collection */
|
|
markdownUrl?: string | undefined;
|
|
}
|
|
|
|
const {
|
|
title,
|
|
seoTitle,
|
|
summary,
|
|
description,
|
|
seoDescription,
|
|
addedIn,
|
|
toc = [],
|
|
docsLibs = [],
|
|
cssPlugins = [],
|
|
related = [],
|
|
hidePagination = false,
|
|
markdownUrl,
|
|
} = Astro.props;
|
|
|
|
// Always-on plugin styles. Heavy demo-only sheets (flags, payments, socials)
|
|
// are opt-in per page via the `css-plugins` frontmatter key; marketing is not
|
|
// used by any docs page.
|
|
const basePlugins = ['themes', 'vendors'];
|
|
const pagePlugins = [...basePlugins, ...cssPlugins.filter((plugin) => !basePlugins.includes(plugin))];
|
|
const environment = process.env.NODE_ENV || 'production';
|
|
// Vercel preview/branch deployments must not be indexed (VERCEL_ENV is unset locally).
|
|
const isVercelPreview = !!process.env.VERCEL_ENV && process.env.VERCEL_ENV !== 'production';
|
|
|
|
// Strip /docs prefix (and optional .html) for the docs namespace URL.
|
|
// Canonical form has no trailing slash (matching vercel.json trailingSlash: false);
|
|
// the root stays "/".
|
|
const docsUrl =
|
|
Astro.props.url ??
|
|
(() => {
|
|
const p = Astro.url.pathname
|
|
.replace(/^\/docs(?=\/|$)/, '')
|
|
.replace(/\.html$/, '')
|
|
.replace(/\/+$/, '');
|
|
return p || '/';
|
|
})();
|
|
|
|
const inSection = (prefix: string) => docsUrl === prefix || docsUrl.startsWith(`${prefix}/`);
|
|
const pageSection = inSection('/ui')
|
|
? 'UI'
|
|
: inSection('/icons')
|
|
? 'Icons'
|
|
: inSection('/illustrations')
|
|
? 'Illustrations'
|
|
: inSection('/emails')
|
|
? 'Emails'
|
|
: '';
|
|
const metaTitle = seoTitle ?? title;
|
|
const metaDescription = seoDescription ?? description;
|
|
const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation';
|
|
const canonicalUrl = new URL(docsUrl, Astro.site ?? site.docsUrl).href;
|
|
const ogImageUrl = new URL('/static/og.png', Astro.site ?? site.docsUrl).href;
|
|
|
|
// Relative asset base: for /ui/components/alert/ -> "../../.."
|
|
const depth = docsUrl.split('/').filter(Boolean).length;
|
|
const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1);
|
|
|
|
// docs-libs + clipboard (always included). In dev, file names stay unchanged
|
|
// (e.g. dist/clipboard.min.js keeps .min).
|
|
type Lib = { npm?: string; js?: string[]; css?: string[] };
|
|
const libEntries = Object.entries(libs as Record<string, Lib>);
|
|
const libUrl = (lib: Lib, file: string) =>
|
|
file.startsWith('http://') || file.startsWith('https://')
|
|
? file
|
|
: `/dist/libs/${lib.npm}/${file}`;
|
|
const docsLibCss = libEntries
|
|
.filter(([name]) => docsLibs.includes(name))
|
|
.flatMap(([, lib]) => (lib.css ?? []).map((file) => libUrl(lib, file)));
|
|
const docsLibJs = libEntries
|
|
.filter(([name]) => docsLibs.includes(name) || name === 'clipboard')
|
|
.flatMap(([, lib]) => (lib.js ?? []).map((file) => libUrl(lib, file)));
|
|
|
|
// Eventually belongs in src/lib/site.ts (out of scope for this task).
|
|
const opencollectiveUrl = 'https://opencollective.com/tabler';
|
|
const xUrl = 'https://x.com/tabler_io';
|
|
const linkedinUrl = 'https://www.linkedin.com/company/tabler-io';
|
|
|
|
const year = new Date().getFullYear();
|
|
|
|
// JSON-LD breadcrumbs. Segment names come from the docs.json menu tree;
|
|
// top-level sections are not in the menu, so they get explicit names.
|
|
const sectionNames: Record<string, string> = {
|
|
'/ui': 'Tabler UI',
|
|
'/icons': 'Tabler Icons',
|
|
'/illustrations': 'Tabler Illustrations',
|
|
'/emails': 'Tabler Emails',
|
|
};
|
|
type MenuNode = { title?: string; url?: string; children?: MenuNode[] };
|
|
const menuTitleByUrl = new Map<string, string>();
|
|
const collectTitles = (nodes: MenuNode[]) => {
|
|
for (const node of nodes) {
|
|
if (node.url?.startsWith('/') && node.title) menuTitleByUrl.set(node.url, node.title);
|
|
if (node.children) collectTitles(node.children);
|
|
}
|
|
};
|
|
collectTitles(docs.menu as MenuNode[]);
|
|
|
|
// WebSite schema on the homepage only — lets Google pick the right site name.
|
|
// No SearchAction: docs search is a modal, there is no /search?q= results URL.
|
|
const websiteJsonLd =
|
|
docsUrl === '/'
|
|
? JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
'name': 'Tabler Documentation',
|
|
'alternateName': 'Tabler Docs',
|
|
'url': `${site.docsUrl}/`,
|
|
})
|
|
: null;
|
|
|
|
// Breadcrumb trail — one source for the visible nav above the h1 and the
|
|
// JSON-LD, so the structured data mirrors what the page shows.
|
|
const breadcrumbs = (() => {
|
|
if (docsUrl === '/' || docsUrl === '/404') return null;
|
|
const segments = docsUrl.split('/').filter(Boolean);
|
|
const items = [{ name: 'Docs', path: '/' }];
|
|
let path = '';
|
|
segments.forEach((segment, index) => {
|
|
path += `/${segment}`;
|
|
const isLast = index === segments.length - 1;
|
|
const name = isLast ? title : (menuTitleByUrl.get(path) ?? sectionNames[path] ?? segment);
|
|
items.push({ name, path });
|
|
});
|
|
return items;
|
|
})();
|
|
|
|
const breadcrumbJsonLd = breadcrumbs
|
|
? JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
itemListElement: breadcrumbs.map((item, index) => ({
|
|
'@type': 'ListItem',
|
|
position: index + 1,
|
|
name: item.name,
|
|
item: item.path === '/' ? `${site.docsUrl}/` : `${site.docsUrl}${item.path}`,
|
|
})),
|
|
})
|
|
: null;
|
|
|
|
type DocsLink = { title: string; url: string; icon: string };
|
|
const docsLinks = docs.links as DocsLink[];
|
|
|
|
// Card data for the "Related" section (title/description from the target
|
|
// page's frontmatter; menu title as fallback).
|
|
const relatedPages = await Promise.all(
|
|
related.map(
|
|
async (url) =>
|
|
(await getDocsPage(url)) ?? { url, title: menuTitleByUrl.get(url) ?? url, description: '', order: 0 },
|
|
),
|
|
);
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<!-- BEGIN META -->
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="theme-color" content={site.themeColor} />
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
|
|
<title>{metaTitle} | {siteName}</title>
|
|
{isVercelPreview && <meta name="robots" content="noindex" />}
|
|
|
|
<!-- Markdown for LLMs: the docs index and this page's own .md mirror. -->
|
|
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
|
|
{markdownUrl && <link rel="alternate" type="text/markdown" href={markdownUrl} title={`${title} (markdown)`} />}
|
|
{
|
|
environment === 'production' && (
|
|
<Fragment>
|
|
{metaDescription && <meta name="description" content={metaDescription} />}
|
|
<link rel="canonical" href={canonicalUrl} />
|
|
|
|
<!-- Open Graph / Social Media Meta Tags -->
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content={canonicalUrl} />
|
|
<meta property="og:title" content={metaTitle} />
|
|
{metaDescription && <meta property="og:description" content={metaDescription} />}
|
|
<meta property="og:site_name" content={siteName} />
|
|
<meta property="og:image" content={ogImageUrl} />
|
|
|
|
<!-- Twitter Card data -->
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:site" content="@tabler_io" />
|
|
<meta name="twitter:title" content={metaTitle} />
|
|
{metaDescription && <meta name="twitter:description" content={metaDescription} />}
|
|
<meta name="twitter:image" content={ogImageUrl} />
|
|
|
|
{websiteJsonLd && <script is:inline type="application/ld+json" set:html={websiteJsonLd} />}
|
|
{breadcrumbJsonLd && <script is:inline type="application/ld+json" set:html={breadcrumbJsonLd} />}
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
{
|
|
environment === 'development' && (
|
|
<Fragment>
|
|
<link rel="icon" href={`${relative}/favicon-dev.ico`} type="image/x-icon" />
|
|
<link rel="shortcut icon" href={`${relative}/favicon-dev.ico`} type="image/x-icon" />
|
|
</Fragment>
|
|
)
|
|
}
|
|
<!-- END META -->
|
|
|
|
<!-- BEGIN GLOBAL MANDATORY STYLES -->
|
|
<link rel="stylesheet" href="/dist/css/tabler.css" />
|
|
<!-- END GLOBAL MANDATORY STYLES -->
|
|
|
|
<!-- BEGIN PLUGINS STYLES -->
|
|
{pagePlugins.map((plugin) => <link href={`/dist/css/tabler-${plugin}.css`} rel="stylesheet" />)}
|
|
<!-- END PLUGINS STYLES -->
|
|
|
|
{
|
|
docsLibCss.length > 0 && (
|
|
<Fragment>
|
|
<Fragment set:html={'<!-- BEGIN PAGE LEVEL STYLES -->'} />
|
|
{docsLibCss.map((href) => <link href={href} rel="stylesheet" />)}
|
|
<Fragment set:html={'<!-- END PAGE LEVEL STYLES -->'} />
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
<!-- BEGIN CUSTOM FONT -->
|
|
<link rel="preconnect" href="https://rsms.me/" />
|
|
<link rel="preconnect" href="https://rsms.me/" crossorigin />
|
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
<!-- END CUSTOM FONT -->
|
|
|
|
<!-- BEGIN DOCS STYLES -->
|
|
<link rel="stylesheet" href="/css/docs.css" />
|
|
<!-- END DOCS STYLES -->
|
|
</head>
|
|
|
|
<body class="d-flex flex-column">
|
|
<a href="#content" class="visually-hidden-focusable skip-link">Skip to main content</a>
|
|
<!-- BEGIN GLOBAL THEME SCRIPT -->
|
|
<script is:inline src="/dist/js/tabler-theme.js"></script>
|
|
<!-- END GLOBAL THEME SCRIPT -->
|
|
<!-- BEGIN NAVBAR -->
|
|
<header role="banner">
|
|
<DocsNavbar />
|
|
</header>
|
|
<!-- END NAVBAR -->
|
|
<!-- BEGIN PAGE BODY -->
|
|
<main id="content" class="flex-fill">
|
|
<div class="container">
|
|
<div class="row g-0">
|
|
<!-- BEGIN DOCS MENU -->
|
|
<div class="col-docs d-none d-lg-block border-end">
|
|
<div class="py-4">
|
|
<div class="space-y space-y-5">
|
|
<nav class="nav nav-vertical" aria-label="Resources">
|
|
{
|
|
docsLinks.map((link) => (
|
|
<a href={link.url} class="nav-link" target="_blank" rel="noopener noreferrer">
|
|
<span class="border me-2 rounded p-1">
|
|
<Icon name={link.icon} />
|
|
</span>
|
|
{link.title}
|
|
</a>
|
|
))
|
|
}
|
|
</nav>
|
|
<div class="flex-fill">
|
|
<DocsMenu url={docsUrl} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END DOCS MENU -->
|
|
<div class="col">
|
|
<div class="py-lg-5 ps-lg-5">
|
|
<div class="py-6 ps-lg-6 p-xxl-6">
|
|
{breadcrumbs && (
|
|
<nav aria-label="Breadcrumb">
|
|
<ol class="breadcrumb breadcrumb-arrows breadcrumb-muted mb-3">
|
|
{breadcrumbs.map((item, index) =>
|
|
index < breadcrumbs.length - 1 ? (
|
|
<li class="breadcrumb-item">
|
|
<a href={item.path}>{item.name}</a>
|
|
</li>
|
|
) : (
|
|
<li class="breadcrumb-item active" aria-current="page">
|
|
{item.name}
|
|
</li>
|
|
),
|
|
)}
|
|
</ol>
|
|
</nav>
|
|
)}
|
|
|
|
<Prose
|
|
data-bs-spy="scroll"
|
|
data-bs-target="#toc"
|
|
data-bs-root-margin="50px 0px -0%"
|
|
data-bs-smooth-scroll="true"
|
|
tabindex="0"
|
|
>
|
|
<div class="d-flex docs-page-header">
|
|
<h1>
|
|
{title}
|
|
</h1>
|
|
|
|
{
|
|
addedIn && (
|
|
<div class="ms-auto">
|
|
<span class="badge bg-primary-lt text-primary-lt-fg">Added in {addedIn}</span>
|
|
</div>
|
|
)
|
|
}
|
|
</div>
|
|
|
|
<p class="text-secondary fs-2 lh-3">{summary}</p>
|
|
|
|
{/* Page must supply headings with ids (MDX does this by default). */}
|
|
<slot />
|
|
|
|
{relatedPages.length > 0 && (
|
|
<Fragment>
|
|
<h2 id="related">Related</h2>
|
|
<div class="row row-deck row-cards">
|
|
{relatedPages.map((page) => (
|
|
<DocsCard href={page.url} title={page.title} description={page.description} />
|
|
))}
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
|
|
{!hidePagination && <DocsPagination url={docsUrl} />}
|
|
|
|
<div class="mt-7">
|
|
<nav aria-label="Documentation sections">
|
|
<ul class="list-inline list-inline-dots mb-0 text-secondary">
|
|
{[
|
|
{ title: 'Getting started', url: '/ui/getting-started' },
|
|
{ title: 'UI components', url: '/ui/components' },
|
|
{ title: 'Icons', url: '/icons' },
|
|
{ title: 'Illustrations', url: '/illustrations' },
|
|
{ title: 'Emails', url: '/emails' },
|
|
{ title: 'FAQ', url: '/ui/getting-started/faq' },
|
|
{ title: 'License', url: '/ui/getting-started/license' },
|
|
].map((section) => (
|
|
<li class="list-inline-item">
|
|
<a href={section.url} class="link-secondary">
|
|
{section.title}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
|
|
<div class="mt-5">
|
|
<div class="row">
|
|
<div class="col text-secondary">
|
|
© {year} Tabler. All rights reserved.
|
|
</div>
|
|
|
|
<div class="col text-end">
|
|
<a href={site.githubUrl} class="link-secondary" target="_blank" rel="noopener noreferrer"><Icon name="brand-github" /></a>
|
|
<a href={site.githubSponsorsUrl} class="link-secondary" target="_blank" rel="noopener noreferrer"><Icon name="heart" /></a>
|
|
<a href={opencollectiveUrl} class="link-secondary" target="_blank" rel="noopener noreferrer"><Icon name="hearts" /></a>
|
|
<a href={xUrl} class="link-secondary" target="_blank" rel="noopener noreferrer"><Icon name="brand-x" /></a>
|
|
<a href={linkedinUrl} class="link-secondary" target="_blank" rel="noopener noreferrer"><Icon name="brand-linkedin" /></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Prose>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- BEGIN DOCS TOC -->
|
|
<div class="col-2 d-none d-xxl-block">
|
|
<div class="py-6 sticky-top">
|
|
<DocsToc toc={toc} />
|
|
</div>
|
|
</div>
|
|
<!-- END DOCS TOC -->
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<!-- END PAGE BODY -->
|
|
|
|
{
|
|
docsLibJs.length > 0 && (
|
|
<Fragment>
|
|
<Fragment set:html={'<!-- BEGIN PAGE LIBRARIES -->'} />
|
|
{docsLibJs.map((src) => <script is:inline src={src} />)}
|
|
<Fragment set:html={'<!-- END PAGE LIBRARIES -->'} />
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
<!-- BEGIN PAGE SCRIPTS -->
|
|
<script is:inline>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const elements = document.querySelectorAll('[data-clipboard-text]');
|
|
|
|
elements.forEach(function (element) {
|
|
const clipboard = new ClipboardJS(element, {
|
|
text: function () {
|
|
return element.getAttribute('data-clipboard-text');
|
|
}
|
|
});
|
|
|
|
clipboard.on('success', function (e) {
|
|
e.clearSelection();
|
|
e.trigger.classList.add('btn-success');
|
|
e.trigger.classList.remove('btn-dark');
|
|
e.trigger.children[0].classList.add('d-none');
|
|
e.trigger.children[1].classList.remove('d-none');
|
|
|
|
setTimeout(function () {
|
|
e.trigger.classList.remove('btn-success');
|
|
e.trigger.classList.add('btn-dark');
|
|
|
|
e.trigger.children[0].classList.remove('d-none');
|
|
e.trigger.children[1].classList.add('d-none');
|
|
}, 2000);
|
|
});
|
|
|
|
clipboard.on('error', function (e) {
|
|
console.error('Error copying text: ', e);
|
|
});
|
|
});
|
|
})
|
|
</script>
|
|
<!-- END PAGE SCRIPTS -->
|
|
|
|
<!-- BEGIN GLOBAL MANDATORY SCRIPTS -->
|
|
<script is:inline src="/dist/js/tabler.js" defer></script>
|
|
<!-- END GLOBAL MANDATORY SCRIPTS -->
|
|
|
|
<PageScripts />
|
|
</body>
|
|
</html>
|