mirror of
https://github.com/tabler/tabler.git
synced 2026-08-07 03:42:27 +04:00
Add screenshots package and fix card-gradient minify bug (#2793)
Co-authored-by: Bartek <xbartoszdobija@gmail.com>
This commit is contained in:
@@ -124,6 +124,14 @@ function flushWrites(): void {
|
||||
async function minify(files: string[]): Promise<void> {
|
||||
const minified = await new CleanCSS({
|
||||
batch: true,
|
||||
// zeroUnits (default true) strips the unit off zero values, e.g. `0%` -> `0`.
|
||||
// clean-css only skips this inside calc/rgb/hsl/rgba/hsla/min/max/clamp —
|
||||
// not color-mix() — so it turns `color-mix(in srgb, x 0%, y)` into
|
||||
// `color-mix(in srgb, x 0, y)`. Unlike <length>, <percentage> has no
|
||||
// unitless-zero exception, so that's invalid CSS and the whole color-mix()
|
||||
// (and the gradient layer using it, e.g. .card-gradient) silently drops.
|
||||
// Covered by .build/build-css.test.ts.
|
||||
compatibility: { properties: { zeroUnits: false } },
|
||||
format: 'breakWith=lf;breaks:afterComment=on',
|
||||
inline: 'local',
|
||||
level: { 1: true },
|
||||
|
||||
@@ -213,6 +213,11 @@ Navbar brand
|
||||
height: $navbar-brand-image-height;
|
||||
}
|
||||
|
||||
.navbar-brand-gray {
|
||||
--navbar-logo-color: var(--tertiary);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
Navbar toggler
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Regression coverage for the multi-layer .card-gradient background: every
|
||||
// zero-value gradient stop must keep its `%` unit. Unlike <length>,
|
||||
// <percentage> has no unitless-zero exception, so `color-mix(in sRGB, x 0,
|
||||
// y)` (missing the %) is invalid CSS and the whole color-mix() — and the
|
||||
// gradient layer using it — silently drops in the browser.
|
||||
//
|
||||
// This only guards the SCSS source; the actual incident was a CSS minifier
|
||||
// (clean-css) stripping a valid `0%` down to `0` post-compile, which is
|
||||
// outside what Sass-level tests can see — see .build/build-css.ts's minify().
|
||||
@use '../ui/cards' as cards;
|
||||
|
||||
@include describe('card-gradient-background') {
|
||||
@include it('keeps the % unit on every zero-value gradient stop') {
|
||||
@include assert() {
|
||||
@include output() {
|
||||
.t {
|
||||
@include cards.card-gradient-background;
|
||||
}
|
||||
}
|
||||
|
||||
@include expect() {
|
||||
.t {
|
||||
background:
|
||||
radial-gradient(ellipse at center, var(--card-bg) 0%, color-mix(in sRGB, var(--card-bg) 0%, transparent) 80%) border-box,
|
||||
linear-gradient(var(--card-gradient-direction), color-mix(in sRGB, var(--card-bg) var(--card-gradient-opacity), transparent) 0%, var(--card-bg) 40%) border-box,
|
||||
linear-gradient(calc(270deg + var(--card-gradient-direction)), var(--card-gradient)) border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,15 +688,22 @@ Card note
|
||||
/**
|
||||
Card gradient
|
||||
*/
|
||||
// Split out so scss/tests/_cards.test.scss can assert on it directly — a bare
|
||||
// top-level ruleset like .card-gradient can't be @include-d or otherwise
|
||||
// referenced in isolation by sass-true.
|
||||
@mixin card-gradient-background {
|
||||
background:
|
||||
radial-gradient(ellipse at center, var(--card-bg) 0%, color-mix(in sRGB, var(--card-bg) 0%, transparent) 80%) border-box,
|
||||
linear-gradient(var(--card-gradient-direction), color-mix(in sRGB, var(--card-bg) var(--card-gradient-opacity), transparent) 0%, var(--card-bg) 40%) border-box,
|
||||
linear-gradient(calc(270deg + var(--card-gradient-direction)), var(--card-gradient)) border-box;
|
||||
}
|
||||
|
||||
.card-gradient {
|
||||
--card-gradient-direction: 180deg;
|
||||
--card-gradient-opacity: 86%;
|
||||
--card-gradient: var(--primary), var(--primary);
|
||||
|
||||
background:
|
||||
radial-gradient(ellipse at center, var(--card-bg) 0%, color-mix(in sRGB, var(--card-bg) 0%, transparent) 80%) border-box,
|
||||
linear-gradient(var(--card-gradient-direction), color-mix(in sRGB, var(--card-bg) var(--card-gradient-opacity), transparent) 0%, var(--card-bg) 40%) border-box,
|
||||
linear-gradient(calc(270deg + var(--card-gradient-direction)), var(--card-gradient)) border-box;
|
||||
@include card-gradient-background();
|
||||
}
|
||||
|
||||
@each $name, $color in map.merge($colors, $theme-colors) {
|
||||
|
||||
@@ -6,7 +6,7 @@ layout: '@layouts/DocsMdxLayout.astro'
|
||||
---
|
||||
|
||||
import Example from '@components/Example.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import CodeDocs from '@components/CodeDocs.astro';
|
||||
|
||||
@@ -31,7 +31,7 @@ The navbar can contain links, buttons, and other elements. You can customize the
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<NavbarLogo class="me-3" />
|
||||
<Logo class="me-3" />
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">
|
||||
|
||||
+2
-2
@@ -20,10 +20,10 @@
|
||||
"lint:scss:fix": "pnpm --filter @tabler/core lint:scss:fix",
|
||||
"lint-md": "markdownlint --config .markdownlint-docs.json \"docs/pages/**/*.mdx\"",
|
||||
"lint-md-fix": "markdownlint --fix --config .markdownlint-docs.json \"docs/pages/**/*.mdx\"",
|
||||
"lint-prettier": "prettier --check \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.ts\" \"preview/scss/**/*.scss\" \"preview/js/**/*.{js,ts}\" \"{preview,docs,shared}/**/*.astro\" \"{preview,docs}/**/*.{mjs,mts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
||||
"lint-prettier": "prettier --check \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.ts\" \"screenshots/.build/**/*.ts\" \"preview/scss/**/*.scss\" \"preview/js/**/*.{js,ts}\" \"{preview,docs,screenshots,shared}/**/*.astro\" \"{preview,docs,screenshots}/**/*.{mjs,mts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
||||
"lint-scss-vars": "pnpm --filter @tabler/core css-lint",
|
||||
"format": "pnpm run format-prettier && pnpm run reformat-md",
|
||||
"format-prettier": "prettier --write \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.ts\" \"preview/scss/**/*.scss\" \"preview/js/**/*.{js,ts}\" \"{preview,docs,shared}/**/*.astro\" \"{preview,docs}/**/*.{mjs,mts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
||||
"format-prettier": "prettier --write \"core/scss/**/*.scss\" \"core/js/**/*.{js,ts}\" \"core/.build/**/*.{ts,mts,mjs}\" \".build/**/*.ts\" \"screenshots/.build/**/*.ts\" \"preview/scss/**/*.scss\" \"preview/js/**/*.{js,ts}\" \"{preview,docs,screenshots,shared}/**/*.astro\" \"{preview,docs,screenshots}/**/*.{mjs,mts}\" \"shared/**/*.{js,mjs,mts,ts}\" --cache",
|
||||
"check": "pnpm run lint && pnpm run type-check",
|
||||
"zip-package": "tsx .build/zip-package.ts",
|
||||
"start": "pnpm dev"
|
||||
|
||||
Generated
+25
@@ -283,6 +283,31 @@ importers:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
screenshots:
|
||||
dependencies:
|
||||
'@tabler/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
astro:
|
||||
specifier: ^7.1.6
|
||||
version: 7.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@26.1.1)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)
|
||||
devDependencies:
|
||||
'@astrojs/check':
|
||||
specifier: ^0.9.4
|
||||
version: 0.9.10(prettier-plugin-astro@0.14.1)(prettier@3.9.6)(typescript@5.9.3)
|
||||
'@types/node':
|
||||
specifier: ^26.1.1
|
||||
version: 26.1.1
|
||||
playwright:
|
||||
specifier: 1.62.0
|
||||
version: 1.62.0
|
||||
shx:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
shared:
|
||||
dependencies:
|
||||
'@tabler/core':
|
||||
|
||||
@@ -2,6 +2,7 @@ packages:
|
||||
- core
|
||||
- preview
|
||||
- docs
|
||||
- screenshots
|
||||
- shared
|
||||
|
||||
allowBuilds:
|
||||
|
||||
@@ -116,13 +116,6 @@ export default defineConfig({
|
||||
to: path('./public/preview'),
|
||||
label: '@tabler/preview',
|
||||
},
|
||||
{
|
||||
// docs.css built by the @tabler/docs sass pipeline (used by docs pages)
|
||||
from: path('../docs/dist/css'),
|
||||
to: path('./public/css'),
|
||||
label: '@tabler/docs',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
// static assets (photos, avatars, tracks, brand svgs...). The real source,
|
||||
// because preview/static is a symlink that may not survive deployment packaging.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
// NOTE: `current=2` passed to ProgressSteps is a no-op (reads `active`, not `current`) — only step 1 is active.
|
||||
import BaseLayout from '@shared/layouts/BaseLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
import ProgressSteps from '@ui/ProgressSteps.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
@@ -13,7 +13,7 @@ import FormGroup from '@ui/FormGroup.astro'
|
||||
<div class="container">
|
||||
<div class="row w-100 align-items-center">
|
||||
<div class="col me-auto">
|
||||
<NavbarLogo class="logo-gray" />
|
||||
<Logo class="logo-gray" />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ProgressSteps count={5} />
|
||||
|
||||
@@ -3,28 +3,13 @@ import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import BackgroundPattern from '@ui/BackgroundPattern.astro'
|
||||
import site from '@data/site.json'
|
||||
|
||||
const patternColors = Object.keys(site.colors)
|
||||
const patternSizes = ['sm', 'md', 'lg', 'xl']
|
||||
const patternSizes = ['sm', 'md', 'lg', 'xl'] as const
|
||||
|
||||
const patterns = [
|
||||
'bg-pattern-diagonal',
|
||||
'bg-pattern-diagonal-2',
|
||||
'bg-pattern-dots',
|
||||
'bg-pattern-rectangles',
|
||||
'bg-pattern-lines',
|
||||
'bg-pattern-lines-vertical',
|
||||
'bg-pattern-grid',
|
||||
'bg-pattern-grid-diagonal',
|
||||
'bg-pattern-blueprint',
|
||||
'bg-pattern-circles',
|
||||
'bg-pattern-diagonal-stripes',
|
||||
'bg-pattern-diagonal-stripes-2',
|
||||
'bg-pattern-zigzag',
|
||||
'bg-pattern-vertical-stripes',
|
||||
'bg-pattern-horizontal-stripes',
|
||||
]
|
||||
const patterns = ['diagonal', 'diagonal-2', 'dots', 'rectangles', 'lines', 'lines-vertical', 'grid', 'grid-diagonal', 'blueprint', 'circles', 'diagonal-stripes', 'diagonal-stripes-2', 'zigzag', 'vertical-stripes', 'horizontal-stripes'] as const
|
||||
---
|
||||
|
||||
<DefaultLayout title="Patterns" pageHeader="Patterns" pageMenu="base.patterns">
|
||||
@@ -37,8 +22,8 @@ const patterns = [
|
||||
{
|
||||
patterns.map((pattern) => (
|
||||
<div class="col">
|
||||
<div class={`${pattern} rounded mb-3`} style="height: 8rem;" />
|
||||
<code>.{pattern}</code>
|
||||
<BackgroundPattern pattern={pattern} class="rounded mb-3" style="height: 8rem;" />
|
||||
<code>.bg-pattern-{pattern}</code>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
@@ -54,7 +39,7 @@ const patterns = [
|
||||
{
|
||||
patternColors.map((color) => (
|
||||
<div class="col">
|
||||
<div class={`bg-pattern-rectangles bg-pattern-${color} rounded mb-3`} style="height: 8rem;" />
|
||||
<BackgroundPattern pattern="rectangles" color={color} class="rounded mb-3" style="height: 8rem;" />
|
||||
<code>.bg-pattern-{color}</code>
|
||||
</div>
|
||||
))
|
||||
@@ -71,7 +56,7 @@ const patterns = [
|
||||
{
|
||||
patternSizes.map((size) => (
|
||||
<div class="col">
|
||||
<div class={`bg-pattern-diagonal bg-pattern-${size} rounded mb-3`} style="height: 8rem;" />
|
||||
<BackgroundPattern pattern="diagonal" size={size} class="rounded mb-3" style="height: 8rem;" />
|
||||
<code>.bg-pattern-{size}</code>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Color swatches use site.themeColors (blue…cyan).
|
||||
// Large prose/nav-segmented block from source template omitted (was commented out).
|
||||
import BaseLayout from '@shared/layouts/BaseLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
import { site } from '@shared/lib/site'
|
||||
---
|
||||
|
||||
@@ -49,7 +49,7 @@ import { site } from '@shared/lib/site'
|
||||
<div class="page page-center align-items-center">
|
||||
<div class="screenshot d-flex flex-column justify-content-center align-items-center px-12">
|
||||
<div class="screenshot-logo">
|
||||
<NavbarLogo class="brand-gray" />
|
||||
<Logo gray />
|
||||
</div>
|
||||
<div class="screenshot-card card p-6">
|
||||
<div class="row g-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
// Photo id=11 → 12th entry of the unfiltered photos list; no horizontal filter.
|
||||
import BaseLayout from '@shared/layouts/BaseLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
import SignInForm from '@shared/components/cards/SignInForm.astro'
|
||||
import Photo from '@ui/Photo.astro'
|
||||
import photos from '@data/photos.json'
|
||||
@@ -12,7 +12,7 @@ import photos from '@data/photos.json'
|
||||
<div class="col-12 col-lg-6 col-xl-4 border-top-wide border-primary d-flex flex-column justify-content-center">
|
||||
<div class="container container-tight my-5 px-lg-5">
|
||||
<div class="text-center mb-4">
|
||||
<NavbarLogo />
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
<h2 class="h3 text-center mb-3">Login to your account</h2>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import SingleLayout from '@shared/layouts/SingleLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
import SignInCard from '@shared/components/cards/SignInCard.astro'
|
||||
import Illustration from '@ui/Illustration.astro'
|
||||
|
||||
@@ -12,7 +12,7 @@ import Illustration from '@ui/Illustration.astro'
|
||||
<div class="col-lg">
|
||||
<div class="container-tight">
|
||||
<div class="text-center mb-4">
|
||||
<NavbarLogo />
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
<SignInCard />
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import StatusMonitoring from '@shared/components/cards/StatusMonitoring.astro'
|
||||
|
||||
interface TrackingItem {
|
||||
status?: 'success' | 'warning' | 'danger'
|
||||
label: string
|
||||
}
|
||||
|
||||
// Mirrors the uptime history shown in docs/pages/ui/components/tracking.mdx.
|
||||
const uptime: TrackingItem[] = [
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'danger', label: 'Downtime' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'warning', label: 'Big load' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'danger', label: 'Downtime' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ label: 'No data' },
|
||||
{ label: 'No data' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
]
|
||||
---
|
||||
|
||||
<DefaultLayout title="Tracking" pageHeader="Tracking" pageMenu="base.tracking">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Basic example</CardTitle>
|
||||
<div class="tracking">
|
||||
{uptime.map((item) => <div class:list={['tracking-block', item.status && `bg-${item.status}`]} data-bs-toggle="tooltip" data-bs-placement="top" title={item.label} />)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<StatusMonitoring />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Squares variant (.tracking-squares)</CardTitle>
|
||||
<div class="tracking tracking-squares">
|
||||
{uptime.map((item) => <div class:list={['tracking-block', item.status && `bg-${item.status}`]} data-bs-toggle="tooltip" data-bs-placement="top" title={item.label} />)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
// Serves the built screenshots/dist via `astro preview` and captures a light
|
||||
// and dark (?theme=dark) PNG — at 1x and @2x — of every page's #screenshot
|
||||
// canvas (the full 1024x768 frame — logo, gradient backdrop and fake cursor
|
||||
// included, not just the cropped component card).
|
||||
// Run via `pnpm run capture` (builds first) — pass slugs as args to capture
|
||||
// only specific pages, e.g. `pnpm run capture button badge`.
|
||||
import { chromium, type Page } from 'playwright'
|
||||
import { spawn, type ChildProcess } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, readdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.join(__dirname, '..')
|
||||
const distDir = path.join(root, 'dist')
|
||||
const outDir = path.join(root, 'captures')
|
||||
|
||||
const PORT = Number(process.env.CAPTURE_PORT ?? 4020)
|
||||
const baseUrl = `http://localhost:${PORT}`
|
||||
|
||||
function discoverSlugs(): string[] {
|
||||
if (!existsSync(distDir)) {
|
||||
console.error(`No build found at ${path.relative(process.cwd(), distDir)} — run \`astro build\` first (or use \`pnpm run capture\`).`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const filter = process.argv.slice(2)
|
||||
return readdirSync(distDir)
|
||||
.filter((file) => file.endsWith('.html') && file !== 'index.html')
|
||||
.map((file) => file.replace(/\.html$/, ''))
|
||||
.filter((slug) => filter.length === 0 || filter.includes(slug))
|
||||
.sort()
|
||||
}
|
||||
|
||||
// Spawns the `astro` binary directly (not via `pnpm exec astro`) so `child.pid`
|
||||
// is the actual server process — killing a `pnpm exec` wrapper doesn't reliably
|
||||
// kill the process it launches, which is how earlier runs left zombie preview
|
||||
// servers squatting on the port for subsequent runs to collide with.
|
||||
const astroBin = path.join(root, 'node_modules/.bin/astro')
|
||||
|
||||
function startPreviewServer(): Promise<ChildProcess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(astroBin, ['preview', '--port', String(PORT)], {
|
||||
cwd: root,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
|
||||
const timer = setTimeout(() => reject(new Error(`astro preview didn't come up on port ${PORT} within 20s`)), 20_000)
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
if (data.toString().includes('Local')) {
|
||||
clearTimeout(timer)
|
||||
child.stdout?.off('data', onData)
|
||||
resolve(child)
|
||||
}
|
||||
}
|
||||
child.stdout?.on('data', onData)
|
||||
child.stderr?.on('data', (data) => process.stderr.write(data))
|
||||
child.once('error', reject)
|
||||
child.once('exit', (code) => {
|
||||
if (code !== null && code !== 0) reject(new Error(`astro preview exited with code ${code}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function filenameFor(slug: string, theme: 'light' | 'dark', scale: 1 | 2): string {
|
||||
const themeSuffix = theme === 'dark' ? '-dark' : ''
|
||||
const scaleSuffix = scale === 2 ? '@2x' : ''
|
||||
return `${slug}${themeSuffix}${scaleSuffix}.png`
|
||||
}
|
||||
|
||||
async function captureOne(page: Page, slug: string, theme: 'light' | 'dark', scale: 1 | 2) {
|
||||
await page.goto(`${baseUrl}/${slug}?theme=${theme}`, { waitUntil: 'load' })
|
||||
await page.waitForFunction(() => document.documentElement.dataset.screenshotReady === 'true', { timeout: 15_000 })
|
||||
|
||||
const filename = filenameFor(slug, theme, scale)
|
||||
await page.locator('#screenshot').screenshot({ path: path.join(outDir, filename) })
|
||||
console.log(` ✓ ${filename}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const slugs = discoverSlugs()
|
||||
if (slugs.length === 0) {
|
||||
console.error('No matching pages found to capture.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
console.log(`Starting preview server on port ${PORT}…`)
|
||||
const server = await startPreviewServer()
|
||||
// Interrupting the script (Ctrl+C) must not leave the preview server behind
|
||||
// squatting on the port for the next run to collide with.
|
||||
const killServer = () => server.kill()
|
||||
process.once('SIGINT', killServer)
|
||||
process.once('SIGTERM', killServer)
|
||||
|
||||
try {
|
||||
const browser = await chromium.launch()
|
||||
const viewport = { width: 1280, height: 800 }
|
||||
// Two pages, not one reused with setViewportSize — deviceScaleFactor is
|
||||
// fixed at context/page creation and can't be changed on an existing page.
|
||||
const page1x = await browser.newPage({ viewport, deviceScaleFactor: 1 })
|
||||
const page2x = await browser.newPage({ viewport, deviceScaleFactor: 2 })
|
||||
|
||||
for (const slug of slugs) {
|
||||
console.log(slug)
|
||||
await captureOne(page1x, slug, 'light', 1)
|
||||
await captureOne(page1x, slug, 'dark', 1)
|
||||
await captureOne(page2x, slug, 'light', 2)
|
||||
await captureOne(page2x, slug, 'dark', 2)
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
console.log(`\n${slugs.length * 4} screenshots written to ${path.relative(process.cwd(), outDir)}/`)
|
||||
} finally {
|
||||
server.kill()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
# build output
|
||||
dist/
|
||||
.astro/
|
||||
|
||||
# public/ is fully generated by .build/copy-assets.ts
|
||||
public/
|
||||
|
||||
# generated by `pnpm run capture`
|
||||
captures/
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,66 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { copyAssets } from '../.build/copy-assets'
|
||||
|
||||
/** @param {string} p */
|
||||
const path = (p) => fileURLToPath(new URL(p, import.meta.url))
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
// pages live at the package root (./pages) — components/lib/data are shared
|
||||
// (see the @shared alias)
|
||||
srcDir: '.',
|
||||
server: {
|
||||
port: 3020,
|
||||
// bind on all interfaces so the dev server is reachable from Docker
|
||||
// port mappings and other devices on the local network
|
||||
host: true,
|
||||
},
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
|
||||
'@ui': fileURLToPath(new URL('../shared/ui', import.meta.url)),
|
||||
'@components': fileURLToPath(new URL('../shared/components', import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
// Emit button.html instead of button/index.html — a stable, predictable
|
||||
// URL per component for the screenshot tool to iterate over.
|
||||
format: 'file',
|
||||
},
|
||||
compressHTML: false,
|
||||
integrations: [
|
||||
copyAssets({
|
||||
repo: path('..'),
|
||||
publicDir: path('./public'),
|
||||
copies: [
|
||||
{
|
||||
// @tabler/core dist (css/js/fonts/img/libs).
|
||||
// Fallback keeps current assets if core rebuilds dist mid-copy in turbo dev.
|
||||
from: path('./node_modules/@tabler/core/dist'),
|
||||
to: path('./public/dist'),
|
||||
label: '@tabler/core',
|
||||
allowDestinationFallback: true,
|
||||
},
|
||||
{
|
||||
// static assets referenced by demo data (avatar photos, brand svgs...).
|
||||
from: path('../shared/static'),
|
||||
to: path('./public/static'),
|
||||
label: 'shared assets',
|
||||
},
|
||||
{ from: path('./assets/favicon.ico'), to: path('./public/favicon.ico'), label: '@tabler/screenshots' },
|
||||
{ from: path('./assets/favicon-dev.ico'), to: path('./public/favicon-dev.ico'), label: '@tabler/screenshots' },
|
||||
],
|
||||
// Watch the real core/dist, not the node_modules symlink the startup copy
|
||||
// reads from — same directory.
|
||||
syncDirs: [
|
||||
{ from: path('../core/dist'), to: path('./public/dist') },
|
||||
{ from: path('../shared/static'), to: path('./public/static') },
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
// 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>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@tabler/screenshots",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"description": "Astro app: one page per UI component, for automated screenshotting",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"capture": "turbo build --filter=@tabler/screenshots && tsx .build/capture.ts",
|
||||
"clean": "shx rm -rf dist public captures/* .astro",
|
||||
"type-check": "astro check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/core": "workspace:*",
|
||||
"astro": "^7.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@types/node": "^26.1.1",
|
||||
"playwright": "1.62.0",
|
||||
"shx": "^0.4.0",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import ActiveUsers from '@shared/components/cards/charts/ActiveUsers.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Active subscriptions" columns={1} pageLibs={['apexcharts']}>
|
||||
<ActiveUsers />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Alert from '@ui/Alert.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Alert" columns={2}>
|
||||
<div class="space-y">
|
||||
<Alert type="success" title="Well done!" description="You successfully read this important alert message." />
|
||||
<Alert type="warning" title="Warning!" description="Better check yourself, you're not looking too good." />
|
||||
<Alert type="danger" title="Oh snap!" description="Change a few things up and try submitting again." showClose />
|
||||
<Alert type="info" title="Heads up!" description="This alert needs your attention, but it's not super important." />
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import AuthLockCard from '@shared/components/cards/AuthLockCard.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Account Locked" columns={1}>
|
||||
<AuthLockCard />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import AuthenticateAccount from '@shared/components/cards/AuthenticateAccount.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Authenticate Your Account" columns={1.5}>
|
||||
<AuthenticateAccount />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
// .bg-pattern-{color} sets --tblr-pattern-color; combine with a pattern-type
|
||||
// class (core/scss/ui/_patterns.scss).
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import BackgroundPattern from '@ui/BackgroundPattern.astro'
|
||||
import site from '@data/site.json'
|
||||
|
||||
const patternColors = Object.keys(site.colors)
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Background pattern colors" columns={2}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Pattern colors</CardTitle>
|
||||
<div class="row row-cols-4 g-3">
|
||||
{
|
||||
patternColors.map((color) => (
|
||||
<div class="col">
|
||||
<BackgroundPattern pattern="rectangles" color={color} class="rounded mb-2" style="height: 5rem;" />
|
||||
<code>.bg-pattern-{color}</code>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
// .bg-pattern-{type} (core/scss/ui/_patterns.scss) — pure CSS background-image
|
||||
// utilities, no color/size modifier needed for the default look (they read
|
||||
// --tblr-body-color at low opacity out of the box).
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import BackgroundPattern from '@ui/BackgroundPattern.astro'
|
||||
|
||||
const patterns = ['diagonal', 'dots', 'rectangles', 'grid', 'blueprint', 'circles', 'zigzag', 'diagonal-stripes'] as const
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Background pattern utilities" columns={2}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Pattern types</CardTitle>
|
||||
<div class="row row-cols-4 g-3">
|
||||
{
|
||||
patterns.map((pattern) => (
|
||||
<div class="col">
|
||||
<BackgroundPattern pattern={pattern} class="rounded mb-2" style="height: 5rem;" />
|
||||
<code>.bg-pattern-{pattern}</code>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import CryptoPrice from '@shared/components/cards/charts/CryptoPrice.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Candlestick chart" columns={2} pageLibs={['apexcharts']}>
|
||||
<CryptoPrice />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
// .card-gradient + .card-gradient-{variant} come from core/scss/ui/_cards.scss
|
||||
// (bundled in the base tabler.css — no cssPlugins needed here). Content follows
|
||||
// shared/components/cards/StatGradient.astro's pattern (subheader + big stat +
|
||||
// icon avatar + progress bar) rather than reusing it directly — its `color`
|
||||
// prop assumes a single real theme color drives BOTH the gradient background
|
||||
// and the icon/progress accent, which doesn't hold for the named palettes
|
||||
// below (rainbow, ocean, ...); accentColor is picked separately so the icon
|
||||
// stays legible in dark mode too ("dark" text is invisible on a dark theme).
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
|
||||
const variants = [
|
||||
{ name: 'rainbow', accentColor: 'pink', label: 'Palettes', icon: 'palette', value: '24', progress: 80 },
|
||||
{ name: 'ocean', accentColor: 'azure', label: 'Revenue', icon: 'droplet', value: '$12.4k', progress: 62 },
|
||||
{ name: 'sun', accentColor: 'orange', label: 'Temperature', icon: 'sun', value: '32°C', progress: 74 },
|
||||
{ name: 'snow', accentColor: 'cyan', label: 'Temperature', icon: 'snowflake', value: '-4°C', progress: 18 },
|
||||
{ name: 'gold', accentColor: 'yellow', label: 'Rank', icon: 'trophy', value: '#1', progress: 96 },
|
||||
{ name: 'disco', accentColor: 'purple', label: 'Tempo', icon: 'music', value: '128 BPM', progress: 55 },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Card gradient" columns={2}>
|
||||
<div class="row row-cards">
|
||||
{
|
||||
variants.map((variant) => (
|
||||
<div class="col-6">
|
||||
<div class={`card card-gradient card-gradient-${variant.name}`}>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col">
|
||||
<Subheader as="h4" class="mb-1">
|
||||
{variant.label}
|
||||
</Subheader>
|
||||
<div class="h3 m-0">{variant.value}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<Avatar icon={variant.icon} color={variant.accentColor} />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<Progress value={variant.progress} color={variant.accentColor} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import ChartRadial from '@ui/ChartRadial.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Task progress" columns={1} pageLibs={['apexcharts']}>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<ChartRadial title="Task progress" value={72} />
|
||||
</div>
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Chat from '@ui/Chat.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Chat" columns={2}>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Chat />
|
||||
</div>
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import ConversionRate from '@shared/components/cards/charts/ConversionRate.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Conversion rate" columns={1}>
|
||||
<ConversionRate />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import TrafficSources from '@shared/components/cards/charts/TrafficSources.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Donut chart" columns={1} pageLibs={['apexcharts']}>
|
||||
<TrafficSources />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Empty from '@ui/Empty.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Empty state" columns={2}>
|
||||
<Empty illustration="not-found.svg" />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import allFlags from '@data/flags.json'
|
||||
|
||||
// 12 most popular countries, by population/economy/recognizability.
|
||||
const popularCodes = ['us', 'gb', 'de', 'fr', 'cn', 'jp', 'in', 'br', 'ca', 'au', 'pl', 'es']
|
||||
const flags = popularCodes.map((code) => allFlags.find((country) => country.flag === code)).filter((country) => country !== undefined)
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Flags list" columns={2} cssPlugins={['flags']} zoom={1.5}>
|
||||
<div class="d-grid gap-4" style="grid-template-columns: repeat(4, auto); justify-content: center; align-items: center;">
|
||||
{
|
||||
flags.map((country) => (
|
||||
<span title={country.name}>
|
||||
<span class={`flag flag-country-${country.flag}`} />
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import HappyBirthday from '@shared/components/cards/HappyBirthday.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Happy Birthday" columns={1}>
|
||||
<HappyBirthday />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import WeeklyActivity from '@shared/components/cards/charts/WeeklyActivity.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Heatmap chart" columns={2} pageLibs={['apexcharts']}>
|
||||
<WeeklyActivity />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
// Every other page under pages/ is a screenshot target. Listing them via a
|
||||
// glob (instead of a hand-maintained array) means adding a new component
|
||||
// page is the only step needed for it to show up here.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
|
||||
const pageModules = import.meta.glob('./*.astro')
|
||||
const pages = Object.keys(pageModules)
|
||||
.map((path) => path.replace(/^\.\//, '').replace(/\.astro$/, ''))
|
||||
.filter((slug) => slug !== 'index')
|
||||
.sort()
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Components" columns={2}>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{
|
||||
pages.map((slug) => (
|
||||
<a href={`/${slug}`} class="d-block">
|
||||
{slug.replace(/-/g, ' ')}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
// MapVectorCard.astro has no room for a header legend/stats row, so the card
|
||||
// is hand-built here (same shape as MapVectorCard.astro) instead of reusing it.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import MapVector from '@ui/MapVector.astro'
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import ScaleLegend from '@ui/ScaleLegend.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Map" columns={2} pageLibs={['jsvectormap']} cssPlugins={['flags']}>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h3 class="card-title m-0">Locations</h3>
|
||||
<ScaleLegend class="ms-auto" />
|
||||
</div>
|
||||
<MapVector mapId="world" color="primary" ratio="21x9" />
|
||||
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-4">
|
||||
<Subheader>Countries</Subheader>
|
||||
<div class="h3 m-0">182</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<Subheader>Top country</Subheader>
|
||||
<div class="h3 m-0 d-flex align-items-center gap-2">
|
||||
<span class="flag flag-xs flag-country-pl"></span>
|
||||
Poland
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<Subheader>Total visits</Subheader>
|
||||
<div class="h3 m-0">84,392</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import NewClients from '@shared/components/cards/charts/NewClients.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="New clients" columns={1} pageLibs={['apexcharts']}>
|
||||
<NewClients />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Payment from '@ui/Payment.astro'
|
||||
import allPayments from '@data/payments.json'
|
||||
|
||||
// 12 most globally recognized payment providers.
|
||||
const popularLogos = ['visa', 'mastercard', 'paypal', 'applepay', 'google-pay', 'stripe', 'americanexpress', 'klarna', 'amazon-pay', 'samsung-pay', 'unionpay', 'alipay']
|
||||
const payments = popularLogos.map((logo) => allPayments.find((item) => item.logo === logo)).filter((item) => item !== undefined)
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Payments list" columns={2} cssPlugins={['payments']} zoom={1.5}>
|
||||
<div class="d-grid gap-4" style="grid-template-columns: repeat(4, auto); justify-content: center; align-items: center;">
|
||||
{
|
||||
payments.map((item) => (
|
||||
<span title={item.name}>
|
||||
<Payment payment={item.logo} dark={true} />
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Revenue from '@shared/components/cards/charts/Revenue.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Revenue" columns={1} pageLibs={['apexcharts']}>
|
||||
<Revenue />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import SalesOverview from '@shared/components/cards/charts/SalesOverview.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Sales overview" pageLibs={['apexcharts']} columns={2}>
|
||||
<SalesOverview />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Sales from '@shared/components/cards/charts/Sales.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Sales" columns={1}>
|
||||
<Sales />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
// .social-app-{file} icon classes come from the "socials" core CSS plugin
|
||||
// (tabler-socials.css) — not bundled in the base tabler.css, hence cssPlugins.
|
||||
// The grid itself is built from scratch here (not preview's .demo-icons-list)
|
||||
// since that class lives in preview/scss/demo.scss, which this app never loads.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import socials from '@data/socials.json'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="List of all social media icons" columns={2} cssPlugins={['socials']} zoom={1.25}>
|
||||
<div class="d-flex flex-wrap gap-4 justify-content-center align-items-center">
|
||||
{
|
||||
socials.map((item) => (
|
||||
<span title={item.name}>
|
||||
<span class={`social social-app-${item.file}`} />
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import SocialReferrals from '@shared/components/cards/charts/SocialReferrals.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Social referrals" columns={2} pageLibs={['apexcharts']}>
|
||||
<SocialReferrals />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import TeamLeaderboard from '@shared/components/cards/TeamLeaderboard.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Team leaderboard" columns={2}>
|
||||
<TeamLeaderboard />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import TotalUsers from '@shared/components/cards/charts/TotalUsers.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Total Users" columns={1} pageLibs={['apexcharts']}>
|
||||
<TotalUsers />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import StatusMonitoring from '@shared/components/cards/StatusMonitoring.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Tracking" columns={1}>
|
||||
<StatusMonitoring />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import UserCardBg from '@shared/components/cards/UserCardBg.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="User card" columns={1}>
|
||||
<UserCardBg />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import InlinePlayer from '@ui/InlinePlayer.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Vimeo Player" columns={2} pageLibs={['plyr']}>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<InlinePlayer id="charlotte" type="vimeo" embedId={707012696} />
|
||||
</div>
|
||||
</div>
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Welcome from '@shared/components/cards/Welcome.astro'
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Welcome" columns={2}>
|
||||
<Welcome />
|
||||
</ScreenshotLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist", "public"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@data/*": ["../shared/data/*"],
|
||||
"@shared/*": ["../shared/*"],
|
||||
"@ui/*": ["../shared/ui/*"],
|
||||
"@components/*": ["../shared/components/*"]
|
||||
},
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
import FormFooter from '@ui/FormFooter.astro'
|
||||
import Button from '@ui/Button.astro'
|
||||
import ButtonList from '@ui/ButtonList.astro'
|
||||
import CaptureScript from '../CaptureScript.astro'
|
||||
---
|
||||
|
||||
<form class="card card-md" action="#" method="get" autocomplete="off" novalidate>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title card-title-lg text-center mb-4">Authenticate Your Account</h2>
|
||||
|
||||
<p class="my-4 text-center">
|
||||
Please confirm your account by entering the authorization code sent to <strong>+1 856-672-8552</strong>.
|
||||
</p>
|
||||
|
||||
<div class="my-5">
|
||||
<div class="row g-4">
|
||||
{
|
||||
Array.from({ length: 2 }).map(() => (
|
||||
<div class="col">
|
||||
<div class="row g-2">
|
||||
{Array.from({ length: 3 }).map(() => (
|
||||
<div class="col">
|
||||
<input type="text" class="form-control form-control-lg text-center px-3 py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input" />
|
||||
Don't ask for codes again on this device
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<FormFooter>
|
||||
<ButtonList class="flex-nowrap">
|
||||
<Button text="Cancel" block href="#" />
|
||||
<Button text="Verify" block color="primary" />
|
||||
</ButtonList>
|
||||
</FormFooter>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<CaptureScript>
|
||||
<!-- BEGIN CODE INPUT SCRIPT -->
|
||||
<script is:inline>
|
||||
const inputs = document.querySelectorAll('[data-code-input]')
|
||||
|
||||
inputs.forEach((input, i) => {
|
||||
input.addEventListener('input', (e) => {
|
||||
const target = e.target
|
||||
if (target.value.length === target.maxLength && i + 1 < inputs.length) {
|
||||
inputs[i + 1].focus()
|
||||
}
|
||||
})
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
const target = e.target
|
||||
if (target.value.length === 0 && e.key === 'Backspace' && i > 0) {
|
||||
inputs[i - 1].focus()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<!-- END CODE INPUT SCRIPT -->
|
||||
</CaptureScript>
|
||||
@@ -2,6 +2,7 @@
|
||||
// Photos filtered to horizontal=true, then sliced by offset/limit.
|
||||
import photos from '@data/photos.json'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import Carousel from '@ui/Carousel.astro'
|
||||
|
||||
interface Props {
|
||||
id?: string
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Trending from '@ui/Trending.astro'
|
||||
|
||||
interface TrackingItem {
|
||||
status?: 'success' | 'warning' | 'danger'
|
||||
label: string
|
||||
}
|
||||
|
||||
// Mirrors the uptime history shown in docs/pages/ui/components/tracking.mdx.
|
||||
const uptime: TrackingItem[] = [
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'danger', label: 'Downtime' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'warning', label: 'Big load' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'danger', label: 'Downtime' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ label: 'No data' },
|
||||
{ label: 'No data' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
{ status: 'success', label: 'Operational' },
|
||||
]
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<Subheader>Status monitoring</Subheader>
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle text-secondary" href="#" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Current month </a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item active" href="#">Current month</a>
|
||||
<a class="dropdown-item" href="#">Last month</a>
|
||||
<a class="dropdown-item" href="#">Last 3 months</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 mb-3 me-2">99.5%</div>
|
||||
<div class="me-auto">
|
||||
<Trending value={2} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="tracking">
|
||||
{uptime.map((item) => <div class:list={['tracking-block', item.status && `bg-${item.status}`]} data-bs-toggle="tooltip" data-bs-placement="top" title={item.label} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import DropdownDays from '@ui/DropdownDays.astro'
|
||||
import Trending from '@ui/Trending.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<Subheader>Conversion rate</Subheader>
|
||||
<div class="ms-auto lh-1">
|
||||
<DropdownDays id="conversion-rate" label="Select time range for conversion rate" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 mb-3 me-2">42.8%</div>
|
||||
<div class="me-auto">
|
||||
<Trending value={5} />
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={43} color="primary" size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Chart from '@ui/Chart.astro'
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Subheader>LTC/BTC</Subheader>
|
||||
<Chart chartId="dashboard-crypto-candlestick" height={12} label="LTC/BTC" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Chart from '@ui/Chart.astro'
|
||||
import Legend from '@ui/Legend.astro'
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||
<div class="mb-3">
|
||||
<Subheader>Sales overview</Subheader>
|
||||
<div class="d-flex align-items-baseline gap-2 mt-1">
|
||||
<div class="h1 mb-0">$284,300</div>
|
||||
<div class="text-green fw-medium">+12.4%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<Legend color="primary">This year</Legend>
|
||||
<Legend color="secondary">Last year</Legend>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chart chartId="sales-overview" height={15} label="Sales overview" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Chart from '@ui/Chart.astro'
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Subheader>Traffic sources</Subheader>
|
||||
<Chart chartId="traffic-sources-donut" height={12} label="Traffic sources" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
import Subheader from '@ui/Subheader.astro'
|
||||
import Chart from '@ui/Chart.astro'
|
||||
---
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<Subheader>Weekly activity</Subheader>
|
||||
<Chart chartId="activity-heatmap" height={12} label="Weekly activity" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
// "active" is hardcoded on Home — not derived from the current page.
|
||||
// Relative base is "../" for one-level-deep marketing pages.
|
||||
import NavbarLogo from '../navbar/NavbarLogo.astro'
|
||||
import Logo from '../navbar/Logo.astro'
|
||||
|
||||
const base = '..'
|
||||
---
|
||||
@@ -9,7 +9,7 @@ const base = '..'
|
||||
<!-- BEGIN MARKETING NAVBAR -->
|
||||
<header class="navbar navbar-expand-lg navbar-transparent py-3">
|
||||
<div class="container">
|
||||
<NavbarLogo href={base} />
|
||||
<Logo href={base} />
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarTogglerDemo01" aria-controls="navbarTogglerDemo01" aria-expanded="false" aria-label="Toggle primary navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import Illustration from '@ui/Illustration.astro'
|
||||
import Carousel from '@ui/Carousel.astro'
|
||||
import CaptureScript from '../../CaptureScript.astro'
|
||||
|
||||
const strings = ['more effective', 'more efficient', 'more productive']
|
||||
@@ -27,27 +28,25 @@ const typedId = 'typed'
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="hero-img-side">
|
||||
<div id="carousel-controls" class="carousel slide" data-bs-ride="carousel" data-interval="4000">
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<Illustration image="boy-with-key.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<Illustration image="boy-girl.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<Illustration image="computer-fix.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<Carousel id="carousel-controls" slideCount={3} interval={4000}>
|
||||
<div class="carousel-item active">
|
||||
<Illustration image="boy-with-key.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<button type="button" class="carousel-control-prev text-secondary" data-bs-target="#carousel-controls" data-bs-slide="prev">
|
||||
<div class="carousel-item">
|
||||
<Illustration image="boy-girl.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<Illustration image="computer-fix.svg" class="d-block mx-auto" height="350" />
|
||||
</div>
|
||||
<a slot="controls" class="carousel-control-prev text-secondary" href="#carousel-controls" role="button" data-bs-slide="prev">
|
||||
<Icon name="chevron-left" class="icon-md" />
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button type="button" class="carousel-control-next text-secondary" data-bs-target="#carousel-controls" data-bs-slide="next">
|
||||
</a>
|
||||
<a slot="controls" class="carousel-control-next text-secondary" href="#carousel-controls" role="button" data-bs-slide="next">
|
||||
<Icon name="chevron-right" class="icon-md" />
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,18 +9,20 @@ interface Props {
|
||||
smallLogo?: boolean
|
||||
hideLogo?: boolean
|
||||
showTitle?: boolean
|
||||
/** neutral gray mark + text instead of the live brand color (e.g. screenshot watermarks) */
|
||||
gray?: boolean
|
||||
}
|
||||
|
||||
const { href = '.', class: className, header = false, smallLogo = false, hideLogo = false, showTitle = false } = Astro.props
|
||||
const { href = '.', class: className, header = false, smallLogo = false, hideLogo = false, showTitle = false, gray = false } = Astro.props
|
||||
|
||||
const brandClasses = ['navbar-brand navbar-brand-autodark', className]
|
||||
const brandClasses = ['navbar-brand navbar-brand-autodark', gray && 'navbar-brand-gray', className]
|
||||
const logoClass = `navbar-brand-image${showTitle ? ' me-3' : ''}`
|
||||
|
||||
const logoMark =
|
||||
'<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#066fd1" style="fill: var(--tblr-primary, #066fd1)" />'
|
||||
'<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#066fd1" style="fill: var(--tblr-navbar-logo-color, var(--tblr-primary, #066fd1))" />'
|
||||
|
||||
const logoText =
|
||||
'<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#4a4a4a"/>'
|
||||
'<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#4a4a4a" style="fill: var(--tblr-navbar-logo-color, #4a4a4a)"/>'
|
||||
|
||||
const logoSvg = smallLogo ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 68" width="32" height="32" aria-label="${site.title}" class="${logoClass}">${logoMark}</svg>` : `<svg xmlns="http://www.w3.org/2000/svg" width="110" height="32" viewBox="0 0 232 68" class="${logoClass}">${logoMark}${logoText}</svg>`
|
||||
---
|
||||
@@ -7,7 +7,7 @@
|
||||
// fluidSearch has no effect on output (search in the condensed branch is dead
|
||||
// code); the prop is kept for call-site compatibility.
|
||||
import Icon from '@ui/Icon.astro';
|
||||
import NavbarLogo from './NavbarLogo.astro';
|
||||
import Logo from './Logo.astro';
|
||||
import NavbarToggler from './NavbarToggler.astro';
|
||||
import NavbarSide from './NavbarSide.astro';
|
||||
import NavbarMenu from './NavbarMenu.astro';
|
||||
@@ -90,7 +90,7 @@ const headerClass = [
|
||||
|
||||
{
|
||||
!hideBrand && (
|
||||
<NavbarLogo
|
||||
<Logo
|
||||
header
|
||||
class="d-none-navbar-horizontal pe-0 pe-md-3"
|
||||
smallLogo={smallLogo}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import NavbarToggler from './NavbarToggler.astro'
|
||||
import NavbarLogo from './NavbarLogo.astro'
|
||||
import Logo from './Logo.astro'
|
||||
import NavbarSide from './NavbarSide.astro'
|
||||
import NavbarMenu from './NavbarMenu.astro'
|
||||
|
||||
@@ -25,7 +25,7 @@ const asideClass = ['navbar navbar-vertical', end && 'navbar-end', `navbar-expan
|
||||
<div class="container-fluid">
|
||||
<NavbarToggler target="sidebar-menu" />
|
||||
|
||||
<NavbarLogo header />
|
||||
<Logo header />
|
||||
|
||||
{/* Only the Sponsor button renders (no show-* flags). */}
|
||||
<NavbarSide class="d-lg-none" breakpoint={breakpoint} />
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import AvatarList from '@ui/AvatarList.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import Datagrid from '@ui/Datagrid.astro'
|
||||
import DatagridItem from '@ui/DatagridItem.astro'
|
||||
import people from '@data/people.json'
|
||||
|
||||
interface Person {
|
||||
@@ -14,72 +16,36 @@ interface Person {
|
||||
const creator = (people as Person[])[0]
|
||||
---
|
||||
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Registrar</div>
|
||||
<div class="datagrid-content">Third Party</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Nameservers</div>
|
||||
<div class="datagrid-content">Third Party</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Port number</div>
|
||||
<div class="datagrid-content">3306</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Expiration date</div>
|
||||
<div class="datagrid-content">–</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Creator</div>
|
||||
<div class="datagrid-content">
|
||||
<div class="d-flex align-items-center">
|
||||
<Avatar person={creator} size="xs" class="me-2" />
|
||||
{creator.full_name}
|
||||
</div>
|
||||
<Datagrid>
|
||||
<DatagridItem title="Registrar">Third Party</DatagridItem>
|
||||
<DatagridItem title="Nameservers">Third Party</DatagridItem>
|
||||
<DatagridItem title="Port number">3306</DatagridItem>
|
||||
<DatagridItem title="Expiration date">–</DatagridItem>
|
||||
<DatagridItem title="Creator">
|
||||
<div class="d-flex align-items-center">
|
||||
<Avatar person={creator} size="xs" class="me-2" />
|
||||
{creator.full_name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Age</div>
|
||||
<div class="datagrid-content">15 days</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Edge network</div>
|
||||
<div class="datagrid-content">
|
||||
<span class="status status-green"> Active </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Avatars list</div>
|
||||
<div class="datagrid-content">
|
||||
<AvatarList stacked={true} size="xs" text="+3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Checkbox</div>
|
||||
<div class="datagrid-content">
|
||||
<label class="form-check">
|
||||
<input class="form-check-input" type="checkbox" checked />
|
||||
<span class="form-check-label">Click me</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Icon</div>
|
||||
<div class="datagrid-content">
|
||||
<Icon name="check" color="green" />
|
||||
Checked
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Form control</div>
|
||||
<div class="datagrid-content">
|
||||
<input type="text" class="form-control form-control-flush" placeholder="Input placeholder" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="datagrid-item">
|
||||
<div class="datagrid-title">Longer description</div>
|
||||
<div class="datagrid-content">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</div>
|
||||
</div>
|
||||
</div>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Age">15 days</DatagridItem>
|
||||
<DatagridItem title="Edge network">
|
||||
<span class="status status-green"> Active </span>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Avatars list">
|
||||
<AvatarList stacked={true} size="xs" text="+3" />
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Checkbox">
|
||||
<label class="form-check">
|
||||
<input class="form-check-input" type="checkbox" checked />
|
||||
<span class="form-check-label">Click me</span>
|
||||
</label>
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Icon">
|
||||
<Icon name="check" color="green" />
|
||||
Checked
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Form control">
|
||||
<input type="text" class="form-control form-control-flush" placeholder="Input placeholder" />
|
||||
</DatagridItem>
|
||||
<DatagridItem title="Longer description">Lorem ipsum dolor sit amet, consectetur adipisicing elit.</DatagridItem>
|
||||
</Datagrid>
|
||||
|
||||
@@ -612,6 +612,129 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"traffic-sources-donut": {
|
||||
"type": "donut",
|
||||
"legend": true,
|
||||
"series": [
|
||||
{ "name": "Organic search", "color": "azure", "data": 38 },
|
||||
{ "name": "Direct", "color": "primary", "data": 27 },
|
||||
{ "name": "Social media", "color": "purple", "data": 18 },
|
||||
{ "name": "Referral", "color": "orange", "data": 12 },
|
||||
{ "name": "Email", "color": "green", "data": 5 }
|
||||
]
|
||||
},
|
||||
"activity-heatmap": {
|
||||
"type": "heatmap",
|
||||
"color": "green",
|
||||
"hide-tooltip": false,
|
||||
"series": [
|
||||
{
|
||||
"name": "Mon",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 2 },
|
||||
{ "x": "W2", "y": 4 },
|
||||
{ "x": "W3", "y": 0 },
|
||||
{ "x": "W4", "y": 6 },
|
||||
{ "x": "W5", "y": 3 },
|
||||
{ "x": "W6", "y": 8 },
|
||||
{ "x": "W7", "y": 1 },
|
||||
{ "x": "W8", "y": 5 },
|
||||
{ "x": "W9", "y": 7 },
|
||||
{ "x": "W10", "y": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tue",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 5 },
|
||||
{ "x": "W2", "y": 1 },
|
||||
{ "x": "W3", "y": 7 },
|
||||
{ "x": "W4", "y": 3 },
|
||||
{ "x": "W5", "y": 9 },
|
||||
{ "x": "W6", "y": 2 },
|
||||
{ "x": "W7", "y": 6 },
|
||||
{ "x": "W8", "y": 0 },
|
||||
{ "x": "W9", "y": 4 },
|
||||
{ "x": "W10", "y": 8 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wed",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 8 },
|
||||
{ "x": "W2", "y": 6 },
|
||||
{ "x": "W3", "y": 2 },
|
||||
{ "x": "W4", "y": 9 },
|
||||
{ "x": "W5", "y": 4 },
|
||||
{ "x": "W6", "y": 1 },
|
||||
{ "x": "W7", "y": 7 },
|
||||
{ "x": "W8", "y": 3 },
|
||||
{ "x": "W9", "y": 5 },
|
||||
{ "x": "W10", "y": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Thu",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 1 },
|
||||
{ "x": "W2", "y": 9 },
|
||||
{ "x": "W3", "y": 4 },
|
||||
{ "x": "W4", "y": 2 },
|
||||
{ "x": "W5", "y": 7 },
|
||||
{ "x": "W6", "y": 5 },
|
||||
{ "x": "W7", "y": 0 },
|
||||
{ "x": "W8", "y": 8 },
|
||||
{ "x": "W9", "y": 3 },
|
||||
{ "x": "W10", "y": 6 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Fri",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 6 },
|
||||
{ "x": "W2", "y": 3 },
|
||||
{ "x": "W3", "y": 9 },
|
||||
{ "x": "W4", "y": 1 },
|
||||
{ "x": "W5", "y": 5 },
|
||||
{ "x": "W6", "y": 7 },
|
||||
{ "x": "W7", "y": 4 },
|
||||
{ "x": "W8", "y": 2 },
|
||||
{ "x": "W9", "y": 8 },
|
||||
{ "x": "W10", "y": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sat",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 0 },
|
||||
{ "x": "W2", "y": 2 },
|
||||
{ "x": "W3", "y": 1 },
|
||||
{ "x": "W4", "y": 3 },
|
||||
{ "x": "W5", "y": 0 },
|
||||
{ "x": "W6", "y": 2 },
|
||||
{ "x": "W7", "y": 1 },
|
||||
{ "x": "W8", "y": 0 },
|
||||
{ "x": "W9", "y": 2 },
|
||||
{ "x": "W10", "y": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sun",
|
||||
"heatmap-data": [
|
||||
{ "x": "W1", "y": 1 },
|
||||
{ "x": "W2", "y": 0 },
|
||||
{ "x": "W3", "y": 2 },
|
||||
{ "x": "W4", "y": 0 },
|
||||
{ "x": "W5", "y": 1 },
|
||||
{ "x": "W6", "y": 3 },
|
||||
{ "x": "W7", "y": 0 },
|
||||
{ "x": "W8", "y": 1 },
|
||||
{ "x": "W9", "y": 0 },
|
||||
{ "x": "W10", "y": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scatter": {
|
||||
"demo": true,
|
||||
"type": "scatter",
|
||||
@@ -691,6 +814,25 @@
|
||||
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"y-max": 110
|
||||
},
|
||||
"sales-overview": {
|
||||
"type": "line",
|
||||
"stroke-width": [3, 2],
|
||||
"stroke-dash": [0, 6],
|
||||
"series": [
|
||||
{
|
||||
"name": "This year",
|
||||
"color": "primary",
|
||||
"data": [180, 195, 210, 205, 225, 240, 235, 260, 255, 270, 285, 300]
|
||||
},
|
||||
{
|
||||
"name": "Last year",
|
||||
"color": "secondary",
|
||||
"data": [150, 160, 168, 172, 180, 188, 190, 200, 205, 210, 215, 225]
|
||||
}
|
||||
],
|
||||
"categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
"y-max": 320
|
||||
},
|
||||
"new-clients": {
|
||||
"type": "line",
|
||||
"datetime": true,
|
||||
|
||||
@@ -231,6 +231,10 @@
|
||||
"title": "Toasts",
|
||||
"url": "toasts.html"
|
||||
},
|
||||
"tracking": {
|
||||
"title": "Tracking",
|
||||
"url": "tracking.html"
|
||||
},
|
||||
"typography": {
|
||||
"title": "Typography",
|
||||
"url": "typography.html"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
// Minimal navbar (small logo + close button) then a .page with the content.
|
||||
import BaseLayout from './BaseLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@@ -18,7 +18,7 @@ const { title, pageLibs, bodyClass } = Astro.props
|
||||
<!-- BEGIN NAVBAR -->
|
||||
<header class="navbar">
|
||||
<div class="container-fluid">
|
||||
<NavbarLogo smallLogo />
|
||||
<Logo smallLogo />
|
||||
|
||||
<div>
|
||||
<a href="." class="btn btn-close"></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro'
|
||||
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
|
||||
import Logo from '@shared/components/navbar/Logo.astro'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@@ -18,7 +18,7 @@ const { title, containerSize = 'tight', hideLogo } = Astro.props
|
||||
{
|
||||
!hideLogo && (
|
||||
<div class="text-center mb-4">
|
||||
<NavbarLogo />
|
||||
<Logo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ type Serie = {
|
||||
'color'?: string
|
||||
'color-opacity'?: string
|
||||
'candlestick-data'?: { x: number; y: number[] }[]
|
||||
/** one row of a heatmap: { x: category label, y: intensity value } per cell */
|
||||
'heatmap-data'?: { x: string; y: number }[]
|
||||
}
|
||||
|
||||
export type ChartData = {
|
||||
@@ -160,6 +162,15 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
config.dataLabels = { enabled: Boolean(data.datalabels) }
|
||||
}
|
||||
|
||||
if (type === 'heatmap') {
|
||||
// Rounded cells + a hairline gap read less like a spreadsheet, more like
|
||||
// a deliberate "contribution graph" grid. Single-hue intensity shading
|
||||
// (light -> the series color) is ApexCharts' own default once `colors`
|
||||
// is set below, so no colorScale.ranges override is needed here.
|
||||
config.plotOptions = { heatmap: { radius: 3 } }
|
||||
config.dataLabels = { enabled: false }
|
||||
}
|
||||
|
||||
if (data['fill-type']) {
|
||||
config.fill = {
|
||||
type: data['fill-type'],
|
||||
@@ -212,7 +223,7 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
} else {
|
||||
config.series = series.map((s) => ({
|
||||
name: s.name ?? '',
|
||||
data: s['candlestick-data'] ? s['candlestick-data'].map((c) => ({ x: c.x, y: c.y })) : (s.data as number[]),
|
||||
data: s['candlestick-data'] ? s['candlestick-data'].map((c) => ({ x: c.x, y: c.y })) : s['heatmap-data'] ? s['heatmap-data'].map((c) => ({ x: c.x, y: c.y })) : (s.data as number[]),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -223,6 +234,9 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
padding: data.sparkline ? undefined : { top: -20, right: 0, left: -4, bottom: -4 },
|
||||
show: data['hide-grid'] ? false : undefined,
|
||||
strokeDashArray: data['hide-grid'] ? undefined : 4,
|
||||
// ApexCharts' own default (#e0e0e0) is a light-mode-only gray — near
|
||||
// invisible, and wrong-toned, against a dark background.
|
||||
borderColor: 'var(--tblr-border-color)',
|
||||
xaxis: !data['hide-grid'] && data['show-x'] ? { lines: { show: true } } : undefined,
|
||||
}
|
||||
|
||||
@@ -231,9 +245,11 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
config.dataLabels = { enabled: true }
|
||||
}
|
||||
|
||||
if (data.categories || data.datetime) {
|
||||
if (data.categories || data.datetime || type === 'heatmap') {
|
||||
config.xaxis = {
|
||||
labels: { padding: 0 },
|
||||
// ApexCharts' own default label color (#373d3f) is a light-mode-only
|
||||
// dark gray — low-contrast (near-invisible) against a dark background.
|
||||
labels: { padding: 0, style: { colors: 'var(--tblr-secondary)' } },
|
||||
tooltip: { enabled: false },
|
||||
axisBorder: type === 'area' || type === 'bar' ? { show: false } : undefined,
|
||||
categories: data.categories?.map(String),
|
||||
@@ -243,7 +259,7 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
|
||||
if (!isRound) {
|
||||
config.yaxis = {
|
||||
labels: { padding: 4 },
|
||||
labels: { padding: 4, style: { colors: 'var(--tblr-secondary)' } },
|
||||
max: data['y-max'],
|
||||
title: data['y-title'] ? { text: escapeHtml(data['y-title']) } : undefined,
|
||||
tooltip: data['y-tooltip'] ? { enabled: true } : undefined,
|
||||
@@ -268,6 +284,9 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
|
||||
offsetY: 12,
|
||||
markers: { width: 10, height: 10, radius: 100 },
|
||||
itemMargin: { horizontal: 8, vertical: 8 },
|
||||
// Same fixed light-mode-only default (#373d3f) as the axis labels —
|
||||
// near-invisible on a dark background.
|
||||
labels: { colors: 'var(--tblr-secondary)' },
|
||||
}
|
||||
: { show: false }
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
// .bg-pattern-{type} utility (core/scss/ui/_patterns.scss) as a component;
|
||||
// combine with a theme color and/or size, e.g. <BackgroundPattern pattern="dots" color="primary" size="lg" />.
|
||||
|
||||
interface Props {
|
||||
pattern: 'diagonal' | 'diagonal-2' | 'dots' | 'rectangles' | 'lines' | 'lines-vertical' | 'grid' | 'grid-diagonal' | 'blueprint' | 'cross-dots' | 'circles' | 'diagonal-stripes' | 'diagonal-stripes-2' | 'zigzag' | 'vertical-stripes' | 'horizontal-stripes'
|
||||
/** theme color the pattern ramps up to, e.g. 'primary' */
|
||||
color?: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
/** element to render instead of div */
|
||||
as?: string
|
||||
class?: string
|
||||
/** remaining attributes (style, data-*, …) are forwarded to the element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { pattern, color, size, as: Element = 'div', class: className, ...rest } = Astro.props
|
||||
---
|
||||
|
||||
<Element class:list={[`bg-pattern-${pattern}`, color && `bg-pattern-${color}`, size && `bg-pattern-${size}`, className]} {...rest}>
|
||||
<slot />
|
||||
</Element>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
// Self-contained carousel (.carousel). Default slot holds the .carousel-item elements
|
||||
// (the caller marks the first one "active" — see CarouselCard.astro / hero/Side.astro);
|
||||
// indicators/controls are generated from props. A "controls" slot overrides the default
|
||||
// icon-span controls with custom markup (e.g. an <Icon>-based pair).
|
||||
|
||||
interface Props {
|
||||
id?: string
|
||||
/** slide count, used to generate indicator buttons */
|
||||
slideCount: number
|
||||
fade?: boolean
|
||||
/** data-bs-interval in ms, or false to disable autoplay */
|
||||
interval?: number | false
|
||||
indicators?: boolean
|
||||
indicatorsVariant?: 'dot' | 'thumb'
|
||||
indicatorsVertical?: boolean
|
||||
/** ratio ratio-4x3 on each indicator button, typically paired with indicatorsVariant="thumb" */
|
||||
indicatorsThumbRatio?: boolean
|
||||
/** background-image per slide, used when indicatorsVariant="thumb" */
|
||||
indicatorsImages?: string[]
|
||||
controls?: boolean
|
||||
class?: string
|
||||
/** remaining attributes forwarded to the element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { id, slideCount, fade, interval, indicators, indicatorsVariant, indicatorsVertical, indicatorsThumbRatio, indicatorsImages, controls, class: className, ...rest } = Astro.props
|
||||
|
||||
const target = id ? `#${id}` : undefined
|
||||
const slideIndexes = Array.from({ length: slideCount }, (_, i) => i)
|
||||
const hasCustomControls = Astro.slots.has('controls')
|
||||
---
|
||||
|
||||
<div id={id} class:list={['carousel', 'slide', fade && 'carousel-fade', className]} data-bs-ride="carousel" data-bs-interval={interval === false ? 'false' : interval} {...rest}>
|
||||
{
|
||||
indicators && (
|
||||
<div class:list={['carousel-indicators', indicatorsVertical && 'carousel-indicators-vertical', indicatorsVariant === 'dot' && 'carousel-indicators-dot', indicatorsVariant === 'thumb' && 'carousel-indicators-thumb']}>
|
||||
{slideIndexes.map((i) => (
|
||||
<button type="button" data-bs-target={target} data-bs-slide-to={i} class:list={[indicatorsThumbRatio && 'ratio ratio-4x3', i === 0 && 'active']} style={indicatorsVariant === 'thumb' && indicatorsImages?.[i] ? `background-image: url(${indicatorsImages[i]})` : undefined} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="carousel-inner">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{
|
||||
hasCustomControls ? (
|
||||
<slot name="controls" />
|
||||
) : (
|
||||
controls && (
|
||||
<Fragment>
|
||||
<a class="carousel-control-prev" href={target} role="button" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true" />
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href={target} role="button" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true" />
|
||||
<span class="visually-hidden">Next</span>
|
||||
</a>
|
||||
</Fragment>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -50,11 +50,42 @@ const elementId = `chart-${id}`;
|
||||
<script define:vars={{ chartKey, elementId, config, xFormatterExpr }}>
|
||||
window.tabler_chart ??= {};
|
||||
|
||||
// Heatmap shades each cell by computing off the literal base color in JS
|
||||
// (light -> full color-mix by value), unlike other chart types where the
|
||||
// SVG `fill` attribute just holds `var(--chart-...)` and the browser paints
|
||||
// it directly. A raw var()/color-mix() string can't be parsed by that color
|
||||
// math, so it falls back to gray — and ApexCharts' heatmap parser only
|
||||
// accepts hex, not rgb()/rgba() either. Probing a real element's computed
|
||||
// `color` resolves var()/color-mix() (Chromium serializes it as a CSS Color 4
|
||||
// `color(srgb ...)` string), then a 1x1 canvas round-trip converts that to hex.
|
||||
function resolveCssVar(value) {
|
||||
if (typeof value !== 'string' || !value.includes('var(')) return value;
|
||||
const probe = document.createElement('span');
|
||||
probe.style.cssText = 'position:absolute;visibility:hidden';
|
||||
probe.style.color = value;
|
||||
document.body.appendChild(probe);
|
||||
const resolved = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
if (!resolved) return value;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = resolved;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
if (xFormatterExpr && config.xaxis?.labels) {
|
||||
config.xaxis.labels.formatter = new Function('val', `return (${xFormatterExpr})`);
|
||||
}
|
||||
|
||||
if (config.chart?.type === 'heatmap' && Array.isArray(config.colors)) {
|
||||
config.colors = config.colors.map(resolveCssVar);
|
||||
}
|
||||
|
||||
window.ApexCharts &&
|
||||
(window.tabler_chart[chartKey] = new ApexCharts(document.getElementById(elementId), config)).render();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
// Data grid container (.datagrid); holds DatagridItem children.
|
||||
|
||||
interface Props {
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { class: className } = Astro.props
|
||||
---
|
||||
|
||||
<div class:list={['datagrid', className]}>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
// A single label/value pair (.datagrid-item); slot renders the value.
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { title, class: className } = Astro.props
|
||||
---
|
||||
|
||||
<div class:list={['datagrid-item', className]}>
|
||||
<div class="datagrid-title">{title}</div>
|
||||
<div class="datagrid-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
// A single legend item: a colored .legend dot + label, e.g. the "This year" /
|
||||
// "Last year" chart legend in SalesOverview.astro.
|
||||
interface Props {
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { color = 'primary', class: className } = Astro.props
|
||||
---
|
||||
|
||||
<div class:list={['d-flex align-items-center gap-2', className]}>
|
||||
<span class={`legend bg-${color}`}></span>
|
||||
<span class="text-secondary"><slot /></span>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
// "Less -> More" gradient legend, e.g. for a choropleth map's color scale.
|
||||
interface Props {
|
||||
/** number of swatches */
|
||||
steps?: number
|
||||
/** theme color the swatches ramp up to, e.g. 'primary' */
|
||||
color?: string
|
||||
minLabel?: string
|
||||
maxLabel?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { steps = 10, color = 'primary', minLabel = 'Less', maxLabel = 'More', class: className } = Astro.props
|
||||
|
||||
const items = Array.from({ length: steps }, (_, i) => i + 1)
|
||||
---
|
||||
|
||||
<div class:list={['d-flex align-items-center gap-2', className]}>
|
||||
<div class="text-secondary">{minLabel}</div>
|
||||
<div class="d-flex gap-1">
|
||||
{items.map((step) => <span class="legend" style={`background: color-mix(in srgb, transparent, var(--tblr-${color}) ${(step / steps) * 100}%);`} />)}
|
||||
</div>
|
||||
<div class="text-secondary">{maxLabel}</div>
|
||||
</div>
|
||||
+20
@@ -47,6 +47,19 @@
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"@tabler/screenshots#build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
"$TURBO_ROOT$/shared/**"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**"
|
||||
],
|
||||
"cache": true
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
@@ -74,6 +87,13 @@
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@tabler/screenshots#dev": {
|
||||
"dependsOn": [
|
||||
"@tabler/core#dev-prepare"
|
||||
],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"@tabler/docs#dev": {
|
||||
"dependsOn": [
|
||||
"@tabler/core#dev-prepare",
|
||||
|
||||
Reference in New Issue
Block a user