Add Bootstrap v6 migration plan, 2.0 policy rule and agents (#2967)

This commit is contained in:
Paweł Kuna authored and Bartek committed 2026-10-02 12:02:57 +02:00
1 parent 448866618d
commit 02f75b7a34
10 files changed
+700 -12

No files matched your search

+4 -11
View File
@@ -65,17 +65,10 @@ additively instead of creating parallel variants.
## Page scripts and modals
- Capture markup with `CaptureScript` / `CaptureModal` (HTML in the slot, not
template strings). They register via `addPageScript()` / `addPageModal()`
(`@shared/lib/page-scripts.ts` / `page-modals.ts`).
Wrap at the call site, e.g. `<CaptureModal><Modal …>…</Modal></CaptureModal>`.
Registration MUST be synchronous in the component frontmatter (before the
first `await`) — Astro renders siblings concurrently, and a registration
after `await Astro.slots.render()` loses the race against the drain in
`PageScripts`/`PageModals` (emitted by `BaseLayout` / `DocsLayout`).
- Third-party page libraries: list names in the layout's `pageLibs` prop —
resolved via `@tabler/core/libs.json` (a full `http` URL in there is emitted
verbatim; `head: true` libs go into `<head>`).
Component scripts and modals are captured with `CaptureScript` / `CaptureModal` and
rendered at the end of `<body>` by `PageScripts` / `PageModals`; the registration must
be synchronous in the frontmatter, before the first `await`. Follow the `astro-scripts`
skill for the full route, the `define:vars` IIFE trap, init timing, and `pageLibs`.
## Data
+97
View File
@@ -0,0 +1,97 @@
---
name: bootstrap-v6-reference
description: Read-only research agent for the Bootstrap v6 source. Use when a Tabler 2.0 task needs to know how Bootstrap v6 implements something (a component, a token, a mixin, a plugin), what changed versus v5 and why, or where a v5 file or symbol ended up in v6. Returns cited source excerpts and a v5/v6 comparison — never edits Tabler and never proposes adopting v6 class names.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You answer questions about Bootstrap v6 by reading its source, and you report what you found with
exact paths. You never modify files. You do not recommend renaming anything in Tabler — Tabler 2.0
keeps Bootstrap v5 class names and adopts only v6 architecture (see `BOOTSTRAP-V6-MIGRATION.md`,
section 2). Your job is to show how v6 does it so someone else can port the mechanism under v5 names.
## Where the source is
- Checkout: `/Users/chomik/htdocs/bootstrap`. Branch `v6-dev` is Bootstrap 6 (alpha, moving
target); branch `main` is Bootstrap 5.3.x.
- Never `cd` into it — the shell cwd resets between calls. Always use `git -C /Users/chomik/htdocs/bootstrap …`
and absolute paths.
- Never `git fetch`, `pull` or `checkout` there unless explicitly asked. The working tree is on
`v6-dev`; read other revisions with `git show <ref>:<path>` instead of switching.
- Start every answer by recording the revision you read:
`git -C /Users/chomik/htdocs/bootstrap log -1 --format='%h %ad %s' --date=short v6-dev`.
## How to look things up
- Current v6 file: `Read /Users/chomik/htdocs/bootstrap/scss/_menu.scss` (working tree is `v6-dev`).
- v5 counterpart: `git -C … show main:scss/_dropdown.scss`.
- What changed: `git -C … diff main...v6-dev -- scss/_card.scss` (three dots — merge base).
- Why it changed: `git -C … log --format='%h %ad %s' --date=short main..v6-dev -- <path>` and
`git -C … log -S'<symbol>' --format='%h %s' main..v6-dev`. Commit subjects carry PR numbers.
- Compiled output when the Sass is hard to read: `dist/css/bootstrap.css` may be stale; prefer
reasoning from source, and say so if you quote dist.
- Prose: `site/src/content/docs/**/*.mdx`. Key files:
- `guides/migration.mdx` — the official v5→v6 changelog (539 lines). Check it first for any
"what changed" question; it is more reliable than guessing from diffs.
- `customize/sass.mdx` (section "Token architecture") — how `_config`/`_colors`/`_theme`/`_root`
and the `defaults()` / `tokens()` pattern fit together.
- `components/*.mdx`, `forms/*.mdx`, `utilities/*.mdx` — per-component docs with markup examples.
- `skills/bootstrap-v5-v6-migration/SKILL.md` — upstream's agent checklist for migrating *to* v6.
Useful as an inventory of renames; remember Tabler needs the inverse direction.
## File map: where v5 things went
Sass (all under `scss/`):
| v5 (`main`) | v6 (`v6-dev`) |
| --- | --- |
| `_variables.scss`, `_variables-dark.scss`, `_maps.scss` | `_config.scss`, `_colors.scss`, `_theme.scss`, `_root.scss` |
| `_mixins.scss`, `_helpers.scss`, `_forms.scss` | `mixins/index.scss`, `helpers/index.scss`, `forms/index.scss` |
| `_grid.scss`, `_containers.scss`, `mixins/_breakpoints.scss`, `mixins/_container.scss` | `layout/_grid.scss`, `layout/_containers.scss`, `layout/_breakpoints.scss` |
| `_reboot.scss`, `_type.scss`, `_images.scss`, `_tables.scss` | `content/_reboot.scss`, `content/_lists.scss` + `content/_blockquote.scss`, `content/_images.scss`, `content/_tables.scss`; new `content/_prose.scss` |
| `_buttons.scss`, `_button-group.scss`, `_close.scss`, `mixins/_buttons.scss` | `buttons/_button.scss`, `buttons/_button-group.scss`, `buttons/_close.scss` (no button mixin — `.theme-*` tokens) |
| `_modal.scss` | `_dialog.scss` + `mixins/_dialog-shared.scss` |
| `_offcanvas.scss` | `_drawer.scss` |
| `_dropdown.scss` | `_menu.scss` |
| `_placeholders.scss`, `_spinners.scss` | `_placeholder.scss`, `_spinner.scss` |
| `forms/_form-check.scss` | `forms/_check.scss`, `forms/_radio.scss`, `forms/_switch.scss` |
| `forms/_form-select.scss` | removed — merged into `forms/_form-control.scss` |
| `forms/_form-variables.scss`, `mixins/_forms.scss` | removed / `mixins/_form-validation.scss`; logic in `forms/_validation.scss` |
| `mixins/_alert.scss`, `_list-group.scss`, `_pagination.scss`, `_table-variants.scss` | removed — replaced by `.theme-*` tokens in `_theme.scss` |
| `helpers/_ratio.scss`, `helpers/_clearfix.scss`, `helpers/_color-bg.scss`, `helpers/_colored-links.scss` | utilities API (`_utilities.scss`) |
| `vendor/_rfs.scss` | removed — `clamp()` in `_config.scss` font sizes |
| — | new: `_avatar.scss`, `_chip.scss`, `_stepper.scss`, `_datepicker.scss`, `_nav-overflow.scss`, `_banner.scss`, `forms/_combobox.scss`, `forms/_chip-input.scss`, `forms/_otp-input.scss`, `forms/_strength.scss`, `forms/_form-field.scss`, `forms/_form-adorn.scss`, `mixins/_focus-ring.scss`, `mixins/_mask-icon.scss`, `mixins/_tokens.scss` |
JavaScript (`js/src/`):
| v5 | v6 |
| --- | --- |
| `modal.js` | `dialog.js` + `dialog-base.js` |
| `offcanvas.js` | `drawer.js` (also on `dialog-base.js`) |
| `dropdown.js` | `menu.js` (includes submenus) |
| `util/backdrop.js`, `util/focustrap.js`, `util/scrollbar.js` | removed — native `<dialog>` provides them |
| Popper via `util/index.js` | `util/floating-ui.js` |
| `carousel.js` (float/translate) | `carousel.js` rewritten on scroll snap |
| `scrollspy.js` | rewritten on `IntersectionObserver` |
| — | new: `chips.js`, `combobox.js`, `datepicker.js`, `nav-overflow.js`, `otp-input.js`, `range.js`, `strength.js`, `toggler.js` |
Tabler's vendored v5 copies live in `core/scss/bootstrap/` and `core/js/src/bootstrap/` (a
TypeScript port). When asked to compare, compare against those too, not only upstream `main`.
## What a good answer contains
1. Revision line (SHA + date) for `v6-dev`.
2. The v6 source, quoted with `path:line-range`, trimmed to the relevant part.
3. The v5 counterpart the same way (upstream `main` and/or Tabler's vendored copy).
4. What changed in mechanism — not in names. Names are noise for Tabler; mechanism is the point.
Say explicitly which parts are architecture (tokens, layers, native elements, selectors like
`:has()`, container queries) and which are just renames.
5. Browser requirements the mechanism introduces (`oklch()`, `color-mix()`, `:has()`,
`<dialog>`, `:user-invalid`, container queries, `scrollbar-gutter`).
6. Where it touches Tabler: which `core/scss/ui/*`, `core/scss/bootstrap/*`, `core/js/src/*` files
implement the same thing today. Point, do not edit.
7. The migration-guide paragraph, if one covers it.
Keep answers factual. If the migration guide and the source disagree, the source wins — say so and
quote both. If something is not in v6 at all, say "not present in v6-dev at <sha>" rather than
inferring.
+148
View File
@@ -0,0 +1,148 @@
---
name: v2-policy-reviewer
description: Read-only reviewer for Tabler 2.0 changes on v2-dev. Use before opening or merging any PR into v2-dev, or when asked whether a change respects the 2.0 policy (Bootstrap v5 class names and public API kept, Bootstrap v6 architecture adopted). Finds leaked v6 names, silent value remaps, broken public contracts and missing proof for output-neutral phases. Reports findings; does not fix them.
tools: Read, Grep, Glob, Bash
---
You review a diff against the Tabler 2.0 policy and report what violates it. You do not edit files.
## The policy in one paragraph
Tabler 2.0 keeps the **Bootstrap v5 public contract** — class names, `data-bs-*` attributes,
event names, JavaScript plugin names and methods, and the *values* behind existing utility classes —
and adopts the **Bootstrap v6 architecture** underneath: token files, cascade layers, `oklch()` +
`color-mix()`, `.theme-*` tokens, logical properties, native `<dialog>` / `<details>`, Floating UI,
scroll snap. The internal Sass API (variable names, maps, mixins, file layout) is *not* promised
and may change freely as long as compiled output stays as intended. Full text:
`BOOTSTRAP-V6-MIGRATION.md` sections 2 and 3, and the decisions in section 5. Confirmed decisions:
no `md:` prefix syntax in 2.0; everything targets `v2-dev`.
## Getting the diff
- Default: the branch against `v2-dev` — `git diff v2-dev...HEAD` (three dots) plus `git status --short`
for uncommitted work; include `git diff` of the working tree when it is not empty.
- If given a PR number: `gh pr diff <n>`; also read the PR body — it should name the phase and carry
the proof described below.
- Never review `dev` — it is frozen for 1.x. If HEAD is not on top of `v2-dev`, say so first.
Exempt from name checks: files that *describe* v6 (`BOOTSTRAP-V6-MIGRATION.md`, upgrade guides,
`docs/content/**` prose that explains a migration), test fixtures that deliberately contain v6
markup, and `.claude/agents/*`.
## Pass 1 — leaked v6 names (blockers)
Grep the diff's added lines (`git diff … | grep '^+'`) for these. Each hit is a blocker unless it
sits in an exempt file.
| What | Pattern (extended regex) | v5 form that must be used instead |
| --- | --- | --- |
| Responsive/state prefix | `PREFIX` regex in the block below (the leading `["' .]` keeps SCSS pseudo-classes like `&:focus:not(…)` out) | infix: `.col-md-6`, `.d-md-none`, `.opacity-50-hover` |
| Dialog classes | `\.dialog(-[a-z-]+)?\b` | `.modal`, `.modal-header`, `.modal-body`, `.modal-footer`, `.modal-title`, `.modal-sm/lg/xl/fullscreen` |
| Drawer classes | `\.drawer(-[a-z-]+)?\b` | `.offcanvas`, `.offcanvas-start/end/top/bottom`, `.offcanvas-header/body/title` |
| Menu classes | `\.menu(-item|-header|-divider)?\b`, `\.submenu\b` | `.dropdown-menu`, `.dropdown-item`, `.dropdown-header`, `.dropdown-divider` |
| Button composition | `\.btn-(solid|subtle|text|styled)\b` (`.btn-outline` alone is an existing Tabler class — not a hit) | `.btn-primary`, `.btn-outline-primary`, `.btn-ghost-*` (Tabler) |
| Badge composition | `\.badge-subtle\b` (`.badge-outline` is an existing Tabler class — not a hit) | `.badge.bg-*`, Tabler badge variants |
| Data attributes | `data-bs-toggle="(dialog|drawer|menu|toggler)"`, `data-bs-dismiss="(dialog|drawer)"`, `data-bs-(autoplay|ends|validate|bubble)\b`, `data-bs-modal=` | `modal`, `offcanvas`, `dropdown`; `data-bs-ride`, `data-bs-wrap`, `.needs-validation` |
| Events | `\.bs\.(dialog|drawer|menu|chips|otpInput|strength|range|datepicker)\b` | `.bs.modal`, `.bs.offcanvas`, `.bs.dropdown` |
| JS exports | `\b(Dialog|Drawer|Menu|DialogBase)\b` as a class or import in `core/js/` | `Modal`, `Offcanvas`, `Dropdown` |
| Colour utilities | `\.fg-[a-z]`, `\.bg-[1-4]\b`, `\.bg-(subtle|muted)-`, `\.fg-reset\b` | `.text-*`, `.bg-body-*`, `.text-reset` |
| Type utilities | `\.fs-(xs|sm|md|lg|[2-6]?xl)\b`, `\.lh-md\b`, `\.text-(xs|sm|md|lg)\b` | `.fs-1`…`.fs-6`, `.lh-base` |
| Sizing utilities | `\.max-[wh]-`, `\.underline-` | `.mw-*`, `.mh-*`, `.link-underline-*` |
| Form classes | `\.form-range-input\b`, `\.form-field\b`, `\.check\b` on an input, `\.accordion-icon\b` | `.form-range`, Tabler form layout, `.form-check-input`, accordion as today |
| Removed-in-v6 classes we keep | additions that *delete* `.form-select`, `.accordion-button`, `.dropdown-toggle`, `.modal-dialog`, `.modal-content`, `.alert-dismissible`, `.lead`, `.display-*` from `core/scss` | keep them, at least as aliases |
Copy-ready form of the patterns (extended regex, run over added lines only):
```bash
git diff v2-dev...HEAD | grep '^+' | grep -En \
-e '(^|["'"'"' .])(sm|md|lg|xl|2xl|hover|focus|active|print)(-down)?\\?:[a-z][a-z0-9-]*' \
-e '\.(dialog|drawer)(-[a-z-]+)?\b' -e '\.menu(-item|-header|-divider)?\b' -e '\.submenu\b' \
-e '\.btn-(solid|subtle|text|styled)\b' -e '\.badge-subtle\b' \
-e 'data-bs-toggle="(dialog|drawer|menu|toggler)"' -e 'data-bs-dismiss="(dialog|drawer)"' \
-e 'data-bs-(autoplay|ends|validate|bubble|modal)\b' \
-e '\.bs\.(dialog|drawer|menu|chips|otpInput|strength|range|datepicker)\b' \
-e '\.fg-[a-z]' -e '\.bg-[1-4]\b' -e '\.bg-(subtle|muted)-' \
-e '\.fs-(xs|sm|md|lg|[2-6]?xl)\b' -e '\.lh-md\b' -e '\.max-[wh]-' -e '\.underline-' \
-e '\.form-range-input\b' -e '\.form-field\b' -e '\.accordion-icon\b'
```
Verified 2026-09-06: this set returns zero hits on the untouched `core/scss`, `shared/ui`,
`preview/pages` and `docs/content` trees, so any hit in a diff is real.
`.theme-*` tokens are **allowed** (they are v6 architecture) — but flag markup where a `.theme-*`
class *replaces* a v5 colour class instead of being emitted by it. `floatingConfig` is allowed once
phase 5 lands (decided in the plan). Tabler-specific classes that happen to contain these words
(`.navbar-menu`, `.dropdown-menu-*`) are not hits; check the full token, not the substring.
## Pass 2 — silent value remaps (blockers)
Names surviving with different values is the failure mode users cannot grep for. For any change in
`core/scss/` diff the *values* of:
- `$spacers` (v6 remapped 0–5 to 0–9), `$sizes`
- `$border-radius*` / `$radii` and what `.rounded-N` resolves to
- `$font-sizes` / `.fs-N`, `$display-font-sizes`, `$line-height-*`
- `$grid-breakpoints` / `$breakpoints` (v6: lg 1024, xl 1280, 2xl 1536) and `$container-max-widths`
- `$theme-colors` keys, `$zindex-*`
Compare against `git show v2-dev:<path>` (or `dev` for the very first phase). A key that keeps its
name but changes its value is a blocker unless the PR says explicitly that the reflow is intended
and section 5 of the plan records the decision.
## Pass 3 — public contract (blockers)
For any touched component, confirm these still hold, by reading the code, not the description:
- Class names emitted for every variant that existed on `dev` (`check-markup-classes` baseline is
the reference; `pnpm run check-markup-classes` if it runs in this environment).
- `data-bs-toggle` / `data-bs-dismiss` / `data-bs-target` values unchanged.
- Event names `show/shown/hide/hidden.bs.<plugin>` unchanged; `hidePrevented.bs.modal` kept.
- JS: `getInstance`, `getOrCreateInstance`, `show`, `hide`, `toggle`, `dispose` present; the
`window.bootstrap`-style access documented for 1.x still works unless the ESM-only decision
(section 5) has been taken.
- `--tblr-*` custom properties that 1.x documents as public are still defined (may be aliases).
- Both `data-bs-*` and `data-tblr-*` attribute spellings still work in JS.
## Pass 4 — proof for the claimed phase (warnings)
Identify the phase from the branch name, PR body or commit messages, then check the required proof
is present or reproducible:
- Phases 1, 3, 4 (output-neutral): a zero byte-diff of `core/dist/css/tabler.css` and a zero
`html-diff` run. If the PR does not show them, run what is cheap (`pnpm --filter @tabler/core run css`
then `git diff --stat` — only if no dev server is running) and report the result; otherwise
request them.
- Phase 2 (colours): a contrast-gate run and a dark-mode screenshot review listed in the PR.
- Phase 6 (native components): the a11y checklist — focus trap, Escape, backdrop click, focus
return to the trigger, `aria-*` unchanged — and a note that `html-diff` differs by design.
- Any phase: a changeset in `.changeset/` and, when a class was added or moved, updated `classnames`
front matter in `docs/content/**`.
Missing proof is a warning, not a blocker — but say clearly what is missing.
## Pass 5 — open questions (notes)
If the change silently answers one of section 5's open questions (browser baseline, ESM-only,
`.form-select`, `$spacers`, component ownership of `.avatar`/`.chip`/`.stepper`, navbar-as-drawer),
note it: the decision should be recorded in the plan, not implied by code.
## Report format
```
Reviewed: <branch/PR> vs v2-dev at <sha>, phase <n> (<claimed by>)
BLOCKERS
- path:line — <pattern hit or contract broken> → <v5 form / what must hold>
WARNINGS
- ...
NOTES
- ...
Contract check: classes ✓/✗ · data-bs ✓/✗ · events ✓/✗ · JS API ✓/✗ · custom props ✓/✗
Proof: byte-diff <yes/no/n.a.> · html-diff <yes/no/n.a.> · a11y <yes/no/n.a.> · changeset <yes/no>
```
Every finding cites `path:line`. Quote the offending line. Do not pad the report with things that
are fine; an empty BLOCKERS section is the goal, and "nothing found" is a valid, complete result.
+2
View File
@@ -51,6 +51,8 @@ description: Practical description of when and why to use it.
No `layout:` key — the route picks the layout.
Quote `summary:` and `description:` values that contain a colon followed by a space (`Cards: use them…`) — unquoted, js-yaml reads the text as a nested map and the page breaks: a 500 on the dev server and a failed docs build on Vercel. Open the page on `:3010` before pushing.
Optional keys: `seoTitle`, `seoDescription`, `icon`, `order`, `related`, `docs-libs`, `css-plugins`, `hide-pagination`, `added-in`. Anything else is rejected.
### Examples
+1 -1
View File
@@ -7,7 +7,7 @@ alwaysApply: true
# Tabler — project rules
Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical agent configuration lives in `.agents/` (`rules/` + `skills/` + `agents/`); `.claude/{skills,agents}` and `.cursor/{rules,skills,agents}` are symlinks into it.
Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical agent configuration lives in `.agents/` (`rules/` + `skills/` + `agents/`); `.claude/{skills,agents}` and `.cursor/{rules,skills,agents}` are symlinks into it, and the root `CLAUDE.md` only `@`-imports `rules/*.mdc` for Claude Code — edit the files under `.agents/` only. Rules in `rules/*.mdc` (`main`, `docs`, `v2`) always apply.
## Project structure
+52
View File
@@ -0,0 +1,52 @@
---
description: Tabler 2.0 policy — target branch, what stays from Bootstrap v5, what is adopted from v6, proof required per phase
globs:
alwaysApply: true
---
# Tabler 2.0 — branch and compatibility policy
Applies to every change on `v2-dev`. The plan, phases and open questions live in
`BOOTSTRAP-V6-MIGRATION.md` at the repo root — read sections 2 and 3 before starting 2.0 work.
## Branches
- All 2.0 work targets **`v2-dev`**. Cut feature branches from it and open PRs back into it.
- `dev` is frozen for 1.x maintenance. Never merge 2.0 work into `dev`.
- Every 2.0 PR names the phase it belongs to and carries that phase's proof (below).
- Every PR into `v2-dev` goes into the GitHub milestone **2.0**: `gh pr create --base v2-dev --milestone "2.0" …` (or `gh pr edit <n> --milestone "2.0"` afterwards).
## Kept from Bootstrap v5 — the public contract
- Class names: `.modal`, `.offcanvas`, `.dropdown-menu`, `.btn-primary`, `.col-md-6`, `.d-md-none`, `.text-*`, `.fs-1`…`.fs-6`, `.rounded-*`, and every class documented for 1.x.
- `data-bs-*` attributes and their values (`data-bs-toggle="modal"`), with the `data-tblr-*` alias still working.
- Event names (`shown.bs.modal`), JavaScript plugin names and methods (`Modal.getInstance`, `show`, `hide`, `dispose`).
- The **values** behind existing utilities: `$spacers`, `.rounded-N`, `.fs-N`, breakpoints and container widths do not change silently.
## Not adopted from v6 (decided 2026-09-06)
- The `md:` / `hover:` prefix syntax — infix names (`.col-md-6`) stay. Revisit no earlier than 3.0.
- Renames: `.dialog`, `.drawer`, `.menu`, `.fg-*`, `.bg-1`…`.bg-4`, `.fs-xs`…`.fs-6xl`, `.max-w-*`, `.underline-*`, `data-bs-toggle="dialog|drawer|menu"`, `*.bs.dialog|drawer|menu` events, `Dialog`/`Drawer`/`Menu` exports.
- The v6 remaps of `$spacers` (0–9) and `.rounded-*`.
## Adopted from v6 — the architecture
- Token files `_config` / `_colors` / `_theme` / `_root` with the `defaults()` + `tokens()` pattern; cascade `@layer`; custom properties prefixed by PostCSS, never by a Sass `$prefix`.
- `oklch()` colours, `color-mix()` scales, `.theme-*` tokens — **emitted by the v5 classes**, not replacing them in markup (`.btn-primary` sets the theme tokens; markup never needs `.btn-solid .theme-primary`).
- Logical properties in spacing and border utilities (class names unchanged).
- Native `<dialog>` for modal/offcanvas, `<details>` for accordion, CSS scroll snap for carousel — under the v5 names, events and data attributes.
- Floating UI instead of Popper; one `focus-ring()` mixin; the numeric `$radii` scale mapped so `.rounded-N` keeps its v5 values.
- The internal Sass API (variable names, maps, mixins, file layout) is **not** promised and may change, as long as compiled output stays as intended.
## Proof per phase
- Output-neutral phases (1 Sass split, 3 token scales, 4 logical properties): byte-identical `core/dist/css/tabler.css` and a zero `html-diff` run — see the `html-diff` skill.
- Phase 2 (colours): contrast gate passed, dark mode reviewed by screenshot.
- Phase 6 (native components): the a11y checklist — focus trap, Escape, backdrop click, focus returns to the trigger, `aria-*` unchanged — and a note that `html-diff` differs by design.
- Every phase: a changeset (`generate-changeset` skill), updated `classnames` front matter when a class moved (`class-reference` skill).
## Agents
- Before opening a PR into `v2-dev`, run the `v2-policy-reviewer` agent on the diff and fix its blockers.
- To learn how Bootstrap v6 implements something, ask the `bootstrap-v6-reference` agent; it reads the upstream source and reports mechanism, not names.
- The open questions in section 5 of the plan are decided by the maintainer and recorded in the plan — never implied by code.
+2
View File
@@ -23,6 +23,8 @@ Never ask the user to check a page manually. Start the server, look at the page,
`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.
**Port 3000 belongs to the main checkout.** In a git worktree the ports above are taken, and the worktree has no built assets yet. Prepare once with `pnpm turbo run dev:prepare --filter @tabler/preview` (builds `core` and copies the assets into the worktree's `public/`), then start `pnpm --dir preview exec astro dev --port 3001` — the `preview-alt` entry in `.claude/launch.json`. An `html-diff` baseline must be captured from the same server as the comparison, because the toolbar embeds the server path in the HTML.
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
+2
View File
@@ -124,3 +124,5 @@ Write the **title** and **full MR body** in **simple English**, even if the user
## 6. After output
Offer to open/create the MR if the user uses **GitLab** (project MCP or UI) or **GitHub** (`gh pr create`), without running destructive git commands unless they ask.
When the base branch is **`v2-dev`** (Tabler 2.0 work), create the PR with `--milestone "2.0"` — every 2.0 PR belongs to that GitHub milestone. If the PR already exists, add it with `gh pr edit <n> --milestone "2.0"`.
+389
View File
@@ -0,0 +1,389 @@
# Bootstrap v5 → v6: analysis and a phased plan for Tabler 2.0
**Status:** analysis only, nothing implemented. Written 2026-09-06.
**Source of truth:** local Bootstrap checkout at `~/htdocs/bootstrap`, branch `v6-dev`
(`6.0.0-alpha1`, 495 commits ahead of `main`/5.3.8, work from 2025-08-26 to 2026-07-22).
Primary reference: `site/src/content/docs/guides/migration.mdx` (539 lines).
Upstream also ships `skills/bootstrap-v5-v6-migration`, a phase-by-phase migration skill for
coding agents — worth reading as a ready-made checklist.
Diff size: `scss/` 150 files, +11 585 / −8 889. `js/src/` 36 files, +6 208 / −2 149.
Tabler's exposure: `core/scss/bootstrap/` holds ~5 000 lines of vendored upstream Sass and
`core/js/src/bootstrap/` ~4 900 lines of a vendored TypeScript port of the v5 plugins.
---
## 1. What actually changed upstream
### 1.1 Class naming moved from infix to prefix
Every responsive and state class now uses a Tailwind-style `prefix:class` pattern.
| v5 | v6 |
| --- | --- |
| `.col-md-6` | `.md:col-6` |
| `.d-md-none` | `.md:d-none` |
| `.navbar-expand-md` | `.md:navbar-expand` |
| `.offset-md-2`, `.g-md-3` | `.md:offset-2`, `.md:g-3` |
| `.container-sm` | `.sm:container` |
| `.table-responsive-md` | `.md:table-responsive` |
| `.list-group-horizontal-md` | `.md:list-group-horizontal` |
| `.sticky-md-top` | `.md:sticky-top` |
| `.opacity-50-hover:hover` | `.hover:opacity-50` |
| `.d-print-none` | `.print:d-none` |
| `.dialog-fullscreen-sm-down` | `.sm-down:dialog-fullscreen` |
In Sass, `breakpoint-infix()` became `breakpoint-prefix()` and returns `"md\:"`;
`loop-breakpoints-up` / `loop-breakpoints-down` expose `$prefix` instead of `$infix`.
Breakpoints themselves moved: `lg` 992 → 1024, `xl` 1200 → 1280, `xxl` renamed to `2xl` and
1400 → 1536. Containers: `xl` 1140 → 1200, `2xl` 1320 → 1440. `$grid-breakpoints` → `$breakpoints`.
### 1.2 Sass architecture
- `_variables.scss`, `_variables-dark.scss` and `_maps.scss` are gone. New files: `_config.scss`
(all variables), `_colors.scss`, `_theme.scss`, and a rewritten `_root.scss`.
- CSS cascade layers declared in `_root.scss` and applied across every partial. Upstream hit the
same limitation we did: Sass will not allow `@use`/`@forward` inside `@layer`.
- `$prefix` was removed from Sass. Custom properties are authored unprefixed and prefixed at build
time by `postcss-prefix-custom-properties` — the same approach Tabler already shipped in 1.5
(`.build/build-css.ts`).
- Node Sass support dropped. RFS removed entirely (`scss/vendor/_rfs.scss` deleted); responsive
type now uses `clamp()`.
- New directory grouping with `index.scss` per group: `scss/layout/`, `scss/content/`,
`scss/buttons/`, `scss/forms/`, `scss/mixins/`, `scss/helpers/`.
- All `$*-focus-box-shadow` variables removed in favour of one `focus-ring()` mixin driven by
`--focus-ring*` custom properties.
- `$enable-dark-mode` removed — dark mode is always compiled, switched at runtime via `data-bs-theme`.
### 1.3 Colour system
- Every colour variable is `oklch()`; tint/shade scales are generated with `color-mix(in lab, …)`
in the compiled CSS.
- **All `--*-rgb` custom properties and `rgba()` patterns are gone.** This is a hard browser
requirement on `oklch()` and `color-mix()`.
- A `.theme-*` class sets `--theme-bg`, `--theme-fg`, `--theme-border`, `--theme-contrast` and
components only read those tokens. Per-colour component classes are replaced by composition:
- `.btn-primary` → `.btn-solid .theme-primary`
- `.btn-outline-primary` → `.btn-outline .theme-primary`
- `.badge.bg-primary` → `.badge-subtle .theme-primary`
- the same for alerts, tables, cards, accordions and pagination
- New button variants: `.btn-solid`, `.btn-outline`, `.btn-subtle`, `.btn-text`, `.btn-styled`,
`.btn-icon`, plus a `.btn-xs` size. Buttons and inputs share `--btn-input-*` sizing tokens.
### 1.4 Components rebuilt on native browser APIs
This is the substantive part of the release, not the renames.
- **Modal → Dialog** on the native `<dialog>` element with `showModal()`/`show()`/`close()`.
The `.modal-dialog` / `.modal-content` wrappers and the `.modal-backdrop` element are gone
(`::backdrop` instead), and with them the internal `util/backdrop`, `util/focustrap` and
`util/scrollbar` helpers — the browser supplies the backdrop, the focus trap and the inert top
layer. Scroll lock is `:root.dialog-open` plus `scrollbar-gutter: stable`.
- **Offcanvas → Drawer**, sharing a `DialogBase` class with Dialog, plus swipe-to-dismiss and a
`.drawer-sheet` variant.
- **Responsive navbar uses Drawer, not Collapse.** The toggler targets a `<dialog class="drawer">`.
`.navbar-light` / `.navbar-dark` removed.
- **Accordion on `<details>`/`<summary>`**, no JavaScript for open/close. Exclusive groups use the
HTML `name` attribute instead of `data-bs-parent`. `.accordion-button` and `.accordion-collapse`
are gone.
- **Carousel on CSS scroll snap.** No more `float` + `translateX` or a custom swipe handler. New
multi-slide, peek, gap and centre modes. `ride` → `autoplay` (boolean, opt-in), `wrap` →
`ends: loop|wrap|stop`. Removed `.carousel-caption`, `.carousel-dark`, `.carousel-control-prev/next`
and the transitional `.carousel-item-*` classes.
- **Dropdown → Menu.** `.dropdown-menu` → `.menu`, `.dropdown-item` → `.menu-item`,
`data-bs-toggle="dropdown"` → `"menu"`, events `*.bs.dropdown` → `*.bs.menu`. The `.dropdown`
wrapper and `.dropdown-toggle` class are gone and markup flattens from `<ul><li><a>` to
`<div class="menu"><a class="menu-item">`. Direction comes from `data-bs-placement`.
- Icons (close button, navbar toggler, breadcrumb divider, carousel controls, checkbox marks) are
drawn with `mask-image` tinted by `currentcolor` instead of embedded SVG or filters.
`.btn-close-white` and `$btn-close-white-filter` removed.
- `.card-group` and `.*:list-group-horizontal` switched from media queries to **container queries** —
without a query container (e.g. `.contains-inline`) on a parent they stay stacked.
### 1.5 JavaScript
- **ESM only.** No UMD bundles, no `window.bootstrap` global; `<script type="module">` is required.
Data attribute APIs are unchanged.
- **Popper replaced by Floating UI** (`@floating-ui/dom`, a peer dependency). The `popperConfig`
option is now `floatingConfig`.
- **Vanilla Calendar Pro** added as a peer dependency for the new Datepicker.
- ScrollSpy rewritten on `IntersectionObserver` with an activation line (`topMargin`, default `12%`);
the deprecated `offset` and `method` options are gone.
- New plugins: `Menu`, `Dialog`, `Drawer`, `Combobox`, `Chips`, `OtpInput`, `Strength`, `Range`,
`Toggler`, `Datepicker`, `NavOverflow`.
### 1.6 Utilities — the quiet breakage
Class names survive but their values change, so old markup compiles and silently looks wrong.
- `$spacers` grew from 0–5 to 0–9 and **remapped**: v5 `3` (1rem) is v6 `4`; v5 `4` (1.5rem) is v6 `6`;
v5 `5` (3rem) is v6 `9`.
- `.rounded-N` regenerated from a numeric `$radii` map (`--radius-0`…`--radius-9`); v5 `.rounded-1`
is roughly v6 `.rounded-3`, v5 `.rounded-4` is v6 `.rounded-8`.
- `.fs-1`…`.fs-6` replaced by ascending t-shirt sizes `.fs-xs`…`.fs-6xl`; `.lh-base` → `.lh-md`;
`.display-1`…`.display-6` and `.lead` removed with no single-class replacement.
- `.text-*` colour utilities → `.fg-*`; `.text-bg-*`, `.bg-light`, `.bg-dark` and `.bg-body-*`
removed in favour of a neutral `.bg-1`…`.bg-4` scale; `.text-reset` → `.fg-reset`.
- `.mh-*` / `.mw-*` → `.max-h-*` / `.max-w-*`; new `.min-h-*` / `.min-w-*`.
- Negative margins reduced to `.ms--1`, `.ms--2`, `.me--1`, `.me--2`.
- Spacing and border utilities now emit **logical properties** (`margin-block-start`,
`padding-inline-start`, `border-inline-end`) with unchanged class names.
- Layered, themeable shadows: `.shadow-xs`…`.shadow-xl`, `.shadow-{color}`, `.shadow-opacity-*`.
- `link-*` utilities folded into `underline-*`.
- New `.contains-inline` / `.contains-size` container-query utilities, `.grid-cols-*`,
`.place-items`, `.justify-items`, `.border-keyline` (0.5px).
### 1.7 Forms
- **`.form-select` removed** — use `.form-control` on `<select>`.
- `_form-check.scss` split into `_checkbox`/`_radio`/`_switch`; `.check` goes directly on the
`<input>` with no wrapper. `.btn-check` moves to the `<label>` with a nested input, using `:has()`
instead of `id`/`for` pairs.
- Validation rewritten: no `.was-validated`, no bare `:valid`/`:invalid`. Opt in with
`data-bs-validate` on the `<form>` and style `:user-invalid`. `$enable-validation-icons` and all
background validation icons removed. `$form-validation-states` → `$validation-states`.
- `.form-range` becomes a wrapper; the input takes `.form-range-input` and the component is
JavaScript-driven.
- New `.form-field` / `.form-group` grid layout primitive (label + control + help + feedback).
Input groups lose `flex-wrap` and `.has-validation`.
### 1.8 Bootstrap moved onto Tabler's turf
v6 adds components Tabler already ships under the same names: **avatar** (`core/scss/ui/_avatars.scss`),
**chip** (`_chips.scss`), **stepper** (Tabler's `_steps.scss`), plus progress and status work.
It also adds ones Tabler does not have: **otp-input**, **datepicker**, **combobox**, **prose**,
**form-adorn**, **password strength**, **nav-overflow**, **form-field**.
The class names collide while the APIs and tokens do not. Before 2.0 we have to decide, per
component, whose definition of `.avatar`, `.chip` and `.stepper` wins.
---
## 2. Fit with the agreed 2.0 direction
The decision on record for 2.0 is: our own core, **v5 class names with v6 architecture**, no
wholesale port of v6. Against that, the upstream diff splits fairly cleanly.
**Take:**
`_config` / `_colors` / `_theme` / `_root` split with the `defaults()` + `tokens()` pattern ·
cascade layers · PostCSS prefixing (already shipped in 1.5) · one `focus-ring()` mixin ·
logical properties · native `<dialog>` and `<details>` (removes JavaScript and fixes several
findings from our a11y audit) · Floating UI (already in progress on
`migrate-popper-to-floating-ui`) · the `$radii` scale · scroll-snap carousel.
**Skip or defer:**
the `md:` prefix syntax · the `modal`→`dialog`, `offcanvas`→`drawer`, `dropdown`→`menu` renames
(take the implementations, keep the v5 names) · the `$spacers` and `.rounded-*` remapping ·
`.text-*` → `.fg-*`.
**Cannot be taken halfway:**
`oklch()` + `color-mix()` and the death of `--*-rgb` — they are the foundation of the `.theme-*`
system and every colour variant, so adopting the v6 token architecture means adopting the browser
requirement too. Same for ESM-only JavaScript.
---
## 3. Phased migration plan
Phases are ordered by dependency. **All 2.0 work targets the long-lived `v2-dev` branch** (already on
origin); each phase lands as its own feature branch cut from `v2-dev` and merged back by PR, never
into `dev`, which is frozen for 1.x. Every phase that
claims to be output-neutral must be proved with the `html-diff` harness; every phase that changes
class emission must go through `check-markup-classes`.
### Phase 0 — Decisions and prerequisites
No code. Settle the things that later phases cannot proceed without.
- Browser baseline: are `oklch()` and `color-mix()` in? This gates phases 2 and 3.
- ESM-only distribution: does 2.0 drop the UMD `tabler.js` bundle? This gates phase 5.
- Class-name policy: confirm v5 names stay, and write down the exceptions we are willing to make.
- Component namespace policy for `.avatar` / `.chip` / `.stepper` (see phase 9).
- Read `~/htdocs/bootstrap/skills/bootstrap-v5-v6-migration/SKILL.md` and lift anything reusable
into our own migration notes.
**Deliverable:** a short decision record; 2.0 upgrade guide skeleton in `UPGRADE.md`; this document
committed on `v2-dev` (it currently sits there untracked).
### Phase 1 — Sass module architecture
Restructure without changing a single byte of compiled CSS.
- Split `core/scss/_variables.scss`, `_variables-dark.scss` and `_maps.scss` into `_config.scss`,
`_colors.scss`, `_theme.scss`, reworking `_props.scss` / `_root` accordingly.
- Add `index.scss` per directory (`ui/`, `forms/`, `mixins/`, `helpers/`, `layout/`) and route
`tabler.scss` through them.
- Introduce the `defaults()` + `tokens()` pattern so compile-time and runtime overrides are one
mechanism.
- Keep `@layer` out of this phase — it is a separate, previously withdrawn prototype.
**Files:** `core/scss/_variables.scss`, `_variables-dark.scss`, `_maps.scss`, `_config.scss`,
`_props.scss`, `_settings.scss`, `_core.scss`, `tabler.scss`.
**Gate:** `html-diff` must show zero diff; byte-diff of `core/dist/css/tabler.css`.
**Risk:** low mechanically, high in volume. This is the phase where `@use … as *` writing back to
config bites, per the existing SCSS module notes.
### Phase 2 — Colour system on oklch and theme tokens
The point of no return for browser support.
- Convert colour variables to `oklch()`; generate tint/shade with `color-mix(in lab, …)`.
- Introduce `--theme-bg` / `--theme-fg` / `--theme-border` / `--theme-contrast` and a `.theme-*`
class family.
- Remove `--tblr-*-rgb` and every `rgba(var(--tblr-*-rgb), …)` call site.
- Decide whether Tabler's colour variants keep emitting `.btn-primary` etc. as thin compositions
over `.theme-primary`, so v5 markup keeps working.
**Files:** `core/scss/_colors.scss` (new), `_theme.scss` (new), `_props.scss`, all of
`core/scss/ui/**` that uses `-rgb`.
**Gate:** contrast gate from the a11y work; visual review of dark mode; `html-diff` will legitimately
differ here, so review it by hand.
**Risk:** high. The earlier standalone `light-dark()` / `color-mix()` attempt (issue #2720, branch
`dev-scss-light-dark`) is abandoned; this phase supersedes it, so reuse its lessons (cascade order
of `color-scheme`, the token-dump verification harness) but not its code.
### Phase 3 — Token scales
- Adopt the numeric `$radii` map and `--radius-0`…`--radius-9`, keeping `.rounded-*` mapped to the
v5 values so existing markup does not shift.
- Decide on `$spacers`: adopting the v6 0–9 scale silently changes every `.p-3` / `.m-4` in every
Tabler template. Recommendation: keep the v5 scale, add finer steps at new keys.
- Replace all `$*-focus-box-shadow` with one `focus-ring()` mixin over `--focus-ring*` tokens.
- Consider the layered shadow scale (`.shadow-xs`…`.shadow-xl`) and `.border-keyline`.
- Skip the `.fs-*` renaming — it inverts the scale direction and breaks every page.
**Files:** `core/scss/_config.scss`, `_props.scss`, `core/scss/mixins/**`, `core/scss/_utilities.scss`.
**Gate:** `html-diff` zero diff for the radius and focus-ring work.
### Phase 4 — Logical properties
Already scoped on the `migrate-css-physical-directions-to-logical` branch. Upstream did the same,
which validates the direction.
- Emit `margin-block-*` / `padding-inline-*` / `border-inline-*` from spacing and border utilities
with unchanged class names.
- Reconcile with the `--tblr-dir` multiplier work for `translateX`, and keep the `/*rtl:ignore*/`
workaround for rtlcss negating `calc()`.
**Gate:** RTL build comparison; `html-diff` zero diff.
### Phase 5 — JavaScript runtime
- Decide ESM-only, then drop the UMD build from `core/package.json` (`js:build:standalone`,
`js:min:*`) or keep a compatibility bundle for one more major.
- Finish the Floating UI migration and rename `popperConfig` → `floatingConfig` in our API.
- Take the ScrollSpy rewrite (`IntersectionObserver` + activation line) into
`core/js/src/bootstrap/scrollspy.ts`.
- Prepare to delete `util/backdrop.ts`, `util/focustrap.ts`, `util/scrollbar.ts` — but only after
phase 6 lands, since they are what the native `<dialog>` replaces.
**Files:** `core/js/src/bootstrap/**` (~4 900 lines), `core/js/src/tabler.ts`, `core/package.json`,
`core/.build/vite.config.mts`.
**Gate:** `core` vitest suites; browser smoke test of every preview page that initialises a plugin.
### Phase 6 — Native components
The phase with the best effort-to-value ratio: less code, better accessibility, same class names.
- Rebuild the modal on `<dialog>` while keeping `.modal` / `.modal-header` / `.modal-body` /
`data-bs-toggle="modal"`. Watch for Tabler's own modal variants and the `preview/` modal
architecture.
- Rebuild offcanvas on the same `DialogBase`.
- Rebuild the accordion on `<details>`/`<summary>`; `.accordion-button` disappears, so the docs
class reference and every accordion example need updating (~126 occurrences across 6 files).
- Rebuild the carousel on CSS scroll snap.
- Consider the responsive navbar moving from Collapse to a drawer — but Tabler's navbar and sidebar
are heavily customised, including the 1.5 folded sidebar, so evaluate this separately.
- Convert close button, navbar toggler and breadcrumb divider icons to `mask-image` + `currentcolor`.
**Files:** `core/scss/bootstrap/_modal.scss`, `_offcanvas.scss`, `_carousel.scss`,
`core/scss/ui/_modals.scss`, `_offcanvas.scss`, `_carousel.scss`, `_accordion.scss`, `_close.scss`,
`core/js/src/bootstrap/{modal,offcanvas,carousel,collapse}.ts`, plus `shared/ui/**` and
`preview/pages/**` markup.
**Gate:** `html-diff` will differ by design — review page by page; keyboard and screen-reader passes.
**Risk:** high, but each component can ship independently.
### Phase 7 — Forms
- Decide on `.form-select`: dropping it means touching ~650 occurrences across the repo. Options are
to keep it as an alias over `.form-control` or to defer entirely.
- Split `_form-check.scss` into checkbox / radio / switch, with `.check` directly on the input.
- Move validation to `data-bs-validate` + `:user-invalid`; drop the built-in validation icons.
- Make `.form-range` a wrapper with a JavaScript-driven filled track.
- Evaluate `.form-field` as a layout primitive against Tabler's existing form markup.
**Files:** `core/scss/bootstrap/forms/**`, `core/scss/ui/forms/**`, `shared/ui/` form components,
`docs/content/**` form pages.
**Risk:** medium-high, and the most visible to template users.
### Phase 8 — Utilities and the responsive naming question
Kept last because it is the most disruptive and the most optional.
- Container-query utilities (`.contains-inline`, `.contains-size`) and grid utilities
(`.grid-cols-*`, `.place-items`, `.justify-items`) are pure additions — take them.
- The `md:` prefix syntax is **not adopted in 2.0** (decided 2026-09-06). Adopting it would mean
rewriting roughly 1 800 class occurrences across ~200 files in `preview/`, `docs/` and `shared/`,
and breaking every third-party Tabler template. v5 infix names (`.col-md-6`, `.d-md-none`) stay.
- Revisit for 3.0 at the earliest; if ever adopted, ship both spellings for one major and add a codemod.
- Breakpoint value changes (`lg` 1024, `xl` 1280, `2xl` 1536) can be considered independently of the
naming, but they reflow every layout, so treat them as their own decision.
**Gate:** `check-markup-classes` is the gate that makes any of this reviewable.
### Phase 9 — Component namespace collisions
For each of `.avatar`, `.chip`, `.stepper` (Tabler's `.steps`), `.progress` and `.status`, decide
whether Tabler keeps its own definition, adopts upstream's, or renames. Then decide whether to
implement v6's genuinely new components ourselves: **otp-input**, **combobox**, **datepicker**,
**password strength**, **prose**, **form-adorn**, **nav-overflow**, **form-field**.
Note that our datepicker plans and upstream's choice of Vanilla Calendar Pro may or may not agree —
worth checking before committing to a library.
**Deliverable:** a component ownership table in the 2.0 planning notes.
### Phase 10 — Documentation and gates
- Write the 2.0 upgrade guide the way 1.5's was written, with before/after for each breaking change.
- Update `classnames` front matter across `docs/content/**` for every class that moved.
- Extend `check-markup-classes` baselines.
- Add changesets per phase.
---
## 4. Suggested ordering
```
Phase 0 decisions
↓
Phase 1 Sass architecture ──────────────┐
↓ │
Phase 2 oklch + theme tokens │ (Phase 5 JS can run in parallel
↓ │ from the start, except for the
Phase 3 token scales │ util/* deletions, which wait
↓ │ on Phase 6)
Phase 4 logical properties ─────────────┘
↓
Phase 6 native components (per component, independently shippable)
↓
Phase 7 forms
↓
Phase 8 utilities / naming decision
↓
Phase 9 component ownership
↓
Phase 10 docs and gates
```
## 5. Open questions
1. Browser baseline for `oklch()` and `color-mix()` — the gate on phases 2 and 3.
2. ESM-only, or one more major with a UMD bundle?
3. `.form-select`: alias, removal, or defer?
4. `$spacers`: keep the v5 scale, or accept the silent reflow of every template?
5. Who owns `.avatar` / `.chip` / `.stepper` when upstream ships them too?
6. Does the v6 navbar-as-drawer pattern survive contact with the folded sidebar from 1.5?
+3
View File
@@ -0,0 +1,3 @@
@.agents/rules/main.mdc
@.agents/rules/docs.mdc
@.agents/rules/v2.mdc