mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Merge branch 'dev' of https://github.com/tabler/tabler into dev-dm-sans
This commit is contained in:
commit
5bce29ce67
1224 files changed
+29669
-30707
No files matched your search
@@ -31,7 +31,7 @@ Consult these guides before working on related tasks:
|
||||
|
||||
## Writing documentation pages
|
||||
|
||||
Pages live in `pages/**/*.mdx`. Use the flat convention: leaf pages are `foo.mdx`; only pages with sub-pages use `foo/index.mdx` (both render as `/foo/`).
|
||||
Pages live in `content/**/*.mdx` — the `docs` content collection, rendered by `pages/[...slug].astro`. Use the flat convention: leaf pages are `foo.mdx`; only pages with sub-pages use `foo/index.mdx` (both render as `/foo/`).
|
||||
|
||||
- Write in simple English: short sentences, common words, direct verbs (`Use`, `Add`, `Set`).
|
||||
- Component docs use singular naming in frontmatter and filename: `title: Card`, `card.mdx` (not `cards.mdx`).
|
||||
@@ -39,18 +39,19 @@ Pages live in `pages/**/*.mdx`. Use the flat convention: leaf pages are `foo.mdx
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Static YAML only. Required keys plus the layout:
|
||||
Static YAML only, validated by the collection schema in `docs/content.config.ts` — an unknown or mistyped key fails the build. Required keys:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Alert
|
||||
summary: Short one-sentence summary of what this component does.
|
||||
description: Practical description of when and why to use it.
|
||||
layout: '@shared/layouts/DocsMdxLayout.astro'
|
||||
---
|
||||
```
|
||||
|
||||
Optional: `bootstrapLink: components/alerts/` when the component extends a Bootstrap component.
|
||||
No `layout:` key — the route picks the layout.
|
||||
|
||||
Optional keys: `seoTitle`, `seoDescription`, `icon`, `order`, `related`, `docs-libs`, `css-plugins`, `hide-pagination`, `added-in`. Anything else is rejected.
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
+35
-2
@@ -13,11 +13,33 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
|
||||
- `core/` — `@tabler/core`: SCSS sources (`core/scss/`), JS, the distributable framework.
|
||||
- `preview/` — `@tabler/preview`: demo site built with Astro (pages in `preview/pages/*.astro`).
|
||||
- `docs/` — `@tabler/docs`: documentation site built with Astro (pages in `docs/pages/**/*.mdx`); docs-specific rules: `.agents/rules/docs.mdc`.
|
||||
- `docs/` — `@tabler/docs`: documentation site built with Astro (content in `docs/content/**/*.mdx`); docs-specific rules: `.agents/rules/docs.mdc`.
|
||||
- `shared/` — Astro components (`ui/`, `components/`, `layouts/`, `lib/`) shared by `preview` and `docs` via `@shared`; JSON data in `data/` via `@data`; plus `static/`.
|
||||
- `docs/components/` — docs-only Astro components (`Example`, menu, …) via `@components`.
|
||||
- Package manager: pnpm workspaces + turbo. Node.js >= 22.12.
|
||||
|
||||
## Skills
|
||||
|
||||
Task-specific instructions live in `.agents/skills/<name>/SKILL.md` — read the matching one before starting, it is the source of truth for that area.
|
||||
|
||||
| Skill | Use it when |
|
||||
| --- | --- |
|
||||
| `ui-component` | creating or extending a component in `shared/ui/` or `shared/components/` |
|
||||
| `astro-scripts` | a component or page needs client-side JavaScript |
|
||||
| `page-layouts` | choosing or changing the page chrome (`shared/layouts/`) |
|
||||
| `shared-lib` | moving logic into `shared/lib/` and testing it |
|
||||
| `core-scss` | writing or changing styles in `core/scss/` |
|
||||
| `core-js` | working on the framework JavaScript in `core/js/` |
|
||||
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
||||
| `write-docs` | writing or updating a page in `docs/content/` |
|
||||
| `class-reference` | a docs page needs its `classnames` table added or refreshed |
|
||||
| `navigation` | menus, page URLs and docs redirects |
|
||||
| `astro-dev` | starting a dev server and verifying a change in the browser |
|
||||
| `html-diff` | proving a refactor changes no rendered HTML |
|
||||
| `build-pipeline` | assets missing, stale or growing; editing `.build/` or the asset manifests |
|
||||
| `generate-changeset` | writing a changeset (always — never hand-write the file) |
|
||||
| `mr-description` | drafting an MR/PR title and description |
|
||||
|
||||
## Language
|
||||
|
||||
- Write all repository content in English: code, comments, documentation, commit messages, PR titles and descriptions.
|
||||
@@ -28,6 +50,10 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
### Astro pages and components
|
||||
|
||||
- Pages are Astro components; reusable markup lives in `shared/components/`.
|
||||
- Building or changing a shared component: follow the `ui-component` skill (Props interface, prop names, class composition, slots).
|
||||
- Client-side JavaScript: follow the `astro-scripts` skill. Always `<script is:inline>` inside `<CaptureScript>` — never a bundled script.
|
||||
- Page chrome (header, navbar, sidebar, wrappers) comes from a layout in `shared/layouts/` — see the `page-layouts` skill. Pages never hand-roll it.
|
||||
- Logic beyond assembling classes belongs in `shared/lib/*.ts` with a sibling `*.test.ts` — see the `shared-lib` skill.
|
||||
- Icons: use the shared component — `<Icon name="home" />` (import from `@ui/Icon.astro`).
|
||||
- Links between preview pages are relative: `./job-listing.html` (root-level pages).
|
||||
- Boolean HTML attributes in Astro: `selected={true}` renders a bare attribute, but some attributes (e.g. `multiple`) render `="true"` — use `multiple ? '' : undefined` when a bare attribute is required.
|
||||
@@ -48,8 +74,14 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
- Use semantic HTML elements and proper heading hierarchy.
|
||||
- Include ARIA labels (e.g. `aria-label` for icon-only buttons) and alt text for images.
|
||||
|
||||
### Verifying changes
|
||||
|
||||
- Start dev servers and check pages in the browser with the `astro-dev` skill (preview 3000, docs 3010); never build while a dev server is running.
|
||||
- For a change that should not alter rendered HTML, prove it with the `html-diff` skill.
|
||||
|
||||
## SCSS guidelines
|
||||
|
||||
- Follow the `core-scss` skill for anything under `core/scss/`: custom-property pattern, the build-time `--tblr-` prefix, dark mode, RTL, sass-true tests and the lint/size gates.
|
||||
- Use semantic class names that describe purpose, not appearance.
|
||||
- Keep Bootstrap-compatible class naming conventions.
|
||||
- Group related styles together with clear comments; keep consistent spacing and indentation.
|
||||
@@ -84,8 +116,9 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
|
||||
### Changesets
|
||||
|
||||
- **Always use the `generate-changeset` skill** (`.agents/skills/generate-changeset/SKILL.md`) to write a changeset — both when asked for one and on your own before committing or opening a PR that touches `core/`, `preview/`, or `docs/`. Do not hand-write the file.
|
||||
- Each change gets a separate file in `.changeset/` with a descriptive kebab-case filename (e.g. `progress-sizes.md`).
|
||||
- One sentence per changeset, starting with an action verb (`Added`, `Updated`, `Fixed`, `Removed`), with backticks for code elements (`.btn-ghost`, `stroke-width`, `1.5`, `arrow-up`).
|
||||
- One sentence per changeset, **130 characters or fewer** (hard ceiling 160, only when a longer list is the actual content), starting with an action verb (`Added`, `Updated`, `Fixed`, `Removed`), with backticks for code elements (`.btn-ghost`, `stroke-width`, `1.5`, `arrow-up`). A changeset is one changelog line — keep the reasons, implementation notes, and affected-file lists in the PR description. Always name a concrete component, class, variable, page, or dependency; "Fixed mixed declarations in SCSS" tells a reader nothing.
|
||||
- Version bumps: **major** = breaking changes; **minor** = new features, components, or pages; **patch** = bug fixes, small improvements, style/accessibility tweaks.
|
||||
- Packages: `"@tabler/core"` for SCSS/JS/classes, `"@tabler/preview"` for demo pages, `"@tabler/docs"` for documentation; list multiple packages when the change spans areas.
|
||||
- Example:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: astro-dev
|
||||
description: >-
|
||||
Start a Tabler Astro dev server and verify a change in the browser. Use
|
||||
whenever a change to `preview/`, `docs/`, `shared/` or `core/` has to be
|
||||
seen running — before handing a page or component back — and whenever the
|
||||
dev server behaves oddly: stale CSS/JS, assets that vanish, a page that will
|
||||
not reload. Covers which server to start on which port, background mode, the
|
||||
rule against building while a dev server runs, and the verification loop.
|
||||
---
|
||||
|
||||
# Run and verify the dev servers
|
||||
|
||||
Never ask the user to check a page manually. Start the server, look at the page, and show what you found.
|
||||
|
||||
## 1. Which server
|
||||
|
||||
| Package | Command | Port | Serves |
|
||||
| --- | --- | --- | --- |
|
||||
| `@tabler/preview` | `pnpm --dir preview run dev` | 3000 | demo pages (`preview/pages/*.astro`) |
|
||||
| `@tabler/docs` | `pnpm --dir docs run dev` | 3010 | documentation (`docs/content/**/*.mdx`) |
|
||||
| `@tabler/screenshots` | `pnpm --dir screenshots run dev` | 3020 | screenshot capture pages |
|
||||
|
||||
`preview_start` returns a `tabId` — pass it to every later `navigate` / `read_page` / `computer` call so they act on that tab and not on whatever is fronted. The same entries exist in `.claude/launch.json` (`preview`, `docs`, `screenshots`) — start them with `preview_start` so the browser pane opens on the right port, and use `preview-attach` when a server is already running. `pnpm dev` at the repo root starts everything through turbo; only do that when the change spans packages, because the persistent tasks make the output hard to read.
|
||||
|
||||
A `shared/` change shows up in both preview and docs. Verify in the package the user asked about, and open the other one too when the component is used there.
|
||||
|
||||
## 2. Full dev vs. astro-only
|
||||
|
||||
Each package's `dev` script is `astro dev` plus its asset watchers (`concurrently`), and turbo runs `dev-prepare` first so `tmp-assets/` and `public/` exist:
|
||||
|
||||
- **SCSS or `core/` touched → use the full `dev` script.** `astro dev` alone will not rebuild the stylesheets.
|
||||
- **Only `.astro` / `.mdx` / `shared/lib` touched → `astro dev --background` inside the package is enough** and faster, provided the assets have been built before. Manage it with `astro dev status`, `astro dev logs`, `astro dev stop`; the log file is `.astro/dev.log` in the package. Add `--force` to replace a server that is already running.
|
||||
|
||||
## 3. Do not build while a dev server is running
|
||||
|
||||
`pnpm build` (or a per-package build) run next to a live dev server fights the watchers: the build's asset copy overwrites what the watcher just wrote into `public/`, and the two file watchers together can exhaust file handles (`EMFILE`). Verify through the running server and HMR instead. If a build is genuinely needed, stop the dev server first.
|
||||
|
||||
## 4. The verification loop
|
||||
|
||||
1. `preview_start` with the package's config, then open the page (`/badges` in dev; the built file is `/badges.html`).
|
||||
2. `read_console_messages` — errors first, then warnings.
|
||||
3. `read_page` or `curl` for structure; assert instead of eyeballing when the page is generated from data:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/badges | grep -o '<h[1-6]' | sort | uniq -c
|
||||
```
|
||||
|
||||
4. `computer` clicks/typing for anything interactive, then read the state back.
|
||||
5. `resize_window` for responsive or dark-mode work; `colorScheme` for `data-bs-theme`.
|
||||
6. Screenshot the result for the user when the change is visual.
|
||||
|
||||
## 5. When the page looks stale or unstyled
|
||||
|
||||
In order:
|
||||
|
||||
- **CSS did not change** — the SCSS watcher is not running. Restart with the full `dev` script rather than `astro dev`.
|
||||
- **Assets 404 after a restart** — `public/preview` (or `docs/public`) was wiped by a build or a `clean`. Run `pnpm --dir <pkg> run dev-prepare`, then start dev again.
|
||||
- **A `public/` file changed but the browser did not reload** — Astro does not watch `public/`; the `copyAssets` integration reloads the listed dirs, everything else needs a manual refresh.
|
||||
- **`shared/` change not picked up** — the file is aliased, not watched through `node_modules`; confirm the import uses `@shared`/`@ui`, not a path into `node_modules`.
|
||||
- Still wrong: `pnpm --dir <pkg> run clean`, then `dev-prepare`, then `dev`.
|
||||
|
||||
## 6. Gates before handing work back
|
||||
|
||||
Run at the repo root and read the **full** output — a tail hides TypeScript errors:
|
||||
|
||||
```bash
|
||||
pnpm run type-check && pnpm run format-prettier && pnpm run lint
|
||||
```
|
||||
|
||||
`type-check` is `astro check` per package, so it covers `.astro` frontmatter and templates. For a markup refactor that should not change output at all, also run the `html-diff` skill.
|
||||
|
||||
## 7. Checklist
|
||||
|
||||
- [ ] Right package started, on its own port
|
||||
- [ ] Full `dev` script when SCSS or `core/` is involved
|
||||
- [ ] No build started while a dev server was running
|
||||
- [ ] Page opened, console clean, interaction exercised
|
||||
- [ ] Screenshot for visual changes
|
||||
- [ ] Repo-level `type-check` / `format-prettier` / `lint` clean, full output read
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: astro-scripts
|
||||
description: >-
|
||||
Add or fix client-side JavaScript in a Tabler Astro component or page —
|
||||
plugin initialisation, Bootstrap wiring, anything that needs a `<script>` in
|
||||
the rendered HTML. Use whenever a component needs browser behaviour, a
|
||||
third-party library has to be initialised, an inline script does not run, or
|
||||
a script has to be moved out of a page. Covers `CaptureScript` /
|
||||
`PageScripts`, `script is:inline define:vars`, the IIFE trap, init timing,
|
||||
per-instance registries, and when a script must be assembled as a string.
|
||||
---
|
||||
|
||||
# Client-side scripts in Astro components
|
||||
|
||||
The preview HTML is the product: users copy a page out of `preview/dist` and expect the scripts in it to be readable and self-contained. That single fact drives every rule below.
|
||||
|
||||
## 1. Never let Astro bundle a script
|
||||
|
||||
Always `<script is:inline>`. A plain `<script>` is bundled, hashed, and moved into a module chunk — invisible in the copied HTML — and a plain `<style>` makes Astro stamp `data-astro-cid-*` on every element of the component.
|
||||
|
||||
## 2. Where the script ends up
|
||||
|
||||
Scripts render at the **end of `<body>`**, not where the component sits, so they run after their markup and after the vendor libraries in `<head>`. The route:
|
||||
|
||||
```text
|
||||
<CaptureScript> → addPageScript() → page-scripts registry → <PageScripts />
|
||||
```
|
||||
|
||||
- `shared/components/CaptureScript.astro` captures its slot and registers it.
|
||||
- `shared/layouts/BaseLayout.astro` and `docs/layouts/DocsLayout.astro` drain the registry through `<PageScripts />`.
|
||||
- Registration is **synchronous** in `CaptureScript`'s frontmatter on purpose: Astro renders siblings concurrently, so a script registered after an `await` can miss the drain. Do not make `CaptureScript` async.
|
||||
- The registry does not dedupe. Two instances of a component emit two scripts — write them so that is harmless (see section 6).
|
||||
|
||||
## 3. The standard snippet
|
||||
|
||||
```astro
|
||||
---
|
||||
import CaptureScript from '../components/CaptureScript.astro'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
}
|
||||
|
||||
const { id } = Astro.props
|
||||
const selector = `#wysiwyg-${id}`
|
||||
---
|
||||
|
||||
<textarea id={`wysiwyg-${id}`}></textarea>
|
||||
<CaptureScript>
|
||||
<!-- BEGIN WYSIWYG -->
|
||||
<script is:inline define:vars={{ selector }}>
|
||||
function initWysiwyg() {
|
||||
hugeRTE.init({ selector, height: 300 })
|
||||
}
|
||||
|
||||
document.readyState !== 'loading' ? initWysiwyg() : document.addEventListener('DOMContentLoaded', initWysiwyg, { once: true })
|
||||
</script>
|
||||
<!-- END WYSIWYG -->
|
||||
</CaptureScript>
|
||||
```
|
||||
|
||||
- `BEGIN … / END …` comments mark the block in the copied HTML. Use the component name in caps.
|
||||
- A named `init…` function plus the `readyState` guard, never a bare call: the script may be parsed after `DOMContentLoaded` has already fired.
|
||||
- The component takes a required `id` prop and derives ids and selectors from it in the frontmatter. Never query by class or by tag — a page renders many instances.
|
||||
|
||||
## 4. Passing data with `define:vars`
|
||||
|
||||
`define:vars` JSON-serialises the values and declares them as consts at the top of the script. It carries data only: no functions, no DOM nodes, no closures over frontmatter helpers. Pass config objects whole (`chartOptions`, `markers`, `sortKeys`) instead of interpolating them into a template string.
|
||||
|
||||
Two known snags:
|
||||
|
||||
- **Astro wraps a `define:vars` script in an IIFE.** Anything the markup calls (`onclick="setPageListItems(this)"`) must be assigned to `window` explicitly — see `shared/components/demo/AdvancedTable.astro`. If several instances define the same global, make it stateless and resolve the instance from the clicked element.
|
||||
- The type checker does not see `define:vars` bindings used inside template literals. `shared/components/marketing/hero/Side.astro` carries a `// @ts-nocheck` for exactly that; copy the comment with its explanation rather than restructuring the script.
|
||||
|
||||
## 5. When the script has to be a string
|
||||
|
||||
If caller-supplied JS must land *inside* a function body, `define:vars` cannot do it — it injects variables, not code. Then build the script as a template string in the frontmatter and render it with `<Fragment set:html={script} />` inside `<CaptureScript>` (see `shared/ui/Signature.astro`). Serialise every interpolated value with `JSON.stringify` and keep raw-JS props documented as an escape hatch.
|
||||
|
||||
## 6. Instance registries
|
||||
|
||||
Plugin instances go into a namespaced global keyed by id, so demo pages, docs examples, and the browser console can reach them:
|
||||
|
||||
```js
|
||||
window.tabler_rating ??= {}
|
||||
window.tabler_rating[ratingKey] = new StarRating(ratingSelector, { … })
|
||||
```
|
||||
|
||||
`window.tabler_list`, `window.tabler_rating`, … follow the same shape. Never store an instance in a bare global.
|
||||
|
||||
## 7. Loading the library
|
||||
|
||||
The library itself is not imported — it is a global loaded from `libs.json`:
|
||||
|
||||
1. Check `core/libs.json` for the plugin (`npm`, `js`, `css`, `head`); add an entry if it is missing.
|
||||
2. Preview pages declare it on the layout: `pageLibs={['nouislider', 'tom-select']}` on `DefaultLayout` / `BaseLayout`.
|
||||
3. Docs pages declare it in front matter: `docs-libs: apexcharts` (and `css-plugins:` for extra `tabler-*.css`).
|
||||
|
||||
A component that silently assumes a global is a broken page whenever someone forgets the prop — say which lib it needs in a frontmatter comment and in the docs page.
|
||||
|
||||
## 8. Prettier caveat
|
||||
|
||||
`prettier-plugin-astro` cannot parse HTML comments or `<script>` tags inside a JSX expression (`{cond && (<Fragment><!-- … --><script>…)}`). Files that do this on purpose are listed at the bottom of `.prettierignore`. Prefer a plain (non-conditional) `<CaptureScript>` block so the file stays formattable; only add a new entry to `.prettierignore` when the conditional really is required, and keep it in the commented group.
|
||||
|
||||
## 9. Verify
|
||||
|
||||
An inline script that throws leaves the page looking fine but dead. Never hand one over unchecked:
|
||||
|
||||
1. Start the dev server and open the page (`astro-dev` skill).
|
||||
2. `read_console_messages` — zero errors.
|
||||
3. Interact with the component (click, type) and confirm the behaviour, or read the instance back from `window.tabler_*` with `javascript_tool`.
|
||||
|
||||
## 10. Checklist
|
||||
|
||||
- [ ] `<script is:inline>` inside `<CaptureScript>`, with `BEGIN`/`END` comments
|
||||
- [ ] Required `id` prop; selectors derived from it, no class-wide queries
|
||||
- [ ] Data passed via `define:vars`, not string interpolation
|
||||
- [ ] `init…()` + `readyState` guard
|
||||
- [ ] Globals the markup needs assigned to `window` (IIFE trap)
|
||||
- [ ] Instance stored in a `window.tabler_*` registry keyed by id
|
||||
- [ ] Library present in `core/libs.json` and declared via `pageLibs` / `docs-libs`
|
||||
- [ ] Two instances on one page still work
|
||||
- [ ] Console clean in the browser; `pnpm run format-prettier` and `pnpm run type-check` clean
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: build-pipeline
|
||||
description: >-
|
||||
Understand or repair the Tabler asset pipeline — `core/dist`, `tmp-assets/`,
|
||||
the generated `public/` directories, the `copy-assets` Astro integration,
|
||||
`build-css.ts`, the vite/terser JS builds and the turbo dev graph. Use when
|
||||
assets are missing, stale, duplicated or growing between builds, when a
|
||||
watcher's output disappears after a restart, when a page 404s on
|
||||
`/dist/css/tabler.css`, and before editing anything under `.build/`, a
|
||||
package's asset manifest in `astro.config.mjs`, or the dev/build scripts.
|
||||
---
|
||||
|
||||
# The asset pipeline
|
||||
|
||||
Astro only renders pages. Every stylesheet, script, font and image on a page arrives through a separate pipeline, and almost every "weird assets" bug is a directory being written by the wrong step.
|
||||
|
||||
## 1. Who writes what
|
||||
|
||||
| Directory | Written by | Notes |
|
||||
| --- | --- | --- |
|
||||
| `core/dist/` | `@tabler/core` (`css-build`, `js-build`, `copy`) | the framework: css, js, fonts, img, vendored libs |
|
||||
| `preview/tmp-assets/` | `preview` `assets` (`build-css.ts` + vite/terser) | demo css/js, isolated from Astro's output |
|
||||
| `docs/tmp-assets/css/` | `docs` `css` / `watch-css` (sass) | docs stylesheets |
|
||||
| `<pkg>/public/` | **`copy-assets` only** | fully generated, wiped and rebuilt on every Astro start |
|
||||
| `<pkg>/dist/` | `astro build` | the shipped site |
|
||||
|
||||
`public/`, `tmp-assets/` and `dist/` are all git-ignored per package. Never hand-edit a file in any of them, and never commit one.
|
||||
|
||||
## 2. `copy-assets` (`.build/copy-assets.ts`)
|
||||
|
||||
An Astro integration, configured per package in `astro.config.mjs`:
|
||||
|
||||
- On `astro:config:done` it **deletes `public/`** and rebuilds it from the `copies` manifest (core dist, the package's `tmp-assets`, `shared/static`, favicons). Running it twice must never accumulate content — that is why it starts from a clean directory.
|
||||
- It is skipped when the Astro command is `sync`, so `astro check` (and CI's type-check job) does not need built workspace assets.
|
||||
- In dev it watches `syncDirs` and copies changed files into `public/`, then sends one coalesced `full-reload` (250 ms window, `.map` files ride along with their source file). `reloadDirs` are directories a watcher already writes into directly — those only get the reload.
|
||||
- `allowDestinationFallback` keeps the existing copy when the source vanishes mid-copy — `@tabler/core` cleaning `dist/` while `turbo dev` starts the dependents.
|
||||
|
||||
**Rule for watchers:** because `public/` is wiped at startup, a watcher writing straight into it (preview's `watch-css` / `watch-js`) only works when `dev-prepare` has produced the same output into `tmp-assets` first, so the startup copy seeds it. The safer arrangement is docs': the watcher writes into `tmp-assets/css` and `copy-assets` syncs it to `public/css`. Prefer that for anything new.
|
||||
|
||||
## 3. Ordering
|
||||
|
||||
`turbo.json` encodes the graph: `@tabler/core#dev-prepare` → `@tabler/preview#dev-prepare` → the `dev` tasks (docs depends on both). `dev-prepare` is what guarantees `core/dist` and `preview/tmp-assets` exist before a dependent package's `copy-assets` runs. If you add a package or an asset dependency, add the edge here too — inside a package, ordering comes from Astro's own lifecycle, not from a pre-script.
|
||||
|
||||
## 4. Traps already paid for
|
||||
|
||||
Do not undo these; each has a comment at the site:
|
||||
|
||||
- **Build output must not be an Astro output dir.** `preview/.build/vite.config.mts` writes to `tmp-assets/js`, not inside `dist/`: `astro build` copies `public/` into `dist/`, so an `outDir` under `dist/` gets re-seeded and re-copied, growing without bound across builds.
|
||||
- **No leading-dot output path.** terser's CLI `--source-map` parser rejects a path segment starting with a dot (`.build/out/…`) with a bogus "not a supported option".
|
||||
- **One write per output file.** `build-css.ts` runs sass + postcss + clean-css in-process so each file is written once, fully processed; it also skips writes when the content is unchanged, to keep watchers quiet. Do not reintroduce a step that rewrites a finished file in place — that is what shifted every source-map mapping when the banner was added afterwards (#2766).
|
||||
- **Vendor copies are not pages.** `preview`'s `prettify-html` integration formats built HTML on `astro:build:done` but excludes `dist/preview/` and `dist/dist/`, which are asset copies and contain third-party HTML that breaks the parser.
|
||||
|
||||
## 5. Diagnosing
|
||||
|
||||
Work down this list; each step is cheap:
|
||||
|
||||
1. **404 on `/dist/css/tabler.css` or `/preview/css/demo.css`** — the source was never built. `pnpm --dir core run dev-prepare`, then `pnpm --dir preview run dev-prepare`.
|
||||
2. **Assets vanished after restarting dev** — something wrote into `public/` that `copy-assets` does not know about; it was wiped at startup. Add it to the manifest, or write it into `tmp-assets` instead.
|
||||
3. **CSS/JS edits do not appear** — the watcher is not running (started `astro dev` alone instead of the package's `dev` script), or its output dir is not in `syncDirs`/`reloadDirs`.
|
||||
4. **Stale content that survives a rebuild** — `pnpm --dir <pkg> run clean`, then `dev-prepare`.
|
||||
5. **`dist/` grows between identical builds** — an output dir is nested inside another copy step (see the first trap).
|
||||
6. **CI type-check fails on missing assets** — something made `copy-assets` run for the `sync` command; keep the early return.
|
||||
|
||||
Read the dev server's own log first: `copy-assets` logs `public/ rebuilt from workspace assets`, each fallback warning, and every reload with the file that caused it.
|
||||
|
||||
## 6. When you change the pipeline
|
||||
|
||||
- Verify **both** paths: `pnpm --dir <pkg> run dev` and a clean `pnpm --dir <pkg> run build`.
|
||||
- Verify **both** packages when the change is in `.build/` — preview and docs share those files, with different manifests.
|
||||
- Run the build twice in a row and compare `du -sh <pkg>/dist` — equal sizes are the accumulation check.
|
||||
- Confirm `git status` stays clean: nothing generated may become tracked.
|
||||
- Do not start a build while a dev server is running (`astro-dev` skill).
|
||||
|
||||
## 7. Checklist
|
||||
|
||||
- [ ] Nothing writes into `public/` except `copy-assets`
|
||||
- [ ] New generated output lands in `tmp-assets/`, and is listed in the package's `copies` (and `syncDirs` when it changes during dev)
|
||||
- [ ] Cross-package ordering added to `turbo.json` if a new dependency appeared
|
||||
- [ ] Dev and clean build both verified, build run twice with stable output size
|
||||
- [ ] `git status` clean; `pnpm run type-check` clean without prebuilt assets
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: class-reference
|
||||
description: Write or update the `classnames` front matter that renders the class reference table on a Tabler docs page. Use whenever a component page needs its class list added, a component gains or loses a class in `core/scss/**`, or the user asks for a class reference, a class table, or a `classnames` block. Also consult it proactively after adding a class to an existing component, since the table is hand-written and will not notice on its own.
|
||||
---
|
||||
|
||||
# Class reference front matter
|
||||
|
||||
Every component page carries a `classnames` block in its front matter. `DocsClassnames.astro` renders it as a table at the end of the page, `docs/pages/llms.txt.ts` reprints it for agents, and `docs/content.config.ts` validates it. Editing the table means editing the front matter — there is no markdown table to touch.
|
||||
|
||||
## 1. Shape
|
||||
|
||||
```yaml
|
||||
classnames:
|
||||
component:
|
||||
- class: alert
|
||||
desc: Container element
|
||||
part:
|
||||
- class: alert-icon
|
||||
desc: Icon slot, sized and colored with the alert
|
||||
color:
|
||||
- class: alert-{color}
|
||||
desc: Any base color, for example alert-success or alert-blue
|
||||
```
|
||||
|
||||
The schema is strict (`docs/content.config.ts`), so a mistyped key fails the build. Each entry takes `class`, `desc`, and optionally `default: true`, which renders a "default" badge.
|
||||
|
||||
## 2. The eight types
|
||||
|
||||
Rendered in this order regardless of the order in the file, so group by meaning rather than by position:
|
||||
|
||||
| Type | Holds | Examples |
|
||||
| --- | --- | --- |
|
||||
| `component` | the class that makes the thing | `alert`, `btn`, `card` |
|
||||
| `part` | sub-elements it is built from | `card-header`, `alert-icon` |
|
||||
| `style` | surface treatment | `btn-outline`, `btn-ghost`, `alert-minor` |
|
||||
| `modifier` | shape or box changes | `btn-pill`, `btn-square`, `accordion-flush` |
|
||||
| `behavior` | state and motion | `btn-loading`, `icon-pulse`, `table-hover` |
|
||||
| `direction` | placement and orientation | `offcanvas-start`, `steps-vertical` |
|
||||
| `color` | color variants | `alert-{color}`, `btn-{color}` |
|
||||
| `size` | size variants | `btn-sm`, `avatar-{size}` |
|
||||
|
||||
The split between `style`, `modifier` and `behavior` follows daisyUI: how it looks, what shape it is, how it reacts.
|
||||
|
||||
## 3. Collapse generated families
|
||||
|
||||
Tabler generates colors, sizes, breakpoints and directions with loops. Listing each one turns a reference into a wall — `btn` alone compiles to 141 classes. Write one row with a brace placeholder and say what fills it:
|
||||
|
||||
```yaml
|
||||
- class: btn-{color}
|
||||
desc: Any base or social color, for example btn-primary or btn-facebook
|
||||
- class: table-mobile-{breakpoint}
|
||||
desc: Stacks rows into blocks below the breakpoint
|
||||
- class: switch-icon-slide-{direction}
|
||||
desc: Slides the icons; up, down, left, right, start or end
|
||||
```
|
||||
|
||||
Collapse when the family is mechanical and complete. Keep separate rows when the members differ in meaning — `btn-sm`, `btn-lg` and `btn-xl` are worth spelling out, `flag-country-{code}` is not (258 members).
|
||||
|
||||
## 4. Leave out
|
||||
|
||||
- Classes a script toggles rather than an author writes: `carousel-item-next`, `modal-backdrop`, `accordion-collapse`, `active`, `show`.
|
||||
- Bootstrap utilities that are not part of the component.
|
||||
- Classes that do not exist. This is the failure mode to guard against — see section 6.
|
||||
|
||||
## 5. Writing the description
|
||||
|
||||
Say what the class does, from the rule, not from the name:
|
||||
|
||||
- `alert-minor` — "Drops the tinted background, keeps a plain border", not "minor style".
|
||||
- `td-truncate` — "Cell whose content is cut with an ellipsis instead of stretching the table".
|
||||
|
||||
One line, no trailing period needed, simple English. Read the SCSS before writing:
|
||||
|
||||
```shell
|
||||
sed -n '/^\.alert-minor/,/}/p' core/scss/ui/_alerts.scss
|
||||
```
|
||||
|
||||
## 6. Check every class exists
|
||||
|
||||
Descriptions are hand-written, so a class can be invented or survive a rename. Three wrong entries were caught this way while the tables were first written — `trending` and `map` were never classes at all, and `.map` matched only a stray fragment in the compiled CSS.
|
||||
|
||||
Verify against the compiled stylesheet, and confirm the rule has a body rather than trusting a text match:
|
||||
|
||||
```shell
|
||||
python3 -c "
|
||||
import re
|
||||
css = open('core/dist/css/tabler.css').read()
|
||||
for c in ['alert-minor', 'alert-icon']:
|
||||
m = re.search(r'(^|\})\s*\.' + c + r'\s*\{([^}]*)\}', css)
|
||||
print(c, '->', ' '.join(m.group(2).split())[:70] if m else 'NOT A RULE')
|
||||
"
|
||||
```
|
||||
|
||||
Plugin classes live in their own sheets — `tabler-flags.css`, `tabler-socials.css`, `tabler-payments.css`, `tabler-vendors.css` — so read the right one.
|
||||
|
||||
Components with no class of their own are real: `trending` is built from `text-green` plus an arrow icon, and the map page frames an embed with `ratio`. Document what the page actually uses.
|
||||
|
||||
## 7. Also set `source`
|
||||
|
||||
Pages that carry `classnames` should carry `source` too — the repo-relative file that implements the component, rendered as the "Source code" link:
|
||||
|
||||
```yaml
|
||||
source: core/scss/ui/_alerts.scss
|
||||
```
|
||||
|
||||
Most map to `core/scss/ui/_<name>s.scss`. Exceptions worth knowing: `segmented-control` → `_segmented.scss`, `step` → `_steps.scss`, `star-rating` → `_stars.scss`, `divider` → `_type.scss`, `tab` → `_nav.scss`, `tooltip` → `bootstrap/_tooltip.scss`, `trending` → `shared/ui/Trending.astro`.
|
||||
|
||||
## 8. Finish
|
||||
|
||||
- Load the page and confirm the table renders and "Class reference" reaches the "On this page" rail.
|
||||
- Run `pnpm run lint` and `pnpm --dir docs run type-check`.
|
||||
- New pages also need a menu entry in `shared/data/docs.json` — see the write-docs skill.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: core-js
|
||||
description: >-
|
||||
Work on the framework's own JavaScript in `core/js/` — the `tabler.js` and
|
||||
`tabler-theme.js` bundles, the vendored Bootstrap component port, and their
|
||||
browser tests. Use when a Tabler behaviour needs adding or fixing (a
|
||||
`data-bs-toggle` handler, the theme switcher, a plugin initialiser), when a
|
||||
Bootstrap component's JS misbehaves, and before changing anything under
|
||||
`core/js/src/`. Not for demo-page scripts — that is the `astro-scripts`
|
||||
skill.
|
||||
---
|
||||
|
||||
# The framework JavaScript
|
||||
|
||||
`core/js` builds two shipped bundles. Everything here is public API and is bound by a size budget.
|
||||
|
||||
## 1. Two entry points
|
||||
|
||||
| Entry | Ships as | Holds |
|
||||
| --- | --- | --- |
|
||||
| `js/tabler.ts` | `tabler.js` / `.esm.js` (+ `.min`) | plugin initialisers, the Bootstrap components, the `tabler` helper namespace |
|
||||
| `js/tabler-theme.ts` | `tabler-theme.js` (+ variants) | the colour-mode/theme switcher only |
|
||||
|
||||
`tabler-theme.js` is loaded right after `<body>` and **not deferred**, so the chosen theme applies before the first paint. It stays tiny on purpose (bundlewatch: 1 kB raw, 800 B minified) — do not add anything to it that is not needed before paint.
|
||||
|
||||
## 2. `js/src/bootstrap/**` is a port, not our code
|
||||
|
||||
That directory is Bootstrap's JavaScript rewritten in TypeScript, MIT headers kept, `VERSION = '5.3.8'`. Treat it as vendored:
|
||||
|
||||
- Fix bugs the way upstream did, and keep the file's structure recognisable against `twbs/bootstrap`. Gratuitous restructuring makes the next upstream sync expensive.
|
||||
- `js/src/bootstrap.ts` is the single source of truth for what is exported and for the `bootstrap` namespace object.
|
||||
- Coverage is configured to measure exactly this directory (`js/src/bootstrap/**`), and `js/tests/unit/*.spec.ts` mirrors upstream's suite — a change here is expected to come with its test.
|
||||
|
||||
## 3. Tabler's own modules
|
||||
|
||||
Modules directly in `js/src/*.ts` (`autosize`, `countup`, `dropdown`, `input-mask`, `popover`, `sortable`, `switch-icon`, `tab`, `toast`, `tooltip`) are **side-effect initialisers**: importing them from `tabler.ts` wires the behaviour. The pattern is a `data-bs-toggle` query at import time:
|
||||
|
||||
```ts
|
||||
// js-docs-start switch-icon-init
|
||||
const switchesTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="switch-icon"]'))
|
||||
switchesTriggerList.map(function (switchTriggerEl: HTMLElement) {
|
||||
switchTriggerEl.addEventListener('click', (e: MouseEvent) => {
|
||||
const active = switchTriggerEl.classList.toggle('active')
|
||||
switchTriggerEl.setAttribute('aria-pressed', active ? 'true' : 'false')
|
||||
})
|
||||
})
|
||||
// js-docs-end switch-icon-init
|
||||
```
|
||||
|
||||
- The attribute API stays `data-bs-*`. Renaming it to `data-tblr-*` is a breaking change held for 2.0 — do not start it here.
|
||||
- Keep the accessible state in sync with the class (`aria-pressed`, `aria-expanded`), as above.
|
||||
- A new module must be imported from `js/tabler.ts`, or it never ships.
|
||||
- Shared helpers go to `js/src/tabler.ts` (`prefix`, `hexToRgba`, `getColor`), which reads theme values through `--tblr-*` custom properties.
|
||||
|
||||
## 4. Docs markers
|
||||
|
||||
`js-docs-start <name>` / `js-docs-end <name>` mark the snippet a documentation page shows for that behaviour (`docs/components/CodeDocs.astro`, `docs/lib/llms.ts`). Keep the marked region self-contained and copy-pasteable, and check who references the name before renaming it.
|
||||
|
||||
## 5. Tests
|
||||
|
||||
Tests run in a **real browser** — vitest browser mode with Playwright Chromium, not jsdom:
|
||||
|
||||
```bash
|
||||
pnpm --filter @tabler/core test:js
|
||||
pnpm --filter @tabler/core test:js:watch
|
||||
pnpm --filter @tabler/core test:js:coverage
|
||||
```
|
||||
|
||||
- `js/tests/unit/**/*.spec.ts` — behaviour, using `js/tests/helpers/fixture.ts` to mount markup.
|
||||
- `js/tests/visual/*.html` — pages for looking at a component by hand.
|
||||
- The first run needs Playwright's browser installed; if it fails on a missing binary, install it rather than switching the config to jsdom.
|
||||
|
||||
## 6. Build and budget
|
||||
|
||||
`vite` builds each entry as a library in `es` + `umd` (`BASE_NAME` selects the entry), then terser produces the `.min` variants with source maps. Sizes are enforced:
|
||||
|
||||
| File | Limit |
|
||||
| --- | --- |
|
||||
| `dist/js/tabler.js` | 64 kB |
|
||||
| `dist/js/tabler.min.js` | 48 kB |
|
||||
| `dist/js/tabler-theme.js` | 1 kB |
|
||||
|
||||
```bash
|
||||
pnpm --filter @tabler/core build
|
||||
pnpm run bundlewatch
|
||||
```
|
||||
|
||||
Prefer a small dependency-free implementation over pulling a package in: every kilobyte here lands on every Tabler page.
|
||||
|
||||
## 7. Checklist
|
||||
|
||||
- [ ] New module imported from `js/tabler.ts` (or deliberately kept out of the bundle)
|
||||
- [ ] `data-bs-*` attribute API unchanged; accessible state updated with the class
|
||||
- [ ] Bootstrap-port changes stay close to upstream and come with a `js/tests/unit` spec
|
||||
- [ ] `js-docs-*` markers intact and self-contained
|
||||
- [ ] `pnpm --filter @tabler/core test` (js + scss) clean
|
||||
- [ ] `pnpm run type-check` and `pnpm run lint-prettier` clean
|
||||
- [ ] `pnpm run bundlewatch` within budget, or the new limit justified in the PR
|
||||
- [ ] Docs page updated and changeset written
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: core-scss
|
||||
description: >-
|
||||
Write or change styles in `core/scss/` — the framework itself. Use whenever a
|
||||
component needs a new class, modifier, size or colour variant, when a Sass or
|
||||
CSS custom property is added or renamed, when dark mode or RTL behaviour is
|
||||
involved, and before touching `_variables.scss`, `_props.scss` or anything
|
||||
under `core/scss/ui/`. Covers where a style goes, the custom-property
|
||||
pattern and its build-time `--tblr-` prefix, dark mode, RTL, the docs
|
||||
markers, the SCSS unit tests and the lint/size gates.
|
||||
---
|
||||
|
||||
# Styles in `core/scss`
|
||||
|
||||
`core/scss` is the product: a class that ships here is public API for every Tabler user. Components in `shared/ui` only assemble the class names defined here.
|
||||
|
||||
## 1. Where a style goes
|
||||
|
||||
| Path | Holds |
|
||||
| --- | --- |
|
||||
| `ui/_*.scss` | components — `.badge`, `.card`, `.steps`, one file per component |
|
||||
| `layout/_*.scss` | page chrome — `root`, `core`, `navbar`, `page`, `footer`, `dark`, `animations`, `accessibility` |
|
||||
| `utils/_*.scss` | utility classes — colors, background, text, sizing, shadow, opacity, scroll, hover |
|
||||
| `mixins/`, `helpers/` | mixins, functions, the utilities API |
|
||||
| `bootstrap/` | Tabler's managed copies of Bootstrap's own partials |
|
||||
| `vendor/` | overrides for third-party plugin CSS |
|
||||
| `tests/` | sass-true unit tests (see section 7) |
|
||||
|
||||
Entry points: `tabler.scss` (which forwards `_core.scss`, then `_extends.scss` last), plus the standalone bundles `tabler-flags`, `tabler-marketing`, `tabler-payments`, `tabler-props`, `tabler-socials`, `tabler-themes`, `tabler-vendors`. A new partial is not compiled until it is `@forward`ed from `_core.scss` (or the bundle it belongs to).
|
||||
|
||||
The module graph uses `@use` / `@forward`: a partial starts with `@use '../config' as *`, which is the hub forwarding `settings`, `variables`, `variables-dark`, `maps`, `mixins` and `utilities`. Cross-module `@extend` rules must stay in `_extends.scss`, which loads last.
|
||||
|
||||
## 2. The component pattern
|
||||
|
||||
```scss
|
||||
@use '../config' as *;
|
||||
|
||||
.badge {
|
||||
--badge-padding-x: #{$badge-padding-x};
|
||||
--badge-font-size: #{$badge-font-size};
|
||||
--badge-line-height: 1;
|
||||
display: inline-flex;
|
||||
padding: var(--badge-padding-y) var(--badge-padding-x);
|
||||
font-size: var(--badge-font-size);
|
||||
@include border-radius(var(--badge-border-radius));
|
||||
}
|
||||
```
|
||||
|
||||
- Every themeable value becomes a **custom property declared at the top of the component's root rule**, seeded from a Sass variable (`#{$badge-font-size}`). Declarations below read `var(--badge-*)`, never the Sass variable directly — that is what lets users retheme without recompiling.
|
||||
- A value with no reason to be overridden can be a literal (`--badge-line-height: 1`).
|
||||
- Modifiers set custom properties rather than redeclaring properties: `.badge-sm { --badge-font-size: … }`.
|
||||
- Sass variables go to `_variables.scss` with `!default`, dark-mode counterparts to `_variables-dark.scss`.
|
||||
|
||||
## 3. Custom properties are authored bare
|
||||
|
||||
Write `--badge-bg`, not `--tblr-badge-bg`. The public `--tblr-` prefix is added at build time by `.build/css-var-prefix.ts` (a postcss pass in `build-css.ts`).
|
||||
|
||||
The consequence to remember: **names owned by third-party libraries must not be prefixed.** `cssVarIgnore` lists them (`--bs-`, `--fc-`, `--gl-`, `--litepicker-`, `--plyr-`, `--ts-`, …). Prefixing one detaches the theming with no error anywhere — the library keeps reading its own name and simply never sees the value. When a vendor override introduces a new foreign name, add it to `cssVarIgnore`; `core/scss/tests/css-var-prefix.test.mjs` snapshots every custom property of `tabler-vendors.scss`, so a missing entry shows up as a `--tblr-`-prefixed foreign name in the snapshot diff.
|
||||
|
||||
Global properties (`--dir`, colours, fonts, spacing) live in `_props.scss`, which emits them on `:root, :host`.
|
||||
|
||||
## 4. Dark mode
|
||||
|
||||
- Colour pairs are expressed with `light-dark()` where possible (`_variables.scss`, `layout/_root.scss`), so one declaration covers both modes.
|
||||
- What cannot be expressed that way goes to `_variables-dark.scss`, or to `layout/_dark.scss` for the visibility helpers.
|
||||
- Dark mode is keyed on `.theme-dark`, `[data-bs-theme='dark']` and `[data-theme='dark']` — match all three when you add a selector, and keep the whole block behind `@if $enable-dark-mode`.
|
||||
|
||||
## 5. RTL
|
||||
|
||||
RTL stylesheets are generated by `rtlcss` in `build-css.ts` (`--rtl`), so **do not hand-write RTL overrides**. Two rules:
|
||||
|
||||
- Prefer logical properties (`padding-inline-start`, `inset-inline-end`) — rtlcss then needs no help.
|
||||
- A physical transform that must flip uses the `--dir` multiplier (`translateX(calc(var(--dir) * -50%))`), and the declaration is marked `/* rtl:ignore */` so rtlcss does not negate an already-correct calc. Both patterns are in `_utilities.scss` and `ui/_steps.scss`.
|
||||
|
||||
## 6. Docs markers
|
||||
|
||||
Snippets shown on documentation pages are pulled from the source with markers, so the docs cannot drift:
|
||||
|
||||
```scss
|
||||
// scss-docs-start alert-variables
|
||||
$alert-padding-y: … !default;
|
||||
// scss-docs-end alert-variables
|
||||
```
|
||||
|
||||
`docs/components/CodeDocs.astro` and `docs/lib/llms.ts` read these. When you rename or move a marked block, check who references the marker name before deleting it.
|
||||
|
||||
## 7. Unit tests (sass-true)
|
||||
|
||||
`core/scss/tests/*.test.scss` are real unit tests over mixins and functions, auto-discovered by `core/scss/tests/scss.test.mjs` and run through vitest:
|
||||
|
||||
```bash
|
||||
pnpm --filter @tabler/core test:scss
|
||||
pnpm --filter @tabler/core test # js + scss
|
||||
```
|
||||
|
||||
Add a test when you write a mixin whose output is easy to break silently — the `_cards.test.scss` case (a `0%` that must keep its unit or the whole `color-mix()` drops) is the model. Note that stylelint deliberately ignores `core/scss/tests/**`: autofix there would rewrite the assertions.
|
||||
|
||||
## 8. Gates
|
||||
|
||||
```bash
|
||||
pnpm run lint:scss # stylelint (twbs config; prettier owns formatting)
|
||||
pnpm --filter @tabler/core css-lint # + find-unused-sass-variables
|
||||
pnpm run generate-tokens:check # shared/lib/tokens.ts must match the Sass maps
|
||||
pnpm run lint-prettier # formatting
|
||||
pnpm run bundlewatch # size budgets (tabler.css 80 kB, tabler.min.css 75 kB)
|
||||
```
|
||||
|
||||
- A new entry in `$theme-colors`, `$avatar-sizes` and friends must be regenerated into `shared/lib/tokens.ts` with `pnpm run generate-tokens` — the check gate fails otherwise.
|
||||
- An unused Sass variable fails `css-lint`; delete it or use it.
|
||||
- Growth past a bundlewatch limit is a decision, not an accident: raise the number in `core/package.json` deliberately and say so in the PR.
|
||||
|
||||
## 9. Checklist
|
||||
|
||||
- [ ] Partial in the right directory and `@forward`ed from `_core.scss` or its bundle
|
||||
- [ ] Themeable values as custom properties at the top of the root rule, seeded from `!default` Sass variables
|
||||
- [ ] Custom properties written bare; new foreign names added to `cssVarIgnore`
|
||||
- [ ] Dark mode via `light-dark()` or all three dark selectors, behind `$enable-dark-mode`
|
||||
- [ ] Logical properties, or `--dir` + `/* rtl:ignore */`; no hand-written RTL
|
||||
- [ ] sass-true test for a mixin that can break silently
|
||||
- [ ] `lint:scss`, `css-lint`, `generate-tokens:check`, `lint-prettier`, `test:scss` clean
|
||||
- [ ] Docs page and class table updated (`write-docs`, `class-reference`), changeset written
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: demo-pages
|
||||
description: >-
|
||||
Build or update a Tabler demo page under `preview/pages/*.astro` — the pages
|
||||
that show a component's variants side by side. Use whenever the user asks to
|
||||
add, extend, or clean up a preview/demo page, and on your own after adding a
|
||||
new `shared/ui` component that has no page showing it. Covers the page
|
||||
skeleton, the header (title, description, docs link, actions), the card grid,
|
||||
heading levels, and how to verify the result in the browser.
|
||||
---
|
||||
|
||||
# Write a Tabler demo page
|
||||
|
||||
Demo pages live in `preview/pages/*.astro` and are served at `/<name>` in dev, `/<name>.html` in the build. Each one shows the variants of a single component so a reader can compare them at a glance. They are not documentation — the prose is one line per card; the depth lives in `docs/content/**` (see the `write-docs` skill).
|
||||
|
||||
## 1. Page skeleton
|
||||
|
||||
```astro
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Badges" pageMenu="base.badges" description="Badges highlight a count, status, or short label attached to text, buttons, or menu items.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/badge" />
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Basic</CardTitle>
|
||||
<CardSubtitle>One sentence on what this variant is for.</CardSubtitle>
|
||||
…
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
```
|
||||
|
||||
- `title` — browser title. It also becomes the page header, so **do not repeat it in a `pageHeader` prop**; `pageHeader` exists only for a header that differs from the browser title (`title="Dashboard" pageHeader="Boxed layout"`), or as `pageHeader={false}` for a page with no header at all.
|
||||
- `description` — renders under the page title *and* fills `<meta name="description">`. One sentence, present tense, says what the component is for.
|
||||
- `pageMenu` — the menu entry to highlight, e.g. `base.badges`. Match an existing key in `shared/data/menu.json`.
|
||||
- Never hand-roll a title block inside the page body. The page header is the layout's job.
|
||||
|
||||
## 2. Page header extras
|
||||
|
||||
Anything to the right of the title goes in the `page-header-actions` slot, as a direct child of `<DefaultLayout>`:
|
||||
|
||||
```astro
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/badge" />
|
||||
```
|
||||
|
||||
- **Docs link:** `path` is the page's path under `docs/content/**` without the extension — `docs/content/ui/components/badge.mdx` → `/ui/components/badge`. Add one whenever a matching docs page exists.
|
||||
- **Check the path with `pnpm run check-docs-links`.** It resolves every `<DocsLink path="…">` against `docs/content/**` and fails on a typo, so you do not need a browser for this.
|
||||
- If you do open the link, use `docs-dev.tabler.io`, which serves `dev`. `docs.tabler.io` serves the last release and still has the pre-content-collection urls, so a valid page can 404 there. A 404 on production alone is never a reason to change a path.
|
||||
- **Prebuilt action groups** (`HeaderActionsButtons`, `HeaderActionsPrint`, `HeaderActionsPhotos`, …) live in `shared/components/layout/` and go in the same slot: `<HeaderActionsButtons slot="page-header-actions" />`. There is no `actions="buttons"` prop any more.
|
||||
|
||||
## 3. Card grid
|
||||
|
||||
- One card per variant, in `<div class="row row-cards">` with `col-*` wrappers. Keep column widths consistent down the page.
|
||||
- `CardTitle` names the variant in 1-3 words. `CardSubtitle` explains it in one sentence, naming the prop or class it demonstrates: "Add `showClose` to let users close the alert."
|
||||
- Put the class or prop in `<code>` inside the subtitle. Readers scan for the token.
|
||||
- Never write `<h3 class="card-title">` or `<div class="card-subtitle">` by hand — use the components, which also keep the heading level right.
|
||||
|
||||
## 4. Heading levels
|
||||
|
||||
`CardTitle` and `CardHeader` render `h2`, directly under the page `h1`. Pass `as="h3"` only for a card nested under its own `h2` section heading. Do not introduce raw `<h3>`/`<h4>` headings for card titles; that is what made pages mix levels before.
|
||||
|
||||
## 5. Spacing between inline elements
|
||||
|
||||
Use the list wrappers instead of margin utilities on each child:
|
||||
|
||||
| Wrapper | For |
|
||||
| --- | --- |
|
||||
| `<ButtonList>` | rows of buttons |
|
||||
| `<BadgeList>` | rows of badges |
|
||||
| `<TagList>` | rows of tags |
|
||||
| `<AvatarList>` | rows or stacks of avatars |
|
||||
|
||||
The pluralised class names (`.badges-list`, `.tags-list`) are deprecated aliases of `.badge-list` / `.tag-list`. Do not add `me-2`/`mb-2` to individual items.
|
||||
|
||||
## 6. Data and repetition
|
||||
|
||||
- Loop over `shared/data/*.json` (`site.json` colors, `people.json`, `flags.json`) instead of pasting 20 near-identical blocks.
|
||||
- Cast JSON entries to a named type in the frontmatter when you index into them; `astro check` is part of the quality gates.
|
||||
|
||||
## 7. Registering the page
|
||||
|
||||
- Add the page to `shared/data/menu.json` so it is reachable, and use the same key in `pageMenu`.
|
||||
- A new `shared/ui` component usually needs both a demo page and a docs page. After finishing here, check `docs/content/**` and consult the `write-docs` skill if it is missing.
|
||||
|
||||
## 8. Verify in the browser
|
||||
|
||||
Never hand the page to the user unchecked.
|
||||
|
||||
1. `preview_start` with the `preview` config from `.claude/launch.json`, then open `/<page-name>`.
|
||||
2. Screenshot it, and read the console for errors.
|
||||
3. For a page built by looping over data, assert the rendered structure instead of eyeballing it:
|
||||
|
||||
```shell
|
||||
curl -s http://localhost:3000/badges | grep -o '<h[1-6]' | sort | uniq -c
|
||||
```
|
||||
|
||||
A demo page should show exactly one `h1` and then `h2`s — no `h3` in the card grid.
|
||||
|
||||
## 9. Quality checklist
|
||||
|
||||
- [ ] `title` and `description` set; no `pageHeader` repeating the title
|
||||
- [ ] `DocsLink` present when a docs page exists; `pnpm run check-docs-links` passes
|
||||
- [ ] One card per variant; `CardTitle` + `CardSubtitle` on each
|
||||
- [ ] No raw `card-title` / `card-subtitle` / `page-title` markup
|
||||
- [ ] List wrappers instead of per-item margins
|
||||
- [ ] `pnpm run format-prettier` and `pnpm run type-check` clean (repo root, not a tail-filtered subset)
|
||||
- [ ] Page opened in the browser, console clean, heading order checked
|
||||
- [ ] Changeset written with the `generate-changeset` skill
|
||||
@@ -2,9 +2,10 @@
|
||||
name: generate-changeset
|
||||
description: >-
|
||||
Creates a Tabler changeset file in `.changeset/` from the current code
|
||||
changes. Use when the user asks for a changeset, version bump, release note
|
||||
entry, or changelog entry for @tabler/core, @tabler/preview, or @tabler/docs.
|
||||
disable-model-invocation: true
|
||||
changes. Use this skill for every changeset in this repo — when the user asks
|
||||
for a changeset, version bump, release note, or changelog entry, and also on
|
||||
your own whenever a change under `core/`, `preview/`, or `docs/` is about to
|
||||
be committed or opened as a PR. Never hand-write the file instead.
|
||||
---
|
||||
|
||||
# Generate changeset
|
||||
@@ -46,8 +47,13 @@ When unsure between patch and minor: prefer **patch** for fixes and visual tweak
|
||||
|
||||
## 4. Write the description
|
||||
|
||||
- **Exactly one sentence**
|
||||
A changeset is one line in a changelog. It is not a summary of the work.
|
||||
|
||||
- **One sentence, 130 characters or fewer**, including backticks. This is the limit to write to.
|
||||
- **160 characters is a hard ceiling.** Go over 130 only when a shorter version would drop a fact the reader needs — a long list of affected components, for example. Never go over 160.
|
||||
- Start with: `Added`, `Updated`, `Fixed`, or `Removed`
|
||||
- Say **what** changed and **where** (component, page, utility). Then stop.
|
||||
- **Name at least one concrete thing**: a component, class, CSS variable, Sass variable, page, data file, or dependency. "Fixed mixed declarations in SCSS" and "Updated flags and avatars styling" name nothing — a reader cannot tell whether the change affects them. There is no minimum length, but an entry under ~40 characters that names nothing is almost always too vague; a dependency bump like `Updated Bootstrap to v5.3.8.` is fine at 28, because the dependency and version are the content.
|
||||
- Use **backticks** for code tokens:
|
||||
- Classes: `.btn-ghost`, `.progress-lg`
|
||||
- Properties: `stroke-width`, `border-radius`
|
||||
@@ -55,15 +61,64 @@ When unsure between patch and minor: prefer **patch** for fixes and visual tweak
|
||||
- Icons: `arrow-up`
|
||||
- Attributes: `aria-label`, `data-*`
|
||||
- Functions: `addEventListener()`
|
||||
- Be specific about **what** changed and **where** (component, page, mixin)
|
||||
|
||||
**Patterns from this repo:**
|
||||
**Count the characters before writing the file:**
|
||||
|
||||
- Component: `Added Progress Steps component for step-by-step navigation indicators.`
|
||||
- Size variant: `Added \`.progress-lg\` and \`.progress-xl\` size variants for the progress component.`
|
||||
- Bug fix: `Fixed dark mode text selection contrast by adding \`--tblr-selection-bg\` CSS variable with \`0.4\` opacity override in dark mode.`
|
||||
- New page: `Added new onboarding page with progress indicator and navigation layout.`
|
||||
- Cross-package: `Added background pattern utilities and documentation, including updated preview demos.`
|
||||
```shell
|
||||
printf '%s' 'Added Progress Steps component for step-by-step navigation indicators.' | wc -c
|
||||
```
|
||||
|
||||
If it is over the limit, **cut words**. Never split it into two sentences, a second paragraph, or a bullet list.
|
||||
|
||||
### Leave this out
|
||||
|
||||
These belong in the PR description or the commit body, not in the changeset:
|
||||
|
||||
- Why the change was made, or what was broken before
|
||||
- How it was built: file paths, mixin internals, refactor steps
|
||||
- A list of every affected component, page, or variable
|
||||
- Migration and rollout notes, unless the change is breaking
|
||||
|
||||
**Cut the tail first.** An over-long entry in this repo is almost always a good sentence with a trailing justification clause glued on. Delete that clause and the length problem is usually solved:
|
||||
|
||||
```text
|
||||
… and removed duplicate exports from `tabler.js` for better maintainability.
|
||||
… with `var(--tblr-bg-surface-secondary)` so it adapts to dark mode.
|
||||
… live in one `:root` declaration instead of separate dark-mode overrides.
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Good — short and specific:
|
||||
|
||||
```text
|
||||
(70) Added Progress Steps component for step-by-step navigation indicators.
|
||||
(81) Added `.progress-lg` and `.progress-xl` size variants for the progress component.
|
||||
(78) Fixed dark mode text selection contrast with a `--tblr-selection-bg` variable.
|
||||
(58) Removed `license_key` option from the HugeRTE init object.
|
||||
```
|
||||
|
||||
Too vague — name the thing that changed:
|
||||
|
||||
```text
|
||||
(33) ❌ Fixed mixed declarations in SCSS.
|
||||
(79) ✅ Fixed Sass mixed-declaration warnings in the navbar, card, nav and table styles.
|
||||
(26) ❌ Updated activity messages.
|
||||
(82) ✅ Updated the activity feed messages in `activity.json` and the activity preview page.
|
||||
```
|
||||
|
||||
Too long — trim it:
|
||||
|
||||
```text
|
||||
(175) ❌ Fixed accessibility issues for skip links, keyboard focus, `prefers-reduced-motion`, form labels, and action controls that incorrectly used links instead of `button` elements.
|
||||
(117) ✅ Fixed accessibility issues in skip links, keyboard focus, `prefers-reduced-motion`, form labels, and action controls.
|
||||
```
|
||||
|
||||
Over 130 but justified — the list of components is the content, and cutting it would say nothing:
|
||||
|
||||
```text
|
||||
(137) Added missing ARIA roles and states to `Pagination`, `NavSegmented`, `Accordion`, `Steps`, tabs, `Modal`, `Offcanvas` and `CarouselCard`.
|
||||
```
|
||||
|
||||
Write in **simple English**, even if the user asked in another language.
|
||||
|
||||
@@ -97,12 +152,15 @@ Rules:
|
||||
- Frontmatter uses quoted package names: `"@tabler/core": patch`
|
||||
- Blank line after closing `---`
|
||||
- No title heading, no bullet list in the body
|
||||
- Body is a single line — no second paragraph
|
||||
- Bump values: `patch`, `minor`, or `major` only
|
||||
|
||||
## 7. Validate before writing
|
||||
|
||||
- [ ] Every listed package has touched paths in the diff
|
||||
- [ ] Description is one sentence with action verb, in simple English
|
||||
- [ ] Description is **130 characters or fewer** — counted, not estimated; over 130 only with a reason, never over 160
|
||||
- [ ] Description names a concrete component, class, variable, page, file, or dependency
|
||||
- [ ] Code tokens use backticks
|
||||
- [ ] Filename is kebab-case and not already used for a different change
|
||||
- [ ] SCSS/CSS/JS behavior changes include `@tabler/core` when under `core/`
|
||||
@@ -112,7 +170,8 @@ Rules:
|
||||
After creating the file:
|
||||
|
||||
1. Show the full changeset content in a fenced `markdown` block (for easy copy/review)
|
||||
2. Briefly explain **why** each package and bump level was chosen
|
||||
3. If no changeset is needed (docs-only CI, lockfile-only, etc.), say so and why
|
||||
2. Give the description length, e.g. `112 / 130 characters`; if it is over 130, say why the extra words earn their place
|
||||
3. Briefly explain **why** each package and bump level was chosen
|
||||
4. If no changeset is needed (docs-only CI, lockfile-only, etc.), say so and why
|
||||
|
||||
Do **not** run `changeset version` or `changeset publish` unless the user explicitly asks.
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: html-diff
|
||||
description: >-
|
||||
Prove that a refactor changes no rendered HTML, using the `html-diff`
|
||||
harness over every preview page. Use before and after any change that is
|
||||
meant to be output-neutral — extracting or renaming a component, replacing
|
||||
hand-written markup with a `shared/ui` component, reordering props,
|
||||
restructuring a layout, migrating SCSS class emission — and whenever the
|
||||
user asks whether a change is safe or byte-identical. Covers capturing the
|
||||
baseline, reading the diff, and what counts as an acceptable difference.
|
||||
---
|
||||
|
||||
# Prove a refactor changes no HTML
|
||||
|
||||
`.build/html-diff.ts` renders every preview page from a running dev server and compares it to a stored baseline. It is the only fast way to refactor shared components with confidence: one run covers every page that uses them.
|
||||
|
||||
## 1. When to use it
|
||||
|
||||
Use it for any change that **should not** alter output: component extraction, replacing repeated markup with a component, renaming props, moving code between `shared/` files, layout restructuring, prettier/format passes.
|
||||
|
||||
Do not use it for a change that intentionally alters markup — the whole run will be red and tell you nothing. There, review the diff of the pages you meant to change instead.
|
||||
|
||||
## 2. Capture the baseline first
|
||||
|
||||
The baseline must come from the tree **before** the change. If you have already started editing, stash first.
|
||||
|
||||
```bash
|
||||
git stash # only if the working tree already has the change
|
||||
pnpm --dir preview run dev # or preview_start with the `preview` config
|
||||
pnpm run html-diff:baseline
|
||||
git stash pop
|
||||
```
|
||||
|
||||
Before editing, list the pages that render what you are about to change:
|
||||
|
||||
```bash
|
||||
grep -rl "Steps.astro" preview/pages docs/content
|
||||
```
|
||||
|
||||
Green on everything is not the only result worth checking — knowing that `all-elements.html` **must** stay identical while `steps.html` is allowed to differ is what turns the run into a proof.
|
||||
|
||||
Snapshots land in `.cache/html-diff/` (one HTML file per page) and the directory is wiped on every baseline run. The dev server must be reachable at `http://localhost:3000`; override with `HTML_DIFF_BASE` if you run it elsewhere.
|
||||
|
||||
## 3. Check after the change
|
||||
|
||||
Keep the same dev server running — it recompiles pages on demand — and run:
|
||||
|
||||
```bash
|
||||
pnpm run html-diff
|
||||
```
|
||||
|
||||
Every page byte-identical:
|
||||
|
||||
```
|
||||
✓ All 128 pages are byte-identical to the baseline.
|
||||
```
|
||||
|
||||
Anything else exits non-zero and prints one line per differing page.
|
||||
|
||||
## 4. Reading the output
|
||||
|
||||
| Line | Meaning |
|
||||
| --- | --- |
|
||||
| `± page: differs at line N` with `-`/`+` | real difference — the first differing line of that page |
|
||||
| `± page: WHITESPACE-ONLY difference` | identical after collapsing inter-tag whitespace |
|
||||
| `± page: new page (no baseline snapshot)` | page added since the baseline |
|
||||
| `± page: page removed` | page deleted, or it now fails to render |
|
||||
| `✖ N page(s) failed to render` | the dev server is down, or a page throws — fix that before reading anything else |
|
||||
|
||||
A whitespace-only difference is usually inert (flex containers, `<select>` internals) but is **not** automatically fine: inside inline contexts whitespace is a rendered space. Open such a page and compare visually before waving it through.
|
||||
|
||||
For a full diff of one page, compare the snapshot to the live render:
|
||||
|
||||
```bash
|
||||
diff <(cat .cache/html-diff/badges.html) <(curl -s http://localhost:3000/badges)
|
||||
```
|
||||
|
||||
## 5. What the harness does not cover
|
||||
|
||||
- **Preview pages only.** Docs pages and dynamic routes (`[...]` in the filename) are skipped.
|
||||
- **Dev output, not built output.** The build additionally prettifies HTML; a formatting-only difference can still appear in `dist`.
|
||||
- The `Generated <date>` footer is normalised away; nothing else is.
|
||||
- Pages are compared as text, so an attribute reorder counts as a difference even when the DOM is equivalent. That is the point — Tabler ships the HTML.
|
||||
|
||||
## 6. Checklist
|
||||
|
||||
- [ ] Baseline captured from the pre-change tree, with the dev server running
|
||||
- [ ] Same dev server used for baseline and check
|
||||
- [ ] `pnpm run html-diff` green, or every remaining difference explained
|
||||
- [ ] Whitespace-only pages opened in the browser before accepting them
|
||||
- [ ] New/removed pages are intentional
|
||||
@@ -5,7 +5,6 @@ description: >-
|
||||
from the current branch versus origin/dev (Tabler). Use when the
|
||||
user asks for an MR/PR description, GitLab merge request text, or a branch
|
||||
summary for reviewers.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# MR / PR description from branch
|
||||
@@ -46,17 +45,33 @@ Use this to infer **intent**, **user-visible behavior**, and **risk**—not only
|
||||
|
||||
**Vercel preview URL:** only when the diff has **visual / UI changes** a reviewer can check in the browser (CSS, preview pages, docs examples, components, layout). Skip the preview URL for non-visual work (agent skills, CI, tooling, docs prose-only, lockfile, config with no rendered UI effect).
|
||||
|
||||
When a preview URL is needed and the branch is pushed, Vercel deploys a preview. Build the link from the branch name:
|
||||
When a preview URL is needed and the branch is pushed, Vercel deploys a preview. **The repo has two separate Vercel projects — pick the one matching the diff:**
|
||||
|
||||
1. Take the current branch name from `git branch --show-current`.
|
||||
2. Replace `/` with `-`, **remove dots entirely** (do not replace them with dashes), and use lowercase (e.g. `feature/pricing-banner` → `feature-pricing-banner`, `update-icons-3.45.0` → `update-icons-3450`).
|
||||
3. Insert into: `https://tabler-git-{branch-slug}-tabler-io.vercel.app/`
|
||||
| Diff touches | Project | Host |
|
||||
| --- | --- | --- |
|
||||
| `preview/**`, `core/**` (demo pages) | `tabler` | `https://tabler-git-{branch-slug}-tabler-io.vercel.app/` |
|
||||
| `docs/**` (documentation site) | `tabler-docs` | `https://tabler-docs-git-{branch-slug}-tabler-io.vercel.app/` |
|
||||
|
||||
Examples:
|
||||
- branch `bundle-framing-posthog-flag` → `https://tabler-git-bundle-framing-posthog-flag-tabler-io.vercel.app/`
|
||||
- branch `update-icons-3.45.0` → `https://tabler-git-update-icons-3450-tabler-io.vercel.app/`
|
||||
Using the `tabler` host for a docs page returns 404 — the pages simply do not exist in that project.
|
||||
|
||||
If the diff touches specific routes or pages, append that path to the preview URL. **Preview pages use the `.html` extension**: a page from `preview/pages/icons.astro` is served at `/icons.html` (e.g. `…vercel.app/icons.html`), not `/icons`. Docs pages use directory URLs without extension: a page from `docs/pages/ui/components/badge.mdx` is served at `/ui/components/badge/`. Mention the exact path(s) in **Preview**.
|
||||
Build `{branch-slug}` from the branch name (`git branch --show-current`): replace `/` with `-`, **remove dots entirely** (do not replace them with dashes), lowercase. Examples: `feature/pricing-banner` → `feature-pricing-banner`, `update-icons-3.45.0` → `update-icons-3450`.
|
||||
|
||||
**Verify before pasting.** The slug rule is a convention, not a guarantee. Read the real url from the deployment, then `curl` each link you intend to put in the body:
|
||||
|
||||
```shell
|
||||
gh api repos/tabler/tabler/deployments --jq '[.[] | select(.environment=="Preview – tabler-docs")][0].id'
|
||||
gh api repos/tabler/tabler/deployments/<id>/statuses --jq '.[0].environment_url'
|
||||
curl -s -o /dev/null -w '%{http_code}\n' <link>
|
||||
```
|
||||
|
||||
**Links to the published docs** (not a Vercel preview) belong to a different pair of hosts: `docs.tabler.io` serves the last release, `docs-dev.tabler.io` serves `dev`. Check any `docs.tabler.io` link you mention against `docs-dev.tabler.io` — production still serves the pre-content-collection URLs, so a page that exists in `docs/content/**` can 404 there without being a broken link.
|
||||
|
||||
If the diff touches specific routes or pages, append that path. Path shape differs per project:
|
||||
|
||||
- **Preview pages use the `.html` extension**: `preview/pages/icons.astro` → `/icons.html`, not `/icons`.
|
||||
- **Docs pages use directory urls with no extension and no trailing slash** (`vercel.json` sets `trailingSlash: false`): `docs/content/ui/components/badge.mdx` → `/ui/components/badge`. A trailing slash still resolves, but through a 308 redirect.
|
||||
|
||||
Mention the exact path(s) in **Preview**.
|
||||
|
||||
## 3. Title
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: navigation
|
||||
description: >-
|
||||
Make a page reachable and keep its links valid — the preview sidebar/navbar
|
||||
menu in `shared/data/menu.json`, the docs menu in `shared/data/docs.json`,
|
||||
and the redirects in `docs/astro.config.mjs`. Use whenever a page is added,
|
||||
renamed, moved or removed in `preview/pages/` or `docs/content/`, whenever a
|
||||
`pageMenu` key does not highlight the right entry, and whenever a docs URL
|
||||
changes. Covers both menu formats, the `pageMenu` key rules and the link
|
||||
gate.
|
||||
---
|
||||
|
||||
# Menus, URLs and redirects
|
||||
|
||||
A new page that no menu points at is invisible, and a renamed docs URL breaks every external link to it. Both menus are hand-maintained JSON; nothing discovers pages automatically.
|
||||
|
||||
## 1. Preview menu — `shared/data/menu.json`
|
||||
|
||||
An object of top-level groups, each with nested `children`:
|
||||
|
||||
```json
|
||||
"base": {
|
||||
"title": "Interface",
|
||||
"icon": "package",
|
||||
"columns": 2,
|
||||
"children": {
|
||||
"badges": { "url": "badges.html", "title": "Badges", "badge": "New" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
| --- | --- |
|
||||
| `title` / `title-long` | label; `title-long` is used where the navbar has room |
|
||||
| `icon` | icon name for the group (top level) |
|
||||
| `url` | **with the `.html` extension** — `badges.html`, matching `build.format: 'file'` |
|
||||
| `badge` | small label on the entry, e.g. `"New"` |
|
||||
| `columns` | split a long child list into N columns |
|
||||
| `active`, `disabled`, `right` | force highlight, grey out, push to the right side |
|
||||
| `children` | nested entries (a third level is allowed) |
|
||||
|
||||
**The `pageMenu` prop is the path of keys, dot-separated**: `base.badges` matches the `badges` child of the `base` group. `NavbarMenu.astro` splits on `.` and highlights by key, so a mismatch between the page prop and the JSON key silently highlights nothing. `menu-sample.json` is a separate, smaller menu used by sample layouts — leave it alone unless the task is about those.
|
||||
|
||||
## 2. Docs menu — `shared/data/docs.json`
|
||||
|
||||
An array-based tree (`menu`) plus a `links` list for the external links in the docs header:
|
||||
|
||||
```json
|
||||
{ "title": "Getting started", "url": "/ui/getting-started",
|
||||
"children": [ { "title": "Installation", "url": "/ui/getting-started/installation" } ] }
|
||||
```
|
||||
|
||||
- URLs are **absolute paths without an extension and without a trailing slash** (`/ui/components/badge`) — `vercel.json` sets `trailingSlash: false`, so a trailing slash costs a 308.
|
||||
- The path mirrors the file: `docs/content/ui/components/badge.mdx` → `/ui/components/badge`; an `index.mdx` drops the last segment.
|
||||
- A page in `docs/content/**` that is missing here still renders — it is simply unreachable by navigation, which is easy to miss because no build step complains.
|
||||
|
||||
## 3. Renamed or removed docs URLs
|
||||
|
||||
Add an entry to `redirects` in `docs/astro.config.mjs`, next to the existing ones:
|
||||
|
||||
```js
|
||||
'/ui/base/markdown': { status: 301, destination: '/ui/base/prose' },
|
||||
```
|
||||
|
||||
The plural component slugs kept there (`alerts` → `alert`, `cards` → `card`, …) are the pre-Astro URLs still present in Google's index and in external links — that is the standard this repo holds itself to: an old URL keeps working. The Vercel adapter turns these into real HTTP redirects; without an adapter they would only be meta-refresh pages.
|
||||
|
||||
Update in the same pass: `shared/data/docs.json`, any `related:` front matter pointing at the old path, and `<DocsLink path="…">` on the matching preview page.
|
||||
|
||||
## 4. Verify with the link gate
|
||||
|
||||
```bash
|
||||
pnpm run check-docs-links
|
||||
```
|
||||
|
||||
It resolves, from source and without a build: markdown links and `related:` in `docs/content/**/*.mdx`, menu and link URLs in `shared/data/docs.json`, redirect destinations from `docs/astro.config.mjs`, `href` literals in docs components, `<DocsLink path="…">` and `getDocsUrl('…')` in the demo site, plus asset paths that really render. Anchors are checked against real heading slugs.
|
||||
|
||||
It does **not** check `menu.json`: a wrong preview URL or a `pageMenu` key that matches nothing shows up only in the browser. Open the page and confirm the menu entry is highlighted (`astro-dev` skill).
|
||||
|
||||
## 5. Checklist
|
||||
|
||||
- [ ] New preview page added to `menu.json` with a `.html` url, and its `pageMenu` prop equals the dot-path of the keys
|
||||
- [ ] New docs page added to `docs.json` at the right depth, url absolute and extension-free
|
||||
- [ ] Renamed or removed docs URL has a 301 in `docs/astro.config.mjs`
|
||||
- [ ] `related:` entries and `<DocsLink path="…">` updated to the new path
|
||||
- [ ] `pnpm run check-docs-links` clean
|
||||
- [ ] Menu highlighting checked in the browser
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: page-layouts
|
||||
description: >-
|
||||
Pick and configure the right layout for a Tabler preview page, and change or
|
||||
add a layout in `shared/layouts/`. Use whenever a page needs different page
|
||||
chrome — sidebar, navbar variant, boxed/fluid body, centered card, error or
|
||||
marketing page — whenever a page hand-rolls a page header or wrapper markup,
|
||||
and before adding a new layout file. Covers the layout map, the
|
||||
`DefaultLayout` props, the page-header slot, and the rules for a new layout.
|
||||
---
|
||||
|
||||
# Page layouts
|
||||
|
||||
Layouts own the page chrome: `<head>`, assets, body classes, navbar/sidebar, page header, footer. A page supplies content and a handful of props — never its own header block, wrapper divs, or `<html>` scaffolding.
|
||||
|
||||
## 1. Which layout
|
||||
|
||||
| Layout | Use for |
|
||||
| --- | --- |
|
||||
| `DefaultLayout` | the normal app page — navbar/sidebar, page header, footer (98 of the preview pages) |
|
||||
| `SingleLayout` | centered card on an empty page: sign-in, sign-up, error-ish flows (`containerSize`, `hideLogo`) |
|
||||
| `SettingsLayout` | settings pages — wraps the body in a card with the settings nav (`active`) |
|
||||
| `ProseLayout` | long-form text in a `card-lg` + `.prose` body (terms, changelog) |
|
||||
| `ErrorLayout` | error pages, content driven by `pageError` keyed into `errors.json` |
|
||||
| `MarketingLayout` | marketing/landing pages with the marketing navbar and footer |
|
||||
| `PayLayout` | minimal checkout chrome: small logo + close button |
|
||||
| `RedirectLayout` | bare meta-refresh document (`url`, `base`) |
|
||||
| `BaseLayout` | only when a page needs no chrome at all (screenshot/playground pages). Everything above wraps it. |
|
||||
|
||||
Docs pages do not choose a layout — `docs/pages/[...slug].astro` renders the collection through `DocsLayout`, and MDX front matter has no `layout:` key.
|
||||
|
||||
## 2. `DefaultLayout` — content props
|
||||
|
||||
| Prop | Effect |
|
||||
| --- | --- |
|
||||
| `title` | `<title>` **and**, by default, the page header title |
|
||||
| `pageHeader` | header title when it differs from `title`; `pageHeader={false}` removes the header |
|
||||
| `pretitle` | small line above the title (`page-pretitle`) |
|
||||
| `description` | line under the title *and* `<meta name="description">` |
|
||||
| `pageMenu` | menu entry to highlight, e.g. `base.badges` — must match a key in `shared/data/menu.json` |
|
||||
| `pageLibs` | third-party libs to load, keyed into `core/libs.json` (`['apexcharts']`) |
|
||||
| `pageHeaderFile` | swaps the title block for a prebuilt header (`"profile"`, `"uptime"`) |
|
||||
|
||||
Do not pass `pageHeader` when it repeats `title` — the fallback already covers it.
|
||||
|
||||
## 3. `DefaultLayout` — chrome flags
|
||||
|
||||
Each flag maps to one documented body/layout class, so reach for the prop instead of `bodyClass`:
|
||||
|
||||
| Prop | Class |
|
||||
| --- | --- |
|
||||
| `sidebar`, `sidebarDark`, `sidebarEnd` | `layout-sidebar`, `layout-sidebar-dark`, `layout-sidebar-end` |
|
||||
| `hideTopbar` | `layout-hide-topbar` |
|
||||
| `navbarTransparent`, `navbarCondensed`, `navbarDark`, `navbarOverlap`, `navbarSticky`, `navbarHideBrand` | the matching `layout-navbar-*` classes |
|
||||
| `navbarClass` | extra classes on the navbar |
|
||||
| `wrapperFull` | `page-wrapper-full`, drops the `.container-xl` around the body |
|
||||
| `containerCentered`, `containerClass` | `my-auto` / extra classes on the `.container-xl` |
|
||||
| `bodyClass` | free-form body classes — `layout-boxed`, `layout-fluid` |
|
||||
| `rtl` | `dir="rtl"` plus the RTL stylesheets |
|
||||
|
||||
## 4. The page-header slot
|
||||
|
||||
Anything to the right of the title goes in the `page-header-actions` slot, as a direct child of the layout:
|
||||
|
||||
```astro
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/badge" />
|
||||
<HeaderActionsButtons slot="page-header-actions" />
|
||||
```
|
||||
|
||||
Prebuilt groups live in `shared/components/layout/` (`HeaderActionsButtons`, `HeaderActionsPrint`, `HeaderActionsPhotos`, `HeaderActionsBreadcrumb`, …). The header renders the actions column only when the slot has content.
|
||||
|
||||
## 5. Never hand-roll chrome
|
||||
|
||||
If a page contains `<div class="page-header">`, `<h2 class="page-title">`, `page-wrapper`, `page-body`, or its own `<head>` markup, that is a layout prop that was missed. Add or extend the prop instead of duplicating the markup — the same header exists on 100+ pages and drifts the moment one copy is edited.
|
||||
|
||||
## 6. Adding or changing a layout
|
||||
|
||||
- A new layout wraps `BaseLayout` (or `DefaultLayout` when it only reshapes the body, like `SettingsLayout` and `ProseLayout`). It never re-emits `<html>`, assets, or theme settings.
|
||||
- Every prop gets a doc comment naming the class or front-matter key it maps to — follow the existing style in `DefaultLayout`.
|
||||
- Booleans default to off; content props are optional with a sensible fallback.
|
||||
- Keep `<!-- BEGIN … -->` / `<!-- END … -->` markers around structural blocks: users copy the rendered HTML.
|
||||
- Pass `pageLibs` through to `BaseLayout` so pages using the new layout can still load plugins.
|
||||
- Changing a shared layout touches every page that uses it — prove it with the `html-diff` skill.
|
||||
|
||||
## 7. Checklist
|
||||
|
||||
- [ ] Layout chosen from the table, not `BaseLayout` plus hand-written chrome
|
||||
- [ ] `title`, `description`, `pageMenu` set; no `pageHeader` repeating `title`
|
||||
- [ ] Chrome via named flags, `bodyClass` only for `layout-boxed` / `layout-fluid`
|
||||
- [ ] Header extras in the `page-header-actions` slot
|
||||
- [ ] `pageLibs` listed for every plugin the page initialises
|
||||
- [ ] Page opened in the browser (`astro-dev` skill); `html-diff` run if a shared layout changed
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: shared-lib
|
||||
description: >-
|
||||
Move logic out of Astro frontmatter into `shared/lib/*.ts` and cover it with
|
||||
a vitest test. Use when a component's or page's frontmatter grows past
|
||||
assembling classes, when demo data needs shaping or formatting, when the
|
||||
same helper appears in two components, or when the user asks for a helper,
|
||||
utility or unit test under `shared/`. Covers what belongs there, module
|
||||
conventions, the sibling `*.test.ts` rule, deterministic demo data, and the
|
||||
strict-TypeScript patterns the repo uses.
|
||||
---
|
||||
|
||||
# Helpers in `shared/lib`
|
||||
|
||||
`shared/lib/*.ts` is the only place in `shared/` where real logic lives. Components stay markup; anything with a branch worth testing moves here, gets a name, and gets a test.
|
||||
|
||||
## 1. What belongs here
|
||||
|
||||
Move it to `shared/lib` when it is:
|
||||
|
||||
- **formatting** — dates, numbers, strings (`date-format.ts`, `string-format.ts`)
|
||||
- **data access** — turning a JSON file into a typed export (`people.ts`, `site.ts`)
|
||||
- **generation** — deterministic demo values (`pseudo-random.ts`), SVG/icon handling (`svg.ts`), markdown rendering (`render-markdown.ts`)
|
||||
- **anything duplicated** in two components, or longer than a few lines in frontmatter
|
||||
|
||||
Keep it in the component when it is class assembly, a ternary over props, or a single `.map()` over a prop — pushing that into `lib` only adds an import.
|
||||
|
||||
Do not put in `shared/lib`: DOM code (that is an inline script — see the `astro-scripts` skill), Astro-specific rendering (that is a component), or build-time tooling (that is `.build/`).
|
||||
|
||||
## 2. Module conventions
|
||||
|
||||
```ts
|
||||
// Demo data date formatting quirks:
|
||||
// - unix seconds are shifted by local timezone offset (not the true epoch)
|
||||
const MONTHS = ['January', 'February', /* … */]
|
||||
|
||||
/** Formats a date as "Month DD, YYYY" using its UTC components. */
|
||||
export function formatLongDate(date: Date): string {
|
||||
return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate()).padStart(2, '0')}, ${date.getUTCFullYear()}`
|
||||
}
|
||||
```
|
||||
|
||||
- Named exports only, one topic per file, filename in kebab-case matching the topic.
|
||||
- Explicit return types; pure functions with no module-level mutable state (`page-scripts.ts` is the deliberate exception — a render-scoped registry).
|
||||
- A file-header comment when the module encodes a quirk (why the epoch is shifted, why the RNG is fake). A `/** … */` line on each exported function.
|
||||
- Export the types consumers need (`export interface Person`), so call sites do not re-cast.
|
||||
- Imports inside `shared/` are relative (`./array`, `../data/people.json`). Any bare npm import must be declared in `shared/package.json`.
|
||||
|
||||
## 3. Every module has a sibling test
|
||||
|
||||
`shared/vitest.config.mts` collects `lib/**/*.test.ts`, so `foo.ts` gets `foo.test.ts` next to it. This is not optional — every module in the directory has one.
|
||||
|
||||
```bash
|
||||
pnpm --filter @tabler/shared test # one run
|
||||
pnpm --filter @tabler/shared test:watch # while writing
|
||||
pnpm run test # all packages, via turbo
|
||||
```
|
||||
|
||||
Test the branches, not the happy path only: the fallback when a regex does not match, the empty range, the thrown error. `globals: true` is set, but the existing files still import `describe`/`it`/`expect` from `vitest` — keep that style.
|
||||
|
||||
## 4. Demo data must be deterministic
|
||||
|
||||
Pages are rebuilt constantly and the rendered HTML is compared byte-for-byte (`html-diff` skill), so a helper that produces different output on each run breaks the harness and pollutes diffs.
|
||||
|
||||
- Use `pseudo-random.ts` (`randomNumber`, `randomDate`, `randomItem`, `timeagoLabel`), seeded by the loop index — never `Math.random()`.
|
||||
- Never seed demo data from `Date.now()` / `new Date()` without arguments. Fixed dates only.
|
||||
|
||||
## 5. Strict TypeScript patterns
|
||||
|
||||
Every package extends `astro/tsconfigs/strictest`, so indexing an array yields `T | undefined`:
|
||||
|
||||
- Use `requireIndex(items, i)` from `array.ts` when the demo data is trusted to have the entry — it throws with a useful message instead of rendering `undefined`.
|
||||
- Cast a JSON import to its type **once**, in the lib module, and export the typed value (`export const people = peopleData as Person[]`). Call sites import the typed export; they never re-cast the JSON.
|
||||
- Give such interfaces an index signature (`[key: string]: unknown`) when the JSON has more fields than the components read.
|
||||
|
||||
`pnpm run type-check` runs `astro check` per package and covers these files.
|
||||
|
||||
## 6. Checklist
|
||||
|
||||
- [ ] Logic is out of the frontmatter and named after what it does
|
||||
- [ ] Named exports, explicit return types, no hidden state
|
||||
- [ ] Header comment for quirks, doc comment per exported function
|
||||
- [ ] Sibling `*.test.ts` covering the branches, not just the happy path
|
||||
- [ ] Deterministic: `pseudo-random.ts`, no `Math.random()`, no unseeded dates
|
||||
- [ ] JSON cast once in the lib module, `requireIndex` for trusted indexing
|
||||
- [ ] `pnpm --filter @tabler/shared test`, repo-level `type-check` and `format-prettier` clean
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: ui-component
|
||||
description: >-
|
||||
Build or extend a shared Astro component in `shared/ui/*.astro` (or
|
||||
`shared/components/**`) — the library that both the preview and the docs
|
||||
render. Use whenever the user asks for a new component, for a new prop,
|
||||
variant or size on an existing one, or for a refactor of a shared component.
|
||||
Covers the Props interface, the prop-name dictionary, class composition,
|
||||
slots, the `as` pitfall, and the follow-ups (demo page, docs, changeset) a
|
||||
new component needs.
|
||||
---
|
||||
|
||||
# Build a shared UI component
|
||||
|
||||
`shared/ui/*.astro` is the component library. `@tabler/preview` and `@tabler/docs` render the same files, so every change here lands on demo pages and in documentation examples at once. Components are markup: the design lives in `core/scss/`, and logic longer than a few lines lives in `shared/lib/*.ts`.
|
||||
|
||||
## 1. Before creating a file
|
||||
|
||||
- **Extend, do not fork.** Read the `Props` interface of the closest existing component first. A new look is a prop on the existing component (`variant`, `size`, `light`), never a parallel `ButtonAlt.astro`.
|
||||
- **Check the demo page before the component.** If `preview/pages/<name>.astro` hand-writes variants the component cannot express, those are the missing props, and the component has drifted from the markup it is supposed to own. Close that gap instead of adding one more hand-written block.
|
||||
- **Find the CSS first.** A component wraps classes that exist in `core/scss/ui/_*.scss`. If the class is missing, writing it is part of the task — never invent class names in the component.
|
||||
- Where it goes: generic building blocks in `shared/ui/`; fixed demo content (a dashboard card with its own data) in `shared/components/cards/`; docs-only components in `docs/components/`.
|
||||
- Imports inside `shared/` are relative (`./Icon.astro`, `../components/CaptureScript.astro`). `@shared` / `@ui` / `@data` are for the site packages. Any bare npm import used in `shared/` must be declared in `shared/package.json`.
|
||||
|
||||
## 2. Skeleton
|
||||
|
||||
```astro
|
||||
---
|
||||
import Icon from './Icon.astro'
|
||||
|
||||
/** core/scss/ui/_badges.scss `.badge-{size}` modifier classes */
|
||||
type BadgeSize = 'sm' | 'lg'
|
||||
|
||||
interface Props {
|
||||
text?: string
|
||||
color?: string
|
||||
size?: BadgeSize | undefined
|
||||
/** -lt colour variant + text-{color}-lt-fg */
|
||||
light?: boolean
|
||||
class?: string
|
||||
icon?: string
|
||||
/** remaining attributes (id, data-*, aria-*, …) are forwarded to the root element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { text, color, size, light, class: className, icon, ...rest } = Astro.props
|
||||
|
||||
const classes = ['badge', size && `badge-${size}`, color && `bg-${color}${light ? '-lt' : ''}`, className]
|
||||
---
|
||||
|
||||
<span class:list={classes} {...rest}>{icon && <Icon name={icon} />}{text}</span>
|
||||
```
|
||||
|
||||
Rules the file above follows, and every component should:
|
||||
|
||||
- `interface Props` + destructuring with defaults. No untyped `Astro.props`.
|
||||
- The `class` prop keeps its HTML name and is destructured as `class: className`.
|
||||
- A union type for each modifier set (`'sm' | 'lg'`), with a doc comment naming the SCSS file the classes come from. Export the type (`export type IconSize`) when a sibling component reuses it.
|
||||
- `[key: string]: unknown` + `...rest` spread on the root element, so callers can pass `id`, `data-*`, `aria-*` without a prop for each. Add it unless the component deliberately controls its own attributes.
|
||||
- One doc comment per non-obvious prop, saying which class or attribute it emits.
|
||||
|
||||
## 3. Prop-name dictionary
|
||||
|
||||
Names are shared across the library so the API stays predictable (and stays portable to React/Vue later). Reuse these before inventing a name:
|
||||
|
||||
| Prop | Meaning |
|
||||
| --- | --- |
|
||||
| `class` | extra classes, merged last so callers can override |
|
||||
| `id` | element id; **required** for components that a script initialises |
|
||||
| `color` | palette colour (`primary`, `azure`, …), emitted as `-{color}` |
|
||||
| `size` | `sm` / `md` / `lg` (…), where `md` emits no class |
|
||||
| `variant` | style variant of the same component (`outline`, `ghost`, `important`) |
|
||||
| `text`, `title`, `description`, `label` | content passed as a prop instead of a slot |
|
||||
| `icon`, `iconEnd` | icon names, rendered through `Icon.astro` |
|
||||
| `href`, `external` | link target; `external` adds `target="_blank" rel="noreferrer"` |
|
||||
| `disabled`, `active`, `required`, `invalid` | state booleans, named after the HTML/CSS state |
|
||||
| `show…` (`showClose`, `showValue`) | opt-in parts of the markup |
|
||||
| `ariaLabel` | accessible name when the visible content is an icon or a colour |
|
||||
|
||||
Props are camelCase. Booleans default to `false` — never `showClose = true`.
|
||||
|
||||
## 4. Class composition
|
||||
|
||||
- Use `class:list={[...]}` with falsy entries for conditionals; it drops them and merges the caller's `className` for you. `.filter(Boolean).join(' ')` is only for a string you have to pass to a helper (`iconSvg`).
|
||||
- Put `className` last so the caller wins.
|
||||
- Do not emit a class for a default (`size !== 'md' && \`btn-${size}\``).
|
||||
- No `<style>` blocks and no CSS imports in the frontmatter. Styling is `core/scss/`.
|
||||
|
||||
## 5. Slots and containers
|
||||
|
||||
Container components (`Card`, `CardBody`, `ButtonList`, …) take a `<slot />` rather than a content prop. Use `Astro.slots.has('name')` when the markup differs depending on whether a named slot was filled.
|
||||
|
||||
Polymorphic containers take an `as` prop — with one trap:
|
||||
|
||||
```astro
|
||||
---
|
||||
import type { HTMLTag } from 'astro/types'
|
||||
|
||||
// `as` must stay out of the Props body: Astro's frontmatter scanner bails on a member
|
||||
// literally named `as` and silently falls back to untyped props.
|
||||
type PolymorphicProps = { as?: HTMLTag }
|
||||
|
||||
interface Props extends PolymorphicProps {
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { as: Element = 'div', class: className }: Props = Astro.props
|
||||
---
|
||||
|
||||
<Element class:list={['card', className]}><slot /></Element>
|
||||
```
|
||||
|
||||
Heading components pick their own level (`CardTitle` renders `h2`, `as="h3"` for a nested section) so pages cannot skip levels.
|
||||
|
||||
## 6. Escape hatches and Astro traps
|
||||
|
||||
- Content that may contain markup or entities (`…`) → `set:html` / `<Fragment set:html={…} />`. Entities in attribute strings must be passed as an expression (`title={"……"}`), because JSX decodes them inside string literals.
|
||||
- Boolean attributes are inconsistent: `selected={true}` renders bare, but some (e.g. `multiple`) render `="true"`. Use `multiple ? '' : undefined` when a bare attribute is required, and check the rendered HTML.
|
||||
- Client-side behaviour (plugin init, event wiring) does **not** go in an ad-hoc `<script>` — see the `astro-scripts` skill.
|
||||
- Icons always through `<Icon name="…" />`, never inline SVG.
|
||||
|
||||
## 7. Logic belongs in `shared/lib`
|
||||
|
||||
Anything beyond assembling classes and simple ternaries (formatting, data shaping, SVG handling, deterministic demo data) goes to `shared/lib/*.ts` and is imported by the component. Every module there has a sibling `*.test.ts` run by vitest (`pnpm --filter @tabler/shared test`) — add one for the function you introduce, and keep it pure so it is testable without a DOM.
|
||||
|
||||
Use `pseudo-random.ts` instead of `Math.random()` for demo data: pages are rebuilt constantly and random output would change the HTML on every build.
|
||||
|
||||
## 8. After the component exists
|
||||
|
||||
A new component is not done until it is visible and documented:
|
||||
|
||||
1. **Demo page** — a page in `preview/pages/` showing its variants (`demo-pages` skill).
|
||||
2. **Docs page** — `docs/content/**` (`write-docs` skill), plus the class table (`class-reference` skill) when the component introduces classes.
|
||||
3. **Changeset** — `generate-changeset` skill, `minor` for a new component, `patch` for a fix.
|
||||
|
||||
## 9. Checklist
|
||||
|
||||
- [ ] Extends an existing component instead of duplicating one
|
||||
- [ ] Classes exist in `core/scss/`; no invented class names
|
||||
- [ ] `interface Props` typed, prop names taken from the dictionary in section 3
|
||||
- [ ] `class: className` merged last, `...rest` forwarded
|
||||
- [ ] Accessible: semantic element, `ariaLabel` where the content is an icon
|
||||
- [ ] Logic in `shared/lib` with a test, not in the frontmatter
|
||||
- [ ] `pnpm run format-prettier` and `pnpm run type-check` clean at the repo root (full output, not a tail)
|
||||
- [ ] Demo page + docs page + changeset
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: write-docs
|
||||
description: Write, update, or suggest Tabler documentation pages in simple English using the current docs schema. Use whenever the user asks to create docs, edit docs, add new feature docs to an existing page, or standardize docs structure across any docs category. Also consult this skill proactively — without being asked — whenever a new visual UI component (shared/ui/*.astro), plugin, or other user-facing feature has just been added or changed and has no matching, up-to-date page under docs/pages/**, since undocumented components are easy to forget about.
|
||||
description: Write, update, or suggest Tabler documentation pages in simple English using the current docs schema. Use whenever the user asks to create docs, edit docs, add new feature docs to an existing page, or standardize docs structure across any docs category. Also consult this skill proactively — without being asked — whenever a new visual UI component (shared/ui/*.astro), plugin, or other user-facing feature has just been added or changed and has no matching, up-to-date page under docs/content/**, since undocumented components are easy to forget about.
|
||||
---
|
||||
|
||||
# Write Tabler Docs
|
||||
@@ -9,7 +9,7 @@ Follow the current Tabler documentation schema. How far to go depends on how the
|
||||
|
||||
## 1. Scope and behavior
|
||||
|
||||
- Works for any docs page type under `docs/pages/**` (components, utilities, forms, layout, plugins, base, getting started, emails, illustrations, icons, index pages). Pages are MDX: leaf pages `foo.mdx`, parents with sub-pages `foo/index.mdx`.
|
||||
- Works for any docs page type under `docs/content/**` (components, utilities, forms, layout, plugins, base, getting started, emails, illustrations, icons, index pages). Pages are MDX: leaf pages `foo.mdx`, parents with sub-pages `foo/index.mdx`.
|
||||
- Edit existing pages when the user asks to document new functionality in an existing component/page.
|
||||
- Create new pages when needed.
|
||||
- Use simple English in all prose.
|
||||
@@ -36,15 +36,15 @@ Default frontmatter (required unless user asks otherwise):
|
||||
title: ...
|
||||
summary: ...
|
||||
description: ...
|
||||
layout: '@shared/layouts/DocsMdxLayout.astro'
|
||||
---
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Keep frontmatter static YAML only.
|
||||
- By default, include only `title`, `summary`, `description`, and `layout`.
|
||||
- Add extended keys (for example `bootstrapLink`, `order`, `plugin`, `docs-libs`, `redirect`) only when the user explicitly asks for them or nearby pages in the same category use them.
|
||||
- No `layout:` key — `docs/pages/[...slug].astro` renders every page.
|
||||
- By default, include only `title`, `summary` and `description`.
|
||||
- Add extended keys only when the user explicitly asks for them or nearby pages in the same category use them. The full set is `seoTitle`, `seoDescription`, `icon`, `order`, `related`, `docs-libs`, `css-plugins`, `hide-pagination`, `added-in` — the collection schema in `docs/content.config.ts` is strict, so anything else fails the build.
|
||||
|
||||
## 4. Documentation schema to follow
|
||||
|
||||
@@ -94,16 +94,17 @@ Prose and example markup are only as accurate as their source. Before writing ex
|
||||
|
||||
## 7. Registering new pages in the docs menu
|
||||
|
||||
`docs/pages/**` is not scanned automatically to build navigation — the sidebar tree is frozen in `shared/data/docs.json` (see the comment in `docs/components/DocsMenu.astro`). A new leaf page with no entry there exists but is unreachable from the docs site.
|
||||
`docs/content/**` is not scanned automatically to build navigation — the sidebar tree is frozen in `shared/data/docs.json` (see the comment in `docs/components/DocsMenu.astro`). A new leaf page with no entry there exists but is unreachable from the docs site.
|
||||
|
||||
- Only touch `docs.json` for genuinely **new** pages. Editing an existing page needs no menu change.
|
||||
- Find the matching section in the `menu` array by directory: `docs/pages/ui/components/*` → the `"Components"` entry under `"Tabler UI"`, `docs/pages/ui/plugins/*` → `"Plugins"`, `docs/pages/icons/libraries/*` → `"Libraries"` under `"Tabler Icons"`, and so on — the section `title`/`url` pairs mirror the `docs/pages/ui/*` and `docs/pages/icons/*` subdirectory names.
|
||||
- Add `{ "title": "<Title Case name>", "url": "/<matching>/<slug>/" }` to that section's `children`, in the same alphabetical position its neighbors already follow.
|
||||
- Find the matching section in the `menu` array by directory: `docs/content/ui/components/*` → the `"Components"` entry under `"Tabler UI"`, `docs/content/ui/plugins/*` → `"Plugins"`, `docs/content/icons/libraries/*` → `"Libraries"` under `"Tabler Icons"`, and so on — the section `title`/`url` pairs mirror the `docs/content/ui/*` and `docs/content/icons/*` subdirectory names.
|
||||
- Add `{ "title": "<Title Case name>", "url": "/<matching>/<slug>" }` to that section's `children`, in the same alphabetical position its neighbors already follow.
|
||||
- **No trailing slash on `url`.** The menu compares it against the page url, which never has one, so a trailing slash silently breaks the active-item highlight for that page.
|
||||
- The `title` and `url` must match the new page's frontmatter `title` and its file path exactly, or the sidebar entry will point at the wrong place.
|
||||
|
||||
## 8. Workflow for each request
|
||||
|
||||
1. Identify target file(s) in `docs/pages/**` (or determine none exist yet — see section 7).
|
||||
1. Identify target file(s) in `docs/content/**` (or determine none exist yet — see section 7).
|
||||
2. Read the target page and 2-3 nearby pages in the same category to match tone and conventions.
|
||||
3. Read the underlying component/plugin source per section 6 so examples are accurate, not guessed.
|
||||
4. Apply the schema from section 4.
|
||||
@@ -113,6 +114,17 @@ Prose and example markup are only as accurate as their source. Before writing ex
|
||||
8. Ensure prose is in simple English.
|
||||
9. Verify heading hierarchy (`##` then `###`) and snippet validity.
|
||||
|
||||
## 8a. Checking a page on the published site
|
||||
|
||||
Two hosts serve the docs, and they are not in sync:
|
||||
|
||||
| Host | Serves |
|
||||
| --- | --- |
|
||||
| `docs.tabler.io` | the last release — can be many commits behind `dev` |
|
||||
| `docs-dev.tabler.io` | the `dev` branch |
|
||||
|
||||
Check a page's live url on `docs-dev.tabler.io`. Production still serves the pre-content-collection urls, so a page that exists in `docs/content/**` can return 404 there while being perfectly fine. A 404 on `docs.tabler.io` alone never justifies renaming a page or rewriting a link — confirm on `docs-dev.tabler.io` first.
|
||||
|
||||
## 9. Rules while updating existing docs
|
||||
|
||||
- Preserve valid existing content that is still correct.
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env node
|
||||
// Validates internal links in the docs at the source level (no build needed):
|
||||
// - markdown links and `related:` frontmatter in docs/content/**/*.mdx
|
||||
// - menu/link urls in shared/data/docs.json
|
||||
// - redirect destinations in docs/astro.config.mjs
|
||||
// - href string literals in docs .astro components
|
||||
// - docs paths linked from the demo site: <DocsLink path="…"> and getDocsUrl('…')
|
||||
// - asset paths that actually render: src, poster and CSS url() in mdx and in
|
||||
// docs .astro components
|
||||
// Anchors are checked against heading slugs computed with github-slugger —
|
||||
// the same library the markdown pipeline uses. Demo markup inside <Example>
|
||||
// blocks and code fences is ignored; its links are illustrative by design.
|
||||
// Assets are the exception: an <img> inside an <Example> really renders on the
|
||||
// page, so those are checked. Code fences stay exempt — a snippet points at
|
||||
// files in the reader's own project (/media/clip.mp4), not at ours.
|
||||
// Run: pnpm run check-docs-links
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { join, dirname, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob'
|
||||
import GithubSlugger from 'github-slugger'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '..')
|
||||
const pagesDir = join(repoRoot, 'docs', 'pages')
|
||||
// Docs content sits outside the routing dir — pages/[...slug].astro renders it
|
||||
// from the `docs` collection (see docs/content.config.ts).
|
||||
const contentDir = join(repoRoot, 'docs', 'content')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route table: every docs page, keyed by its url.
|
||||
// ---------------------------------------------------------------------------
|
||||
const routeFiles = new Map<string, string>()
|
||||
const addRoute = (baseDir: string, file: string) => {
|
||||
const url =
|
||||
'/' +
|
||||
relative(baseDir, file)
|
||||
.replace(/\/?index\.(?:mdx|astro)$/, '')
|
||||
.replace(/\.(?:mdx|astro)$/, '')
|
||||
routeFiles.set(url === '/404' ? url : url.replace(/\/$/, '') || '/', file)
|
||||
}
|
||||
for (const file of sync(join(contentDir, '**', '*.mdx'))) addRoute(contentDir, file)
|
||||
// dynamic routes are expanded from the content dir above, not routes of their own
|
||||
for (const file of sync(join(pagesDir, '**', '*.astro'))) {
|
||||
if (!file.includes('[')) addRoute(pagesDir, file)
|
||||
}
|
||||
// Endpoints keep their extension in the route: robots.txt.ts → /robots.txt,
|
||||
// sitemap.xml.ts → /sitemap.xml, llms.txt.ts → /llms.txt.
|
||||
for (const file of sync(join(pagesDir, '**', '*.ts'))) {
|
||||
if (file.includes('[')) continue
|
||||
routeFiles.set(`/${relative(pagesDir, file).replace(/\.ts$/, '')}`, file)
|
||||
}
|
||||
|
||||
// Redirect sources are valid link targets; their destinations must exist.
|
||||
const astroConfig = readFileSync(join(repoRoot, 'docs', 'astro.config.mjs'), 'utf8')
|
||||
const redirectSources = new Set<string>()
|
||||
const redirectDestinations: string[] = []
|
||||
// static entries: '/from': { status: 301, destination: '/to' }
|
||||
for (const match of astroConfig.matchAll(/'(\/[^']+)':\s*\{\s*status:\s*\d+,\s*destination:\s*'(\/[^']+)'/g)) {
|
||||
redirectSources.add(match[1] ?? '')
|
||||
redirectDestinations.push(match[2] ?? '')
|
||||
}
|
||||
// generated entries: ['alerts', 'alert'], … mapped onto /ui/components/<slug>
|
||||
for (const match of astroConfig.matchAll(/\['([a-z-]+)',\s*'([a-z-]+)'\]/g)) {
|
||||
redirectSources.add(`/ui/components/${match[1]}`)
|
||||
redirectDestinations.push(`/ui/components/${match[2]}`)
|
||||
}
|
||||
|
||||
// Asset urls are served from these source directories (synced by copyAssets).
|
||||
const assetRoots: Record<string, string[]> = {
|
||||
'/img': [join(repoRoot, 'docs', 'assets', 'img')],
|
||||
'/static': [join(repoRoot, 'shared', 'static')],
|
||||
}
|
||||
// Built artifacts — not resolvable without a build. /css/ is compiled from
|
||||
// docs/scss/ into tmp-assets/css (see the docs `css` script).
|
||||
const skipPrefixes = ['/dist/', '/preview/', '/css/', '/_vercel/', '/_astro/']
|
||||
// Element ids rendered by DocsLayout, valid as anchors on every page.
|
||||
const layoutIds = new Set(['content', 'related', 'docsearch', 'menu', 'toc'])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heading slugs per page, computed like the markdown pipeline does.
|
||||
// ---------------------------------------------------------------------------
|
||||
const slugCache = new Map<string, Set<string>>()
|
||||
const headingSlugsOf = (file: string): Set<string> => {
|
||||
let slugs = slugCache.get(file)
|
||||
if (!slugs) {
|
||||
const slugger = new GithubSlugger()
|
||||
slugs = new Set<string>()
|
||||
if (file.endsWith('.mdx')) {
|
||||
for (const line of stripDemosAndCode(body(file)).split('\n')) {
|
||||
const heading = line.match(/^#{1,6}\s+(.*)$/)
|
||||
if (heading) {
|
||||
const text = (heading[1] ?? '').replace(/[`*_]|\[([^\]]*)\]\([^)]*\)/g, '$1').trim()
|
||||
slugs.add(slugger.slug(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
slugCache.set(file, slugs)
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
|
||||
const body = (file: string): string => {
|
||||
const source = readFileSync(file, 'utf8')
|
||||
return source.startsWith('---') ? (source.split(/^---$/m, 3)[2] ?? '') : source
|
||||
}
|
||||
|
||||
const frontmatter = (file: string): string => {
|
||||
const source = readFileSync(file, 'utf8')
|
||||
return source.startsWith('---') ? (source.split(/^---$/m, 3)[1] ?? '') : ''
|
||||
}
|
||||
|
||||
const stripCode = (text: string): string => text.replace(/^```[\s\S]*?^```/gm, '').replace(/`[^`\n]*`/g, '')
|
||||
|
||||
const stripDemosAndCode = (text: string): string =>
|
||||
stripCode(text)
|
||||
.replace(/<Example\b[\s\S]*?<\/Example>/g, '')
|
||||
.replace(/<Example\b[^>]*\/>/g, '')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
const errors: string[] = []
|
||||
let checkedLinks = 0
|
||||
|
||||
const checkTarget = (sourceLabel: string, link: string, sourceFile?: string) => {
|
||||
checkedLinks++
|
||||
const [pathPart = '', fragment] = link.split('#') as [string, string | undefined]
|
||||
|
||||
if (pathPart === '') {
|
||||
if (fragment && sourceFile && !headingSlugsOf(sourceFile).has(fragment) && !layoutIds.has(fragment)) {
|
||||
errors.push(`${sourceLabel}: dead anchor "#${fragment}"`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (skipPrefixes.some((prefix) => pathPart.startsWith(prefix))) return
|
||||
|
||||
const cleanPath = pathPart.replace(/\/+$/, '') || '/'
|
||||
const assetRoot = Object.keys(assetRoots).find((prefix) => cleanPath.startsWith(`${prefix}/`))
|
||||
if (assetRoot) {
|
||||
const roots = assetRoots[assetRoot] ?? []
|
||||
if (!roots.some((root) => existsSync(join(root, cleanPath.slice(assetRoot.length + 1))))) {
|
||||
errors.push(`${sourceLabel}: dead asset link "${link}"`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Root-level files (favicon.ico, apple-touch-icon.png, …) live in docs/assets
|
||||
// or docs/public and are copied to the site root at build time. Endpoint routes
|
||||
// look the same (/llms.txt) but are generated, so they are matched as routes below.
|
||||
if (!routeFiles.has(cleanPath) && /^\/[^/]+\.[a-z0-9]+$/i.test(cleanPath)) {
|
||||
const file = cleanPath.slice(1)
|
||||
if (![join(repoRoot, 'docs', 'assets', file), join(repoRoot, 'docs', 'public', file)].some((p) => existsSync(p))) {
|
||||
errors.push(`${sourceLabel}: dead asset link "${link}"`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (redirectSources.has(cleanPath)) return
|
||||
const targetFile = routeFiles.get(cleanPath)
|
||||
if (!targetFile) {
|
||||
errors.push(`${sourceLabel}: dead link "${link}"`)
|
||||
return
|
||||
}
|
||||
if (fragment && !headingSlugsOf(targetFile).has(fragment) && !layoutIds.has(fragment)) {
|
||||
errors.push(`${sourceLabel}: dead anchor "${link}"`)
|
||||
}
|
||||
}
|
||||
|
||||
// Asset references that the browser really fetches: src, poster and CSS url().
|
||||
// Only literal absolute paths are checked — `src={...}` is built at render time
|
||||
// and cannot be resolved here.
|
||||
const assetRefPattern = /(?:src|poster)="(\/[^"]+)"|url\((\/[^)"']+)\)/g
|
||||
|
||||
const checkAsset = (sourceLabel: string, reference: string) => {
|
||||
checkedLinks++
|
||||
const path = (reference.split(/[?#]/)[0] ?? '').replace(/\/+$/, '')
|
||||
if (skipPrefixes.some((prefix) => path.startsWith(prefix))) return
|
||||
|
||||
const assetRoot = Object.keys(assetRoots).find((prefix) => path.startsWith(`${prefix}/`))
|
||||
if (!assetRoot) {
|
||||
// Outside a known root the file cannot be resolved from the repo, so the
|
||||
// reference is only reportable when it claims a directory we do own.
|
||||
return
|
||||
}
|
||||
const roots = assetRoots[assetRoot] ?? []
|
||||
if (!roots.some((root) => existsSync(join(root, path.slice(assetRoot.length + 1))))) {
|
||||
errors.push(`${sourceLabel}: dead asset "${reference}"`)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Markdown links + `related:` frontmatter in every mdx page.
|
||||
for (const file of sync(join(contentDir, '**', '*.mdx'))) {
|
||||
const label = relative(repoRoot, file)
|
||||
const prose = stripDemosAndCode(body(file))
|
||||
|
||||
for (const match of prose.matchAll(/\]\(([^)\s]+)\)/g)) {
|
||||
const link = match[1] ?? ''
|
||||
if (/^(?:https?:|mailto:|tel:)/.test(link)) continue
|
||||
if (!link.startsWith('/') && !link.startsWith('#')) {
|
||||
errors.push(`${label}: relative link "${link}" (use an absolute path)`)
|
||||
continue
|
||||
}
|
||||
checkTarget(label, link, file)
|
||||
}
|
||||
|
||||
// Assets render from demo markup too, so this uses the code-only stripper.
|
||||
for (const match of stripCode(body(file)).matchAll(assetRefPattern)) {
|
||||
checkAsset(label, match[1] ?? match[2] ?? '')
|
||||
}
|
||||
|
||||
// `related:` accepts both the array and the scalar form (see content.config.ts).
|
||||
const related = frontmatter(file).match(/^related:\s*(.+)$/m)
|
||||
if (related) {
|
||||
const value = (related[1] ?? '').trim()
|
||||
const entries = value.startsWith('[') ? value.replace(/^\[|\]$/g, '').split(',') : [value]
|
||||
for (const url of entries.map((entry) => entry.trim().replace(/^['"]|['"]$/g, ''))) {
|
||||
if (url) checkTarget(`${label} (related)`, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Menu and resource links in docs.json.
|
||||
const docsJson = JSON.parse(readFileSync(join(repoRoot, 'shared', 'data', 'docs.json'), 'utf8')) as {
|
||||
menu?: unknown
|
||||
links?: { url?: string }[]
|
||||
}
|
||||
const walkMenu = (nodes: { url?: string; children?: unknown }[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.url?.startsWith('/')) checkTarget('shared/data/docs.json', node.url)
|
||||
if (Array.isArray(node.children)) walkMenu(node.children as { url?: string; children?: unknown }[])
|
||||
}
|
||||
}
|
||||
walkMenu((docsJson.menu as { url?: string; children?: unknown }[]) ?? [])
|
||||
|
||||
// 3. Redirect destinations must resolve to real pages.
|
||||
for (const destination of redirectDestinations) {
|
||||
checkedLinks++
|
||||
if (!routeFiles.has(destination)) {
|
||||
errors.push(`docs/astro.config.mjs: redirect destination "${destination}" does not exist`)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Internal href literals in docs components, layouts and astro pages.
|
||||
for (const file of sync(join(repoRoot, 'docs', '{components,layouts,pages}', '**', '*.astro'))) {
|
||||
const label = relative(repoRoot, file)
|
||||
const source = readFileSync(file, 'utf8')
|
||||
for (const match of source.matchAll(/href(?:=|:\s*)["'](\/[^"']*)["']/g)) {
|
||||
checkTarget(label, match[1] ?? '')
|
||||
}
|
||||
for (const match of source.matchAll(assetRefPattern)) {
|
||||
checkAsset(label, match[1] ?? match[2] ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Docs paths used by the demo site — <DocsLink path="…"> and getDocsUrl('…').
|
||||
// These render as absolute docs.tabler.io urls, so a typo only shows up in a browser.
|
||||
for (const file of sync(join(repoRoot, '{preview,shared}', '**', '*.astro'), { ignore: '**/node_modules/**' })) {
|
||||
const label = relative(repoRoot, file)
|
||||
const source = readFileSync(file, 'utf8')
|
||||
for (const match of source.matchAll(/<DocsLink\b[^>]*?\spath=["'](\/[^"']*)["']/g)) {
|
||||
checkTarget(label, match[1] ?? '')
|
||||
}
|
||||
for (const match of source.matchAll(/getDocsUrl\(\s*["'](\/[^"']*)["']/g)) {
|
||||
checkTarget(label, match[1] ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueErrors = [...new Set(errors)]
|
||||
if (uniqueErrors.length > 0) {
|
||||
console.error(`Found ${uniqueErrors.length} dead reference(s) (${checkedLinks} links and assets checked):\n`)
|
||||
for (const error of uniqueErrors) console.error(` ${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`OK — ${checkedLinks} links and assets checked across ${routeFiles.size} pages, nothing dead.`)
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates shared/lib/tokens.ts from the core SCSS maps, so the TS
|
||||
// unions used by components can never drift from the stylesheet source of
|
||||
// truth. Values are read through the real Sass compiler (after every merge and
|
||||
// !default), not by parsing the source text.
|
||||
// Run: pnpm run generate-tokens — (re)write the file
|
||||
// pnpm run generate-tokens:check — fail when the committed file is stale
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||
import { join, dirname, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { compileString } from 'sass'
|
||||
import { format, resolveConfig } from 'prettier'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '..')
|
||||
const scssDir = join(repoRoot, 'core', 'scss')
|
||||
const outFile = join(repoRoot, 'shared', 'lib', 'tokens.ts')
|
||||
const mode = process.argv[2] === 'check' ? 'check' : 'generate'
|
||||
|
||||
// One entry per generated const/type; `source` is the Sass member the keys come
|
||||
// from — `kind: 'map'` reads map.keys(), `kind: 'list'` reads the list itself.
|
||||
const TOKENS = [
|
||||
{ name: 'themeColors', type: 'ThemeColor', module: 'v', variable: '$theme-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'socialColors', type: 'SocialColor', module: 'v', variable: '$social-colors', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'avatarSizes', type: 'AvatarSize', module: 'v', variable: '$avatar-sizes', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'aspectRatios', type: 'AspectRatio', module: 'v', variable: '$aspect-ratios', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'breakpoints', type: 'Breakpoint', module: 's', variable: '$grid-breakpoints', kind: 'map', source: 'core/scss/_settings.scss' },
|
||||
{ name: 'breadcrumbVariants', type: 'BreadcrumbVariant', module: 'v', variable: '$breadcrumb-variants', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'formValidationStates', type: 'FormValidationState', module: 'v', variable: '$form-validation-states', kind: 'map', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'paymentProviders', type: 'PaymentProvider', module: 'v', variable: '$payment-providers', kind: 'list', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'flagCountries', type: 'FlagCountry', module: 'v', variable: '$flag-countries', kind: 'list', source: 'core/scss/_variables.scss' },
|
||||
{ name: 'patternSizes', type: 'PatternSize', module: 'p', variable: '$sizes', kind: 'map', source: 'core/scss/ui/_patterns.scss' },
|
||||
] as const
|
||||
|
||||
const entry = `
|
||||
@use 'sass:map';
|
||||
@use 'variables' as v;
|
||||
@use 'settings' as s;
|
||||
@use 'ui/patterns' as p;
|
||||
${TOKENS.map((t) => `@debug 'TOKEN ${t.name}=#{${t.kind === 'map' ? `map.keys(${t.module}.${t.variable})` : `${t.module}.${t.variable}`}}';`).join('\n')}
|
||||
`
|
||||
|
||||
const keysByName = new Map<string, string[]>()
|
||||
compileString(entry, {
|
||||
loadPaths: [scssDir],
|
||||
logger: {
|
||||
debug(message) {
|
||||
const match = message.match(/^TOKEN (\w+)=(.*)$/)
|
||||
if (match?.[1] && match[2] !== undefined) {
|
||||
keysByName.set(
|
||||
match[1],
|
||||
match[2].split(',').map((key) => key.trim()),
|
||||
)
|
||||
}
|
||||
},
|
||||
warn() {
|
||||
/* deprecations from core scss are handled by the css build, not here */
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let output = `// Generated from the core SCSS maps by .build/generate-tokens.ts — DO NOT EDIT.
|
||||
// Regenerate with: pnpm run generate-tokens
|
||||
`
|
||||
for (const token of TOKENS) {
|
||||
const keys = keysByName.get(token.name)
|
||||
if (!keys || keys.length === 0) {
|
||||
console.error(`✖ No keys extracted for ${token.variable} (${token.name}) — check the sass entry.`)
|
||||
process.exit(1)
|
||||
}
|
||||
output += `
|
||||
/** Keys of \`${token.variable}\` (${token.source}). */
|
||||
export const ${token.name} = [${keys.map((key) => `'${key}'`).join(', ')}] as const
|
||||
export type ${token.type} = (typeof ${token.name})[number]
|
||||
`
|
||||
}
|
||||
|
||||
// Wrapped in main() because the root package is CJS (no top-level await).
|
||||
const main = async () => {
|
||||
// Format with the repo prettier config so the generated file passes lint-prettier.
|
||||
const prettierConfig = await resolveConfig(outFile)
|
||||
const formatted = await format(output, { ...prettierConfig, filepath: outFile })
|
||||
|
||||
if (mode === 'check') {
|
||||
const current = existsSync(outFile) ? readFileSync(outFile, 'utf8') : ''
|
||||
if (current !== formatted) {
|
||||
console.error(`✖ ${relative(repoRoot, outFile)} is stale — run "pnpm run generate-tokens" and commit the result.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`✓ ${relative(repoRoot, outFile)} is up to date.`)
|
||||
} else {
|
||||
writeFileSync(outFile, formatted)
|
||||
console.log(`✓ Generated ${relative(repoRoot, outFile)} (${TOKENS.map((t) => t.name).join(', ')}).`)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
// Renders every preview page from a running dev server and compares the HTML
|
||||
// against a stored baseline, so internal refactors can prove they don't change
|
||||
// the rendered output at all. Volatile content (the "Generated <date>" footer)
|
||||
// is normalized away before comparing.
|
||||
// Run: pnpm run html-diff:baseline — capture baseline snapshots (before refactoring)
|
||||
// pnpm run html-diff — re-render and diff against the baseline
|
||||
// The dev server defaults to http://localhost:3000; override with HTML_DIFF_BASE.
|
||||
|
||||
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'
|
||||
import { join, dirname, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '..')
|
||||
const pagesDir = join(repoRoot, 'preview', 'pages')
|
||||
const snapshotDir = join(repoRoot, '.cache', 'html-diff')
|
||||
const baseUrl = process.env.HTML_DIFF_BASE ?? 'http://localhost:3000'
|
||||
const mode = process.argv[2] === 'baseline' ? 'baseline' : 'check'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route table: every preview page url, keyed by its snapshot file name.
|
||||
// ---------------------------------------------------------------------------
|
||||
const routes = new Map<string, string>()
|
||||
for (const file of sync(join(pagesDir, '**', '*.astro'))) {
|
||||
if (file.includes('[')) continue // dynamic routes have no single url
|
||||
const url =
|
||||
'/' +
|
||||
relative(pagesDir, file)
|
||||
.replace(/\/?index\.astro$/, '')
|
||||
.replace(/\.astro$/, '')
|
||||
routes.set((url === '/' ? 'index' : url.slice(1)).replaceAll('/', '_') + '.html', url)
|
||||
}
|
||||
|
||||
// The footer stamps the render time into every page — meaningless for diffing.
|
||||
const normalize = (html: string) => html.replace(/Generated \d{4}[^<]*/g, 'Generated')
|
||||
|
||||
const fetchPage = async (url: string): Promise<string> => {
|
||||
const response = await fetch(baseUrl + url)
|
||||
if (!response.ok) throw new Error(`${response.status} ${baseUrl + url}`)
|
||||
return normalize(await response.text())
|
||||
}
|
||||
|
||||
// Render with a small worker pool — the dev server compiles pages on demand.
|
||||
// Wrapped in main() because the root package is CJS (no top-level await).
|
||||
const CONCURRENCY = 8
|
||||
|
||||
const main = async () => {
|
||||
const entries = [...routes.entries()]
|
||||
const failures: string[] = []
|
||||
const pages = new Map<string, string>()
|
||||
let cursor = 0
|
||||
await Promise.all(
|
||||
Array.from({ length: CONCURRENCY }, async () => {
|
||||
while (cursor < entries.length) {
|
||||
const entry = entries[cursor++]
|
||||
if (!entry) break
|
||||
const [name, url] = entry
|
||||
try {
|
||||
pages.set(name, await fetchPage(url))
|
||||
} catch (error) {
|
||||
failures.push(`${url} — ${error instanceof Error ? error.message : error}`)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`✖ ${failures.length} page(s) failed to render (is the dev server running at ${baseUrl}?):`)
|
||||
for (const failure of failures) console.error(` ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (mode === 'baseline') {
|
||||
rmSync(snapshotDir, { recursive: true, force: true })
|
||||
mkdirSync(snapshotDir, { recursive: true })
|
||||
for (const [name, html] of pages) writeFileSync(join(snapshotDir, name), html)
|
||||
console.log(`✓ Baseline captured: ${pages.size} pages → ${relative(repoRoot, snapshotDir)}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!existsSync(snapshotDir)) {
|
||||
console.error(`✖ No baseline found in ${relative(repoRoot, snapshotDir)} — run "pnpm run html-diff:baseline" first.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Compare against the baseline; report the first differing line per page.
|
||||
let differing = 0
|
||||
for (const [name, html] of pages) {
|
||||
const baselineFile = join(snapshotDir, name)
|
||||
if (!existsSync(baselineFile)) {
|
||||
console.error(`± ${name}: new page (no baseline snapshot)`)
|
||||
differing++
|
||||
continue
|
||||
}
|
||||
const baseline = readFileSync(baselineFile, 'utf8')
|
||||
if (baseline === html) continue
|
||||
differing++
|
||||
// Classify: inter-tag whitespace reshuffles are usually visually inert
|
||||
// (flex containers, select internals) — but review inline contexts manually.
|
||||
const squash = (s: string) => s.replace(/>\s+</g, '><').replace(/\s+/g, ' ')
|
||||
if (squash(baseline) === squash(html)) {
|
||||
console.error(`± ${name}: WHITESPACE-ONLY difference (identical after normalization — verify inline contexts)`)
|
||||
continue
|
||||
}
|
||||
const baselineLines = baseline.split('\n')
|
||||
const currentLines = html.split('\n')
|
||||
// findIndex misses the case where one side is a strict prefix of the other
|
||||
// (lines appended/removed at the end) — point at the first extra line then.
|
||||
let line = baselineLines.findIndex((l, i) => l !== currentLines[i])
|
||||
if (line === -1) line = Math.min(baselineLines.length, currentLines.length)
|
||||
console.error(`± ${name}: differs at line ${line + 1}`)
|
||||
console.error(` - ${(baselineLines[line] ?? '<missing>').trim().slice(0, 160)}`)
|
||||
console.error(` + ${(currentLines[line] ?? '<missing>').trim().slice(0, 160)}`)
|
||||
}
|
||||
for (const name of sync(join(snapshotDir, '*.html'))) {
|
||||
const snapshotName = relative(snapshotDir, name)
|
||||
if (!pages.has(snapshotName)) {
|
||||
console.error(`± ${snapshotName}: page removed (baseline snapshot has no live page)`)
|
||||
differing++
|
||||
}
|
||||
}
|
||||
|
||||
if (differing > 0) {
|
||||
console.error(`✖ ${differing} of ${pages.size} pages differ from the baseline.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`✓ All ${pages.size} pages are byte-identical to the baseline.`)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
// Imports payment provider assets from the tabler/tabler-payments repo, so the
|
||||
// payments plugin never drifts from the icon source of truth:
|
||||
// - core/img/payments/ <- src/light/*.svg + src/dark/*.svg (as *-dark.svg)
|
||||
// - $payment-providers in core/scss/_variables.scss
|
||||
// - shared/data/payments.json <- payments.json (name + logo, demo data)
|
||||
//
|
||||
// Source checkout resolution order:
|
||||
// 1. TABLER_PAYMENTS_DIR env var
|
||||
// 2. ../tabler-payments (sibling checkout)
|
||||
// 3. fresh shallow clone into a temp dir (the repo is private — needs git auth)
|
||||
//
|
||||
// Run: pnpm run import-payments
|
||||
// Then: pnpm run generate-tokens — refreshes PaymentProvider in shared/lib/tokens.ts
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, dirname, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '..')
|
||||
const imgDir = join(repoRoot, 'core', 'img', 'payments')
|
||||
const scssFile = join(repoRoot, 'core', 'scss', '_variables.scss')
|
||||
const dataFile = join(repoRoot, 'shared', 'data', 'payments.json')
|
||||
|
||||
let cloneDir: string | undefined
|
||||
|
||||
function resolveSourceDir(): string {
|
||||
const candidates = [process.env.TABLER_PAYMENTS_DIR, join(repoRoot, '..', 'tabler-payments')].filter((dir): dir is string => Boolean(dir))
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(join(dir, 'src', 'light'))) return dir
|
||||
}
|
||||
cloneDir = mkdtempSync(join(tmpdir(), 'tabler-payments-'))
|
||||
console.log('No local checkout found, cloning tabler/tabler-payments…')
|
||||
execFileSync('git', ['clone', '--depth', '1', 'https://github.com/tabler/tabler-payments.git', cloneDir], { stdio: 'inherit' })
|
||||
return cloneDir
|
||||
}
|
||||
|
||||
// Provider order everywhere follows the SVG filename (with extension), so
|
||||
// 'amazon-pay' sorts before 'amazon' — same as an `ls` of the source dir.
|
||||
const svgNames = (dir: string) =>
|
||||
readdirSync(dir)
|
||||
.filter((file) => file.endsWith('.svg'))
|
||||
.sort()
|
||||
.map((file) => file.slice(0, -'.svg'.length))
|
||||
|
||||
try {
|
||||
const sourceDir = resolveSourceDir()
|
||||
const lightDir = join(sourceDir, 'src', 'light')
|
||||
const darkDir = join(sourceDir, 'src', 'dark')
|
||||
const metaFile = join(sourceDir, 'payments.json')
|
||||
for (const path of [lightDir, darkDir, metaFile]) {
|
||||
if (!existsSync(path)) throw new Error(`import-payments: missing ${path} — not a tabler-payments checkout?`)
|
||||
}
|
||||
|
||||
const providers = svgNames(lightDir)
|
||||
const darkProviders = svgNames(darkDir)
|
||||
if (providers.join() !== darkProviders.join()) {
|
||||
const light = new Set(providers)
|
||||
const dark = new Set(darkProviders)
|
||||
const onlyLight = providers.filter((name) => !dark.has(name))
|
||||
const onlyDark = darkProviders.filter((name) => !light.has(name))
|
||||
throw new Error(`import-payments: light/dark sets differ (only light: ${onlyLight.join(', ') || '—'}; only dark: ${onlyDark.join(', ') || '—'})`)
|
||||
}
|
||||
|
||||
// 1. core/img/payments — clean replace, dark variants get a `-dark` suffix
|
||||
const previous = existsSync(imgDir) ? new Set(svgNames(imgDir).map((name) => name.replace(/-dark$/, ''))) : new Set<string>()
|
||||
rmSync(imgDir, { recursive: true, force: true })
|
||||
mkdirSync(imgDir, { recursive: true })
|
||||
for (const name of providers) {
|
||||
copyFileSync(join(lightDir, `${name}.svg`), join(imgDir, `${name}.svg`))
|
||||
copyFileSync(join(darkDir, `${name}.svg`), join(imgDir, `${name}-dark.svg`))
|
||||
}
|
||||
|
||||
// 2. $payment-providers in core/scss/_variables.scss
|
||||
const scss = readFileSync(scssFile, 'utf8')
|
||||
const listPattern = /(\$payment-providers: \(\n)[\s\S]*?(\n\);)/
|
||||
if (!listPattern.test(scss)) throw new Error(`import-payments: $payment-providers list not found in ${scssFile}`)
|
||||
const listBody = providers.map((name) => ` '${name}'`).join(',\n')
|
||||
writeFileSync(scssFile, scss.replace(listPattern, `$1${listBody}$2`))
|
||||
|
||||
// 3. shared/data/payments.json — demo data keeps its `{ name, logo }` shape
|
||||
const meta = JSON.parse(readFileSync(metaFile, 'utf8')) as { name: string; logo: string }[]
|
||||
const names = new Map(meta.map((entry) => [entry.logo, entry.name]))
|
||||
for (const logo of names.keys()) {
|
||||
if (!providers.includes(logo)) console.warn(`import-payments: payments.json entry '${logo}' has no SVG — skipped`)
|
||||
}
|
||||
const data = providers.map((logo) => {
|
||||
const name = names.get(logo)
|
||||
if (!name) console.warn(`import-payments: no payments.json metadata for '${logo}' — using the slug as name`)
|
||||
return { name: name ?? logo, logo }
|
||||
})
|
||||
writeFileSync(dataFile, `${JSON.stringify(data, null, 2)}\n`)
|
||||
|
||||
const added = providers.filter((name) => !previous.has(name))
|
||||
const removed = [...previous].filter((name) => !providers.includes(name))
|
||||
console.log(`Imported ${providers.length} payment providers from ${relative(repoRoot, sourceDir)} (${added.length} added, ${removed.length} removed)`)
|
||||
if (added.length) console.log(` added: ${added.join(', ')}`)
|
||||
if (removed.length) console.log(` removed: ${removed.join(', ')}`)
|
||||
console.log('Now run `pnpm run generate-tokens` to refresh shared/lib/tokens.ts')
|
||||
} finally {
|
||||
if (cloneDir) rmSync(cloneDir, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob'
|
||||
import * as prettier from 'prettier'
|
||||
import { compile } from '@mdx-js/mdx'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const docs: string[] = sync(join(__dirname, '..', 'docs', 'content', '**', '*.mdx'))
|
||||
|
||||
// Finding nothing means the docs moved and this glob was not updated. Exiting 0
|
||||
// with no output would look like "everything is already formatted".
|
||||
if (docs.length === 0) {
|
||||
console.error('No docs pages found — check the glob in this script.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function formatHTML(htmlString: string, options: prettier.Options = {}): Promise<string> {
|
||||
try {
|
||||
const formattedHtml = await prettier.format(htmlString, {
|
||||
parser: 'html',
|
||||
printWidth: 100,
|
||||
...options,
|
||||
})
|
||||
return formattedHtml
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error formatting HTML:', errorMessage)
|
||||
return htmlString // Return original in case of an error
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceAsync(str: string, regex: RegExp, asyncFn: (...args: string[]) => Promise<string>): Promise<string> {
|
||||
const matches = [...str.matchAll(regex)]
|
||||
|
||||
const replacements = await Promise.all(matches.map(async (match: RegExpMatchArray) => asyncFn(...(match as unknown as string[]))))
|
||||
|
||||
// Splice by match index. A string `.replace()` would hit the first occurrence
|
||||
// every time, so two identical examples in one file would rewrite the same
|
||||
// spot twice and leave the other untouched.
|
||||
let result = ''
|
||||
let last = 0
|
||||
matches.forEach((match: RegExpMatchArray, i: number) => {
|
||||
const start = match.index ?? 0
|
||||
result += str.slice(last, start) + replacements[i]
|
||||
last = start + match[0].length
|
||||
})
|
||||
|
||||
return result + str.slice(last)
|
||||
}
|
||||
|
||||
/**
|
||||
* `<Example>` slots hold the demo markup, so they get the same treatment as an
|
||||
* ```html fence.
|
||||
*
|
||||
* Slots containing `{` are skipped. In MDX every unescaped brace is a JSX
|
||||
* expression — a component prop (`title={…}`), or the template literal that
|
||||
* `<script>` and `<style>` demos are wrapped in so their JS/CSS braces survive.
|
||||
* The html parser either throws on those ("Opening tag not terminated") or
|
||||
* reformats the embedded code and breaks the expression. Embedded formatting is
|
||||
* off as a second guard.
|
||||
*
|
||||
* Empty lines are stripped: a blank line inside the slot would make MDX start a
|
||||
* new paragraph. Raw text is glued to the preceding markup line: markdown
|
||||
* syntax is line-based, so a text line of its own can turn into a heading
|
||||
* (`=`), a list (`+ 2`) or a blockquote (`>`); glued after a tag it stays
|
||||
* phrasing text, and the docs pipeline unwraps the paragraph MDX wraps it in
|
||||
* (docs/lib/satteri-unwrap-jsx-paragraphs.mjs).
|
||||
*
|
||||
* The real MDX compiler has the final word: an example whose formatted form
|
||||
* it rejects keeps its original form.
|
||||
*/
|
||||
async function formatExamples(source: string): Promise<string> {
|
||||
return replaceAsync(source, /(<Example\b[^>]*>\n)([\s\S]*?)(\n<\/Example>)/g, async (_m: string, open: string, inner: string, close: string) => {
|
||||
if (inner.includes('{')) return _m
|
||||
|
||||
// Whitespace sensitivity is off: MDX drops whitespace between sibling JSX
|
||||
// elements anyway, and with the default `css` sensitivity prettier glues
|
||||
// inline siblings together (`</span><span`) and wraps inside their tags.
|
||||
const formatted = glueTextLines((await formatHTML(inner, { embeddedLanguageFormatting: 'off', htmlWhitespaceSensitivity: 'ignore' })).replace(/^\s*[\r\n]/gm, '').trim())
|
||||
if (!formatted) return _m
|
||||
|
||||
if (!(await compilesAsMdx(open + formatted + close))) {
|
||||
console.warn(`Left one example as-is (MDX cannot parse the formatted form)`)
|
||||
return _m
|
||||
}
|
||||
|
||||
return open + formatted + close
|
||||
})
|
||||
}
|
||||
|
||||
async function compilesAsMdx(source: string): Promise<boolean> {
|
||||
try {
|
||||
await compile(source)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// A complete tag; character classes also match newlines, so a tag spread over
|
||||
// several lines (one attribute per line) matches too.
|
||||
const tagPattern = /<\/?[a-zA-Z][\w.-]*(?:"[^"]*"|'[^']*'|[^<>"'])*?\/?>/g
|
||||
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
|
||||
|
||||
/** opens minus closes over the tags of one line */
|
||||
function lineBalance(line: string): number {
|
||||
let depth = 0
|
||||
for (const m of line.matchAll(/<(\/)?([a-zA-Z][\w.-]*)((?:"[^"]*"|'[^']*'|[^<>"'])*?)(\/)?>/g)) {
|
||||
if (m[1]) depth--
|
||||
else if (!m[4] && !voidTags.has(m[2]!.toLowerCase())) depth++
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
const hasText = (line: string): boolean => line.replace(/<[^<>]*>/g, '').trim().length > 0
|
||||
|
||||
/** The tags of a fragment in order, for comparing markup before and after formatting. */
|
||||
const tagSequence = (html: string): string => [...html.matchAll(/<(\/?)([a-zA-Z][\w.-]*)/g)].map((m) => `${m[1]}${m[2]!.toLowerCase()}`).join(',')
|
||||
|
||||
/**
|
||||
* Merge raw-text lines back into their markup (see formatExamples). MDX parses
|
||||
* a line that mixes text and tags as one markdown paragraph, and every tag in
|
||||
* it must open and close within that paragraph — so a text line joins the
|
||||
* preceding line, and the joined line keeps absorbing following lines until
|
||||
* its tags balance. Tags spread over several lines are folded up first so the
|
||||
* balance is countable per line; examples without raw text keep prettier's
|
||||
* output untouched.
|
||||
*/
|
||||
function glueTextLines(formatted: string): string {
|
||||
const lines = formatted.split('\n')
|
||||
if (!lines.some((line) => hasText(line))) return formatted
|
||||
|
||||
const out: string[] = []
|
||||
for (const line of formatted.replace(tagPattern, (tag) => tag.replace(/\s+/g, ' ').replace(/ >$/, '>')).split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
const tail = out[out.length - 1]
|
||||
const absorbing = tail !== undefined && hasText(tail) && lineBalance(tail) > 0
|
||||
if (tail !== undefined && trimmed && (absorbing || !trimmed.startsWith('<'))) out[out.length - 1] = `${tail} ${trimmed}`
|
||||
else out.push(line)
|
||||
// A text line can also land after a closing tag whose opener sits on an
|
||||
// earlier line (label after a multi-line svg) — pull those lines up too.
|
||||
while (out.length > 1 && hasText(out[out.length - 1]!) && lineBalance(out[out.length - 1]!) < 0) {
|
||||
const merged = `${out[out.length - 2]} ${out[out.length - 1]!.trim()}`
|
||||
out.splice(out.length - 2, 2, merged)
|
||||
}
|
||||
}
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
async function processFiles(): Promise<void> {
|
||||
for (const file of docs) {
|
||||
const oldContent = readFileSync(file, 'utf8')
|
||||
|
||||
// Code fences, anchored to column 0: an indented fence belongs to a list
|
||||
// item, and reprinting it without that indentation breaks the list.
|
||||
let content = await replaceAsync(oldContent, /^(```([a-z0-9]+).*?\n)([\s\S]*?)^(```)/gm, async (m: string, m1: string, m2: string, m3: string, m4: string) => {
|
||||
if (m2 === 'html') {
|
||||
let formattedHtml = await formatHTML(m3)
|
||||
|
||||
// remove empty lines
|
||||
formattedHtml = formattedHtml.replace(/^\s*[\r\n]/gm, '')
|
||||
|
||||
// Fences hold illustrative snippets, not always whole documents, and
|
||||
// js-beautify silently closes what it thinks is unclosed. Only take
|
||||
// its output when it rewrapped the same tags it was given.
|
||||
if (tagSequence(formattedHtml) !== tagSequence(m3)) return m
|
||||
|
||||
return m1 + formattedHtml.trim() + '\n' + m4
|
||||
}
|
||||
return m.trim()
|
||||
})
|
||||
|
||||
content = await formatExamples(content)
|
||||
|
||||
if (content !== oldContent) {
|
||||
writeFileSync(file, content, 'utf8')
|
||||
console.log(`Reformatted ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles().catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error processing files:', errorMessage)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob'
|
||||
import * as prettier from 'prettier'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const docs: string[] = sync(join(__dirname, '..', 'docs', 'pages', '**', '*.mdx'))
|
||||
|
||||
async function formatHTML(htmlString: string): Promise<string> {
|
||||
try {
|
||||
const formattedHtml = await prettier.format(htmlString, {
|
||||
parser: 'html',
|
||||
printWidth: 100,
|
||||
})
|
||||
return formattedHtml
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error formatting HTML:', errorMessage)
|
||||
return htmlString // Return original in case of an error
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceAsync(str: string, regex: RegExp, asyncFn: (...args: string[]) => Promise<string>): Promise<string> {
|
||||
const matches = [...str.matchAll(regex)]
|
||||
|
||||
const replacements = await Promise.all(matches.map(async (match: RegExpMatchArray) => asyncFn(...match)))
|
||||
|
||||
let result = str
|
||||
matches.forEach((match: RegExpMatchArray, i: number) => {
|
||||
result = result.replace(match[0], replacements[i])
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function processFiles(): Promise<void> {
|
||||
for (const file of docs) {
|
||||
const oldContent = readFileSync(file, 'utf8')
|
||||
|
||||
// get codeblocks from markdown
|
||||
const content = await replaceAsync(oldContent, /(```([a-z0-9]+).*?\n)(.*?)(```)/gs, async (m: string, m1: string, m2: string, m3: string, m4: string) => {
|
||||
if (m2 === 'html') {
|
||||
let formattedHtml = await formatHTML(m3)
|
||||
|
||||
// remove empty lines
|
||||
formattedHtml = formattedHtml.replace(/^\s*[\r\n]/gm, '')
|
||||
|
||||
return m1 + formattedHtml.trim() + '\n' + m4
|
||||
}
|
||||
return m.trim()
|
||||
})
|
||||
|
||||
if (content !== oldContent) {
|
||||
writeFileSync(file, content, 'utf8')
|
||||
console.log(`Reformatted ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles().catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error processing files:', errorMessage)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Converted `<a href="#">` controls that only trigger JavaScript (dropdown toggles, toast dismiss, modal and carousel controls) into real `<button>` elements so they work with the keyboard, and removed `tabindex="-1"` from navbar toggles and links that were wrongly skipped when tabbing.
|
||||
Fixed keyboard access by turning JavaScript-only `<a href="#">` controls into real `<button>` elements.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added missing ARIA roles and states to `Pagination`, `NavSegmented`, `Accordion`, `Steps`, tab components, `Modal`, `Offcanvas` and `CarouselCard`, including `aria-current`, `aria-controls`, `aria-labelledby`, `aria-modal` and `role="tabpanel"`, and fixed a bug where the active `Steps` index was never highlighted because it compared a string prop to a number.
|
||||
Added missing ARIA roles and states to `Pagination`, `NavSegmented`, `Accordion`, `Steps`, tabs, `Modal`, `Offcanvas` and `CarouselCard`.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed keyboard focus indicators that were invisible or removed on `.btn-action`, `.switch-icon`, calendar dates and the toast close button, added `prefers-reduced-motion` and `forced-colors` support, stopped overriding the browser's default root font size, and enlarged the `.btn-close` touch target to `24px`.
|
||||
Fixed invisible keyboard focus indicators and added `prefers-reduced-motion` and `forced-colors` support.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added `aria-label` to the `Datepicker` month-navigation buttons and made the `Dropzone` upload area keyboard-operable with `role="button"`, `tabindex="0"` and an `Enter`/`Space` handler.
|
||||
Added `aria-label` to the `Datepicker` navigation buttons and made the `Dropzone` upload area keyboard-operable.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Wrapped radio and checkbox groups in form demos, `ThemeSettings`, `onboarding` and modal content with `<fieldset>` and `<legend>` so screen readers announce the group's purpose before each option.
|
||||
Added `<fieldset>` and `<legend>` around radio and checkbox groups so screen readers announce the group purpose.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Wired every form label to its control with matching `for`/`id` pairs, added `aria-invalid` and `aria-describedby` to validation demos, added correct `autocomplete` tokens to auth and payment forms, and gave the password show/hide toggle in `InputGroup` a working `aria-pressed` state.
|
||||
Fixed form accessibility with matching `for`/`id` labels, `aria-invalid`, `aria-describedby` and `autocomplete` tokens.
|
||||
@@ -3,4 +3,4 @@
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed the "Skip to main content" link so it becomes visible on keyboard focus, added missing `<main>` landmarks to layouts that had none, gave the sidebar and docs navigation `<nav aria-label>` landmarks instead of unlabeled `<div>`s, and fixed heading hierarchy and duplicate page `<title>`s across several preview pages.
|
||||
Fixed the skip link to appear on focus and added missing `<main>` and labelled `<nav>` landmarks.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added a `label` prop to `Flag`, `Payment`, `StatusDot`, `Trending` and the `Avatar` status badge so color-only indicators get a real accessible name, added `role="img"` and `aria-label` to `Chart`, `Map` and `MapVector` containers, and made the `label` prop on `Chart` required.
|
||||
Added a `label` prop to `Flag`, `Payment`, `StatusDot`, `Trending` and `Avatar` status badges for screen readers.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
"@tabler/preview": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed accessibility issues in skip links, keyboard focus, `prefers-reduced-motion`, form labels and action controls.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a Printing docs page covering `d-print-*` utilities and the `media-print` mixin.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": minor
|
||||
---
|
||||
|
||||
Added `.text-gray-*` utility classes (`.text-gray-50` through `.text-gray-950`) alongside the existing `.bg-gray-*` and `.text-gray-*-fg` utilities.
|
||||
Added `.text-gray-50` through `.text-gray-950` utility classes alongside the existing `.bg-gray-*` utilities.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added `added-in` badges to Card gradient, Progress steps and new getting-started guides; fixed background patterns' version.
|
||||
@@ -3,4 +3,4 @@
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Updated the `preview` and `docs` packages to build with Astro instead of Eleventy, keeping the generated pages identical and moving shared components to `shared/astro/`.
|
||||
Updated the `preview` and `docs` packages to build with Astro instead of Eleventy.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
"@tabler/preview": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated `.badges-list` to `.badge-list` and `.tags-list` to `.tag-list`, keeping the old names as deprecated aliases.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Refactored Bootstrap exports to use single source of truth in `bootstrap.js` and removed duplicate exports from `tabler.js` for better maintainability.
|
||||
Updated Bootstrap exports to a single source of truth in `bootstrap.js` and removed the duplicates from `tabler.js`.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": minor
|
||||
---
|
||||
|
||||
Vendored Bootstrap 5.3.8 into core: migrated SCSS and JS to Tabler source tree, refactored mixins to module syntax, converted Bootstrap components to TypeScript.
|
||||
Added Bootstrap 5.3.8 to the core source tree, with SCSS as modules and components converted to TypeScript.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Fixed root-absolute `Button` hrefs in demos, so error-page links work in the downloadable package.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added a short description and a docs link to each card on the Buttons demo page, grouped into Styles and Colors.
|
||||
@@ -3,4 +3,3 @@
|
||||
---
|
||||
|
||||
Added `.card-gradient` component with gradient variants, direction modifiers, and animated backgrounds.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": minor
|
||||
---
|
||||
|
||||
Added `--card-header-bg` and `--card-footer-bg` CSS variables (with `$card-header-bg` and `$card-footer-bg` Sass defaults) so `.card-header` and `.card-footer` backgrounds can be overridden independently, and made `.card-header` transparent by default instead of using `--card-cap-bg`.
|
||||
Added `--card-header-bg` and `--card-footer-bg` variables so both backgrounds can be overridden independently.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Fixed heading order by making `CardTitle` and `CardHeader` render `h2`, so cards no longer skip a level under the page `h1`.
|
||||
@@ -2,5 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added Change Password modal with current password, new password with strength indicator, confirm password validation, and show/hide password toggles.
|
||||
|
||||
Added Change Password modal with a strength indicator, confirm validation and show/hide toggles.
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Changelog generator for Tabler.
|
||||
*
|
||||
* Same as `@changesets/cli/changelog`, except it drops the
|
||||
* "Updated dependencies [...]" block. `@tabler/core`, `@tabler/preview` and
|
||||
* `@tabler/docs` are a linked group, so they always share one version number —
|
||||
* listing every core changeset commit in the preview and docs changelogs added
|
||||
* a hundred lines that said nothing the version number does not already say.
|
||||
*/
|
||||
|
||||
const getReleaseLine = async (changeset) => {
|
||||
const [firstLine, ...futureLines] = changeset.summary.split('\n').map((line) => line.trimEnd())
|
||||
|
||||
let releaseLine = `- ${changeset.commit ? `${changeset.commit.slice(0, 7)}: ` : ''}${firstLine}`
|
||||
|
||||
if (futureLines.length > 0) {
|
||||
releaseLine += `\n${futureLines.map((line) => ` ${line}`).join('\n')}`
|
||||
}
|
||||
|
||||
return releaseLine
|
||||
}
|
||||
|
||||
const getDependencyReleaseLine = async () => ''
|
||||
|
||||
module.exports = {
|
||||
getReleaseLine,
|
||||
getDependencyReleaseLine,
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Replaced hardcoded `rem` and `px` spacing, sizing, and offset values with SCSS variables across icon, avatar, badge, button, card, alert, empty state, ribbon, status, dropdown, form, nav, pagination, progress, steps, and tag components for easier theme customization.
|
||||
Updated hardcoded `rem` and `px` values to SCSS variables across core components for easier theming.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"changelog": "./changelog.cjs",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [
|
||||
@@ -12,7 +12,10 @@
|
||||
],
|
||||
"access": "public",
|
||||
"baseBranch": "dev",
|
||||
"ignore": [],
|
||||
"ignore": [
|
||||
"@tabler/shared",
|
||||
"@tabler/screenshots"
|
||||
],
|
||||
"privatePackages": {
|
||||
"version": true,
|
||||
"tag": false
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added Confirm Delete modal with warning icon, confirmation checkbox, and JavaScript validation to enable delete button only when confirmed.
|
||||
|
||||
Added Confirm Delete modal with a warning icon and a checkbox that enables the delete button.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Updated the npm package README with badges, quick links, CDN usage, documentation and changelog links.
|
||||
@@ -3,4 +3,3 @@
|
||||
---
|
||||
|
||||
Added new Crypto Dashboard page with cryptocurrency portfolio overview, market data, and order history.
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added crypto markets and orders data files (`crypto-markets.json`, `crypto-orders.json`) for cryptocurrency dashboard functionality.
|
||||
|
||||
Added `crypto-markets.json` and `crypto-orders.json` data files for the crypto dashboard.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Updated the demo pages to show their title and description in the page header, with `pageHeader` falling back to `title`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated preview and docs examples to the unified demo component props: `variant`, `color`, `size`, `ariaLabel`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added `Accept: text/markdown` content negotiation for docs pages, with q-value parsing and `406` responses.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added an Accessibility section to every UI documentation page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Documented card status, progress, actions, subtitle, surfaces, scrollable body, tables, links and overlays.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added installation and usage sections to the chart and countup pages, covering the `.chart-*` size classes and sparklines.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a class reference table to every component page, from the new `classnames` front matter.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed highlighted code blocks using hardcoded colors instead of the Tabler surface tokens.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed the copy button in docs examples to be a `button` with an accessible name.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated documentation to explain font sizing and system color CSS variables (`--tblr-primary-rgb`, `--tblr-secondary`, `--tblr-tertiary`, `--tblr-link-color`, `--tblr-gray-*`).
|
||||
Updated the documentation to explain font sizing and system color CSS variables.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added docs pages for the Datepicker and Tom Select form plugins, `form-datepicker` and `form-select-tomselect`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed docs example code blocks gluing inline elements like `<label>` and `<input>` onto one line.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added a `dark` background option to the docs `<Example>` component.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated docs examples to use the `<Icon />` component and consistent Prettier formatting.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed keyboard access in docs examples for carousel controls, disabled links, `btn-loading` and progress bars.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed install snippets pointing at urls that do not resolve on the vector map, inline player and icon pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed docs examples using classes that do not exist, such as `alert-facebook`, `btn-close-white`, `bg-gray` and `btn-xs`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added tooltips and accessible names to the footer icon links.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Stacked form docs examples in a column and tightened the sidebar nested menu indent.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Moved the form pages backed by a third-party library into the Plugins section.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Renamed form docs pages without the `form-` prefix and flattened the Illustrations and Emails sections.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated the How to Contribute guide with starter issues, Codespaces setup, commands and PR conventions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Updated the docs layout: elevated article panel, rounded sidebar navigation, "On this page" rail and restyled prev/next links.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Listed every component class in `llms.txt`, from the `classnames` front matter.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a `llms-full.txt` page with all docs in one file, `Content-Signal` in robots.txt and markdown `Link` alternate headers.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Split the getting started menu group into Getting started and Resources.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated the docs navbar to use `container-lg`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a source link and a copy-as-markdown button, and moved the class reference to the end of the page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added `classnames` to the social icons and flags pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Rewrote the autosize, range slider, WYSIWYG and dropzone plugin pages with installation, usage and accessibility sections.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Moved components that need a third-party library into the Plugins section.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Moved the Website, Preview and Support links from the docs sidebar to the top navbar.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Restyled the related and prev/next cards, and moved prev/next below the article panel.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Restored the `docs.scss` source for the docs styles, compiled with Sass instead of a checked-in `docs.css`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed the search modal using the Algolia palette instead of Tabler colors, including an unreadable selected result.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Darkened the docs sidebar links, kept rows to one line and added a visible focus ring.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated UI component documentation to use singular file names and frontmatter fields (`title`, `summary`, `description`), and added redirects from legacy plural routes.
|
||||
Updated UI component docs to singular file names and frontmatter, with redirects from plural routes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed docs examples using class names that no longer exist, such as `alert-title` and `hr-text-center`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Documented sortable headers, selectable rows, stacked mobile tables, and outline, dot and icon-only badges.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Expanded the social icons, payments, vector map, inline player, PDF, EPS, illustrations preview and references pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Rewrote the timeline and datagrid pages with markup, variants and accessibility notes.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Documented `avatar-square`, `mention`, `offcanvas-narrow`, `btn-floating`, `card-cover`, `td-truncate` and other variants.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed plugin demos showing their stock styles, because library CSS loaded after `tabler-vendors.css`.
|
||||
@@ -3,4 +3,3 @@
|
||||
---
|
||||
|
||||
Added Edit Profile modal with avatar upload, personal information fields, social links, and date of birth.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed html comments in docs code examples running into the closing tag of the element before them.
|
||||
@@ -3,4 +3,4 @@
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated preview and docs Astro code to use shared `date-format`, `string-format`, `pseudo-random`, and `include-args` helpers instead of duplicated inline Liquid filter ports.
|
||||
Updated preview and docs to use the shared `date-format`, `string-format` and `pseudo-random` helpers.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Updated `$border-color-translucent-dark` from `rgba(72, 110, 149, 0.14)` to `rgba(128, 150, 172, 0.2)` to improve visibility of form checkboxes and other form elements in dark mode.
|
||||
Updated `$border-color-translucent-dark` to `rgba(128, 150, 172, 0.2)` for better dark mode visibility.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.card-header` background-color to respect the `--tblr-card-cap-bg` CSS variable instead of being silently overridden by a `background: transparent` shorthand.
|
||||
Fixed `.card-header` background being overridden by a `background: transparent` shorthand.
|
||||
Loaded 100 of 1224 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user