mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Fix the bugs found in the pre-1.6 review of dev (#3112)
This commit is contained in:
1 parent
d9dc168665
commit
7bfeb12ab7
55 files changed
+728
-106
No files matched your search
@@ -30,6 +30,7 @@ Task-specific instructions live in `.agents/skills/<name>/SKILL.md` — read the
|
|||||||
| `shared-lib` | moving logic into `shared/lib/` and testing it |
|
| `shared-lib` | moving logic into `shared/lib/` and testing it |
|
||||||
| `core-scss` | writing or changing styles in `core/scss/` |
|
| `core-scss` | writing or changing styles in `core/scss/` |
|
||||||
| `core-js` | working on the framework JavaScript in `core/js/` |
|
| `core-js` | working on the framework JavaScript in `core/js/` |
|
||||||
|
| `backward-compat` | renaming, removing or changing anything in `core/` a user can touch — minor releases must not break projects |
|
||||||
| `bootstrap-component` | writing a new component class in `core/js/src/bootstrap/` (typed config, events, Data API) |
|
| `bootstrap-component` | writing a new component class in `core/js/src/bootstrap/` (typed config, events, Data API) |
|
||||||
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
||||||
| `write-docs` | writing or updating a page in `docs/content/` |
|
| `write-docs` | writing or updating a page in `docs/content/` |
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
name: backward-compat
|
||||||
|
description: >-
|
||||||
|
Keep a change to `@tabler/core` backward compatible, so a patch or minor release never breaks a project that updates. Use before renaming, removing or changing the meaning of anything a user can touch — a class, a `--tblr-*` custom property, a Sass variable, mixin or function, a `data-*` attribute or option, a JS export, an event, a file under `dist/`, a default value — and whenever a changeset on `dev` looks like it needs a `major` bump. Also consult it proactively when a cleanup or refactor in `core/` deletes something, since "dead" code is often somebody's API. Covers what counts as public API, the deprecated-alias patterns, how to diff the public surface against the last release, and what to do when a break cannot be avoided.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backward compatibility
|
||||||
|
|
||||||
|
The README promises [Semantic Versioning](https://semver.org/): breaking changes only land in major releases. Users install `^1.0.0` and expect `npm update` to be safe. Issue #3108 is what happens when it is not.
|
||||||
|
|
||||||
|
So on `dev`, which ships patch and minor releases, **every change is additive or has an alias**. A change that cannot be made compatible does not go to `dev`. It waits for the next major.
|
||||||
|
|
||||||
|
## 1. What is public API
|
||||||
|
|
||||||
|
Anything a user can write in their own project. If it is in the list, it cannot be removed, renamed or given a new meaning in a minor release.
|
||||||
|
|
||||||
|
| Surface | Examples | Breaks when |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Class names | `.legend`, `.form-hint`, `.card-options` | removed, renamed, or the same name starts to mean something else |
|
||||||
|
| Custom properties | `--tblr-primary-rgb`, `--tblr-btn-focus-box-shadow` | no longer emitted, or no longer read by the component |
|
||||||
|
| Sass variables, maps and keys | `$focus-ring-blur`, a key of `$form-validation-states` | removed, renamed, or the expected type changes (a color becomes a `var()`) |
|
||||||
|
| Sass mixins and functions | `focus-ring($show-border)` | removed, a parameter renamed or reordered, a positional argument changes meaning |
|
||||||
|
| Data attributes and their options | `data-countup='{…}'`, `data-bs-toggle="…"` | removed, renamed, an option dropped, or a value that used to be accepted now throws |
|
||||||
|
| JS exports and globals | `tabler.Modal`, `window.tabler.*`, named exports, published types | removed, renamed, a method signature changed |
|
||||||
|
| Events | `change.bs.switch-icon` | renamed, no longer fired, fired at a different moment |
|
||||||
|
| Files in the package | `dist/libs/litepicker/…`, `dist/css/tabler.rtl.css` | the path 404s after an update |
|
||||||
|
| Dependencies | `dependencies`, `peerDependencies` | a user must install or uninstall something for the update to work |
|
||||||
|
| Defaults | the gray scale, `forceFallback`, the color mode | an untouched project looks or behaves differently in a way the user did not ask for |
|
||||||
|
| Browser support | `.browserslistrc` | the minimum version of any browser goes up |
|
||||||
|
|
||||||
|
Not public: anything under `preview/`, `docs/`, `shared/`, `.build/`, file layout inside `core/scss/` that is not reachable through `@use`, private `_underscore` members of JS classes, and the exact bytes of the compiled CSS.
|
||||||
|
|
||||||
|
Bug fixes are fine even when they change rendering, as long as the old behaviour was clearly wrong (a knob that did not move in RTL). When a fix changes how a correct page looks, treat it as a default change.
|
||||||
|
|
||||||
|
## 2. How to make the change compatible
|
||||||
|
|
||||||
|
### Rename
|
||||||
|
|
||||||
|
Ship both names. The old one becomes an alias that nobody has to touch.
|
||||||
|
|
||||||
|
- **Class**: add the alias to `core/scss/_extends.scss` with `@extend`, next to `.form-hint`. Start the comment with `` `<old>` is deprecated, use `<new>` instead ``.
|
||||||
|
- **Custom property**: keep reading the old name as a fallback — `var(--new, var(--old, <default>))` — or keep emitting the old one with the same value.
|
||||||
|
- **Sass variable**: keep the old variable and default the new one to it: `$new: $old !default;`. For a map key, accept both keys.
|
||||||
|
- **Mixin or function parameter**: add new parameters at the end, with defaults. Never rename a keyword argument; add a second one and map the old onto it.
|
||||||
|
- **Data attribute, option, JS export, event**: accept or fire both. The `data-bs-*` / `data-tblr-*` pair in `core/js/src/bootstrap/dom/manipulator.ts` is the pattern.
|
||||||
|
|
||||||
|
### Remove
|
||||||
|
|
||||||
|
Do not. Mark it deprecated, stop using it in Tabler's own code, keep it working, and list it for removal in the next major. This includes things that look dead: a declaration with no effect inside Tabler may still be what a user overrides.
|
||||||
|
|
||||||
|
### Change what a name means
|
||||||
|
|
||||||
|
Do not reuse the name. `.legend` was a dot in 1.5 and became a whole legend item in 1.6, so old markup rendered as nothing. Either give the new thing a new name, or keep the old use working — `.legend:empty` in `_extends.scss` still draws the dot.
|
||||||
|
|
||||||
|
### Add a name
|
||||||
|
|
||||||
|
Adding is safe, with one trap: a name that fits an existing pattern may already sit in user markup as a no-op. `.text-gray-200` did nothing in 1.4 and turned text light gray in 1.5. When a new utility completes a series (`.text-*`, `.bg-*`, `.border-*`), say so under "Visual changes" in the upgrade guide.
|
||||||
|
|
||||||
|
### Change a default
|
||||||
|
|
||||||
|
Keep the old default and make the new behaviour opt-in: a class, an attribute, an option or a Sass variable. The new default can flip in the next major.
|
||||||
|
|
||||||
|
### Drop a bundled library or a file
|
||||||
|
|
||||||
|
Keep the file in `dist/` and the entry in `core/libs.json` until the next major, even if Tabler's own pages stop using it. Replacing a library with built-in code is fine, as long as the old script tag still loads and the old global still works or is harmless.
|
||||||
|
|
||||||
|
### Validate input more strictly
|
||||||
|
|
||||||
|
New validation must not throw on input that used to work. Coerce the value, or log with `console.warn` and fall back to the default. A throw during page load stops all of `tabler.js`.
|
||||||
|
|
||||||
|
## 3. Check the public surface before the PR
|
||||||
|
|
||||||
|
Build core, then diff the compiled CSS against the last release. Anything in the output is a removed class or custom property and needs an alias or a revert.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
last=$(npm view @tabler/core version)
|
||||||
|
curl -s "https://cdn.jsdelivr.net/npm/@tabler/core@$last/dist/css/tabler.css" -o /tmp/released.css
|
||||||
|
|
||||||
|
surface() { grep -oE "$1" "$2" | sort -u; }
|
||||||
|
class='\.[a-zA-Z][a-zA-Z0-9_-]*'
|
||||||
|
prop='--tblr-[a-zA-Z0-9_-]+'
|
||||||
|
|
||||||
|
# classes, then custom properties, that the release has and the build lost
|
||||||
|
comm -23 <(surface "$class" /tmp/released.css) <(surface "$class" core/dist/css/tabler.css)
|
||||||
|
comm -23 <(surface -e"$prop" /tmp/released.css) <(surface -e"$prop" core/dist/css/tabler.css)
|
||||||
|
```
|
||||||
|
|
||||||
|
For the rest, read the diff against the release tag with the table from section 1 in hand:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git diff "@tabler/core@$last"..HEAD -- core/scss/_variables.scss core/scss/mixins core/js core/libs.json core/package.json .browserslistrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for deleted `$variables`, changed mixin signatures, removed exports in `core/js/tabler.ts`, removed `libs.json` entries and new or removed dependencies.
|
||||||
|
|
||||||
|
## 4. Changesets and the upgrade guide
|
||||||
|
|
||||||
|
- A changeset on `dev` is `patch` or `minor`. If the honest bump is `major`, the change is on the wrong branch — go back to section 2.
|
||||||
|
- Describe a deprecation as one, for example ``Added `.legend-dot`; an empty `.legend` still works but is deprecated.`` Never write `Removed` for something a user could have used.
|
||||||
|
- In the upgrade guide a deprecation goes under a "renamed" heading with "the old name still works" (see the `upgrade-guide` skill). A minor release should have nothing under a breaking-change heading.
|
||||||
|
|
||||||
|
## 5. When a break cannot be avoided
|
||||||
|
|
||||||
|
Some changes have no alias: dropping a whole color model, raising the browser floor, removing a dependency users import. Those belong to the next major release, not to `dev`. Say so in the PR, and do not split the change to sneak half of it into a minor. If you are unsure whether something is a break, assume it is and ask.
|
||||||
@@ -87,6 +87,7 @@ Prefer a small dependency-free implementation over pulling a package in: every k
|
|||||||
|
|
||||||
- [ ] New module imported from `js/tabler.ts` (or deliberately kept out of the bundle)
|
- [ ] 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
|
- [ ] `data-bs-*` attribute API unchanged; accessible state updated with the class
|
||||||
|
- [ ] No export, option or event removed or renamed, and no new throw on input that used to work (`backward-compat`)
|
||||||
- [ ] Bootstrap-port changes stay close to upstream and come with a `js/tests/unit` spec
|
- [ ] Bootstrap-port changes stay close to upstream and come with a `js/tests/unit` spec
|
||||||
- [ ] `js-docs-*` markers intact and self-contained
|
- [ ] `js-docs-*` markers intact and self-contained
|
||||||
- [ ] `pnpm --filter @tabler/core test` (js + scss) clean
|
- [ ] `pnpm --filter @tabler/core test` (js + scss) clean
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ pnpm run bundlewatch # size budgets (tabler.css 80 kB, tab
|
|||||||
- [ ] Custom properties written bare; new foreign names added to `cssVarIgnore`
|
- [ ] Custom properties written bare; new foreign names added to `cssVarIgnore`
|
||||||
- [ ] Dark mode via `light-dark()` or all three dark selectors, behind `$enable-dark-mode`
|
- [ ] Dark mode via `light-dark()` or all three dark selectors, behind `$enable-dark-mode`
|
||||||
- [ ] Logical properties, or `--dir` + `/* rtl:ignore */`; no hand-written RTL
|
- [ ] Logical properties, or `--dir` + `/* rtl:ignore */`; no hand-written RTL
|
||||||
|
- [ ] No class, custom property, Sass variable or mixin parameter removed or renamed without an alias (`backward-compat`)
|
||||||
- [ ] sass-true test for a mixin that can break silently
|
- [ ] sass-true test for a mixin that can break silently
|
||||||
- [ ] `lint:scss`, `check:tokens`, `lint:prettier`, `test:scss` clean
|
- [ ] `lint:scss`, `check:tokens`, `lint:prettier`, `test:scss` clean
|
||||||
- [ ] Docs page and class table updated (`write-docs`, `class-reference`), changeset written
|
- [ ] Docs page and class table updated (`write-docs`, `class-reference`), changeset written
|
||||||
@@ -39,7 +39,7 @@ Include **every affected package** in the frontmatter. Omit packages with no rel
|
|||||||
|-------|------|
|
|-------|------|
|
||||||
| **patch** | Bug fixes, small improvements, style/accessibility tweaks, variable/token fixes, doc typo/format fixes |
|
| **patch** | Bug fixes, small improvements, style/accessibility tweaks, variable/token fixes, doc typo/format fixes |
|
||||||
| **minor** | New components, new pages, new CSS classes/utilities, significant enhancements, new preview demos |
|
| **minor** | New components, new pages, new CSS classes/utilities, significant enhancements, new preview demos |
|
||||||
| **major** | Breaking changes, removed APIs/classes, rewrites that break consumers (rare in this repo) |
|
| **major** | Breaking changes, removed APIs/classes, rewrites that break consumers. Never on `dev`: if the honest bump is `major`, stop and follow the `backward-compat` skill |
|
||||||
|
|
||||||
Packages can differ: e.g. `@tabler/core`: minor + `@tabler/docs`: patch is valid.
|
Packages can differ: e.g. `@tabler/core`: minor + `@tabler/docs`: patch is valid.
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
# Tabler's CSS uses these four with no fallback, so the floor is the first
|
# Tabler's CSS uses these five with no fallback, so the floor is the first
|
||||||
# release that supports all of them:
|
# release that supports all of them:
|
||||||
# light-dark() Chrome 123, Firefox 120, Safari 17.5
|
# light-dark() Chrome 123, Firefox 120, Safari 17.5
|
||||||
# @property Chrome 85, Firefox 128, Safari 16.4
|
# @property Chrome 85, Firefox 128, Safari 16.4
|
||||||
# :has() Chrome 105, Firefox 121, Safari 15.4
|
# :has() Chrome 105, Firefox 121, Safari 15.4
|
||||||
# color-mix() Chrome 111, Firefox 113, Safari 16.2
|
# color-mix() Chrome 111, Firefox 113, Safari 16.2
|
||||||
|
# oklch(from …) Chrome 122, Firefox 128, Safari 16.4
|
||||||
# Opera 109 and Samsung Internet 27 are the first releases built on Chromium 123.
|
# Opera 109 and Samsung Internet 27 are the first releases built on Chromium 123.
|
||||||
Chrome >= 123
|
Chrome >= 123
|
||||||
Edge >= 123
|
Edge >= 123
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
"@tabler/preview": patch
|
"@tabler/preview": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Added `Autosize` component to `tabler.js` with the `autosize` plugin bundled, plus `update()` and `dispose()` methods.
|
Added `Autosize` component to `tabler.js`, with `update()` and `dispose()` methods.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added relative colors, `oklch(from …)`, to the features that set the minimum browser versions on the browser support page.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tabler/docs": minor
|
||||||
|
"@tabler/preview": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added a Sortable docs page covering `data-sortable`, the drag styles and the `Sortable` component.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added a note to the 1.5 upgrade guide that `.text-gray-*` classes, a no-op in 1.4, now set the text color.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added Litepicker, `.legend` and neutral gray sections to the 1.6 upgrade guide; fixed the jsVectorMap CDN link.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `.alert-link` changing color on hover.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `.card-options` being taken out of the `.card-header` flow by `position: absolute`.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `Datepicker` reopening on a second trigger click, its `show` and `hide` events, and its color scheme on light pages.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `--tblr-focus-ring-width` and `--tblr-focus-ring-color` overrides having no effect when set on a component.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed the `.form-switch` knob not moving in RTL and its size in `.form-switch-lg`.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `tabler.js` stopping on page load when one element has an invalid `data-*` config; the error is logged instead.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed missing `IMaskInstance` and `SortableInstance` in `dist/types`; `vanilla-calendar-pro` is an optional peer dependency.
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
"@tabler/docs": minor
|
"@tabler/docs": minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Added `.legend` component with `.legend-dot`, `.legend-value`, `.legend-off` and `.legend-list`, plus `Legend` Astro component.
|
Changed `.legend` from a colored dot into a legend item with `.legend-dot`, `.legend-value`, `.legend-off` and `.legend-list`, plus `Legend` Astro component. An empty `.legend` still renders as a dot, but is deprecated in favor of `.legend-dot`.
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
"@tabler/docs": patch
|
"@tabler/docs": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Added a `switch-icon-loading` state and a `tabler:switch-icon-toggle` event that waits for a promise before switching.
|
Added a `switch-icon-loading` state and a cancelable `toggle.bs.switch-icon` event whose `event.wait(promise)` holds the switch until the promise settles.
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
"@tabler/docs": patch
|
"@tabler/docs": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Added `SwitchIcon`, `CountUp`, `InputMask` and `Sortable` components to `tabler.js`, with countUp.js bundled.
|
Added `SwitchIcon`, `CountUp`, `InputMask` and `Sortable` components to `tabler.js`.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type ComponentConfig = Record<string, never>
|
type ComponentConfig = Record<string, never>
|
||||||
@@ -134,9 +134,7 @@ class Autosize extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start autosize-init
|
// js-docs-start autosize-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, Autosize)
|
||||||
Autosize.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end autosize-init
|
// js-docs-end autosize-init
|
||||||
|
|
||||||
export default Autosize
|
export default Autosize
|
||||||
@@ -86,4 +86,23 @@ const eventActionOnPlugin = (Plugin: PluginComponent, onEvent: string, stringSel
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { enableDismissTrigger, eventAction, eventActionOnPlugin }
|
/**
|
||||||
|
* Creates the component for every element that matches on page load. One
|
||||||
|
* element with a broken config must not stop the rest of the bundle, so the
|
||||||
|
* error is logged and the loop goes on.
|
||||||
|
*/
|
||||||
|
const initAll = (selector: string, Plugin: PluginComponent, filter: ((element: HTMLElement) => boolean) | null = null): void => {
|
||||||
|
for (const element of SelectorEngine.find(selector)) {
|
||||||
|
if (filter && !filter(element)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Plugin.getOrCreateInstance(element)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { enableDismissTrigger, eventAction, eventActionOnPlugin, initAll }
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type ComponentConfig = {
|
type ComponentConfig = {
|
||||||
@@ -190,9 +191,7 @@ class Clipboard extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start clipboard-init
|
// js-docs-start clipboard-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, Clipboard)
|
||||||
Clipboard.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end clipboard-init
|
// js-docs-end clipboard-init
|
||||||
|
|
||||||
export default Clipboard
|
export default Clipboard
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type CountUpFormat = 'number' | 'time'
|
type CountUpFormat = 'number' | 'time'
|
||||||
@@ -312,9 +312,7 @@ class CountUp extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start countup-init
|
// js-docs-start countup-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_COUNTUP)) {
|
initAll(SELECTOR_DATA_COUNTUP, CountUp)
|
||||||
CountUp.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end countup-init
|
// js-docs-end countup-init
|
||||||
|
|
||||||
export default CountUp
|
export default CountUp
|
||||||
+97
-24
@@ -9,6 +9,7 @@ import type { Calendar, DateAny, DateMode, DatesArr, MonthsCount, Options, Posit
|
|||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import { isDisabled } from './bootstrap/util/index'
|
import { isDisabled } from './bootstrap/util/index'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
@@ -106,6 +107,13 @@ const DefaultType: Record<keyof ComponentConfig, string> = {
|
|||||||
vcpOptions: 'object',
|
vcpOptions: 'object',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delegated Data API handlers run in the capture phase, before the plugin's
|
||||||
|
// own click listener, which opens the popup from a `setTimeout`. Two nested
|
||||||
|
// timeouts are the first point that is after it for certain.
|
||||||
|
const afterPluginTimers = (callback: () => void): void => {
|
||||||
|
setTimeout(() => setTimeout(callback))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class definition
|
* Class definition
|
||||||
*
|
*
|
||||||
@@ -120,6 +128,14 @@ class Datepicker extends BaseComponent {
|
|||||||
declare _config: ComponentConfig
|
declare _config: ComponentConfig
|
||||||
_calendar: Calendar | null = null
|
_calendar: Calendar | null = null
|
||||||
_isShown = false
|
_isShown = false
|
||||||
|
// The plugin opens and closes the popup on its own too (its click and focus
|
||||||
|
// listeners, Escape, a click outside), so `onShow` and `onHide` are what
|
||||||
|
// tracks the state and fires the events; these flags tell them who asked.
|
||||||
|
_isShowing = false
|
||||||
|
_isHiding = false
|
||||||
|
_isSilent = false
|
||||||
|
_skipPluginShow = false
|
||||||
|
_resolveShown: (() => void) | null = null
|
||||||
_isInput = false
|
_isInput = false
|
||||||
_isInline = false
|
_isInline = false
|
||||||
_boundInput: HTMLInputElement | null = null
|
_boundInput: HTMLInputElement | null = null
|
||||||
@@ -173,7 +189,9 @@ class Datepicker extends BaseComponent {
|
|||||||
return // Inline calendars are always visible
|
return // Inline calendars are always visible
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._calendar || isDisabled(this._element) || this._isShown) {
|
// The first `calendar.show()` clicks the element to build the popup, and
|
||||||
|
// that click comes back here through the Data API; `_isShowing` stops it.
|
||||||
|
if (!this._calendar || isDisabled(this._element) || this._isShown || this._isShowing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +200,20 @@ class Datepicker extends BaseComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._isShowing = true
|
||||||
|
this._skipPluginShow = false
|
||||||
this._calendar.show()
|
this._calendar.show()
|
||||||
this._isShown = true
|
|
||||||
|
|
||||||
EventHandler.trigger(this._element, EVENT_SHOWN)
|
// The first show builds the popup and opens it a tick later
|
||||||
|
if (!this._isShown) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
this._resolveShown = resolve
|
||||||
|
afterPluginTimers(resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resolveShown = null
|
||||||
|
this._isShowing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async hide(): Promise<void> {
|
async hide(): Promise<void> {
|
||||||
@@ -202,10 +230,16 @@ class Datepicker extends BaseComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this._calendar.hide()
|
// On a click on a button trigger the plugin's own listener runs next and
|
||||||
this._isShown = false
|
// queues a `show()`; without this the popup would open again right away.
|
||||||
|
this._skipPluginShow = true
|
||||||
|
afterPluginTimers(() => {
|
||||||
|
this._skipPluginShow = false
|
||||||
|
})
|
||||||
|
|
||||||
EventHandler.trigger(this._element, EVENT_HIDDEN)
|
this._isHiding = true
|
||||||
|
this._calendar.hide()
|
||||||
|
this._isHiding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
@@ -419,6 +453,58 @@ class Datepicker extends BaseComponent {
|
|||||||
EventHandler.on(document, EVENT_FOCUSIN, this._onFocusIn)
|
EventHandler.on(document, EVENT_FOCUSIN, this._onFocusIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_silently(callback: () => void): void {
|
||||||
|
this._isSilent = true
|
||||||
|
callback()
|
||||||
|
this._isSilent = false
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePluginShow(): void {
|
||||||
|
const calendar = this._calendar
|
||||||
|
if (!calendar) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isSilent && !this._isShown) {
|
||||||
|
// Opened by the plugin itself: right after `hide()` on a trigger click it
|
||||||
|
// must stay closed, otherwise it gets the same cancelable `show` event.
|
||||||
|
const prevented = this._skipPluginShow || (!this._isShowing && EventHandler.trigger(this._element, EVENT_SHOW)?.defaultPrevented)
|
||||||
|
|
||||||
|
if (prevented) {
|
||||||
|
this._silently(() => calendar.hide())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasShown = this._isShown
|
||||||
|
this._isShown = true
|
||||||
|
this._syncThemeAttribute(calendar.context.mainElement)
|
||||||
|
this._alignToPositionElement()
|
||||||
|
|
||||||
|
if (!wasShown) {
|
||||||
|
EventHandler.trigger(this._element, EVENT_SHOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resolveShown?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
_handlePluginHide(): void {
|
||||||
|
const calendar = this._calendar
|
||||||
|
if (!calendar || this._isSilent || !this._isShown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed by the plugin itself (Escape, a click outside): `hide` is still
|
||||||
|
// cancelable, the popup opens again before the browser paints.
|
||||||
|
if (!this._isHiding && EventHandler.trigger(this._element, EVENT_HIDE)?.defaultPrevented) {
|
||||||
|
this._silently(() => calendar.show())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._isShown = false
|
||||||
|
EventHandler.trigger(this._element, EVENT_HIDDEN)
|
||||||
|
}
|
||||||
|
|
||||||
_buildCalendarOptions(): Options {
|
_buildCalendarOptions(): Options {
|
||||||
// The plugin uses 'system' for auto-detection, Bootstrap and Tabler use 'auto'
|
// The plugin uses 'system' for auto-detection, Bootstrap and Tabler use 'auto'
|
||||||
const theme = this._getEffectiveTheme()
|
const theme = this._getEffectiveTheme()
|
||||||
@@ -440,18 +526,8 @@ class Datepicker extends BaseComponent {
|
|||||||
onInit: (self) => {
|
onInit: (self) => {
|
||||||
this._syncThemeAttribute(self.context.mainElement)
|
this._syncThemeAttribute(self.context.mainElement)
|
||||||
},
|
},
|
||||||
onShow: () => {
|
onShow: () => this._handlePluginShow(),
|
||||||
if (!this._calendar) {
|
onHide: () => this._handlePluginHide(),
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isShown = true
|
|
||||||
this._syncThemeAttribute(this._calendar.context.mainElement)
|
|
||||||
this._alignToPositionElement()
|
|
||||||
},
|
|
||||||
onHide: () => {
|
|
||||||
this._isShown = false
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the month of the first selected date
|
// Navigate to the month of the first selected date
|
||||||
@@ -584,14 +660,11 @@ EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function
|
|||||||
|
|
||||||
// Render on load what cannot wait for a focus or a click: inline calendars,
|
// Render on load what cannot wait for a focus or a click: inline calendars,
|
||||||
// and fields with preselected dates or a value, so they show it right away.
|
// and fields with preselected dates or a value, so they show it right away.
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, Datepicker, (element) => {
|
||||||
const { dataset } = element
|
const { dataset } = element
|
||||||
const eager = dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || (element as HTMLInputElement).value
|
|
||||||
|
|
||||||
if (eager) {
|
return dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || Boolean((element as HTMLInputElement).value)
|
||||||
Datepicker.getOrCreateInstance(element)
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
// js-docs-end datepicker-init
|
// js-docs-end datepicker-init
|
||||||
|
|
||||||
export default Datepicker
|
export default Datepicker
|
||||||
|
|||||||
Vendored
+6
-20
@@ -1,23 +1,9 @@
|
|||||||
// IMask (https://imask.js.org) is loaded separately, not bundled
|
// Plugins that are loaded separately, not bundled: IMask (https://imask.js.org),
|
||||||
interface IMaskInstance {
|
// SortableJS (https://sortablejs.github.io/Sortable/) and Vanilla Calendar Pro
|
||||||
value: string
|
// (https://vanilla-calendar.pro). The instance types live in the component
|
||||||
unmaskedValue: string
|
// modules, so they are part of the published `dist/types`.
|
||||||
updateValue(): void
|
|
||||||
updateOptions(options: Record<string, unknown>): void
|
|
||||||
destroy(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortableJS (https://sortablejs.github.io/Sortable/) is loaded separately, not bundled
|
|
||||||
interface SortableInstance {
|
|
||||||
option(name: string, value?: unknown): unknown
|
|
||||||
toArray(): string[]
|
|
||||||
sort(order: string[], useAnimation?: boolean): void
|
|
||||||
destroy(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
// Vanilla Calendar Pro (https://vanilla-calendar.pro) is loaded separately, not bundled
|
|
||||||
VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
|
VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
|
||||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => IMaskInstance
|
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => import('./input-mask').IMaskInstance
|
||||||
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => SortableInstance
|
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => import('./sortable').SortableInstance
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
|
// The part of an IMask instance the component uses
|
||||||
|
interface IMaskInstance {
|
||||||
|
value: string
|
||||||
|
unmaskedValue: string
|
||||||
|
updateValue(): void
|
||||||
|
updateOptions(options: Record<string, unknown>): void
|
||||||
|
destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
type ComponentConfig = {
|
type ComponentConfig = {
|
||||||
mask: string
|
mask: string
|
||||||
lazy: boolean
|
lazy: boolean
|
||||||
@@ -114,9 +123,8 @@ class InputMask extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start input-mask-init
|
// js-docs-start input-mask-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_MASK)) {
|
initAll(SELECTOR_DATA_MASK, InputMask)
|
||||||
InputMask.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end input-mask-init
|
// js-docs-end input-mask-init
|
||||||
|
|
||||||
export default InputMask
|
export default InputMask
|
||||||
|
export type { IMaskInstance }
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type OtpInputType = 'numeric' | 'alphanumeric' | 'alpha'
|
type OtpInputType = 'numeric' | 'alphanumeric' | 'alpha'
|
||||||
@@ -425,9 +426,7 @@ class OtpInput extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start otp-init
|
// js-docs-start otp-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, OtpInput)
|
||||||
OtpInput.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end otp-init
|
// js-docs-end otp-init
|
||||||
|
|
||||||
export default OtpInput
|
export default OtpInput
|
||||||
+11
-4
@@ -6,9 +6,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
|
// The part of a SortableJS instance the component uses
|
||||||
|
interface SortableInstance {
|
||||||
|
option(name: string, value?: unknown): unknown
|
||||||
|
toArray(): string[]
|
||||||
|
sort(order: string[], useAnimation?: boolean): void
|
||||||
|
destroy(): void
|
||||||
|
}
|
||||||
|
|
||||||
type ComponentConfig = Record<string, unknown>
|
type ComponentConfig = Record<string, unknown>
|
||||||
|
|
||||||
type ComponentConfigInput = Record<string, unknown>
|
type ComponentConfigInput = Record<string, unknown>
|
||||||
@@ -108,9 +116,8 @@ class Sortable extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start sortable-init
|
// js-docs-start sortable-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_SORTABLE)) {
|
initAll(SELECTOR_DATA_SORTABLE, Sortable)
|
||||||
Sortable.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end sortable-init
|
// js-docs-end sortable-init
|
||||||
|
|
||||||
export default Sortable
|
export default Sortable
|
||||||
|
export type { SortableInstance }
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ComponentConfig as BaseConfig, ElementSelector } from './bootstrap/types'
|
import type { ComponentConfig as BaseConfig, ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type SparklineType = 'line' | 'bar' | 'circle' | 'tristate'
|
type SparklineType = 'line' | 'bar' | 'circle' | 'tristate'
|
||||||
@@ -603,9 +603,7 @@ class Sparkline extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start sparkline-init
|
// js-docs-start sparkline-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, Sparkline)
|
||||||
Sparkline.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end sparkline-init
|
// js-docs-end sparkline-init
|
||||||
|
|
||||||
export default Sparkline
|
export default Sparkline
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong'
|
type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong'
|
||||||
@@ -270,9 +271,7 @@ class Strength extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start strength-init
|
// js-docs-start strength-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) {
|
initAll(SELECTOR_DATA_STRENGTH, Strength)
|
||||||
Strength.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end strength-init
|
// js-docs-end strength-init
|
||||||
|
|
||||||
export default Strength
|
export default Strength
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import BaseComponent from './bootstrap/base-component'
|
import BaseComponent from './bootstrap/base-component'
|
||||||
import EventHandler from './bootstrap/dom/event-handler'
|
import EventHandler from './bootstrap/dom/event-handler'
|
||||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||||
|
import { initAll } from './bootstrap/util/component-functions'
|
||||||
import type { ElementSelector } from './bootstrap/types'
|
import type { ElementSelector } from './bootstrap/types'
|
||||||
|
|
||||||
type ComponentConfig = Record<string, never>
|
type ComponentConfig = Record<string, never>
|
||||||
@@ -160,9 +161,7 @@ class SwitchIcon extends BaseComponent {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// js-docs-start switch-icon-init
|
// js-docs-start switch-icon-init
|
||||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
initAll(SELECTOR_DATA_TOGGLE, SwitchIcon)
|
||||||
SwitchIcon.getOrCreateInstance(element)
|
|
||||||
}
|
|
||||||
// js-docs-end switch-icon-init
|
// js-docs-end switch-icon-init
|
||||||
|
|
||||||
export default SwitchIcon
|
export default SwitchIcon
|
||||||
@@ -27,6 +27,7 @@ describe('Datepicker', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
|
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
|
||||||
|
const tick = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 20))
|
||||||
|
|
||||||
describe('NAME', () => {
|
describe('NAME', () => {
|
||||||
it('should return plugin name', () => {
|
it('should return plugin name', () => {
|
||||||
@@ -243,5 +244,67 @@ describe('Datepicker', () => {
|
|||||||
expect(instance).not.toBeNull()
|
expect(instance).not.toBeNull()
|
||||||
expect(instance!._isShown).toBe(true)
|
expect(instance!._isShown).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should close on the second click of a button trigger', async () => {
|
||||||
|
fixtureEl.innerHTML = '<button type="button" data-bs-toggle="datepicker">Select date</button>'
|
||||||
|
|
||||||
|
const button = fixtureEl.querySelector('button')!
|
||||||
|
const seen: string[] = []
|
||||||
|
for (const name of ['show', 'shown', 'hide', 'hidden']) {
|
||||||
|
button.addEventListener(`${name}.bs.datepicker`, () => seen.push(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
button.click()
|
||||||
|
await tick()
|
||||||
|
button.click()
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
const instance = Datepicker.getInstance(button) as Datepicker
|
||||||
|
expect(instance._isShown).toBe(false)
|
||||||
|
expect(instance.calendar!.context.isShowInInputMode).toBe(false)
|
||||||
|
expect(seen).toEqual(['show', 'shown', 'hide', 'hidden'])
|
||||||
|
|
||||||
|
button.click()
|
||||||
|
await tick()
|
||||||
|
expect(instance._isShown).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('closed by the plugin', () => {
|
||||||
|
const pressEscape = (): void => {
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should fire hide and hidden on Escape', async () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-toggle="datepicker">'
|
||||||
|
const seen: string[] = []
|
||||||
|
for (const name of ['hide', 'hidden']) {
|
||||||
|
input().addEventListener(`${name}.bs.datepicker`, () => seen.push(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
await instance.show()
|
||||||
|
pressEscape()
|
||||||
|
|
||||||
|
expect(instance._isShown).toBe(false)
|
||||||
|
expect(seen).toEqual(['hide', 'hidden'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stay open when hide is prevented', async () => {
|
||||||
|
fixtureEl.innerHTML = '<input type="text" data-bs-toggle="datepicker">'
|
||||||
|
const seen: string[] = []
|
||||||
|
input().addEventListener('hide.bs.datepicker', (event) => event.preventDefault())
|
||||||
|
for (const name of ['shown', 'hidden']) {
|
||||||
|
input().addEventListener(`${name}.bs.datepicker`, () => seen.push(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new Datepicker(input())
|
||||||
|
await instance.show()
|
||||||
|
pressEscape()
|
||||||
|
|
||||||
|
expect(instance._isShown).toBe(true)
|
||||||
|
expect(instance.calendar!.context.isShowInInputMode).toBe(true)
|
||||||
|
expect(seen).toEqual(['shown'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
|
||||||
import BaseComponent from '../../../src/bootstrap/base-component'
|
import BaseComponent from '../../../src/bootstrap/base-component'
|
||||||
import { enableDismissTrigger, eventActionOnPlugin } from '../../../src/bootstrap/util/component-functions'
|
import { enableDismissTrigger, eventActionOnPlugin, initAll } from '../../../src/bootstrap/util/component-functions'
|
||||||
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
|
import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
|
||||||
|
|
||||||
class DummyClass extends BaseComponent {
|
class DummyClass extends BaseComponent {
|
||||||
@@ -145,4 +145,47 @@ describe('Component Functions', () => {
|
|||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('initAll', () => {
|
||||||
|
it('should create an instance for every matching element', () => {
|
||||||
|
fixtureEl.innerHTML = '<div class="init-all"></div><div class="init-all"></div>'
|
||||||
|
|
||||||
|
initAll('.init-all', DummyClass)
|
||||||
|
|
||||||
|
for (const element of fixtureEl.querySelectorAll<HTMLElement>('.init-all')) {
|
||||||
|
expect(DummyClass.getInstance(element)).toBeInstanceOf(DummyClass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip the elements the filter rejects', () => {
|
||||||
|
fixtureEl.innerHTML = '<div class="init-all" id="skip"></div><div class="init-all" id="keep"></div>'
|
||||||
|
|
||||||
|
initAll('.init-all', DummyClass, (element) => element.id === 'keep')
|
||||||
|
|
||||||
|
expect(DummyClass.getInstance(fixtureEl.querySelector<HTMLElement>('#skip'))).toBeNull()
|
||||||
|
expect(DummyClass.getInstance(fixtureEl.querySelector<HTMLElement>('#keep'))).toBeInstanceOf(DummyClass)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should log the error and go on when one element throws', () => {
|
||||||
|
fixtureEl.innerHTML = '<div class="init-all" id="broken"></div><div class="init-all" id="fine"></div>'
|
||||||
|
|
||||||
|
const error = new TypeError('TEST: bad option')
|
||||||
|
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
const Throwing = {
|
||||||
|
NAME: 'test',
|
||||||
|
getOrCreateInstance(element: HTMLElement | string | null) {
|
||||||
|
if ((element as HTMLElement).id === 'broken') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return DummyClass.getOrCreateInstance(element as HTMLElement)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => initAll('.init-all', Throwing)).not.toThrow()
|
||||||
|
expect(spyError).toHaveBeenCalledWith(error)
|
||||||
|
expect(DummyClass.getInstance(fixtureEl.querySelector<HTMLElement>('#fine'))).toBeInstanceOf(DummyClass)
|
||||||
|
|
||||||
|
spyError.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -245,6 +245,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.8"
|
"@popperjs/core": "^2.11.8"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vanilla-calendar-pro": "^3.3.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vanilla-calendar-pro": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@melloware/coloris": "^0.25.0",
|
"@melloware/coloris": "^0.25.0",
|
||||||
"@types/node": "^26.4.0",
|
"@types/node": "^26.4.0",
|
||||||
|
|||||||
@@ -111,3 +111,10 @@
|
|||||||
// Kept from the old rule, so an inline element still gets the top margin.
|
// Kept from the old rule, so an inline element still gets the top margin.
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before 1.6 `.legend` was the dot itself and had no children. An empty
|
||||||
|
// `.legend` is deprecated, use `.legend-dot` instead (from `ui/_legend.scss`).
|
||||||
|
.legend:empty {
|
||||||
|
@extend .legend-dot;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@@ -122,7 +122,6 @@
|
|||||||
--focus-ring-opacity: #{$focus-ring-opacity};
|
--focus-ring-opacity: #{$focus-ring-opacity};
|
||||||
--focus-ring-opacity-dark: #{$focus-ring-opacity-dark};
|
--focus-ring-opacity-dark: #{$focus-ring-opacity-dark};
|
||||||
--focus-ring-color: #{$focus-ring-color};
|
--focus-ring-color: #{$focus-ring-color};
|
||||||
--focus-ring: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
||||||
// scss-docs-end root-focus-variables
|
// scss-docs-end root-focus-variables
|
||||||
|
|
||||||
// scss-docs-start root-form-validation-variables
|
// scss-docs-start root-form-validation-variables
|
||||||
|
|||||||
@@ -145,7 +145,8 @@
|
|||||||
|
|
||||||
// The knob replaces the masked check icon: a plain circle that slides.
|
// The knob replaces the masked check icon: a plain circle that slides.
|
||||||
&::before {
|
&::before {
|
||||||
inset: 0 auto 0 var(--form-switch-knob-offset);
|
inset-block: 0;
|
||||||
|
inset-inline: var(--form-switch-knob-offset) auto;
|
||||||
width: var(--form-switch-knob-size);
|
width: var(--form-switch-knob-size);
|
||||||
height: var(--form-switch-knob-size);
|
height: var(--form-switch-knob-size);
|
||||||
margin-block: auto;
|
margin-block: auto;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
|
|
||||||
// Runtime counterpart of `adjust-lightness()`: relative color syntax, so the
|
// Runtime counterpart of `adjust-lightness()`: relative color syntax, so the
|
||||||
// token follows `--tblr-*` overrides in the browser. Emit-only, like `mix-color-css()`.
|
// token follows `--tblr-*` overrides in the browser. Emit-only, like `mix-color-css()`.
|
||||||
|
//
|
||||||
|
// Only `l` goes through `calc()`. Safari before 18 resolves `h` to an angle, so
|
||||||
|
// `calc(h + 30)` is invalid there and needs `calc(h + 30deg)`; `l` and `c` are
|
||||||
|
// numbers in every browser. The origin must be a plain color: Safari before 18
|
||||||
|
// does not take `currentcolor`, a system color or `light-dark()` there.
|
||||||
@function adjust-lightness-css($color, $amount) {
|
@function adjust-lightness-css($color, $amount) {
|
||||||
$delta: math.div($amount, 100%);
|
$delta: math.div($amount, 100%);
|
||||||
$sign: if(sass($delta < 0): '-'; else: '+');
|
$sign: if(sass($delta < 0): '-'; else: '+');
|
||||||
|
|||||||
@@ -66,11 +66,14 @@
|
|||||||
gap: var(--list-gap);
|
gap: var(--list-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `--focus-ring` is not set by default: composed on `:root` it would freeze the
|
||||||
|
// width and the colour there, and an override on a component would not reach
|
||||||
|
// the ring. Set it to replace the whole outline at once.
|
||||||
@mixin focus-ring($offset: false, $color: null) {
|
@mixin focus-ring($offset: false, $color: null) {
|
||||||
@if $color != null {
|
@if $color != null {
|
||||||
outline: var(--focus-ring-width) solid #{$color};
|
outline: var(--focus-ring-width) solid #{$color};
|
||||||
} @else {
|
} @else {
|
||||||
outline: var(--focus-ring);
|
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||||
}
|
}
|
||||||
@if $offset {
|
@if $offset {
|
||||||
outline-offset: var(--focus-ring-offset);
|
outline-offset: var(--focus-ring-offset);
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
|
|||||||
"--tblr-chart-5",
|
"--tblr-chart-5",
|
||||||
"--tblr-danger",
|
"--tblr-danger",
|
||||||
"--tblr-focus-ring",
|
"--tblr-focus-ring",
|
||||||
|
"--tblr-focus-ring-color",
|
||||||
"--tblr-focus-ring-offset",
|
"--tblr-focus-ring-offset",
|
||||||
"--tblr-focus-ring-opacity",
|
"--tblr-focus-ring-opacity",
|
||||||
"--tblr-focus-ring-opacity-dark",
|
"--tblr-focus-ring-opacity-dark",
|
||||||
|
"--tblr-focus-ring-width",
|
||||||
"--tblr-font-sans-serif",
|
"--tblr-font-sans-serif",
|
||||||
"--tblr-font-size-h3",
|
"--tblr-font-size-h3",
|
||||||
"--tblr-font-size-h5",
|
"--tblr-font-size-h5",
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
@include expect() {
|
@include expect() {
|
||||||
.t {
|
.t {
|
||||||
outline: var(--focus-ring);
|
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
@include expect() {
|
@include expect() {
|
||||||
.t {
|
.t {
|
||||||
outline: var(--focus-ring);
|
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||||
outline-offset: var(--focus-ring-offset);
|
outline-offset: var(--focus-ring-offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,10 @@
|
|||||||
.alert-link {
|
.alert-link {
|
||||||
font-weight: $alert-link-font-weight;
|
font-weight: $alert-link-font-weight;
|
||||||
color: var(--alert-link-color);
|
color: var(--alert-link-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--alert-link-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-dismissible {
|
.alert-dismissible {
|
||||||
|
|||||||
@@ -405,9 +405,6 @@ Card optinos
|
|||||||
.card-options {
|
.card-options {
|
||||||
--card-options-top: #{$card-options-top};
|
--card-options-top: #{$card-options-top};
|
||||||
--card-options-inset: #{$card-options-inset};
|
--card-options-inset: #{$card-options-inset};
|
||||||
position: absolute;
|
|
||||||
inset-inline-end: var(--card-options-inset);
|
|
||||||
top: var(--card-options-top);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,6 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: var(--calendar-font-size);
|
font-size: var(--calendar-font-size);
|
||||||
color: var(--calendar-color);
|
color: var(--calendar-color);
|
||||||
color-scheme: light dark;
|
|
||||||
|
|
||||||
&[data-bs-theme='light'] {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-bs-theme='dark'] {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ Form switch
|
|||||||
padding-inline-start: var(--form-switch-lg-padding-start);
|
padding-inline-start: var(--form-switch-lg-padding-start);
|
||||||
|
|
||||||
.form-check-input {
|
.form-check-input {
|
||||||
|
--form-switch-knob-size: calc(var(--form-switch-lg-height) * 0.75);
|
||||||
|
--form-switch-knob-offset: calc(var(--form-switch-lg-height) * 0.125);
|
||||||
width: var(--form-switch-lg-width);
|
width: var(--form-switch-lg-width);
|
||||||
height: var(--form-switch-lg-height);
|
height: var(--form-switch-lg-height);
|
||||||
margin-inline-start: var(--form-switch-lg-input-offset);
|
margin-inline-start: var(--form-switch-lg-input-offset);
|
||||||
background-size: var(--form-switch-lg-bg-size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-check-label {
|
.form-check-label {
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ Feature|Used for|Chrome|Firefox|Safari
|
|||||||
`@property`|The animated gradient on cards|85|128|16.4
|
`@property`|The animated gradient on cards|85|128|16.4
|
||||||
`:has()`|State that depends on a child, for example a selected table row or the folded sidebar|105|121|15.4
|
`:has()`|State that depends on a child, for example a selected table row or the folded sidebar|105|121|15.4
|
||||||
`color-mix()`|Tints, shades and translucent surfaces|111|113|16.2
|
`color-mix()`|Tints, shades and translucent surfaces|111|113|16.2
|
||||||
|
Relative colors, `oklch(from …)`|The darker hover shade of every color and the dark mode borders|122|128|16.4
|
||||||
|
|
||||||
Opera 109 and Samsung Internet 27 are the first releases built on Chromium 123.
|
Opera 109 and Samsung Internet 27 are the first releases built on Chromium 123.
|
||||||
|
|
||||||
|
Safari 16.4 to 17 follow an older draft of relative colors, where math on the hue needs a `deg` unit. Tabler only changes the lightness, which works the same in every version. If you override a Tabler color with `light-dark()`, its `-darken` shade needs Safari 18.
|
||||||
|
|
||||||
## Unsupported browsers
|
## Unsupported browsers
|
||||||
|
|
||||||
Internet Explorer is not supported, and neither is any browser released before the versions above. There is no polyfill to add: the features that set the floor are CSS, so a missing one cannot be patched with JavaScript.
|
Internet Explorer is not supported, and neither is any browser released before the versions above. There is no polyfill to add: the features that set the floor are CSS, so a missing one cannot be patched with JavaScript.
|
||||||
@@ -27,6 +27,7 @@ Load `tabler-theme.js`|Pages follow the OS color scheme now - see [Color mode](#
|
|||||||
Use payment provider icons|Check the [removed providers](#removed-payment-providers)
|
Use payment provider icons|Check the [removed providers](#removed-payment-providers)
|
||||||
Link files inside `dist/libs`|Check the [moved paths](#smaller-distlibs-folder)
|
Link files inside `dist/libs`|Check the [moved paths](#smaller-distlibs-folder)
|
||||||
Load `tabler.rtl.css`|You can drop it - plain `tabler.css` now handles RTL
|
Load `tabler.rtl.css`|You can drop it - plain `tabler.css` now handles RTL
|
||||||
|
Have `.text-gray-*` classes in your markup|They color the text now - see [Gray text utilities](#gray-text-utilities)
|
||||||
|
|
||||||
## Update the package
|
## Update the package
|
||||||
|
|
||||||
@@ -277,6 +278,19 @@ These changes are not breaking, but they change how a page looks. Check your scr
|
|||||||
- **Layout**: the `margin-inline-start: calc(100vw - 100%)` hack on the root element is gone, so there is no white gap on the left when a scrollbar is present.
|
- **Layout**: the `margin-inline-start: calc(100vw - 100%)` hack on the root element is gone, so there is no white gap on the left when a scrollbar is present.
|
||||||
- **Trending**: the component uses the `arrow-up` and `arrow-down` icons instead of `trending-up` and `trending-down`.
|
- **Trending**: the component uses the `arrow-up` and `arrow-down` icons instead of `trending-up` and `trending-down`.
|
||||||
|
|
||||||
|
### Gray text utilities
|
||||||
|
|
||||||
|
Tabler 1.4 had no `.text-gray-*` classes, only `.text-gray-*-fg`. A class like `.text-gray-200` in your markup did nothing, and the text kept the color of its parent. Tabler 1.5 adds `.text-gray-50` to `.text-gray-950`, so the same class now sets the color. On `.text-gray-200` that is a very light gray, which is hard to read on a white page.
|
||||||
|
|
||||||
|
Pick the shade you want to see, or remove the class:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <p class="text-gray-200">Last updated 3 hours ago</p>
|
||||||
|
+ <p class="text-gray-600">Last updated 3 hours ago</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
For muted text, `.text-secondary` follows the theme and keeps enough contrast in dark mode.
|
||||||
|
|
||||||
## RTL
|
## RTL
|
||||||
|
|
||||||
The Sass sources use logical properties and a `--tblr-dir` multiplier, so plain `tabler.css` works in both directions. Set `dir` on the document and drop the RTL stylesheet:
|
The Sass sources use logical properties and a `--tblr-dir` multiplier, so plain `tabler.css` works in both directions. Set `dir` on the document and drop the RTL stylesheet:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ order: 1
|
|||||||
related: [/ui/getting-started/upgrade/1-5, /ui/getting-started/customize]
|
related: [/ui/getting-started/upgrade/1-5, /ui/getting-started/customize]
|
||||||
---
|
---
|
||||||
|
|
||||||
Tabler 1.6 is a much smaller step than 1.5 was. If you only load the compiled files from `dist/`, the upgrade usually takes a few minutes. There are three things that can bite you, and they all live in your own code, not in Tabler's: any `rgba(var(--tblr-*-rgb), …)` in your CSS, any call to the old `autosize()`, `countUp` or `ClipboardJS` globals, and any use of `tabler.tabler.getColor()`.
|
Tabler 1.6 is a much smaller step than 1.5 was. If you only load the compiled files from `dist/`, the upgrade usually takes a few minutes. There are four things that can bite you, and they all live in your own code, not in Tabler's: any `rgba(var(--tblr-*-rgb), …)` in your CSS, any call to the old `autosize()`, `countUp`, `ClipboardJS` or `Litepicker` globals, any use of `tabler.tabler.getColor()`, and any `.legend` used as a colored dot.
|
||||||
|
|
||||||
Projects that compile Tabler from the Sass sources should also read [Sass changes](#sass-changes).
|
Projects that compile Tabler from the Sass sources should also read [Sass changes](#sass-changes).
|
||||||
|
|
||||||
@@ -23,6 +23,10 @@ Use `--tblr-primary-rgb` or any `--tblr-*-rgb` variable|Replace it with `color-m
|
|||||||
Load `dist/libs/autosize` or `dist/libs/countup.js`|Remove the script tags, both are part of `tabler.js` now
|
Load `dist/libs/autosize` or `dist/libs/countup.js`|Remove the script tags, both are part of `tabler.js` now
|
||||||
Call `autosize(el)` or `new countUp.CountUp(...)` yourself|Switch to `tabler.Autosize` and `tabler.CountUp`
|
Call `autosize(el)` or `new countUp.CountUp(...)` yourself|Switch to `tabler.Autosize` and `tabler.CountUp`
|
||||||
Load `dist/libs/clipboard` or call `new ClipboardJS(...)`|Switch to the `Clipboard` component, see [Clipboard](#clipboard-is-built-in)
|
Load `dist/libs/clipboard` or call `new ClipboardJS(...)`|Switch to the `Clipboard` component, see [Clipboard](#clipboard-is-built-in)
|
||||||
|
Load `dist/libs/litepicker` or call `new Litepicker(...)`|Switch to the `Datepicker` plugin, see [Litepicker is replaced by Datepicker](#litepicker-is-replaced-by-datepicker)
|
||||||
|
Use `<span class="legend">` as a colored dot|Rename to `.legend-dot` (the old markup still works), see [Legend is a component now](#legend-is-a-component-now)
|
||||||
|
Want the 1.5 grays back|Load `tabler-themes.css` and set `data-bs-theme-base="gray"`, see [The default gray is neutral](#the-default-gray-is-neutral)
|
||||||
|
Use `data-sortable`|Dragging looks different, see [Sortable drags a styled copy](#sortable-drags-a-styled-copy)
|
||||||
Hide `path:first-child` in `.accordion-button-toggle-plus` from your own CSS|Drop that rule, see [Visual changes](#visual-changes)
|
Hide `path:first-child` in `.accordion-button-toggle-plus` from your own CSS|Drop that rule, see [Visual changes](#visual-changes)
|
||||||
Call `tabler.tabler.getColor()` or `tabler.tabler.hexToRgba()`|Read the variable yourself, see [Removed JavaScript helpers](#removed-javascript-helpers)
|
Call `tabler.tabler.getColor()` or `tabler.tabler.hexToRgba()`|Read the variable yourself, see [Removed JavaScript helpers](#removed-javascript-helpers)
|
||||||
Use `.form-hint`|Rename to `.form-text` (the old name still works)
|
Use `.form-hint`|Rename to `.form-text` (the old name still works)
|
||||||
@@ -100,7 +104,7 @@ Component|Attribute|Docs
|
|||||||
`tabler.Autosize`|`data-bs-toggle="autosize"`|[Autosize](/ui/plugins/autosize)
|
`tabler.Autosize`|`data-bs-toggle="autosize"`|[Autosize](/ui/plugins/autosize)
|
||||||
`tabler.CountUp`|`data-countup`|[Countup](/ui/plugins/countup)
|
`tabler.CountUp`|`data-countup`|[Countup](/ui/plugins/countup)
|
||||||
`tabler.InputMask`|`data-mask`|[Input mask](/ui/plugins/input-mask)
|
`tabler.InputMask`|`data-mask`|[Input mask](/ui/plugins/input-mask)
|
||||||
`tabler.Sortable`|`data-sortable`|[Table](/ui/components/table)
|
`tabler.Sortable`|`data-sortable`|[Sortable](/ui/plugins/sortable)
|
||||||
`tabler.SwitchIcon`|`data-bs-toggle="switch-icon"`|[Switch icon](/ui/components/switch-icon)
|
`tabler.SwitchIcon`|`data-bs-toggle="switch-icon"`|[Switch icon](/ui/components/switch-icon)
|
||||||
|
|
||||||
Markup with data attributes keeps working as it did. Tabler still finds every element on page load and creates the component for it. What changes is the JavaScript side.
|
Markup with data attributes keeps working as it did. Tabler still finds every element on page load and creates the component for it. What changes is the JavaScript side.
|
||||||
@@ -140,7 +144,16 @@ Options you passed to countUp.js in `data-countup` keep their names: `startVal`,
|
|||||||
|
|
||||||
### IMask and SortableJS stay external
|
### IMask and SortableJS stay external
|
||||||
|
|
||||||
`InputMask` and `Sortable` are thin wrappers, so you still install [IMask](/ui/plugins/input-mask) and [SortableJS](/ui/components/table) yourself and load them before `tabler.js`. Nothing changes there. What's new is that the component keeps a reference to the library instance, so you can reach it from `getInstance()` instead of keeping your own.
|
`InputMask` and `Sortable` are thin wrappers, so you still install [IMask](/ui/plugins/input-mask) and [SortableJS](/ui/plugins/sortable) yourself and load them before `tabler.js`. Nothing changes there. What's new is that the component keeps a reference to the library instance, so you can reach it from `getInstance()` instead of keeping your own.
|
||||||
|
|
||||||
|
#### Sortable drags a styled copy
|
||||||
|
|
||||||
|
The `Sortable` component now turns on the SortableJS `forceFallback` option. A dragged item is an HTML copy that Tabler styles with `.sortable-drag`, instead of the browser's own drag image. Turn the option off on a list to get the 1.5 behavior back:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <ul class="list-group" data-sortable>
|
||||||
|
+ <ul class="list-group" data-sortable='{"forceFallback":false}'>
|
||||||
|
```
|
||||||
|
|
||||||
### Clipboard is built in
|
### Clipboard is built in
|
||||||
|
|
||||||
@@ -178,6 +191,35 @@ If you created the library yourself, switch to the component and its events:
|
|||||||
|
|
||||||
The browser API works in secure contexts only, so on plain `http` the copy fails and `error.bs.clipboard` fires. `localhost` counts as secure.
|
The browser API works in secure contexts only, so on plain `http` the copy fails and `error.bs.clipboard` fires. `localhost` counts as secure.
|
||||||
|
|
||||||
|
### Litepicker is replaced by Datepicker
|
||||||
|
|
||||||
|
[Litepicker](https://github.com/wakirin/Litepicker) has been archived since 2023, so Tabler dropped it. The package is gone from the dependencies and from `dist/libs`, and its styles are gone from `tabler-vendors.css`. A page that loads `dist/libs/litepicker/dist/litepicker.js` gets a 404 after the upgrade.
|
||||||
|
|
||||||
|
The new [Datepicker](/ui/plugins/date-picker) plugin is part of `tabler.js`. It renders its calendar with [Vanilla Calendar Pro](https://vanilla-calendar.pro), which you load in place of Litepicker:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <script src="/dist/libs/litepicker/dist/litepicker.js"></script>
|
||||||
|
+ <script src="/dist/libs/vanilla-calendar-pro/index.js"></script>
|
||||||
|
<script src="/dist/js/tabler.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm uninstall litepicker
|
||||||
|
npm install vanilla-calendar-pro
|
||||||
|
```
|
||||||
|
|
||||||
|
Then replace the script that created the picker with a data attribute:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <input class="form-control" id="date" />
|
||||||
|
- <script>
|
||||||
|
- new Litepicker({ element: document.getElementById('date') });
|
||||||
|
- </script>
|
||||||
|
+ <input type="text" class="form-control" id="date" data-bs-toggle="datepicker" autocomplete="off" />
|
||||||
|
```
|
||||||
|
|
||||||
|
The Datepicker page has a [table that maps every Litepicker option](/ui/plugins/date-picker#migration-from-litepicker) to its new name. Tabler only removed its own copy and styles, so a project that installs Litepicker on its own keeps working, but without Tabler's look.
|
||||||
|
|
||||||
### New events
|
### New events
|
||||||
|
|
||||||
Every component fires events on its element. If you used to poll or wrap the library callbacks, these are the ones to listen for:
|
Every component fires events on its element. If you used to poll or wrap the library callbacks, these are the ones to listen for:
|
||||||
@@ -208,6 +250,17 @@ const primaryHalf = `color-mix(in oklab, ${primary} 50%, transparent)`;
|
|||||||
|
|
||||||
Every place that accepts a CSS color, including canvas and most chart libraries that render to SVG, takes the `oklch()` string as it is. A library that parses colors itself and only understands hex or `rgb()` needs the value converted first. For ApexCharts you don't have to do anything, its theme reads the `--tblr-chart-*` variables since 1.5.
|
Every place that accepts a CSS color, including canvas and most chart libraries that render to SVG, takes the `oklch()` string as it is. A library that parses colors itself and only understands hex or `rgb()` needs the value converted first. For ApexCharts you don't have to do anything, its theme reads the `--tblr-chart-*` variables since 1.5.
|
||||||
|
|
||||||
|
## Legend is a component now
|
||||||
|
|
||||||
|
In 1.5 `.legend` was a small colored dot. In 1.6 `.legend` is a whole legend item with a dot, a label and an optional value, and the dot is `.legend-dot`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <span class="legend bg-primary"></span>
|
||||||
|
+ <span class="legend-dot bg-primary"></span>
|
||||||
|
```
|
||||||
|
|
||||||
|
An empty `.legend` still renders as a dot, so nothing breaks today. It is deprecated, so rename it when you touch that markup. See [Legend](/ui/components/legend) for the new item, the list and the interactive states.
|
||||||
|
|
||||||
## Form hint is form text
|
## Form hint is form text
|
||||||
|
|
||||||
Tabler had its own `.form-hint` class for the helper text under a field, while Bootstrap calls the same thing `.form-text`. 1.6 uses the Bootstrap class and keeps `.form-hint` as a deprecated alias, so nothing breaks. Rename when you touch the markup anyway:
|
Tabler had its own `.form-hint` class for the helper text under a field, while Bootstrap calls the same thing `.form-text`. 1.6 uses the Bootstrap class and keeps `.form-hint` as a deprecated alias, so nothing breaks. Rename when you touch the markup anyway:
|
||||||
@@ -321,6 +374,22 @@ A lot of Bootstrap variables that were plain Sass colors are now `color-mix()` e
|
|||||||
|
|
||||||
You can still override them with anything Sass accepts. Just be aware that a Sass color function such as `darken()` or `rgba()` won't work on their default values any more, because the default isn't a color, it's a string with `var()` inside.
|
You can still override them with anything Sass accepts. Just be aware that a Sass color function such as `darken()` or `rgba()` won't work on their default values any more, because the default isn't a color, it's a string with `var()` inside.
|
||||||
|
|
||||||
|
## The default gray is neutral
|
||||||
|
|
||||||
|
Every gray in Tabler comes from the `--tblr-gray-50` … `--tblr-gray-950` scale. In 1.5 that scale had a blue tint. In 1.6 the default is `neutral`, a pure gray, so backgrounds, borders and muted text look slightly different. The Sass defaults `$gray-50` … `$gray-950` changed the same way.
|
||||||
|
|
||||||
|
The old scale ships in `tabler-themes.css` as `gray`. To keep the 1.5 look, load that stylesheet and set the base on `<html>`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- <html>
|
||||||
|
+ <html data-bs-theme-base="gray">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/dist/css/tabler.min.css" />
|
||||||
|
+ <link rel="stylesheet" href="/dist/css/tabler-themes.min.css" />
|
||||||
|
```
|
||||||
|
|
||||||
|
`tabler-theme.js` also treats `neutral` as the default now. A visitor who picked `gray` in the theme settings keeps it. See [Theme base colors](/ui/plugins/theme-base-colors) for the other palettes.
|
||||||
|
|
||||||
## Visual changes
|
## Visual changes
|
||||||
|
|
||||||
None of these break markup, but they change how a page looks or scrolls. Worth a quick look at your screens after the upgrade.
|
None of these break markup, but they change how a page looks or scrolls. Worth a quick look at your screens after the upgrade.
|
||||||
@@ -338,7 +407,9 @@ None of these break markup, but they change how a page looks or scrolls. Worth a
|
|||||||
|
|
||||||
Besides the changes above, 1.6 adds:
|
Besides the changes above, 1.6 adds:
|
||||||
|
|
||||||
|
- [Datepicker](/ui/plugins/date-picker), a calendar popup or inline calendar with single, multiple and range selection, see above.
|
||||||
- [Legend](/ui/components/legend), a colored marker plus label for chart series, with an interactive mode that toggles series on click.
|
- [Legend](/ui/components/legend), a colored marker plus label for chart series, with an interactive mode that toggles series on click.
|
||||||
|
- [Signal](/ui/components/signal), a row of bars that shows a level, such as priority or signal strength.
|
||||||
- [Sparkline](/ui/components/sparkline), inline SVG line, bar and circle charts rendered from `data-bs-values`, with no chart library.
|
- [Sparkline](/ui/components/sparkline), inline SVG line, bar and circle charts rendered from `data-bs-values`, with no chart library.
|
||||||
- [Password strength](/ui/forms/strength), a segmented meter that scores the password as the user types and fires `change.bs.strength`.
|
- [Password strength](/ui/forms/strength), a segmented meter that scores the password as the user types and fires `change.bs.strength`.
|
||||||
- [OTP input](/ui/forms/otp-input), grouped single-character slots for one-time codes, with masking and validation states.
|
- [OTP input](/ui/forms/otp-input), grouped single-character slots for one-time codes, with masking and validation states.
|
||||||
@@ -347,6 +418,8 @@ Besides the changes above, 1.6 adds:
|
|||||||
- A `switch-icon-loading` state for [Switch icon](/ui/components/switch-icon). Pass a promise to `event.wait()` in `toggle.bs.switch-icon` and the button shows a spinner until it settles.
|
- A `switch-icon-loading` state for [Switch icon](/ui/components/switch-icon). Pass a promise to `event.wait()` in `toggle.bs.switch-icon` and the button shows a spinner until it settles.
|
||||||
- `Autosize`, `CountUp`, `InputMask`, `Sortable` and `SwitchIcon` as real components with instances, methods and events, see above.
|
- `Autosize`, `CountUp`, `InputMask`, `Sortable` and `SwitchIcon` as real components with instances, methods and events, see above.
|
||||||
- `data-tblr-countup` as an alias of `data-countup`.
|
- `data-tblr-countup` as an alias of `data-countup`.
|
||||||
|
- `.navbar-floating` for [navbars](/ui/layout/navbars) and `.offcanvas-floating` for [offcanvas](/ui/components/offcanvas), which detach the element from the edge of the screen.
|
||||||
|
- Drag styles for [Sortable](/ui/plugins/sortable): `.sortable-ghost`, `.sortable-chosen` and `.sortable-drag`.
|
||||||
- A billing page in the preview, with plan usage, payment method and invoices.
|
- A billing page in the preview, with plan usage, payment method and invoices.
|
||||||
- The Bootstrap component port is compiled with strict TypeScript, so the type declarations in the package are tighter. Configs are typed per component.
|
- The Bootstrap component port is compiled with strict TypeScript, so the type declarations in the package are tighter. Configs are typed per component.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,20 @@ The plugin itself ships in `tabler.js`. The calendar it renders comes from Vanil
|
|||||||
|
|
||||||
<TabsPackage name="vanilla-calendar-pro" />
|
<TabsPackage name="vanilla-calendar-pro" />
|
||||||
|
|
||||||
|
If you use a bundler, put the library on `window` before Tabler loads. The plugin reads `window.VanillaCalendarPro` and does nothing without it. Imports run before the rest of a file, so do it in a file of its own and import that one first:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// vanilla-calendar-global.js
|
||||||
|
import * as VanillaCalendarPro from 'vanilla-calendar-pro'
|
||||||
|
|
||||||
|
window.VanillaCalendarPro = VanillaCalendarPro
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import './vanilla-calendar-global.js'
|
||||||
|
import '@tabler/core'
|
||||||
|
```
|
||||||
|
|
||||||
Or include it from a CDN, before `tabler.js`:
|
Or include it from a CDN, before `tabler.js`:
|
||||||
|
|
||||||
<Code lang="html" code={`<script src="${site.cdnUrl}/dist/libs/vanilla-calendar-pro/index.js"></script>`} />
|
<Code lang="html" code={`<script src="${site.cdnUrl}/dist/libs/vanilla-calendar-pro/index.js"></script>`} />
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
title: Sortable
|
||||||
|
summary: Sortable lets users reorder the items of a list, a table or a grid by dragging them. It wraps SortableJS in a Tabler component and styles the dragged item.
|
||||||
|
description: Make lists, table rows and cards draggable with the data-sortable attribute, SortableJS options as JSON and the Sortable component.
|
||||||
|
docs-libs: [sortablejs]
|
||||||
|
added-in: '1.6.0'
|
||||||
|
related: [/ui/components/list-group, /ui/components/table]
|
||||||
|
---
|
||||||
|
|
||||||
|
import Example from '@components/Example.astro'
|
||||||
|
import TabsPackage from '@components/TabsPackage.astro'
|
||||||
|
import CodeDocs from '@components/CodeDocs.astro';
|
||||||
|
import { Code } from 'astro:components'
|
||||||
|
import { site } from '@shared/lib/site.ts'
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use Sortable where the order is up to the user, such as a task list, table rows or dashboard cards. The component is part of `tabler.js`. The dragging itself comes from [SortableJS](https://sortablejs.github.io/Sortable/), which you load separately.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the SortableJS library:
|
||||||
|
|
||||||
|
<TabsPackage name="sortablejs" />
|
||||||
|
|
||||||
|
If you use a bundler, put the library on `window` before Tabler loads. The component reads `window.Sortable` and does nothing without it. Imports run before the rest of a file, so do it in a file of its own and import that one first:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// sortable-global.js
|
||||||
|
import Sortable from 'sortablejs'
|
||||||
|
|
||||||
|
window.Sortable = Sortable
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import './sortable-global.js'
|
||||||
|
import '@tabler/core'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or include it from a CDN, before `tabler.js`:
|
||||||
|
|
||||||
|
<Code lang="html" code={`<script src="${site.cdnUrl}/dist/libs/sortablejs/Sortable.min.js"></script>`} />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic list
|
||||||
|
|
||||||
|
Add `data-sortable` to the parent element. Its direct children become draggable.
|
||||||
|
|
||||||
|
<Example column>
|
||||||
|
<ul class="list-group" data-sortable>
|
||||||
|
<li class="list-group-item">Design the landing page</li>
|
||||||
|
<li class="list-group-item">Write the release notes</li>
|
||||||
|
<li class="list-group-item">Review open pull requests</li>
|
||||||
|
</ul>
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
Pass [SortableJS options](https://github.com/SortableJS/Sortable#options) as JSON in the attribute. This list animates the items as they move.
|
||||||
|
|
||||||
|
<Example column>
|
||||||
|
<ul class="list-group" data-sortable='{"animation":150}'>
|
||||||
|
<li class="list-group-item">First</li>
|
||||||
|
<li class="list-group-item">Second</li>
|
||||||
|
<li class="list-group-item">Third</li>
|
||||||
|
</ul>
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
### Drag handle
|
||||||
|
|
||||||
|
Set `handle` to a selector to start the drag only from that element. The rest of the item stays clickable.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ul class="list-group" data-sortable='{"animation":150,"handle":".sortable-handle"}'>
|
||||||
|
<li class="list-group-item d-flex gap-2">
|
||||||
|
<span class="sortable-handle cursor-move">⋮⋮</span>
|
||||||
|
Design the landing page
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table rows
|
||||||
|
|
||||||
|
Put `data-sortable` on the `tbody` to reorder the rows of a table.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<table class="table">
|
||||||
|
<tbody data-sortable='{"animation":150}'>
|
||||||
|
<tr><td>First row</td></tr>
|
||||||
|
<tr><td>Second row</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drag styles
|
||||||
|
|
||||||
|
Tabler styles the classes SortableJS adds while dragging: `.sortable-ghost` marks the drop position, `.sortable-chosen` the picked item and `.sortable-drag` the copy that follows the pointer.
|
||||||
|
|
||||||
|
The component turns on the `forceFallback` option by default. SortableJS then drags an HTML copy of the item instead of the browser's drag image, so `.sortable-drag` can style it. To get the browser's drag image back, turn the option off:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ul class="list-group" data-sortable='{"forceFallback":false}'>
|
||||||
|
…
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript
|
||||||
|
|
||||||
|
Tabler wraps the plugin in a `Sortable` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-sortable`. This is the code that runs:
|
||||||
|
|
||||||
|
<CodeDocs name="sortable-init" file="core/js/src/sortable.ts" />
|
||||||
|
|
||||||
|
Create the component yourself for a list added later. Options come from `data-sortable`, from the config object, or both. The object wins.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const list = new tabler.Sortable(document.getElementById('tasks'), { animation: 150 });
|
||||||
|
```
|
||||||
|
|
||||||
|
Method|Description
|
||||||
|
---|---
|
||||||
|
`toArray()`|Returns the `data-id` of every item, in the current order.
|
||||||
|
`sort(order, useAnimation)`|Reorders the items to match an array of ids.
|
||||||
|
`dispose()`|Destroys the SortableJS instance and removes the component.
|
||||||
|
|
||||||
|
The `sortable` getter returns the SortableJS instance, so everything from the [SortableJS API](https://github.com/SortableJS/Sortable#methods) is available:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const list = tabler.Sortable.getOrCreateInstance(document.getElementById('tasks'));
|
||||||
|
list.sortable.option('disabled', true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
- Dragging needs a pointer. SortableJS has no keyboard support, so give keyboard users another way to change the order, such as "Move up" and "Move down" buttons.
|
||||||
|
- Use a drag handle when the items contain links or buttons, so a click on them does not start a drag.
|
||||||
|
- Save the new order only after the drop, and tell the user when saving fails.
|
||||||
|
|
||||||
|
## SCSS variables
|
||||||
|
|
||||||
|
<CodeDocs name="sortable-variables" file="core/scss/_variables.scss" />
|
||||||
@@ -28,7 +28,7 @@ Or include it from a CDN. You need three files: the stylesheet, the library, and
|
|||||||
|
|
||||||
<Code
|
<Code
|
||||||
lang="html"
|
lang="html"
|
||||||
code={`<link rel="stylesheet" href="${site.cdnUrl}/dist/libs/jsvectormap/dist/jsvectormap.min.css" />
|
code={`<link rel="stylesheet" href="${site.cdnUrl}/dist/libs/jsvectormap/dist/jsvectormap.css" />
|
||||||
<script src="${site.cdnUrl}/dist/libs/jsvectormap/dist/jsvectormap.min.js"></script>
|
<script src="${site.cdnUrl}/dist/libs/jsvectormap/dist/jsvectormap.min.js"></script>
|
||||||
<script src="${site.cdnUrl}/dist/libs/jsvectormap/dist/maps/world.js"></script>`}
|
<script src="${site.cdnUrl}/dist/libs/jsvectormap/dist/maps/world.js"></script>`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||||
|
import DocsLink from '@ui/DocsLink.astro'
|
||||||
import Avatar from '@ui/Avatar.astro'
|
import Avatar from '@ui/Avatar.astro'
|
||||||
import Card from '@ui/Card.astro'
|
import Card from '@ui/Card.astro'
|
||||||
import CardHeader from '@ui/CardHeader.astro'
|
import CardHeader from '@ui/CardHeader.astro'
|
||||||
@@ -50,6 +51,7 @@ const sections = [
|
|||||||
---
|
---
|
||||||
|
|
||||||
<DefaultLayout title="Sortable" pageMenu="plugins.sortable" pageLibs={['sortablejs']} description="Drag and drop to reorder lists, table rows, grids and kanban boards with SortableJS.">
|
<DefaultLayout title="Sortable" pageMenu="plugins.sortable" pageLibs={['sortablejs']} description="Drag and drop to reorder lists, table rows, grids and kanban boards with SortableJS.">
|
||||||
|
<DocsLink slot="page-header-actions" path="/ui/plugins/sortable" />
|
||||||
<div class="row row-cards">
|
<div class="row row-cards">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h2 class="page-section-title">{sections[0]!.title}</h2>
|
<h2 class="page-section-title">{sections[0]!.title}</h2>
|
||||||
|
|||||||
@@ -500,6 +500,10 @@
|
|||||||
"title": "Social icons",
|
"title": "Social icons",
|
||||||
"url": "/ui/plugins/social-icons"
|
"url": "/ui/plugins/social-icons"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Sortable",
|
||||||
|
"url": "/ui/plugins/sortable"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Theme base colors",
|
"title": "Theme base colors",
|
||||||
"url": "/ui/plugins/theme-base-colors"
|
"url": "/ui/plugins/theme-base-colors"
|
||||||
|
|||||||
Reference in new issue
Block a user