mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Fix chart options, marketing paths and silent failures in shared components (#3093)
This commit is contained in:
1 parent
bf576f10a4
commit
8636174130
39 files changed
+157
-73
No files matched your search
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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'))
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
@@ -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('../..')
|
||||
})
|
||||
})
|
||||
@@ -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('/') : '.'
|
||||
}
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user