Merge branch 'dev' of https://github.com/tabler/tabler into v2-dev

This commit is contained in:
codecalm committed 2026-09-08 22:51:56 +02:00
commit 0c878508fd
54 files changed
+617 -335

No files matched your search

-12
View File
@@ -1,7 +1,6 @@
# Classes used in the rendered html that no stylesheet defines on purpose:
# JavaScript hooks and demo-only markers. One per line; delete a line when the hook goes.
callout-note
checkbox-wrapper-mail
datagrid-content
datagrid-item
datatable
@@ -11,17 +10,7 @@ docs-search
docs-theme-toggle
dropzone-msg-desc
dropzone-msg-title
email-action-icons
email-action-icons-item
email-border
email-content
email-date
email-list
email-sender-info
email-subject
email-title
fallback
file-offcanvas
github-dark
icon-tabler
icon-tabler-pencil
@@ -47,7 +36,6 @@ sort-score
sort-status
sort-tags
sort-type
star-toggle
table-tbody
tblr-illustrations-computer-fix-a
tblr-illustrations-computer-fix-b
+1 -5
View File
@@ -31,11 +31,7 @@ const { version } = JSON.parse(readFileSync(join(repoRoot, 'core', 'package.json
// The `## <version>` block of a changelog, without its heading.
const versionBlock = (changelog: string): string => {
const lines = changelog.split('\n')
// `## 1.5.0`, or `## 1.5.0 - 2026-09-05` once the date has been stamped on it.
const start = lines.findIndex((line) => {
const heading = line.trim()
return heading === `## ${version}` || heading.startsWith(`## ${version} - `)
})
const start = lines.findIndex((line) => line.trim() === `## ${version}`)
if (start === -1) return ''
const rest = lines.slice(start + 1)
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env node
// Adds the release date to the heading `changeset version` has just written, so
// every entry in a changelog reads `## 1.5.0 - 2026-09-05`.
//
// The date is the day the version pull request was last regenerated, which the
// bot redoes on every push to dev. It can therefore be a day or two before the
// release when nothing lands in between.
//
// Run: pnpm run stamp-changelog-dates (part of `pnpm run version`)
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '..')
const packages = ['core', 'preview', 'docs']
interface PackageJson {
version: string
}
const today = new Date().toISOString().slice(0, 10)
const stamped: string[] = []
for (const name of packages) {
const changelog = join(repoRoot, name, 'CHANGELOG.md')
if (!existsSync(changelog)) continue
const { version } = JSON.parse(readFileSync(join(repoRoot, name, 'package.json'), 'utf8')) as PackageJson
const content = readFileSync(changelog, 'utf8')
// Only a heading without a date, so running this twice changes nothing.
const lines = content.split('\n')
const index = lines.findIndex((line) => line.trim() === `## ${version}`)
if (index === -1) continue
lines[index] = `## ${version} - ${today}`
writeFileSync(changelog, lines.join('\n'))
stamped.push(`${name}/CHANGELOG.md: ## ${version} - ${today}`)
}
console.log(stamped.length > 0 ? stamped.join('\n') : 'Nothing to stamp.')
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Updated the avatar list and brand card descriptions on the avatars preview page.
@@ -0,0 +1,5 @@
---
'@tabler/docs': patch
---
Added the color palette, flags, payments and social icons tables to the docs `.md` mirrors via a new `MarkdownSource` component.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Fixed `.card-code` so the code block follows the card corners instead of drawing its own `border-radius`.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Fixed invisible box-shadows in dark mode with a new global `--tblr-shadow-color` `light-dark()` token.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/docs": patch
---
Fixed the docs "On this page" list running past the viewport; the sticky rail now scrolls on its own.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Fixed the `.dropdown-menu-arrow` border color and replaced the deprecated `clip` with `clip-path`.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Fixed the unstyled message list on the `email-inbox` page: a hoverable list group with label dots, dates and row actions.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Fixed the folded sidebar flashing the active submenu as a flyout on reload before `tabler.js` closed it.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/core": patch
---
Fixed the invisible `.form-switch` knob in light mode by restoring a gray `$form-switch-color`.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Fixed the preview icons page to render all icons in production builds, keeping the 20-icon limit only for the dev server.
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Updated the preview card descriptions to name CSS classes and markup instead of Astro component props.
+23
View File
@@ -44,6 +44,29 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# `changesets/action` fills the version pull request with one section per
# package (`## @tabler/core@1.5.1`, …), so a changeset that touches several
# packages shows up several times. Replace that body with the same notes the
# GitHub release gets: the intro from `.github/release-notes/<version>.md`
# and the changes grouped as core / demo / docs, each listed once. The
# action rewrites the body on every push to dev, so this runs right after it.
- name: Write the release notes into the version pull request
if: steps.changesets.outputs.pr-number != ''
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.changesets.outputs.pr-number }}
run: |
git fetch --depth=1 origin changeset-release/dev
git checkout --force --detach origin/changeset-release/dev
version=$(node -p "require('./core/package.json').version")
{
echo "Merging this pull request publishes \`@tabler/core@${version}\` to npm and creates the GitHub release. It is regenerated on every push to \`dev\`."
echo
pnpm run --silent release-notes
} > "${RUNNER_TEMP}/pr-body.md"
gh pr edit "${PR_NUMBER}" --body-file "${RUNNER_TEMP}/pr-body.md"
# `pnpm run build` above zipped preview/dist into packages-zip/; attach it to
# the GitHub release that changesets/action just created for @tabler/core.
- name: Attach the preview zip to the GitHub release
+33 -33
View File
@@ -1,6 +1,6 @@
# @tabler/core
## 1.5.0 - 2026-09-05
## 1.5.0
### Minor Changes
@@ -118,7 +118,7 @@
- 70ec683: Updated `$card-status-size` default from `$border-width-wide` to `3px`.
- 666ccd6: Updated shadow tokens (`--tblr-shadow-*`) to use the new `xs`–`2xl` and `overlay` values.
## 1.4.0 - 2025-07-13
## 1.4.0
### Minor Changes
@@ -137,19 +137,19 @@
- 666a296: Fix list group item hoverable only with `.list-group-hoverable` class
- cfd4cb6: Fix `.pagination-link` hover styles to non-active items
## 1.3.2 - 2025-05-19
## 1.3.2
### Patch Changes
- 446c34e: Fix README file in core package
## 1.3.1 - 2025-05-19
## 1.3.1
### Patch Changes
- a7f73d7: Fix README file in core package
## 1.3.0 - 2025-05-19
## 1.3.0
### Minor Changes
@@ -179,7 +179,7 @@
- e675389: Fix missing border-radius to `.card-header-tabs`
- 9007e73: Fix FAQ accordion structure
## 1.2.0 - 2025-04-16
## 1.2.0
### Minor Changes
@@ -214,13 +214,13 @@
- 6d6d1bd: Add responsive font size for form controls on mobile devices
- 6c566cf: Add new advanced table example
## 1.1.1 - 2025-03-01
## 1.1.1
### Patch Changes
- f29c911: Fix Documentation structure
## 1.1.0 - 2025-03-01
## 1.1.0
### Minor Changes
@@ -251,7 +251,7 @@
- 24b944c: Fix `.avatar-upload` double borders
- ca4ba14: Fixes navbar styles with new hover effects and color variables
## 1.0.0 - 2025-01-28
## 1.0.0
### Minor Changes
@@ -373,14 +373,14 @@
- 94b83d4: Add support for changeset tool for more efficient and organized code changes
- c51ff28: Fix colors in date range datepicker
## 1.0.0-beta24 - 2025-01-11
## 1.0.0-beta24
- Enhanced documentation.
- Updated illustrations and improved SVG handling in HTML.
- Updated copyright year in LICENSE file to 2025.
- Added marketing pages plugin.
## 1.0.0-beta23 - 2025-01-07
## 1.0.0-beta23
- Documentation improvements.
- Added countup functionality and updated documentation example.
@@ -417,7 +417,7 @@
- Fixed icon issues.
- Fixed URL in documentation.
## 1.0.0-beta22 - 2025-01-02
## 1.0.0-beta22
- Fixed `@charset` CSS declaration in bundle.
- Fixed cells with inline icons.
@@ -440,7 +440,7 @@
- Read changelog from `CHANGELOG.md` file.
- Initialized VS Code configuration.
## 1.0.0-beta21 - 2024-09-08
## 1.0.0-beta21
- Updated dependencies.
- Updated Tabler Icons to v3.14.0 and the import script.
@@ -477,7 +477,7 @@
- Improved heights, scrolls, and layouts in Docs examples.
- Fixed flags display in preview.
## 1.0.0-beta20 - 2023-08-24
## 1.0.0-beta20
- Update `bootstrap` to v5.3.1
- Add new `Chat` component
@@ -503,7 +503,7 @@
- `Dockerfile` fix
- Switch from `npm` to `pnpm` for faster package installation
## 1.0.0-beta19 - 2023-05-15
## 1.0.0-beta19
- Add customizable Star Ratings component using `star-rating.js` library (#1571)
- Add new "Filled" section to Icons page (#1574)
@@ -511,7 +511,7 @@
- Fix text color in dark version of navbar (#1569)
- Changelog update
## 1.0.0-beta18 - 2023-05-14
## 1.0.0-beta18
- new page: Cookie banner
- Unify dark mode with latest Bootstrap API and improve dark mode elements (#1561)
@@ -534,7 +534,7 @@
- Dark mode lite colors improvement
- Fix non full width selects (#1392)
## 1.0.0-beta17 - 2023-01-28
## 1.0.0-beta17
- update `bootstrap` to v5.3.0
- update icons to v2.1.2
@@ -556,7 +556,7 @@
- move `@tabler/icons` to `dev-dependencies`
- fix #1370: avatar stacked list is not stacked anymore
## 1.0.0-beta16 - 2022-11-12
## 1.0.0-beta16
- new `Photogrid` page
- `Steps` component improvements
@@ -570,7 +570,7 @@
- homepage navbar fix
- fix #1262 - `.bg-opacity-xx` class is not functioning properly
## 1.0.0-beta15 - 2022-11-01
## 1.0.0-beta15
- new `badges` page
- `<kbd>` styling
@@ -587,12 +587,12 @@
- `btn-actions` fixes
- replace `$text-muted` to css variable
## 1.0.0-beta14 - 2022-10-21
## 1.0.0-beta14
- fix active items in dark mode
- update Jekyll to newest version
## 1.0.0-beta13 - 2022-10-18
## 1.0.0-beta13
- update Bootstrap to 5.2.1, update dependencies
- new `tracking` component
@@ -622,7 +622,7 @@
- fix #1275 - remove last border-right on progress bar
- fix #1261 - broken offcanvas bg
## 1.0.0-beta12 - 2022-09-19
## 1.0.0-beta12
- new "Job listing" page
- new "Sign in with cover" page
@@ -647,7 +647,7 @@
- fix form elements demo page radio buttons
- replace `gulp-minify` with `gulp-terser`
## 1.0.0-beta11 - 2022-08-05
## 1.0.0-beta11
- new `Dropzone` component
- new `Lightbox` component
@@ -662,7 +662,7 @@
- fix: #1125 incorrect chart display in the mobile version
- update Bootstrap to 5.2.0
## 1.0.0-beta10 - 2022-04-29
## 1.0.0-beta10
- new `datatable` component
- update Tabler Icons to v1.67
@@ -671,7 +671,7 @@
- replace !important modifier with more specific selectors (#1100)
- new `FAQ` page
## 1.0.0-beta9 - 2022-02-26
## 1.0.0-beta9
- fix: #1061 - list group item colors in light and dark modes
- new `tasks` dashboard
@@ -684,7 +684,7 @@
- add `font-display: swap;` to improve font loading
- new `Boxed` layout
## 1.0.0-beta8 - 2022-02-05
## 1.0.0-beta8
- update dependencies
- new vector maps demos
@@ -693,7 +693,7 @@
- replace `badge` with `status-dot` in `navbar-notifications.html`
- map tooltip fixes
## 1.0.0-beta7 - 2022-02-05
## 1.0.0-beta7
- fix: #1019 - project-overview.html link not working
- fix: #1010 - card with bottom tabs has incorrect border radius
@@ -706,7 +706,7 @@
- RTL stylesheet fixes
- new card action demos
## 1.0.0-beta6 - 2022-01-18
## 1.0.0-beta6
- pricing cards fix
- fix bug `fw-...`, `.fs-...` is missed (#987)
@@ -719,7 +719,7 @@
- fix #959 - `node-sass` does not properly compile nested media queries
- update package dependencies to newest version
## 1.0.0-beta5 - 2021-12-07
## 1.0.0-beta5
**Tabler has finally lived to see dark mode! 🌝🌚**
@@ -736,7 +736,7 @@
- colors unify
- add `tom-select` and remove `choices.js`
## 1.0.0-beta4 - 2021-10-24
## 1.0.0-beta4
- upgrade required node.js version to 14
- upgrade Bootstrap to 5.1
@@ -744,14 +744,14 @@
- fix #775 - litepicker not initializing
- fix `nouislider` import in dev
## 1.0.0-beta3 - 2021-05-08
## 1.0.0-beta3
- upgrade Bootstrap to 5.0
- upgrade dependencies
- change `$border-radius-pill` variable
- badge vertical align fix
## 1.0.0-beta2 - 2021-03-29
## 1.0.0-beta2
- update dependencies
- `li` marker fix
@@ -764,7 +764,7 @@
- charts label fixes
- charts docs
## 1.0.0-beta - 2021-02-17
## 1.0.0-beta
**Initial beta release of Tabler v1.0! Lots more coming soon though 😁**
+1
View File
@@ -76,6 +76,7 @@
}
/** Shadows */
--shadow-color: #{$box-shadow-color};
@each $name, $value in $box-shadows {
--shadow#{if(sass($name): '-#{$name}'; else: '')}: #{$value};
}
+30 -19
View File
@@ -949,30 +949,41 @@ $aspect-ratios: (
) !default;
// Shadows
$box-shadow-xs: 0 1px 2px 0 rgba(18, 18, 23, 0.05) !default;
// Neutral drop-shadow tint. Emitted once as the global `--shadow-color` custom
// property (see `_props.scss`); every shadow below only varies its opacity via
// `color-transparent()`, never its colour.
//
// The light branch is already 60% transparent, so `color-transparent()` scales
// each shadow down from the percentages below to the intended light-mode value
// (e.g. `0.125 * 0.4 = 0.05`). The dark branch is fully opaque black, so the
// same percentage lands 2.5x darker — otherwise a near-black shadow is
// invisible against the dark surfaces. Keep the two in sync when tuning.
$box-shadow-color: light-dark(rgba(18, 18, 23, 0.4), #000) !default;
$box-shadow-xs: 0 1px 2px 0 color-transparent(var(--shadow-color), 0.125) !default;
$box-shadow-sm:
0 1px 3px 0 rgba(18, 18, 23, 0.1),
0 1px 2px 0 rgba(18, 18, 23, 0.06) !default;
0 1px 3px 0 color-transparent(var(--shadow-color), 0.25),
0 1px 2px 0 color-transparent(var(--shadow-color), 0.15) !default;
$box-shadow-md:
0px 2px 4px -1px rgba(18, 18, 23, 0.06),
0px 4px 6px -1px rgba(18, 18, 23, 0.08) !default;
0px 2px 4px -1px color-transparent(var(--shadow-color), 0.15),
0px 4px 6px -1px color-transparent(var(--shadow-color), 0.2) !default;
$box-shadow-lg:
0px 4px 6px -2px rgba(18, 18, 23, 0.05),
0px 10px 15px -3px rgba(18, 18, 23, 0.08) !default;
0px 4px 6px -2px color-transparent(var(--shadow-color), 0.125),
0px 10px 15px -3px color-transparent(var(--shadow-color), 0.2) !default;
$box-shadow-xl:
0px 10px 10px -5px rgba(18, 18, 23, 0.04),
0px 20px 25px -5px rgba(18, 18, 23, 0.1) !default;
$box-shadow-2xl: 0px 25px 50px -12px rgba(18, 18, 23, 0.25) !default;
0px 10px 10px -5px color-transparent(var(--shadow-color), 0.1),
0px 20px 25px -5px color-transparent(var(--shadow-color), 0.25) !default;
$box-shadow-2xl: 0px 25px 50px -12px color-transparent(var(--shadow-color), 0.625) !default;
$box-shadow-overlay:
0px 2px 4px 0px rgba(18, 18, 23, 0.04),
0px 5px 8px 0px rgba(18, 18, 23, 0.04),
0px 10px 18px 0px rgba(18, 18, 23, 0.03),
0px 24px 48px 0px rgba(18, 18, 23, 0.03),
0px 0px 0px 1px rgba(18, 18, 23, 0.1) !default;
0px 2px 4px 0px color-transparent(var(--shadow-color), 0.1),
0px 5px 8px 0px color-transparent(var(--shadow-color), 0.1),
0px 10px 18px 0px color-transparent(var(--shadow-color), 0.075),
0px 24px 48px 0px color-transparent(var(--shadow-color), 0.075),
0px 0px 0px 1px color-transparent(var(--shadow-color), 0.25) !default;
$box-shadow: $box-shadow-md !default;
$box-shadow-transparent: 0 0 0 0 transparent !default;
$box-shadow-border: 0px 0px 0px 1px rgba(18, 18, 23, 0.1) !default;
$box-shadow-border: 0px 0px 0px 1px color-transparent(var(--shadow-color), 0.25) !default;
$box-shadow-input: $box-shadow-xs !default;
$box-shadow-card: $box-shadow-xs !default;
$box-shadow-card-hover: $box-shadow-lg !default;
@@ -995,7 +1006,7 @@ $box-shadows: (
dropdown: $box-shadow-dropdown,
) !default;
$box-shadow-inset: inset 0 1px 2px rgba($black, 0.075) !default;
$box-shadow-inset: inset 0 1px 2px color-transparent(var(--shadow-color), 0.1875) !default;
// Component active
$component-active-color: $white !default;
@@ -1982,14 +1993,14 @@ $form-select-font-size-lg: $input-font-size-lg !default;
$form-select-border-radius-lg: $input-border-radius-lg !default;
$form-select-transition: $input-transition !default;
$form-switch-color: $white !default;
$form-switch-color: $gray-200 !default;
$form-switch-width: 2rem !default;
$form-switch-height: 1.25rem !default;
$form-switch-padding-start: $form-switch-width + 0.5rem !default;
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color}'/></svg>") !default;
$form-switch-border-radius: $form-switch-width !default;
$form-switch-transition: background-position 0.15s ease-in-out !default;
$form-switch-focus-color: $white !default;
$form-switch-focus-color: $input-focus-border-color !default;
$form-switch-focus-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-focus-color}'/></svg>") !default;
$form-switch-checked-color: $white !default;
$form-switch-checked-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-checked-color}'/></svg>") !default;
+10
View File
@@ -622,6 +622,16 @@ Navbar vertical
// a bare :where()/[…] selector would lose to it and never take effect.
html[data-bs-sidebar^='folded'] {
--sidebar-width: var(--sidebar-folded-width);
// The sidebar is rendered for the expanded state, so the active submenu can
// arrive open (.show). Folded, it would paint as a flyout until sidebar.ts
// closes it on DOMContentLoaded — a flicker on every reload. Menus opened by
// the Dropdown plugin carry a popper data attribute (data-tblr-popper from
// tabler.js, data-bs-popper from Bootstrap's own js), so only the
// server-rendered ones hide.
.navbar-vertical .dropdown-menu.show:not([data-bs-popper]):not([data-tblr-popper]) {
display: none;
}
}
.navbar-vertical:is(.navbar-folded, .navbar-folded-hover) {
+12
View File
@@ -568,6 +568,17 @@ Card code
*/
.card-code {
padding: 0;
overflow: hidden;
// The code block is a dark box, so it follows the card's corners: rounded at
// the card's top or bottom edge, square when another section sits next to it.
&:first-child {
@include border-top-radius(var(--card-border-radius));
}
&:last-child {
@include border-bottom-radius(var(--card-border-radius));
}
.highlight {
margin: 0;
@@ -577,6 +588,7 @@ Card code
pre {
margin: 0 !important;
border: 0 !important;
@include border-radius(0);
}
}
+4 -3
View File
@@ -100,11 +100,12 @@ $dropdown-tokens: defaults(
display: block;
width: 14px;
height: 14px;
clip: rect(0, 9px, 9px, 0);
clip-path: inset(0 5px 5px 0);
content: '';
background: inherit;
border: 1px solid;
border-color: inherit;
// The menu draws its edge with the 1px ring in --dropdown-box-shadow and has
// no border, so `inherit` would give the arrow the text color.
border: 1px solid var(--dropdown-border-color);
/* rtl:ignore */
transform: rotate(45deg);
+1 -1
View File
@@ -1,6 +1,6 @@
# @tabler/docs
## 1.5.0 - 2026-09-05
## 1.5.0
### Minor Changes
+1 -5
View File
@@ -26,11 +26,7 @@ export default defineConfig({
},
// Static output + the Vercel adapter: turns `redirects` below into real HTTP
// redirects at Vercel's routing layer (no adapter = meta-refresh HTML pages).
adapter: vercel({
webAnalytics: {
enabled: true,
},
}),
adapter: vercel(),
// renamed/moved pages, shared with middleware.ts
redirects,
// pages live at the package root (./pages) — content-first layout; all
+15 -2
View File
@@ -1,14 +1,26 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
interface Props {
colors: Record<string, { prop: string; title: string }>
colors: Record<string, { prop: string; title: string; hex?: string }>
}
const { colors }: Props = Astro.props
// The .md mirror gets the palette as a table; hex values are listed only when
// the data has real ones (the gray scale points at a CSS variable instead).
const list = Object.values(colors)
const withHex = list.some((color) => color.hex?.startsWith('#'))
const markdown = markdownTable(
['Color', 'CSS variable', ...(withHex ? ['Hex'] : [])],
list.map((color) => [color.title, `\`${color.prop}\``, ...(withHex ? [color.hex?.startsWith('#') ? `\`${color.hex}\`` : ''] : [])]),
)
---
<div class="row row-cols-4 row-cols-md-6 g-3 g-md-4">
{
Object.values(colors).map((color) => {
list.map((color) => {
return (
<div class="text-center">
<div class="p-6 rounded border" style={{ backgroundColor: `var(${color.prop})` }} />
@@ -18,3 +30,4 @@ const { colors }: Props = Astro.props
})
}
</div>
<MarkdownSource name="Colors" markdown={markdown} />
@@ -0,0 +1,91 @@
---
// Fetches this page's Markdown mirror (its url with ".md" appended) and copies
// its content to the clipboard, so the page can be pasted into an LLM directly.
import Icon from '@ui/Icon.astro'
interface Props {
/** Url of the page's `.md` mirror, fetched on click. */
url: string
/** Absolute url of the same `.md` mirror, for the "Ask ChatGPT" / "Ask Claude" prompts. */
absoluteUrl?: string | undefined
class?: string
}
const { url, absoluteUrl, class: className } = Astro.props
// Both ChatGPT and Claude open with this text already in the composer; the
// reader still has to press send.
const prompt = absoluteUrl && `Read ${absoluteUrl} so I can ask questions about it.`
const askChatGptUrl = prompt && `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`
const askClaudeUrl = prompt && `https://claude.ai/new?q=${encodeURIComponent(prompt)}`
---
<div class="btn-group">
<button type="button" class:list={['btn btn-sm', className]} data-docs-copy-markdown-link={url}>
<Icon name="clipboard" />
<Icon name="check" class="d-none" />
<span data-docs-copy-markdown-link-label>Copy to LLM</span>
</button>
<button type="button" class="btn btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href={url} target="_blank">
<Icon name="external-link" />
<span>View as markdown</span>
</a>
{
askChatGptUrl && (
<a class="dropdown-item" href={askChatGptUrl} target="_blank" rel="noopener noreferrer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
<span>Ask ChatGPT</span>
</a>
)
}
{
askClaudeUrl && (
<a class="dropdown-item" href={askClaudeUrl} target="_blank" rel="noopener noreferrer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" />
</svg>
<span>Ask Claude</span>
</a>
)
}
</div>
</div>
<script>
// Wire every instance on the page and read each one's url from its own attribute.
const buttons = document.querySelectorAll<HTMLButtonElement>('[data-docs-copy-markdown-link]')
buttons.forEach((button) => {
const label = button.querySelector<HTMLElement>('[data-docs-copy-markdown-link-label]')
const [clipboardIcon, checkIcon] = button.querySelectorAll<HTMLElement>('svg')
button.addEventListener('click', async () => {
const url = button.dataset.docsCopyMarkdownLink
if (!url || !label) return
try {
const response = await fetch(url)
await navigator.clipboard.writeText(await response.text())
const original = label.textContent
label.textContent = 'Copied'
clipboardIcon?.classList.add('d-none')
checkIcon?.classList.remove('d-none')
setTimeout(() => {
label.textContent = original
clipboardIcon?.classList.remove('d-none')
checkIcon?.classList.add('d-none')
}, 2000)
} catch {
// clipboard permission refused — leave the label alone
}
})
})
</script>
+2 -38
View File
@@ -19,11 +19,9 @@ interface Props {
editUrl?: string | undefined
/** URL of the file implementing the component, from the page's `source` front matter */
sourceUrl?: string | undefined
/** url of this page's markdown mirror, offered as a copy button */
markdownUrl?: string | undefined
}
const { toc = [], editUrl, sourceUrl, markdownUrl } = Astro.props
const { toc = [], editUrl, sourceUrl } = Astro.props
const illustrationsCount = illustrations.length
---
@@ -54,15 +52,8 @@ const illustrationsCount = illustrations.length
</a>
{
(markdownUrl || sourceUrl || editUrl) && (
(sourceUrl || editUrl) && (
<nav class="docs-page-links mt-6" aria-label="Page tools">
{markdownUrl && (
<button type="button" class="docs-page-link" data-docs-copy-markdown={markdownUrl}>
<Icon name="clipboard" />
<span data-docs-copy-label>Copy page as Markdown</span>
</button>
)}
{sourceUrl && (
<a href={sourceUrl} class="docs-page-link" target="_blank" rel="noopener noreferrer">
<Icon name="code" />
@@ -80,30 +71,3 @@ const illustrationsCount = illustrations.length
)
}
<!-- END DOCS TOC -->
<script>
// Fetches the .md mirror of the page and puts it on the clipboard, so the
// whole page can be pasted into an assistant without hunting for the url.
const button = document.querySelector<HTMLButtonElement>('[data-docs-copy-markdown]')
if (button) {
const label = button.querySelector<HTMLElement>('[data-docs-copy-label]')
button.addEventListener('click', async () => {
if (!label) return
const url = button.dataset.docsCopyMarkdown
if (!url) return
try {
const response = await fetch(url)
await navigator.clipboard.writeText(await response.text())
const original = label.textContent
label.textContent = 'Copied'
setTimeout(() => {
label.textContent = original
}, 2000)
} catch {
// clipboard or network refused — leave the label alone
}
})
}
</script>
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import flags from '@data/flags.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['Country', 'Code', 'CSS class'],
flags.map((flag) => [flag.name, `\`${flag.flag}\``, `\`.flag-country-${flag.flag}\``]),
)
---
<table class="table table-vcenter">
@@ -30,3 +38,4 @@ import flags from '@data/flags.json'
}
</tbody>
</table>
<MarkdownSource name="Flags" markdown={markdown} />
+17
View File
@@ -0,0 +1,17 @@
---
// Ships a markdown version of a component's output for the page's .md mirror.
// Render it next to the html; `name` is the tag the MDX uses for the component.
// Only the mirror render (docs/lib/llms.ts, which sets the `markdownMirror`
// local) gets the comment — a browser page never carries it.
// See shared/lib/markdown-source.ts.
import { markdownSource } from '@shared/lib/markdown-source'
interface Props {
name: string
markdown: string
}
const { name, markdown } = Astro.props
---
{Astro.locals.markdownMirror && <Fragment set:html={markdownSource(name, markdown)} />}
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import payments from '@data/payments.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['Provider', 'CSS class', 'Dark variant'],
payments.map((payment) => [payment.name, `\`.payment-provider-${payment.logo}\``, `\`.payment-provider-${payment.logo}-dark\``]),
)
---
<table class="table table-vcenter">
@@ -36,3 +44,4 @@ import payments from '@data/payments.json'
}
</tbody>
</table>
<MarkdownSource name="Payments" markdown={markdown} />
+9
View File
@@ -1,5 +1,13 @@
---
import MarkdownSource from './MarkdownSource.astro'
import { markdownTable } from '@shared/lib/markdown-source'
import socials from '@data/socials.json'
// Same list as a table for the page's .md mirror
const markdown = markdownTable(
['App', 'CSS class'],
socials.map((social) => [social.name, `\`.social-app-${social.file}\``]),
)
---
<table class="table table-vcenter">
@@ -29,3 +37,4 @@ import socials from '@data/socials.json'
}
</tbody>
</table>
<MarkdownSource name="Socials" markdown={markdown} />
+7
View File
@@ -3,3 +3,10 @@
// clipboard.js, loaded via a page lib (libs.json) as a classic <script> global, not an
// npm package — declared ambiently so `astro check` can resolve it in DocsLayout's copy script.
declare var ClipboardJS: new (selector: Element, options: Record<string, unknown>) => { on(event: string, callback: (e: { clearSelection(): void; trigger: HTMLElement }) => void): void }
declare namespace App {
interface Locals {
/** set by docs/lib/llms.ts while rendering a page for its .md mirror; <MarkdownSource /> emits only then */
markdownMirror?: boolean
}
}
+31 -20
View File
@@ -13,6 +13,7 @@ import DocsSearch from '@components/DocsSearch.astro';
import DocsClassnames from '@components/DocsClassnames.astro';
import DocsToc from '@components/DocsToc.astro';
import type { TocItem } from '@components/DocsToc.astro';
import DocsCopyMarkdownLink from '@components/DocsCopyMarkdownLink.astro';
import DocsPagination from '@components/DocsPagination.astro';
import DocsCard from '@components/DocsCard.astro';
import { getDocsPage } from '@lib/docs-pages';
@@ -114,6 +115,9 @@ const metaDescription = seoDescription ?? description;
const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation';
const canonicalUrl = new URL(docsUrl, Astro.site ?? site.docsUrl).href;
const ogImageUrl = new URL('/static/og.png', Astro.site ?? site.docsUrl).href;
// Absolute .md url: fetch() uses the relative markdownUrl (same-origin), but the
// "Ask ChatGPT" link needs a url ChatGPT can actually resolve on its own.
const canonicalMarkdownUrl = markdownUrl ? new URL(markdownUrl, Astro.site ?? site.docsUrl).href : undefined;
// Relative asset base: for /ui/components/alert/ -> "../../.."
const depth = docsUrl.split('/').filter(Boolean).length;
@@ -346,23 +350,30 @@ const relatedPages = await Promise.all(
<div class="col d-flex flex-column">
<div class="docs-article flex-fill">
<div class="p-4 py-6 p-lg-6">
{breadcrumbs && (
<nav aria-label="Breadcrumb">
<ol class="breadcrumb breadcrumb-arrows breadcrumb-muted mb-3">
{breadcrumbs.map((item, index) =>
index < breadcrumbs.length - 1 ? (
<li class="breadcrumb-item">
<a href={item.path}>{item.name}</a>
</li>
) : (
<li class="breadcrumb-item active" aria-current="page">
{item.name}
</li>
),
{
(breadcrumbs || markdownUrl) && (
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
{breadcrumbs && (
<nav aria-label="Breadcrumb">
<ol class="breadcrumb breadcrumb-arrows breadcrumb-muted mb-0">
{breadcrumbs.map((item, index) =>
index < breadcrumbs.length - 1 ? (
<li class="breadcrumb-item">
<a href={item.path}>{item.name}</a>
</li>
) : (
<li class="breadcrumb-item active" aria-current="page">
{item.name}
</li>
),
)}
</ol>
</nav>
)}
</ol>
</nav>
)}
{markdownUrl && <DocsCopyMarkdownLink url={markdownUrl} absoluteUrl={canonicalMarkdownUrl} />}
</div>
)
}
<Prose
data-bs-spy={toc.length > 0 ? 'scroll' : undefined}
@@ -371,14 +382,14 @@ const relatedPages = await Promise.all(
data-bs-smooth-scroll={toc.length > 0 ? 'true' : undefined}
tabindex="0"
>
<div class="d-flex">
<h1 class="docs-page-title">
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-2">
<h1 class="docs-page-title mb-0">
{title}
</h1>
{
addedIn && (
<div class="ms-auto">
<div class="ms-sm-auto">
<span class="badge bg-primary-lt">Added in {addedIn}</span>
</div>
)
@@ -470,7 +481,7 @@ const relatedPages = await Promise.all(
<!-- BEGIN DOCS TOC -->
<div class="col-2 d-none d-xxl-block">
<div class="py-5 sticky-top docs-toc-sticky">
<DocsToc toc={toc} editUrl={`${site.githubUrl}/edit/dev/${editFilePath}`} sourceUrl={sourcePath ? `${site.githubUrl}/blob/dev/${sourcePath}` : undefined} markdownUrl={markdownUrl} />
<DocsToc toc={toc} editUrl={`${site.githubUrl}/edit/dev/${editFilePath}`} sourceUrl={sourcePath ? `${site.githubUrl}/blob/dev/${sourcePath}` : undefined} />
</div>
</div>
<!-- END DOCS TOC -->
+39 -14
View File
@@ -5,9 +5,11 @@
// component props. Stripping components wholesale — the usual llms.txt recipe —
// would delete exactly that. So the components that hold content are unwrapped
// into fenced code blocks instead, and only the decorative ones are dropped.
// A component whose content is data rather than markup (<Colors />, <Flags />)
// renders its own markdown in a comment, see shared/lib/markdown-source.ts.
//
// Prose comes from the MDX source, but example markup comes from the *rendered*
// page (see renderedExamples below): a third of the examples are written with
// page (see renderedPage below): a third of the examples are written with
// docs components — <Icon>, <AvatarList>, <Badge> — and the source form of those
// is of no use to a reader who wants the html.
import type { CollectionEntry } from 'astro:content'
@@ -16,6 +18,7 @@ import { loadRenderers } from 'astro:container'
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
import { getContainerRenderer } from '@astrojs/mdx/container-renderer'
import { beautifyHtml, extractMarkedSnippet } from '@shared/lib/code-example'
import { extractMarkdownSources } from '@shared/lib/markdown-source'
import { site } from '@shared/lib/site'
import { callouts } from '@components/callouts/index.ts'
import docs from '@data/docs.json'
@@ -137,19 +140,29 @@ let container: Promise<AstroContainer> | undefined
const getContainer = () => (container ??= loadRenderers([getContainerRenderer()]).then((renderers) => AstroContainer.create({ renderers })))
type RenderedPage = {
/** final markup of every <Example>, in document order; null when the example renders nothing */
examples: (string | null)[]
/** markdown shipped by components via <MarkdownSource />, per component name */
sources: Map<string, string[]>
}
const EMPTY_PAGE: RenderedPage = { examples: [], sources: new Map() }
/**
* The final markup of every <Example> on a page, in document order — the same
* html the page's copy button hands out, so components are already expanded.
* An entry is null only when the example renders nothing, and the whole list is
* empty when the page cannot be rendered.
* What the rendered page carries for the markdown mirror: the final markup of
* every <Example> — the same html the page's copy button hands out, so
* components are already expanded — and the markdown sources of data
* components. Both lists are empty when the page cannot be rendered.
*/
async function renderedExamples(entry: CollectionEntry<'docs'>): Promise<(string | null)[]> {
async function renderedPage(entry: CollectionEntry<'docs'>): Promise<RenderedPage> {
try {
const { Content } = await render(entry)
// Same components the page route passes, or every page using a callout throws.
const html = await (await getContainer()).renderToString(Content, { props: { components: callouts } })
// The local tells <MarkdownSource /> this render is for the mirror.
const html = await (await getContainer()).renderToString(Content, { props: { components: callouts }, locals: { markdownMirror: true } })
return html
const examples = html
.split(EXAMPLE_MARKER)
.slice(1)
.map((block) => {
@@ -157,18 +170,21 @@ async function renderedExamples(entry: CollectionEntry<'docs'>): Promise<(string
const markup = block.match(/data-clipboard-text="([^"]*)"/) ?? block.match(/data-example-markup="([^"]*)"/)
return markup ? beautifyHtml(decodeEntities(markup[1]!)) : null
})
return { examples, sources: extractMarkdownSources(html) }
} catch (error) {
console.warn(`[llms] Rendering ${entry.id} failed, examples fall back to their MDX source:`, error)
return []
return EMPTY_PAGE
}
}
/**
* Turn one page's MDX body into plain markdown. `examples` comes from
* renderedExamples() and replaces the source of each <Example> slot; it is
* ignored unless it lines up one-to-one with the examples in the source.
* Turn one page's MDX body into plain markdown. `examples` and `sources` come
* from renderedPage(): the examples replace the source of each <Example> slot,
* the sources replace the tags of the components that shipped them. Either is
* ignored unless it lines up one-to-one with the tags in the source.
*/
export async function mdxToMarkdown(body: string, examples: (string | null)[] = []): Promise<string> {
export async function mdxToMarkdown(body: string, examples: (string | null)[] = [], sources: Map<string, string[]> = new Map()): Promise<string> {
// `<Code lang code={`…`} />` first: its template literal contains backticks, which
// would otherwise be mistaken for markdown code spans by protectCode() below.
let text = body.replace(/<Code\b[^>]*?code=\{`([\s\S]*?)`\}[\s\S]*?\/>/g, (match, snippet: string) => {
@@ -210,6 +226,14 @@ export async function mdxToMarkdown(body: string, examples: (string | null)[] =
return `\n${fence(cdnPluginSnippet(plugins))}\n`
})
// data components that shipped their own markdown (<Colors />, <Flags />, …)
for (const [name, markdowns] of sources) {
const tagPattern = new RegExp(`<${name}\\b[^>]*/>`, 'g')
if ((text.match(tagPattern)?.length ?? 0) !== markdowns.length) continue
let sourceIndex = 0
text = text.replace(tagPattern, () => `\n${markdowns[sourceIndex++]}\n`)
}
// the blocks just produced must be protected too, for the same reason
text = protectCode(text, code)
@@ -229,7 +253,8 @@ async function buildPageMarkdown(entry: CollectionEntry<'docs'>, url: string): P
const { title, summary, description } = entry.data
const header = [`# ${title}`, '', `> ${summary}`, '', description, '', `Source: ${url}`, '', '---', ''].join('\n')
return `${header}\n${await mdxToMarkdown(entry.body ?? '', await renderedExamples(entry))}\n`
const { examples, sources } = await renderedPage(entry)
return `${header}\n${await mdxToMarkdown(entry.body ?? '', examples, sources)}\n`
}
// [...slug].md.ts and llms-full.txt.ts render the same pages in one build.
+7 -3
View File
@@ -369,9 +369,9 @@ header > .navbar {
}
}
// Page tools under the table of contents: copy, source, edit. One shape for all
// three, so the button does not read as a different kind of control than the
// links next to it.
// Page tools under the table of contents: source, edit. One shape for both
// links (copying the page as markdown moved to the "Copy to LLM" button above
// the title, see DocsCopyMarkdownLink).
.docs-page-links {
display: flex;
flex-direction: column;
@@ -606,6 +606,10 @@ html {
}
// The table of contents sticks too, so it has to clear the header (#2421).
// A long list must not run past the viewport either: the rail is capped at the
// space below the header and scrolls on its own (#2965).
.docs-toc-sticky {
top: var(--docs-header-height);
max-height: calc(100vh - var(--docs-header-height));
overflow-y: auto;
}
+1 -2
View File
@@ -12,8 +12,7 @@
"test": "turbo test",
"test:watch": "turbo test:watch",
"test:coverage": "turbo test:coverage",
"version": "changeset version && pnpm run stamp-changelog-dates",
"stamp-changelog-dates": "tsx .build/stamp-changelog-dates.ts",
"version": "changeset version",
"publish": "changeset publish",
"format:md": "tsx .build/reformat-mdx.mts",
"html-diff": "tsx .build/html-diff.ts check",
+1 -1
View File
@@ -1,6 +1,6 @@
# @tabler/preview
## 1.5.0 - 2026-09-05
## 1.5.0
### Minor Changes
+5 -5
View File
@@ -25,7 +25,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Flush</CardTitle>
<CardSubtitle>Add <code>flush</code> to remove the card border and rounded corners.</CardSubtitle>
<CardSubtitle>Add <code>accordion-flush</code> to remove the card border and rounded corners.</CardSubtitle>
</CardBody>
<Accordion type={['flush']} id="flush" />
</Card>
@@ -34,7 +34,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Tabs</CardTitle>
<CardSubtitle>Add <code>tabs</code> for a tab-like header instead of the default style.</CardSubtitle>
<CardSubtitle>Add <code>accordion-tabs</code> for a tab-like header instead of the default style.</CardSubtitle>
<Accordion type={['tabs']} id="tabs" />
</CardBody>
</Card>
@@ -43,7 +43,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Inverted</CardTitle>
<CardSubtitle>Add <code>inverted</code> to flip the toggle icon to the start of the header.</CardSubtitle>
<CardSubtitle>Add <code>accordion-inverted</code> to flip the toggle icon to the start of the header.</CardSubtitle>
<Accordion type={['inverted']} id="inverted" />
</CardBody>
</Card>
@@ -52,7 +52,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Inverted with plus icon</CardTitle>
<CardSubtitle>Combine <code>inverted</code> with a custom <code>toggleIcon</code>.</CardSubtitle>
<CardSubtitle>Combine <code>accordion-inverted</code> with <code>accordion-button-toggle-plus</code> and a custom icon in the toggle.</CardSubtitle>
<Accordion id="inverted-plus" type={['inverted']} toggleIcon="plus" />
</CardBody>
</Card>
@@ -61,7 +61,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With icons</CardTitle>
<CardSubtitle>Add <code>showIcon</code> to show an icon next to each panel's title.</CardSubtitle>
<CardSubtitle>Add an <code>accordion-button-icon</code> next to each panel's title.</CardSubtitle>
<Accordion id="icons" showIcon />
</CardBody>
</Card>
+5 -5
View File
@@ -28,7 +28,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With action</CardTitle>
<CardSubtitle>Add a link with the <code>action</code> prop to give users something to do next.</CardSubtitle>
<CardSubtitle>Add an <code>alert-action</code> link to give users something to do next.</CardSubtitle>
<Alert showClose action="Link" color="danger" title="An error occurred!" />
<Alert showClose action="Link" color="warning" title="Some information is missing!" />
<Alert showClose action="Link" color="success" title="Completed successfully!" />
@@ -40,7 +40,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Dismissible</CardTitle>
<CardSubtitle>Add <code>showClose</code> to let users close the alert.</CardSubtitle>
<CardSubtitle>Add <code>alert-dismissible</code> and a <code>btn-close</code> button to let users close the alert.</CardSubtitle>
<Alert showClose color="danger" title="An error occurred!" />
<Alert showClose color="warning" title="Some information is missing!" />
<Alert showClose color="success" title="Completed successfully!" />
@@ -52,7 +52,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With a description</CardTitle>
<CardSubtitle>Add a <code>description</code> or a <code>list</code> when the title alone isn't enough context.</CardSubtitle>
<CardSubtitle>Add an <code>alert-description</code> or an <code>alert-list</code> when the title alone isn't enough context.</CardSubtitle>
<Alert showClose color="danger" title="Password does not meet requirements:" list={['Minimum 8 characters', 'Include a special character']} />
<Alert showClose color="warning" title="Some information is missing!" description="This is a custom alert box with a description." />
<Alert showClose color="success" title="Completed successfully!" description="This is a custom alert box with a description." />
@@ -64,7 +64,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Important</CardTitle>
<CardSubtitle>Add <code>important</code> for a bolder style that stands out from the rest of the page.</CardSubtitle>
<CardSubtitle>Add <code>alert-important</code> for a bolder style that stands out from the rest of the page.</CardSubtitle>
<Alert showClose variant="important" color="danger" title="Password does not meet requirements:" list={['Minimum 8 characters', 'Include a special character']} />
<Alert showClose variant="important" color="success" title="This is a custom alert box!" description="This is a custom alert box with a description." />
<Alert showClose variant="important" color="warning" title="This is a custom alert box!" description="This is a custom alert box with a description." />
@@ -78,7 +78,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Minor</CardTitle>
<CardSubtitle>Add <code>minor</code> for a quieter style that fits inline with other content.</CardSubtitle>
<CardSubtitle>Add <code>alert-minor</code> for a quieter style that fits inline with other content.</CardSubtitle>
<Alert showClose variant="minor" color="danger" title="Password does not meet requirements:" list={['Minimum 8 characters', 'Include a special character']} />
<Alert showClose variant="minor" color="success" title="This is a custom alert box!" description="This is a custom alert box with a description." />
<Alert showClose variant="minor" color="warning" title="This is a custom alert box!" description="This is a custom alert box with a description." />
+7 -7
View File
@@ -42,7 +42,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar with icon</CardTitle>
<CardSubtitle>Use an icon instead of a photo with the <code>icon</code> prop.</CardSubtitle>
<CardSubtitle>Put an icon inside the <code>avatar</code> instead of a photo.</CardSubtitle>
<div class="avatar-list">
{iconIcons.map((icon) => <Avatar icon={icon} />)}
</div>
@@ -64,7 +64,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Simple avatar</CardTitle>
<CardSubtitle>Pass a <code>person</code> object to render their photo.</CardSubtitle>
<CardSubtitle>Show a photo by setting it as the <code>background-image</code> of the avatar.</CardSubtitle>
<div class="avatar-list">
{people8.map((person) => <Avatar person={person} />)}
</div>
@@ -75,7 +75,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar placeholder</CardTitle>
<CardSubtitle>Fall back to initials with the <code>placeholder</code> prop when there's no photo.</CardSubtitle>
<CardSubtitle>Fall back to initials as the avatar text when there's no photo.</CardSubtitle>
<div class="avatar-list">
{people8.map((person) => <Avatar placeholder={firstLetters(person.full_name ?? '')} />)}
</div>
@@ -99,7 +99,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar sizes</CardTitle>
<CardSubtitle>Six sizes from <code>xxs</code> to <code>xl</code>, for both photos and placeholders.</CardSubtitle>
<CardSubtitle>Six sizes from <code>avatar-xxs</code> to <code>avatar-xl</code>, for both photos and placeholders.</CardSubtitle>
<div class="row">
<div class="col-6">
<div class="avatar-list">
@@ -119,7 +119,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar lists</CardTitle>
<CardSubtitle>Stack avatars with <code>&lt;AvatarList&gt;</code> to show a group at a glance.</CardSubtitle>
<CardSubtitle>Stack avatars inside an <code>.avatar-list</code> container to show a group at a glance.</CardSubtitle>
<div class="row g-3">
{
listSizes.map((size) => (
@@ -160,7 +160,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar statuses</CardTitle>
<CardSubtitle>Add a <code>status</code> dot to show online, away, or busy state.</CardSubtitle>
<CardSubtitle>Add a <code>badge</code> inside the avatar for an online, away, or busy status dot.</CardSubtitle>
{statusColors.map((color, i) => <Avatar person={personById(i + 1)} class="rounded-circle" status={color} />)}
</CardBody>
</Card>
@@ -169,7 +169,7 @@ const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch']
<Card>
<CardBody>
<CardTitle>Avatar brands</CardTitle>
<CardSubtitle>Show a recognizable brand icon instead of a person.</CardSubtitle>
<CardSubtitle>Add a recognizable brand icon on top of an avatar.</CardSubtitle>
{brands.map((brand, i) => <Avatar person={personById(i + 1)} brand={brand} />)}
</CardBody>
</Card>
+1 -1
View File
@@ -83,7 +83,7 @@ const sizes = ['sm', 'md', 'lg', 'xl'] as const
<Card>
<CardBody>
<CardTitle>Sizes</CardTitle>
<CardSubtitle>Scale a button from <code>sm</code> to <code>xl</code> without changing its icon layout.</CardSubtitle>
<CardSubtitle>Scale a button from <code>btn-sm</code> to <code>btn-xl</code> without changing its icon layout.</CardSubtitle>
<div class="space-y">
{
sizes.map((size) => (
+1 -1
View File
@@ -16,7 +16,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Two y-axes</CardTitle>
<CardSubtitle>Plot values with different units together. Bind each axis to a serie with <code>seriesName</code> and move the second one to the right with <code>opposite</code>.</CardSubtitle>
<CardSubtitle>Plot values with different units together. Bind each axis to a series with the <code>yaxis.seriesName</code> chart option and move the second one to the right with <code>opposite</code>.</CardSubtitle>
<Chart chartId="revenue-vs-orders" height={16} label="Revenue in dollars and order count per month" />
</CardBody>
</Card>
+77 -87
View File
@@ -10,27 +10,33 @@ import Progress from '@ui/Progress.astro'
import Modal from '@shared/components/modals/Modal.astro'
import CaptureModal from '@shared/components/CaptureModal.astro'
import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import { formatShortDate } from '@shared/lib/date-format'
import mails from '@data/mails.json'
// Label colors shared by the folder sidebar and the dots in the message list.
const labelColors: Record<string, string> = { Updates: 'info', Friends: 'warning', Family: 'success', Social: 'primary', Important: 'danger', Promotions: 'purple' }
---
<DefaultLayout title="Email inbox" pageHeader="Inbox" pageMenu="extra.email-inbox" pageLibs={['hugerte']}>
<Card>
<div class="row g-0">
<div class="col-xxl-3 email-border border-end">
<Offcanvas responsive="xxl" direction="start" class="h-100 file-offcanvas" tabindex="-1" id="emailSidebaroffcanvas">
<div class="col-xxl-3 border-end">
<Offcanvas responsive="xxl" direction="start" class="h-100" tabindex="-1" id="emailSidebaroffcanvas">
<div class="card-body h-100">
<div>
<Button icon="pencil" text="Compose" color="primary" class="d-none d-sm-block" modalId="new-email" />
<Button icon="pencil" text="Compose" color="primary" block class="d-none d-sm-block" modalId="new-email" />
</div>
<div class="mt-3 nav nav-vertical">
<a href="#" class="nav-link text-danger fw-bold">
<div class="mt-3 nav nav-vertical nav-pills">
<a href="#" class="nav-link active">
<Icon name="inbox" class="me-2" />
Inbox<span class="badge bg-danger ms-auto">{mails.length}</span>
Inbox<span class="badge bg-primary-lt ms-auto">{mails.length}</span>
</a>
<a href="#" class="nav-link"><Icon name="star" class="me-2" />Starred</a>
<a href="#" class="nav-link"><Icon name="clock" class="me-2" />Snoozed</a>
<a href="#" class="nav-link"><Icon name="file" class="me-2" />Draft<span class="badge bg-info ms-auto">32</span></a>
<a href="#" class="nav-link"><Icon name="file" class="me-2" />Draft<span class="badge bg-info-lt ms-auto">32</span></a>
<a href="#" class="nav-link"><Icon name="mail-up" class="me-2" />Sent Mail</a>
<a href="#" class="nav-link"><Icon name="trash" class="me-2" />Trash</a>
<a href="#" class="nav-link"><Icon name="tag" class="me-2" />Important</a>
@@ -39,25 +45,14 @@ import mails from '@data/mails.json'
<div class="mt-4">
<Subheader as="h6">Labels</Subheader>
<div class="mt-2 nav nav-vertical">
<a href="#" class="nav-link">
<div class="badge bg-info me-2"></div> Updates
</a>
<a href="#" class="nav-link">
<div class="badge bg-warning me-2"></div> Friends
</a>
<a href="#" class="nav-link">
<div class="badge bg-success me-2"></div> Family
</a>
<a href="#" class="nav-link">
<div class="badge bg-primary me-2"></div> Social
</a>
<a href="#" class="nav-link">
<div class="badge bg-danger me-2"></div> Important
</a>
<a href="#" class="nav-link">
<div class="badge bg-purple me-2"></div> Promotions
</a>
<div class="mt-2 nav nav-vertical nav-pills">
{
Object.entries(labelColors).map(([label, color]) => (
<a href="#" class="nav-link">
<span class={`badge bg-${color} me-2`} /> {label}
</a>
))
}
</div>
</div>
@@ -112,12 +107,11 @@ import mails from '@data/mails.json'
</div>
</ButtonGroup>
<ButtonGroup>
<ButtonGroup class="ms-auto">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<Icon name="dots" /> More
</button>
<div class="dropdown-menu">
<span class="dropdown-header">More Options :</span>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" href="#">Mark as Unread</a>
<a class="dropdown-item" href="#">Add to Tasks</a>
<a class="dropdown-item" href="#">Add Star</a>
@@ -125,70 +119,66 @@ import mails from '@data/mails.json'
</div>
</ButtonGroup>
</div>
<div class="mt-3">
<ul class="email-list">
{
mails && mails.length > 0 ? (
mails.map((mail) => (
<li>
<div class="email-sender-info">
<div class="checkbox-wrapper-mail">
<div class="form-check">
<input type="checkbox" class="form-check-input" id={`mail-${mail.id}`} />
<label class="form-check-label" for={`mail-${mail.id}`} />
</div>
</div>
<span class="star-toggle">
<Icon name="star" />
</span>
<a href="#" class="email-title">
</div>
<div class="card-body p-0">
<ListGroup flush hoverable>
{
mails.length > 0 ? (
mails.map((mail) => (
<ListGroupItem>
<div class="row align-items-center g-2">
<div class="col-auto">
<input type="checkbox" class="form-check-input m-0" aria-label={`Select "${mail.subject}"`} />
</div>
<div class="col-auto">
<a href="#" class:list={['list-group-item-actions', mail.starred && 'show']} aria-label={mail.starred ? 'Remove star' : 'Add star'}>
<Icon name="star" color={mail.starred ? 'yellow' : 'muted'} />
</a>
</div>
<div class="col-3 col-lg-2 text-truncate">
<a href="#" class="text-body">
{mail.sender}
</a>
</div>
<div class="email-content">
<a href="#" class="email-subject">
{mail.subject} &nbsp;–&nbsp;
<span>{mail.preview}</span>
<div class="col text-truncate">
<a href="#" class="text-body">
{mail.subject}
</a>
<div class="email-date">{mail.date}</div>
<span class="text-secondary"> – {mail.preview}</span>
</div>
<div class="email-action-icons">
<ul class="list-inline">
<li class="list-inline-item">
<a href="#" aria-label="Archive">
<Icon name="archive" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Delete">
<Icon name="trash" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Mark as read">
<Icon name="mail-opened" class="email-action-icons-item" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Snooze">
<Icon name="clock" />
</a>
</li>
</ul>
<div class="col-auto d-none d-lg-block">
{mail.labels.map((label) => (
<span class={`badge bg-${labelColors[label] ?? 'secondary'}`} title={label} aria-label={label} />
))}
</div>
</li>
))
) : (
<li class="text-muted">No emails</li>
)
}
</ul>
</div>
<div class="row">
<div class="col-auto d-none d-md-block">
<div class="list-group-item-actions d-flex gap-2">
<a href="#" class="text-secondary" aria-label="Archive">
<Icon name="archive" />
</a>
<a href="#" class="text-secondary" aria-label="Delete">
<Icon name="trash" />
</a>
<a href="#" class="text-secondary" aria-label="Mark as read">
<Icon name="mail-opened" />
</a>
<a href="#" class="text-secondary" aria-label="Snooze">
<Icon name="clock" />
</a>
</div>
</div>
<div class="col-auto text-secondary text-end">{formatShortDate(new Date(mail.date))}</div>
</div>
</ListGroupItem>
))
) : (
<ListGroupItem class="text-secondary">No emails</ListGroupItem>
)
}
</ListGroup>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-7 mt-1">
Showing 1 - {mails.length} of {mails.length}
</div>
+2 -2
View File
@@ -16,7 +16,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Numbered</CardTitle>
<CardSubtitle>Page numbers, with or without <code>text</code> Prev/Next labels.</CardSubtitle>
<CardSubtitle>Page numbers, with or without Prev/Next text labels (<code>page-text</code>).</CardSubtitle>
<Pagination count={5} activeItem={3} />
<Pagination count={5} activeItem={3} text />
</CardBody>
@@ -26,7 +26,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With descriptions</CardTitle>
<CardSubtitle>Add <code>prevDescription</code>/<code>nextDescription</code> to name the adjacent page.</CardSubtitle>
<CardSubtitle>Name the adjacent page with <code>page-item-subtitle</code> for the direction and <code>page-item-title</code> for the page name.</CardSubtitle>
<Pagination count={0} prevDescription="Getting started" nextDescription="Breadcrumbs" />
</CardBody>
</Card>
+2 -2
View File
@@ -59,7 +59,7 @@ const patterns = ['diagonal', 'diagonal-2', 'dots', 'rectangles', 'lines', 'line
<Card>
<CardBody>
<CardTitle>Pattern sizes</CardTitle>
<CardSubtitle>Scale the pattern from <code>sm</code> to <code>xl</code>.</CardSubtitle>
<CardSubtitle>Scale the pattern from <code>bg-pattern-sm</code> to <code>bg-pattern-xl</code>.</CardSubtitle>
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-6 g-3">
{
patternSizes.map((size) => (
@@ -77,7 +77,7 @@ const patterns = ['diagonal', 'diagonal-2', 'dots', 'rectangles', 'lines', 'line
<Card>
<CardBody>
<CardTitle>Pattern opacity</CardTitle>
<CardSubtitle>Scale the pattern strength with <code>lighter</code>, <code>light</code>, <code>dark</code>, or <code>darker</code>.</CardSubtitle>
<CardSubtitle>Scale the pattern strength with the <code>bg-pattern-opacity-*</code> utilities.</CardSubtitle>
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-6 g-3">
{
patternOpacities.map((opacity) => (
+6 -6
View File
@@ -34,7 +34,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With value</CardTitle>
<CardSubtitle>Show the exact percentage inside a larger bar with <code>showValue</code>.</CardSubtitle>
<CardSubtitle>Show the exact percentage as the <code>progress-bar</code> text inside a larger bar.</CardSubtitle>
<div class="space-y">
<Progress value={10} showValue size="lg" />
<Progress value={20} showValue size="lg" />
@@ -61,7 +61,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Sizes</CardTitle>
<CardSubtitle>Scale a bar from <code>sm</code> to <code>xl</code>.</CardSubtitle>
<CardSubtitle>Scale a bar from <code>progress-sm</code> to <code>progress-xl</code>.</CardSubtitle>
<div class="space-y">
<Progress value={20} size="sm" />
<Progress value={40} />
@@ -75,7 +75,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Indeterminate</CardTitle>
<CardSubtitle>Animate a bar with no known end point using <code>indeterminate</code>.</CardSubtitle>
<CardSubtitle>Animate a bar with no known end point with <code>progress-bar-indeterminate</code>.</CardSubtitle>
<div class="space-y">
<Progress indeterminate />
</div>
@@ -98,7 +98,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Striped</CardTitle>
<CardSubtitle>Add diagonal stripes with <code>striped</code> for a textured look.</CardSubtitle>
<CardSubtitle>Add diagonal stripes with <code>progress-bar-striped</code> for a textured look.</CardSubtitle>
<div class="space-y">
<Progress color="blue" value={20} striped />
<Progress color="green" value={40} striped />
@@ -112,7 +112,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Animated</CardTitle>
<CardSubtitle>Combine <code>striped</code> with <code>animated</code> to keep the stripes moving.</CardSubtitle>
<CardSubtitle>Combine <code>progress-bar-striped</code> with <code>progress-bar-animated</code> to keep the stripes moving.</CardSubtitle>
<div class="space-y">
<Progress value={20} striped animated />
<Progress value={40} color="green" striped animated />
@@ -241,7 +241,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle> Progress Description Sizes </CardTitle>
<CardSubtitle>Scale the labeled progress bar from <code>sm</code> to <code>xl</code>.</CardSubtitle>
<CardSubtitle>Scale the labeled progress bar from <code>progress-sm</code> to <code>progress-xl</code>.</CardSubtitle>
<div class="space-y">
<ProgressDescription label="Small progress" value={60} size="sm" color="blue" />
<ProgressDescription label="Default progress" value={70} color="green" />
+8 -8
View File
@@ -16,7 +16,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Basic</CardTitle>
<CardSubtitle>A list of <code>items</code> is enough for a plain control.</CardSubtitle>
<CardSubtitle>A <code>nav-segmented</code> with text <code>nav-link</code> items is enough for a plain control.</CardSubtitle>
<NavSegmented items={['1', '2', '3', '4']} />
</CardBody>
</Card>
@@ -34,7 +34,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Icons only</CardTitle>
<CardSubtitle>Pass <code>icons</code> without <code>items</code> for an icon-only control.</CardSubtitle>
<CardSubtitle>Put only an icon in each <code>nav-link</code> for an icon-only control.</CardSubtitle>
<NavSegmented icons={['home', 'star', 'clock', 'ghost', 'bold', 'italic', 'underline']} />
</CardBody>
</Card>
@@ -43,7 +43,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>With icons</CardTitle>
<CardSubtitle>Combine <code>items</code> and <code>icons</code> to label each option.</CardSubtitle>
<CardSubtitle>Combine an icon and text in each <code>nav-link</code> to label each option.</CardSubtitle>
<NavSegmented items={['List', 'Kanban', 'Calendar', 'Files']} icons={['list', 'layout', 'calendar', 'files']} />
</CardBody>
</Card>
@@ -52,7 +52,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Vertical</CardTitle>
<CardSubtitle>Add <code>vertical</code> to stack the options in a column.</CardSubtitle>
<CardSubtitle>Add <code>nav-segmented-vertical</code> to stack the options in a column.</CardSubtitle>
<NavSegmented items={['List', 'Kanban', 'Calendar', 'Files']} icons={['list', 'layout', 'calendar', 'files']} vertical />
</CardBody>
</Card>
@@ -61,7 +61,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Vertical with disabled items</CardTitle>
<CardSubtitle>Pass indexes to <code>disabled</code> to make specific options unselectable.</CardSubtitle>
<CardSubtitle>Add <code>disabled</code> to a <code>nav-link</code> to make that option unselectable.</CardSubtitle>
<NavSegmented icons={['home', 'star', 'clock', 'ghost', 'bold', 'italic', 'underline']} vertical disabled={[3]} />
</CardBody>
</Card>
@@ -88,7 +88,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Independent group</CardTitle>
<CardSubtitle>Set a unique <code>name</code> so this control doesn't share selection state with others using the same items.</CardSubtitle>
<CardSubtitle>Use radio inputs with a unique <code>name</code> so this control doesn't share selection state with others using the same items.</CardSubtitle>
<NavSegmented items={['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']} name="checkbox" />
</CardBody>
</Card>
@@ -97,7 +97,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Full width</CardTitle>
<CardSubtitle>Add <code>block</code> to stretch the control across its container.</CardSubtitle>
<CardSubtitle>Add <code>w-100</code> to stretch the control across its container.</CardSubtitle>
<NavSegmented items={['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']} block={true} />
</CardBody>
</Card>
@@ -118,7 +118,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Sizes</CardTitle>
<CardSubtitle>Set <code>size</code> to <code>sm</code> or <code>lg</code> — with text, icons, or both.</CardSubtitle>
<CardSubtitle>Add <code>nav-sm</code> or <code>nav-lg</code> to change the size — with text, icons, or both.</CardSubtitle>
<div class="space-y">
<div><NavSegmented items={['List', 'Kanban', 'Calendar', 'Files']} disabled={[3, 5]} size="sm" /></div>
<div><NavSegmented items={['List', 'Kanban', 'Calendar', 'Files']} disabled={[3, 5]} /></div>
+3 -3
View File
@@ -25,7 +25,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Icons</CardTitle>
<CardSubtitle>Swap the star for any icon with the <code>icon</code> prop.</CardSubtitle>
<CardSubtitle>Swap the star for any icon by passing a different SVG to the <code>stars</code> option.</CardSubtitle>
<div class="space-y">
<Rating id="icon-star" value={4} />
<Rating id="icon-heart" icon="heart" value={4} color="red" />
@@ -39,7 +39,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Sizes</CardTitle>
<CardSubtitle>Match the rating to surrounding text with <code>size</code>.</CardSubtitle>
<CardSubtitle>Match the rating to surrounding text with an <code>icon-sm</code> or <code>icon-lg</code> class on the star.</CardSubtitle>
<div class="space-y">
<Rating id="size-sm" value={3} size="sm" />
<Rating id="size-md" value={3} size="md" />
@@ -52,7 +52,7 @@ import DocsLink from '@ui/DocsLink.astro'
<Card>
<CardBody>
<CardTitle>Colors</CardTitle>
<CardSubtitle>Set <code>color</code> to any theme or extended color to match the rest of the page.</CardSubtitle>
<CardSubtitle>Color the star with a <code>text-*</code> class, any theme or extended color, to match the rest of the page.</CardSubtitle>
<div class="space-y">
<Rating id="color-default" value={3} />
<Rating id="color-primary" color="primary" value={3} />
+1 -1
View File
@@ -35,7 +35,7 @@ import DocsLink from '@ui/DocsLink.astro'
<CardHeader>
<div>
<CardTitle>Striped rows</CardTitle>
<CardSubtitle>Alternate row shading with <code>striped</code> to make wide tables easier to scan.</CardSubtitle>
<CardSubtitle>Alternate row shading with <code>table-striped</code> to make wide tables easier to scan.</CardSubtitle>
</div>
</CardHeader>
<Table card={true} striped={true} offset={5} />
+2 -2
View File
@@ -101,7 +101,7 @@ const colors = Object.values(siteData.colors) as { class: string; title: string
<Card>
<CardBody>
<CardTitle>Selectable tags</CardTitle>
<CardSubtitle>Add <code>checkbox</code> to let users toggle a tag on and off.</CardSubtitle>
<CardSubtitle>Add a <code>tag-check</code> checkbox to let users toggle a tag on and off.</CardSubtitle>
<TagList>
{range(1, 6).map((i) => <Tag text={`Label ${i}`} checkbox={true} />)}
{range(7, 12).map((i) => <Tag text={`Label ${i}`} checkbox={true} checked={true} />)}
@@ -114,7 +114,7 @@ const colors = Object.values(siteData.colors) as { class: string; title: string
<Card>
<CardBody>
<CardTitle>Tags with badge</CardTitle>
<CardSubtitle>Add a <code>badge</code> count to the end of the tag.</CardSubtitle>
<CardSubtitle>Add a <code>tag-badge</code> count to the end of the tag.</CardSubtitle>
<TagList>
{range(1, 12).map((i) => <Tag text="Label" badge={i} />)}
</TagList>
+3 -2
View File
@@ -10,8 +10,9 @@ interface Props {
const { title, type = 'outline' } = Astro.props
// Dev preview limits icons to 20 (see BaseLayout).
const limit = 20
// The dev server renders only the first 20 icons so the page stays fast;
// `astro build` (the zip package and production site) renders all of them.
const limit = import.meta.env.DEV ? 20 : Infinity
const entries = Object.entries(icons as Record<string, { svg?: Record<string, string | null> }>)
.filter(([, icon]) => icon.svg?.[type])
+8 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { formatCommitDate, formatLongDate, formatUtcTimestamp, toUnixSeconds } from './date-format'
import { formatCommitDate, formatShortDate, formatLongDate, formatUtcTimestamp, toUnixSeconds } from './date-format'
describe('formatCommitDate', () => {
it('reformats a full timestamp to "D Mon YYYY"', () => {
@@ -36,3 +36,10 @@ describe('formatUtcTimestamp', () => {
expect(formatUtcTimestamp(date)).toBe('2025-11-28 08:05 +0000')
})
})
describe('formatShortDate', () => {
it('formats the UTC day and short month', () => {
expect(formatShortDate(new Date('2025-08-27T11:49:00Z'))).toBe('27 Aug')
expect(formatShortDate(new Date('2025-01-01T00:30:00Z'))).toBe('1 Jan')
})
})
+6
View File
@@ -1,6 +1,7 @@
// 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
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). */
@@ -13,6 +14,11 @@ export function formatLongDate(date: Date): string {
return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate()).padStart(2, '0')}, ${date.getUTCFullYear()}`
}
/** Formats a date as "D Mon" using its UTC components, e.g. "27 Aug". */
export function formatShortDate(date: Date): string {
return `${date.getUTCDate()} ${SHORT_MONTHS[date.getUTCMonth()]}`
}
/** Reformats a commit-style timestamp ("Thu Nov 28 08:48:33 2025 +0100") to "28 Nov 2025". */
export function formatCommitDate(date: string): string {
const m = date.match(/^\w+ (\w+) (\d+) [\d:]+ (\d+)/)
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { extractMarkdownSources, markdownSource, markdownTable } from './markdown-source'
describe('markdownSource', () => {
it('wraps the markdown in a named comment', () => {
expect(markdownSource('Colors', '| a |\n')).toBe('<!--MARKDOWN:Colors-->\n| a |\n<!--/MARKDOWN-->')
})
it('keeps a "-->" in the markdown from closing the comment', () => {
const html = markdownSource('Arrows', 'a --> b')
expect(html.indexOf('-->')).toBe(html.indexOf('<!--MARKDOWN:Arrows-->') + '<!--MARKDOWN:Arrows'.length)
expect(extractMarkdownSources(html).get('Arrows')).toEqual(['a --> b'])
})
})
describe('extractMarkdownSources', () => {
it('collects sources per component name in document order', () => {
const html = `<p>x</p>${markdownSource('Colors', 'first')}<div>${markdownSource('Flags', 'flags')}</div>${markdownSource('Colors', 'second')}`
const sources = extractMarkdownSources(html)
expect(sources.get('Colors')).toEqual(['first', 'second'])
expect(sources.get('Flags')).toEqual(['flags'])
})
it('returns an empty map for a page without sources', () => {
expect(extractMarkdownSources('<p>nothing</p>').size).toBe(0)
})
})
describe('markdownTable', () => {
it('renders a header row, a separator and the rows', () => {
expect(markdownTable(['Name', 'Class'], [['Blue', '`.bg-blue`']])).toBe('| Name | Class |\n| --- | --- |\n| Blue | `.bg-blue` |')
})
it('escapes pipes inside cells', () => {
expect(markdownTable(['a'], [['x | y']])).toContain('| x \\| y |')
})
})
+34
View File
@@ -0,0 +1,34 @@
// A docs component whose output no MDX-to-markdown conversion could rebuild
// (a colour palette, a table built from a data file) can ship its own markdown
// next to its html, inside an html comment. docs/lib/llms.ts reads those
// comments out of the rendered page and puts each one where the component
// sits in the MDX source, so the .md mirror keeps the content instead of a gap.
// The component renders the comment only for that mirror render (it checks
// Astro.locals.markdownMirror), so the html a browser gets never carries it.
const OPEN = '<!--MARKDOWN:'
const CLOSE = '<!--/MARKDOWN-->'
const SOURCE_PATTERN = /<!--MARKDOWN:([A-Za-z]+)-->\n?([\s\S]*?)\n?<!--\/MARKDOWN-->/g
/** The comment a component renders; `name` must match the tag used in the MDX (`<Colors />` → "Colors"). */
export function markdownSource(name: string, markdown: string): string {
// "-->" inside the markdown would end the comment early
return `${OPEN}${name}-->\n${markdown.trim().replaceAll('-->', '--&gt;')}\n${CLOSE}`
}
/** Every markdown source in a rendered page, per component name, in document order. */
export function extractMarkdownSources(html: string): Map<string, string[]> {
const sources = new Map<string, string[]>()
for (const [, name, markdown] of html.matchAll(SOURCE_PATTERN)) {
const list = sources.get(name!) ?? []
list.push(markdown!.replaceAll('--&gt;', '-->'))
sources.set(name!, list)
}
return sources
}
/** A GitHub-flavoured markdown table; pipes inside cells are escaped. */
export function markdownTable(headers: string[], rows: string[][]): string {
const line = (cells: string[]) => `| ${cells.map((cell) => cell.replaceAll('|', '\\|')).join(' | ')} |`
return [line(headers), line(headers.map(() => '---')), ...rows.map(line)].join('\n')
}