Fix chart options, marketing paths and silent failures in shared components (#3093)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-18 16:16:30 +02:00
1 parent bf576f10a4
commit 8636174130
39 files changed
+157 -73

No files matched your search

+11
View File
@@ -145,8 +145,19 @@ export function copyAssets({ repo, publicDir, copies, syncDirs = [] }: CopyAsset
return
}
}
// A file deleted from a source dir (a renamed bundle, a cleaned dist) must
// leave public/ too, or the dev server keeps serving it until a restart.
const remove = (file: string) => {
for (const { from, to } of syncDirs) {
if (!file.startsWith(from + sep)) continue
rmSync(join(to, relative(from, file)), { force: true })
scheduleReload(file)
return
}
}
server.watcher.on('add', sync)
server.watcher.on('change', sync)
server.watcher.on('unlink', remove)
},
},
}
-2
View File
@@ -10,8 +10,6 @@ docs-classnames
docs-page-lead
docs-search
docs-theme-toggle
dropzone-msg-desc
dropzone-msg-title
fallback
github-dark
icon-tabler
-8
View File
@@ -46,14 +46,6 @@ export function createViteConfig({ entry, name, fileName, formats, outDir, banne
define: {
'process.env.NODE_ENV': '"production"',
},
esbuild: {
target: 'es2015',
tsconfigRaw: {
compilerOptions: {
target: 'ES2015',
},
},
},
}
return defineConfig(config)
@@ -0,0 +1,6 @@
---
"@tabler/preview": patch
"@tabler/docs": patch
---
Fixed `Trending` signs, ApexCharts 7 legend markers, marketing asset paths, `pageLibs` checks and the notifications close button.
+1 -1
View File
@@ -78,7 +78,7 @@ Add the `multiple` attribute to the input to accept several files at once, for e
You can replace the default message in the drop area with your own markup, for example an icon and a hint about the accepted file types:
<Example>
<form class="dropzone" id="dropzone-custom" action="." autocomplete="off" novalidate> <div class="fallback"> <input name="file" type="file" /> </div> <div class="dz-message"> <h3 class="dropzone-msg-title">Your text here</h3> <span class="dropzone-msg-desc">Your custom description here</span> <button type="button" class="btn btn-sm mt-3">Select files</button> </div> </form> <script>{`document.addEventListener("DOMContentLoaded", function () { new Dropzone("#dropzone-custom"); });`}</script>
<form class="dropzone" id="dropzone-custom" action="." autocomplete="off" novalidate> <div class="fallback"> <input name="file" type="file" /> </div> <div class="dz-message"> <h3>Your text here</h3> <span>Your custom description here</span> <button type="button" class="btn btn-sm mt-3">Select files</button> </div> </form> <script>{`document.addEventListener("DOMContentLoaded", function () { new Dropzone("#dropzone-custom"); });`}</script>
</Example>
Custom content replaces the button Dropzone renders by default, and that button is the only part of the drop area a keyboard can reach. Keep a focusable control - such as the one above - inside your own message.
+3 -1
View File
@@ -22,10 +22,12 @@ const alternate = (path: string, type: string) => `<${path}>; rel="alternate"; t
export default function middleware(request: Request) {
const url = new URL(request.url)
// redirect keys have no trailing slash, so "/ui/base/markdown/" must match "/ui/base/markdown"
const path = url.pathname.length > 1 ? url.pathname.replace(/\/+$/, '') : url.pathname
// renamed pages fall through to their 301; negotiation reruns on the target
// not Object.hasOwn — Vercel's middleware type-check runs with a pre-es2022 lib
if (Object.prototype.hasOwnProperty.call(redirects, url.pathname)) return next()
if (Object.prototype.hasOwnProperty.call(redirects, path)) return next()
const format = preferredFormat(request.headers.get('accept'))
+1 -1
View File
@@ -70,7 +70,7 @@ function greetUser(name) {
}`
---
<DefaultLayout title="All Elements - Kitchen Sink" pageHeader="All Elements" pageMenu="base.all-elements" pageLibs={['nouislider', 'star-rating.js', 'tabler-flags', 'tabler-payments', 'tom-select']}>
<DefaultLayout title="All Elements - Kitchen Sink" pageHeader="All Elements" pageMenu="base.all-elements" pageLibs={['nouislider', 'star-rating.js', 'tom-select']}>
<div class="row row-cards">
<!-- Typography Section -->
<div class="col-12">
+1 -1
View File
@@ -192,7 +192,7 @@ const operationCurrencies = currencies.slice(0, 20)
<td class="">{market.price}</td>
<td class="d-none d-xl-table-cell">{market.volume}</td>
<td class="d-none d-xl-table-cell text-end">
<Trending value={market.change as unknown as number} />
<Trending value={market.change} />
</td>
</tr>
))
+1 -1
View File
@@ -52,7 +52,7 @@ const rows = [
]
---
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'tabler-flags', 'tabler-payments', 'litepicker', 'tom-select', 'imask']}>
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'litepicker', 'tom-select', 'imask']}>
<DocsLink slot="page-header-actions" path="/ui/forms/elements" />
<div class="row row-cards">
<div class="col-12">
+1 -1
View File
@@ -7,7 +7,7 @@ import FormGroup from '@ui/FormGroup.astro'
import Icon from '@ui/Icon.astro'
---
<PayLayout title="Pay" pageLibs={['tabler-payments', 'imask']}>
<PayLayout title="Pay">
<div class="bg-dark bg-cover" style="background-image: url(./static/bg-cover.jpg)">
<div class="bg-dark bg-opacity-75 pt-5 bg-blur">
<div class="container container-tight">
+1 -1
View File
@@ -102,7 +102,7 @@ import DocsLink from '@ui/DocsLink.astro'
<h2>Supporting images</h2>
<p>Images should support the text and provide useful context, not just decorate the page.</p>
<figure>
<img src="/static/photos/cup-of-coffee-and-an-open-book.jpg" alt="Open book and coffee" />
<img src="./static/photos/cup-of-coffee-and-an-open-book.jpg" alt="Open book and coffee" />
<figcaption class="text-secondary small mt-2">A quiet spot makes for better writing sessions.</figcaption>
</figure>
+2 -2
View File
@@ -4,7 +4,7 @@ import Logo from '@shared/components/navbar/Logo.astro'
import { site } from '@shared/lib/site'
---
<BaseLayout title="Screenshot" pageLibs={['signature_pad']}>
<BaseLayout title="Screenshot">
<style is:inline>
.screenshot {
width: 1024px;
@@ -40,7 +40,7 @@ import { site } from '@shared/lib/site'
}
.avatar-2 {
background-image: url(/static/avatars/032f.jpg);
background-image: url(./static/avatars/032f.jpg);
}
</style>
+1 -1
View File
@@ -8,7 +8,7 @@ import CardSubtitle from '@ui/CardSubtitle.astro'
import DocsLink from '@ui/DocsLink.astro'
---
<DefaultLayout title="Star Ratings" pageMenu="base.stars-rating" pageLibs={['tabler-flags', 'star-rating.js']} description="An interactive star rating built on the star-rating.js widget, backed by a hidden select for accessibility and forms.">
<DefaultLayout title="Star Ratings" pageMenu="base.stars-rating" pageLibs={['star-rating.js']} description="An interactive star rating built on the star-rating.js widget, backed by a hidden select for accessibility and forms.">
<DocsLink slot="page-header-actions" path="/ui/components/star-rating" />
<div class="row row-cards mb-5">
+4 -4
View File
@@ -57,7 +57,7 @@ const homepage = siteData.homepage
Heading {i} by{' '}
<a class="mention">
<>
<span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)" />
<span class="mention-avatar" style="background-image: url(./static/avatars/035f.jpg)" />
<span class="visually-hidden">@</span>
</>
JohnDoe
@@ -76,9 +76,9 @@ const homepage = siteData.homepage
</p>
<p>
Hey <a class="mention"><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JohnDoe</a>, have you seen the latest updates on <a class="mention">#WebDevelopment<span class="mention-count">16</span></a>? <a class="mention"
><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JaneSmith</a
> just shared an interesting article about <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/messenger.svg)"></span>Messenger</a> and <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/netflix.svg)"></span>Netflix</a>!
Hey <a class="mention"><span class="mention-avatar" style="background-image: url(./static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JohnDoe</a>, have you seen the latest updates on <a class="mention">#WebDevelopment<span class="mention-count">16</span></a>? <a class="mention"
><span class="mention-avatar" style="background-image: url(./static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JaneSmith</a
> just shared an interesting article about <a class="mention"><span class="mention-app" style="background-image: url(./static/brands/messenger.svg)"></span>Messenger</a> and <a class="mention"><span class="mention-app" style="background-image: url(./static/brands/netflix.svg)"></span>Netflix</a>!
</p>
<p>
+1 -1
View File
@@ -15,7 +15,7 @@ import DocsLink from '@ui/DocsLink.astro'
<p class="lead">Good coffee isn't about expensive gear — it's about <mark>consistency</mark>. Get the ratio, the grind, and the water temperature right, and almost any method will taste better.</p>
<figure>
<img src="/static/photos/cup-of-coffee-and-an-open-book.jpg" alt="A cup of coffee next to an open book" />
<img src="./static/photos/cup-of-coffee-and-an-open-book.jpg" alt="A cup of coffee next to an open book" />
<figcaption class="text-secondary small mt-2">Ten minutes of prep, and the rest of the morning is yours.</figcaption>
</figure>
+7 -3
View File
@@ -44,9 +44,13 @@ const avatarClass = [color ? `bg-${color}${lt ? '-lt' : ' text-white'}` : '', 'a
</div>
</div>
<div class="col-auto">
<Trending value={changeValue as unknown as number} unit={changeValueUnit} />
</div>
{
changeValue !== undefined && (
<div class="col-auto">
<Trending value={changeValue} unit={changeValueUnit} />
</div>
)
}
</div>
</div>
</div>
+10 -2
View File
@@ -31,8 +31,16 @@ const {
} = Astro.props;
const id = idProp ?? keyProp;
if (!id) {
throw new Error('DataSelect: pass an `id` or a `key` — the select needs one for its id');
}
const key = keyProp ?? 'people';
const data = (selects as Record<string, any>)[key] ?? {};
let data = (selects as Record<string, any>)[key];
if (!data && !values) {
throw new Error(`DataSelect: unknown key "${key}" — not in @data/selects.json`);
}
// `values` alone needs no selects.json entry.
data ??= {};
const isMultiple = Boolean(data.multiple);
@@ -77,4 +85,4 @@ if (values) {
}
---
{id && <Select id={id} class={className} state={state} placeholder={placeholder} multiple={isMultiple} options={options} optgroups={optgroups} />}
<Select id={id} class={className} state={state} placeholder={placeholder} multiple={isMultiple} options={options} optgroups={optgroups} />
@@ -1,8 +1,8 @@
---
// Relative base is "../" for one-level-deep marketing pages.
import Logo from '../navbar/Logo.astro'
import { relativeBase } from '@shared/lib/url'
const base = '..'
const base = relativeBase(Astro.url.pathname)
---
<!-- BEGIN MARKETING NAVBAR -->
@@ -16,7 +16,7 @@ const base = '..'
<nav aria-label="Primary">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href={`${base}/marketing`}><span class="nav-link-title">Home</span></a>
<a class="nav-link active" href={`${base}/marketing.html`}><span class="nav-link-title">Home</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href={`${base}/marketing/testimonials.html`}><span class="nav-link-title">Testimonials</span></a>
@@ -1,6 +1,9 @@
---
import Icon from '@ui/Icon.astro'
import { site } from '@shared/lib/site'
import { relativeBase } from '@shared/lib/url'
const base = relativeBase(Astro.url.pathname)
---
<!-- BEGIN HERO -->
@@ -38,7 +41,7 @@ import { site } from '@shared/lib/site'
</div>
</div>
<picture>
<img src="../static/marketing/preview.png" srcset="../static/marketing/preview.png 1x, ../static/marketing/preview@2x.png 2x" alt="" class="img-fluid" width="1040" height="760" loading="lazy" />
<img src={`${base}/static/marketing/preview.png`} srcset={`${base}/static/marketing/preview.png 1x, ${base}/static/marketing/preview@2x.png 2x`} alt="" class="img-fluid" width="1040" height="760" loading="lazy" />
</picture>
</div>
</div>
@@ -1,8 +1,8 @@
---
// Marketing pages live one level deep, so the avatar asset base is "..", passed to Avatar via `base`.
import testimonials from '@data/testimonials.json'
import { people } from '@shared/lib/people'
import Avatar from '@ui/Avatar.astro'
import { relativeBase } from '@shared/lib/url'
interface Props {
class?: string
@@ -12,12 +12,15 @@ interface Props {
const { class: className, limit = 99, hideHeader } = Astro.props
const base = relativeBase(Astro.url.pathname)
const sectionClass = ['section', className]
// Slice testimonials and split into 3 columns.
const list = (testimonials as string[]).slice(0, limit)
const n = 3
const chunkSize = Math.round(list.length / n)
// At least 1, or a limit below 2 would never advance the loop below.
const chunkSize = Math.max(1, Math.round(list.length / n))
const groups: string[][] = []
for (let i = 0; i < list.length; i += chunkSize) {
groups.push(list.slice(i, i + chunkSize))
@@ -56,7 +59,7 @@ const cards = groups.map((group) =>
<div class="card-body">
<div class="row mb-3">
<div class="col-auto">
<Avatar person={person} size="md" base=".." />
<Avatar person={person} size="md" base={base} />
</div>
<div class="col">
<h3 class="h3 m-0">{person.full_name}</h3>
+4 -4
View File
@@ -1,6 +1,6 @@
---
import Icon from '@ui/Icon.astro'
import NavbarMenuItem from './NavbarMenuItem.astro'
import NavbarMenuItem, { type Level2 } from './NavbarMenuItem.astro'
import menu from '@data/menu.json'
import menuSample from '@data/menu-sample.json'
@@ -16,7 +16,7 @@ interface Level1 {
'columns'?: number
/** section label rendered before this item (sidebar only, needs the `sections` prop) */
'section'?: string
'children'?: Record<string, Record<string, unknown>>
'children'?: Record<string, Level2>
}
interface Props {
@@ -84,13 +84,13 @@ const chunk = <T,>(arr: T[], size: number): T[][] => {
{columns.map((column) => (
<div class="dropdown-menu-column">
{column.map(([childKey, child]) => (
<NavbarMenuItem level1Key={key} itemKey={childKey} item={child as any} currentPage={currentPage} keepOpen={keepOpen} />
<NavbarMenuItem level1Key={key} itemKey={childKey} item={child} currentPage={currentPage} keepOpen={keepOpen} />
))}
</div>
))}
</div>
) : (
children.map(([childKey, child]) => <NavbarMenuItem level1Key={key} itemKey={childKey} item={child as any} currentPage={currentPage} keepOpen={keepOpen} />)
children.map(([childKey, child]) => <NavbarMenuItem level1Key={key} itemKey={childKey} item={child} currentPage={currentPage} keepOpen={keepOpen} />)
)}
</div>
)}
@@ -3,13 +3,13 @@
import { isExternal } from '@shared/lib/url'
import Icon from '@ui/Icon.astro'
interface Level3 {
export interface Level3 {
title: string
url?: string
badge?: string
}
interface Level2 extends Level3 {
export interface Level2 extends Level3 {
icon?: string
color?: string
children?: Record<string, Level3>
@@ -99,11 +99,13 @@ import CaptureScript from '../CaptureScript.astro'
<CaptureScript>
<!-- BEGIN NAVBAR NOTIFICATIONS CLOSE -->
<script is:inline>
// onclick, not addEventListener: a page with several navbars runs this once per navbar,
// and assigning the property keeps it at one handler per button.
for (const close of document.querySelectorAll('[data-navbar-notifications-close]')) {
close.addEventListener('click', () => {
close.onclick = () => {
const toggle = close.closest('.dropdown').querySelector('[data-bs-toggle="dropdown"]')
window.tabler.bootstrap.Dropdown.getOrCreateInstance(toggle).hide()
})
}
}
</script>
<!-- END NAVBAR NOTIFICATIONS CLOSE -->
+4
View File
@@ -36,6 +36,10 @@ const min = isDevelopment ? '' : '.min'
const rtlSuffix = rtl ? '.rtl' : ''
type Lib = { npm?: string; js?: string[]; css?: string[]; head?: boolean }
const unknownLibs = pageLibs.filter((name) => !(name in libs))
if (unknownLibs.length > 0) {
throw new Error(`BaseLayout: unknown pageLibs ${unknownLibs.map((name) => `"${name}"`).join(', ')} — use a key from core/libs.json`)
}
const pageLibEntries = Object.entries(libs as Record<string, Lib>).filter(([name]) => pageLibs.includes(name))
// Google Maps API key comes from the environment (GOOGLE_MAPS_KEY in .env or the
// host's settings), never from the source. Without it the script loads keyless.
+2 -1
View File
@@ -5,6 +5,7 @@ import MarketingNavbar from '@shared/components/marketing/MarketingNavbar.astro'
import Payment from '@ui/Payment.astro'
import Icon from '@ui/Icon.astro'
import { site } from '@shared/lib/site'
import { relativeBase } from '@shared/lib/url'
interface Props {
title?: string
@@ -14,7 +15,7 @@ interface Props {
const { title, pageLibs } = Astro.props
const base = '..'
const base = relativeBase(Astro.url.pathname)
---
<BaseLayout title={title} pageLibs={pageLibs} bodyClass="body-marketing body-gradient" base={base}>
+11
View File
@@ -136,4 +136,15 @@ describe('chartConfig', () => {
expect(xFormatterExpr).toBe('val + "K"')
expect(JSON.stringify(config)).not.toContain('val + "K"')
})
it('uses the ApexCharts 7 legend marker options', () => {
const { config } = chartConfig({ id: 'demo', data: { series: [], legend: true }, height: 10 })
expect(config.legend).toMatchObject({ markers: { size: 5, strokeWidth: 0, shape: 'circle' } })
})
it('hides line points, including the hover dot, with hide-points', () => {
const { config } = chartConfig({ id: 'demo', data: { 'series': [], 'hide-points': true }, height: 10 })
expect(config.markers).toEqual({ size: 0, hover: { sizeOffset: 0 } })
expect(config).not.toHaveProperty('point')
})
})
+4 -2
View File
@@ -444,7 +444,8 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
show: true,
position: 'bottom',
offsetY: 12,
markers: { width: 10, height: 10, radius: 100 },
// v7 sizes the legend dot as (size + strokeWidth) * 2 — a 10px circle.
markers: { size: 5, strokeWidth: 0, shape: 'circle' },
itemMargin: { horizontal: 8, vertical: 8 },
}
: { show: false }
@@ -458,7 +459,8 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
}
if (data['hide-points']) {
config.point = { show: false }
// hover.size 0 counts as unset in v7, so the hover dot is removed through sizeOffset.
config.markers = { size: 0, hover: { sizeOffset: 0 } }
}
if (data['show-markers']) {
+7 -3
View File
@@ -12,9 +12,13 @@ describe('formatCommitDate', () => {
})
describe('toUnixSeconds', () => {
it('shifts the wall-clock epoch by the local timezone offset', () => {
const date = new Date(Date.UTC(2024, 0, 1, 0, 0, 0))
expect(toUnixSeconds(date)).toBe(Math.floor(date.getTime() / 1000) + date.getTimezoneOffset() * 60)
it('returns the true epoch seconds', () => {
expect(toUnixSeconds(new Date(Date.UTC(2024, 0, 1, 0, 0, 0)))).toBe(1704067200)
})
it('does not depend on DST (summer and winter dates)', () => {
expect(toUnixSeconds(new Date(Date.UTC(2024, 6, 1, 12, 0, 0)))).toBe(1719835200)
expect(toUnixSeconds(new Date(Date.UTC(2024, 0, 1, 12, 0, 0, 999)))).toBe(1704110400)
})
})
+4 -5
View File
@@ -1,12 +1,11 @@
// Demo data date formatting quirks:
// - unix seconds are shifted by local timezone offset (not the true epoch)
// - long-date format uses UTC wall-clock components
// Demo data is rendered at build time, so every formatter reads UTC components
// and never the local timezone — output must match between CI and local builds.
const SHORT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
/** Unix seconds shifted by the local timezone offset (not the true epoch). */
/** Unix epoch seconds, independent of the build machine's timezone. */
export function toUnixSeconds(date: Date): number {
return Math.floor(date.getTime() / 1000) + date.getTimezoneOffset() * 60
return Math.floor(date.getTime() / 1000)
}
/** Formats a date as "Month DD, YYYY" using its UTC components. */
+4
View File
@@ -42,6 +42,10 @@ describe('freeIllustrationSource', () => {
it('returns the auto-dark source of a bundled illustration', () => {
expect(freeIllustrationSource('not-found')).toContain('<svg ')
})
it('throws for a name that is not bundled instead of rendering nothing', () => {
expect(() => freeIllustrationSource('no-such-illustration' as never)).toThrow(/Unknown illustration/)
})
})
describe('illustrationSvg', () => {
+4 -1
View File
@@ -11,7 +11,10 @@ export type IllustrationImage = FreeIllustration | `${FreeIllustration}.svg`
/** Source SVG of a bundled illustration, in its auto-dark variant. */
export function freeIllustrationSource(image: IllustrationImage): string {
return freeIllustrations.autodark[image.replaceAll('.svg', '') as FreeIllustration] ?? ''
const name = image.replace(/\.svg$/, '')
const source = (freeIllustrations.autodark as Record<string, string | undefined>)[name]
if (!source) throw new Error(`Unknown illustration "${image}" — not in @data/free-illustrations.json`)
return source
}
type IconRecord = { svg: Record<string, string | null | undefined> }
+16 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { isExternal } from './url'
import { isExternal, relativeBase } from './url'
describe('isExternal', () => {
it('is true for absolute http(s) urls', () => {
@@ -24,3 +24,18 @@ describe('isExternal (anchored)', () => {
expect(isExternal('/redirect?to=https://example.com')).toBe(false)
})
})
describe('relativeBase', () => {
it('is "." for top-level pages, including a folder index built as a root file', () => {
expect(relativeBase('/')).toBe('.')
expect(relativeBase('/marketing')).toBe('.')
expect(relativeBase('/marketing/')).toBe('.')
expect(relativeBase('/marketing.html')).toBe('.')
})
it('climbs one level per folder', () => {
expect(relativeBase('/marketing/about')).toBe('..')
expect(relativeBase('/marketing/about.html')).toBe('..')
expect(relativeBase('/a/b/c.html')).toBe('../..')
})
})
+9
View File
@@ -4,3 +4,12 @@
export function isExternal(url?: string): boolean {
return !!url && /^https?:\/\//.test(url)
}
/**
* Relative path from a page back to the site root: "." for a top-level page, ".." one level deep.
* Pages are built with `format: 'file'`, so "/marketing" (or "/marketing.html") is a root file.
*/
export function relativeBase(pathname: string): string {
const depth = pathname.split('/').filter(Boolean).length - 1
return depth > 0 ? Array.from({ length: depth }, () => '..').join('/') : '.'
}
+2 -2
View File
@@ -32,8 +32,8 @@ const classes = ['breadcrumb', className, separator && `breadcrumb-${separator}`
) : (
<li class="breadcrumb-item">
{index === 0 && homeIcon ? (
/* Home icon link uses href="/". */
<a href="/">
/* Demo link like the other items; "/" would break a preview hosted in a subfolder. */
<a href="#" aria-label={page}>
<Icon name="home" />
</a>
) : (
+2 -2
View File
@@ -24,8 +24,8 @@ const fileInputId = `${dropzoneId}-file`
{
custom && (
<div class="dz-message">
<h3 class="dropzone-msg-title">{text}</h3>
<span class="dropzone-msg-desc">{description}</span>
<h3>{text}</h3>
<span>{description}</span>
</div>
)
}
+2 -2
View File
@@ -41,8 +41,8 @@ const classes = ['empty', className]
) : null
}
<Title class="empty-title" set:html={title} />
<p class="empty-subtitle text-secondary" set:html={description} />
{title && <Title class="empty-title" set:html={title} />}
{description && <p class="empty-subtitle text-secondary" set:html={description} />}
{
buttonText && (
<div class="empty-action">
+1 -4
View File
@@ -20,8 +20,6 @@ interface Props {
const { id, class: className, state, placeholder, multiple, options = [], optgroups = [] } = Astro.props;
const value = '';
const selectClass = [
'form-select',
className,
@@ -34,8 +32,7 @@ const selectId = `select-${id}`;
<select
class:list={selectClass}
data-placeholder={placeholder}
id={`select-${id}`}
value={value}
id={selectId}
multiple={multiple ? '' : undefined}
>
{
+10 -4
View File
@@ -2,20 +2,26 @@
import Icon from './Icon.astro'
interface Props {
value: number
/** a number, or a signed string such as "+9.5" that is rendered as written */
value: number | string
unit?: string | undefined
class?: string
}
const { value, unit = '%', class: className } = Astro.props
const trendingColor = value > 0 ? 'green' : value === 0 ? 'muted' : 'red'
const trendingIcon = value > 0 ? 'arrow-up' : value === 0 ? 'minus' : 'arrow-down'
const amount = Number(value)
if (Number.isNaN(amount)) {
throw new Error(`Trending: value "${value}" is not a number`)
}
const trendingColor = amount > 0 ? 'green' : amount === 0 ? 'muted' : 'red'
const trendingIcon = amount > 0 ? 'arrow-up' : amount === 0 ? 'minus' : 'arrow-down'
const classes = [`text-${trendingColor} d-inline-flex align-items-center lh-1`, className]
---
<span class:list={classes}>
<span class="visually-hidden">{value > 0 ? 'Increased by' : value < 0 ? 'Decreased by' : 'No change,'} </span>{value}{unit}
<span class="visually-hidden">{amount > 0 ? 'Increased by' : amount < 0 ? 'Decreased by' : 'No change,'} </span>{value}{unit}
<Icon name={trendingIcon} class="ms-0" size="sm" />
</span>
+1 -1
View File
@@ -17,7 +17,7 @@
"@tabler/docs#build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.build/**", "$TURBO_ROOT$/.browserslistrc", "$TURBO_ROOT$/shared/**", "!$TURBO_ROOT$/shared/.turbo/**", "!$TURBO_ROOT$/shared/.astro/**", "!$TURBO_ROOT$/shared/**/node_modules/**"],
"outputs": ["dist/**", "tmp-assets/**"],
"outputs": ["dist/**", "tmp-assets/**", ".vercel/output/**"],
"cache": true
},
"@tabler/screenshots#build": {