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

Fix clean-css zero-unit bug breaking card-gradient color-mix, add Logo component, and expand preview/screenshot pages

This commit is contained in:
codecalm
2026-08-05 00:07:12 +02:00
parent ec82d0d2f9
commit 4597e53429
42 changed files with 788 additions and 192 deletions
+8
View File
@@ -116,6 +116,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 },
+31
View File
@@ -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(--tblr-card-bg) 0%, color-mix(in srgb, var(--tblr-card-bg) 0%, transparent) 80%) border-box,
linear-gradient(var(--tblr-card-gradient-direction), color-mix(in srgb, var(--tblr-card-bg) var(--tblr-card-gradient-opacity), transparent) 0%, var(--tblr-card-bg) 40%) border-box,
linear-gradient(calc(270deg + var(--tblr-card-gradient-direction)), var(--tblr-card-gradient)) border-box;
}
}
}
}
}
+11 -4
View File
@@ -690,15 +690,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(--#{$prefix}card-bg) 0%, color-mix(in srgb, var(--#{$prefix}card-bg) 0%, transparent) 80%) border-box,
linear-gradient(var(--#{$prefix}card-gradient-direction), color-mix(in srgb, var(--#{$prefix}card-bg) var(--#{$prefix}card-gradient-opacity), transparent) 0%, var(--#{$prefix}card-bg) 40%) border-box,
linear-gradient(calc(270deg + var(--#{$prefix}card-gradient-direction)), var(--#{$prefix}card-gradient)) border-box;
}
.card-gradient {
--#{$prefix}card-gradient-direction: 180deg;
--#{$prefix}card-gradient-opacity: 86%;
--#{$prefix}card-gradient: var(--tblr-primary), var(--tblr-primary);
background:
radial-gradient(ellipse at center, var(--#{$prefix}card-bg) 0%, color-mix(in srgb, var(--#{$prefix}card-bg) 0%, transparent) 80%) border-box,
linear-gradient(var(--#{$prefix}card-gradient-direction), color-mix(in srgb, var(--#{$prefix}card-bg) var(--#{$prefix}card-gradient-opacity), transparent) 0%, var(--#{$prefix}card-bg) 40%) border-box,
linear-gradient(calc(270deg + var(--#{$prefix}card-gradient-direction)), var(--#{$prefix}card-gradient)) border-box;
@include card-gradient-background;
}
@each $name, $color in map.merge($colors, $theme-colors) {
+2 -2
View File
@@ -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} />
+7 -22
View File
@@ -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 -2
View File
@@ -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">
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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 />
+106
View File
@@ -0,0 +1,106 @@
---
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 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' },
]
---
<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">
<Card>
<CardBody>
<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>
</CardBody>
</Card>
</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>
+5 -5
View File
@@ -8,7 +8,7 @@
// 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 NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
import Logo from '@shared/components/navbar/Logo.astro'
import PageScripts from '@shared/components/PageScripts.astro'
import libs from '@tabler/core/libs.json'
@@ -54,11 +54,11 @@ const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file)
<title>{title} - Tabler screenshots</title>
<link rel="icon" href="/favicon-dev.ico" type="image/x-icon" />
<link rel="icon" href="./favicon-dev.ico" type="image/x-icon" />
<link href={`/dist/css/tabler${rtlSuffix}${min}.css`} rel="stylesheet" />
<link href={`./dist/css/tabler${rtlSuffix}${min}.css`} rel="stylesheet" />
{cssPlugins.map((plugin) => <link href={`/dist/css/tabler-${plugin}${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" />)}
@@ -119,7 +119,7 @@ const libJsFiles = pageLibEntries.flatMap(([, lib]) => (lib.js ?? []).map((file)
{
showLogo && (
<div class="screenshot-logo">
<NavbarLogo class="brand-gray" />
<Logo gray />
</div>
)
}
+13
View File
@@ -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,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>
+8
View File
@@ -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>
+8
View File
@@ -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>
+8
View File
@@ -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>
+8
View File
@@ -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>
+33 -3
View File
@@ -1,8 +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 MapVectorCard from '@shared/components/cards/MapVectorCard.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']}>
<MapVectorCard title="Locations" />
<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>
+8
View File
@@ -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>
+27 -44
View File
@@ -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
@@ -20,7 +21,7 @@ interface Props {
const { id: idProp, title = 'Carousel', indicators, indicatorsThumb, indicatorsThumbRatio, indicatorsDot, indicatorsVertical, controls, captions, offset = 0, limit = 5, fade } = Astro.props
const carouselId = idProp ?? 'carousel'
const carouselId = `carousel-${idProp ?? 'carousel'}`
const filteredPhotos = (photos as { file: string; horizontal?: boolean }[]).filter((p) => p.horizontal)
const slides = filteredPhotos.slice(offset, offset + limit)
---
@@ -30,51 +31,33 @@ const slides = filteredPhotos.slice(offset, offset + limit)
<CardTitle>{title}</CardTitle>
</div>
<div class="card-body">
<div id={`carousel-${carouselId}`} class={`carousel slide${fade ? ' carousel-fade' : ''}`} data-bs-ride="carousel">
<Carousel
id={carouselId}
fade={fade}
slideCount={slides.length}
indicators={indicators}
indicatorsVariant={indicatorsDot ? 'dot' : indicatorsThumb ? 'thumb' : undefined}
indicatorsVertical={indicatorsVertical}
indicatorsThumbRatio={indicatorsThumbRatio}
indicatorsImages={indicatorsThumb ? slides.map((p) => `./static/photos/${p.file}`) : undefined}
controls={controls}
>
{
indicators && (
<div class={`carousel-indicators${indicatorsVertical ? ' carousel-indicators-vertical' : ''}${indicatorsDot ? ' carousel-indicators-dot' : indicatorsThumb ? ' carousel-indicators-thumb' : ''}`}>
{slides.map((photo, i) => (
<button type="button" data-bs-target={`#carousel-${carouselId}`} data-bs-slide-to={`${i}`} class={`${indicatorsThumbRatio ? ' ratio ratio-4x3' : ''}${i === 0 ? ' active' : ''}`} style={indicatorsThumb ? `background-image: url(./static/photos/${photo.file})` : undefined} />
))}
slides.map((photo, i) => (
<div class:list={['carousel-item', i === 0 && 'active']}>
<img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} />
{captions && (
<Fragment>
<div class="carousel-caption-background d-none d-md-block" />
<div class="carousel-caption d-none d-md-block">
<h3>Slide label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div>
</Fragment>
)}
</div>
)
))
}
<div class="carousel-inner">
{
slides.map((photo, i) => (
<div class={`carousel-item${i === 0 ? ' active' : ''}`}>
<img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} />
{captions && (
<Fragment>
<div class="carousel-caption-background d-none d-md-block" />
<div class="carousel-caption d-none d-md-block">
<h3>Slide label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div>
</Fragment>
)}
</div>
))
}
</div>
{
controls && (
<Fragment>
<a class="carousel-control-prev" href={`#carousel-${carouselId}`} 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={`#carousel-${carouselId}`} role="button" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true" />
<span class="visually-hidden">Next</span>
</a>
</Fragment>
)
}
</div>
</Carousel>
</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} />
</div>
</div>
@@ -1,6 +1,7 @@
---
import Subheader from '@ui/Subheader.astro'
import Chart from '@ui/Chart.astro'
import Legend from '@ui/Legend.astro'
---
<div class="card">
@@ -15,14 +16,8 @@ import Chart from '@ui/Chart.astro'
</div>
<div class="d-flex align-items-center gap-3">
<div class="d-flex align-items-center gap-2">
<span class="legend bg-primary"></span>
<span class="text-secondary">This year</span>
</div>
<div class="d-flex align-items-center gap-2">
<span class="legend bg-secondary"></span>
<span class="text-secondary">Last year</span>
</div>
<Legend color="primary">This year</Legend>
<Legend color="secondary">Last year</Legend>
</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} />
</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} />
</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>
+13 -14
View File
@@ -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>
<a class="carousel-control-prev text-secondary" href="#carousel-controls" role="button" 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>
</a>
<a class="carousel-control-next text-secondary" href="#carousel-controls" role="button" data-bs-slide="next">
<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>
</a>
</div>
</Carousel>
</div>
</div>
</div>
@@ -9,9 +9,11 @@ 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 logoClass = `navbar-brand-image${showTitle ? ' me-3' : ''}`
@@ -22,7 +24,10 @@ const logoMark =
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"/>'
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>`
const grayStyle = gray ? ' style="filter: grayscale(100%)"' : ''
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}"${grayStyle}>${logoMark}</svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" width="110" height="32" viewBox="0 0 232 68" class="${logoClass}"${grayStyle}>${logoMark}${logoText}</svg>`
---
{
+2 -2
View File
@@ -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}
+2 -2
View File
@@ -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} />
+34 -68
View File
@@ -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>
+123
View File
@@ -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",
+4
View File
@@ -231,6 +231,10 @@
"title": "Toasts",
"url": "toasts.html"
},
"tracking": {
"title": "Tracking",
"url": "tracking.html"
},
"typography": {
"title": "Typography",
"url": "typography.html"
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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>
)
}
+23 -4
View File
@@ -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 }
+22
View File
@@ -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>
+67
View File
@@ -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>
+31
View File
@@ -49,11 +49,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();
}
+13
View File
@@ -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>
+17
View File
@@ -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>
+15
View File
@@ -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>
+24
View File
@@ -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>