mirror of
https://github.com/tabler/tabler.git
synced 2026-10-11 20:56:57 +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 |
|
||||
| `core-scss` | writing or changing styles in `core/scss/` |
|
||||
| `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) |
|
||||
| `demo-pages` | adding or updating a page in `preview/pages/` |
|
||||
| `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)
|
||||
- [ ] `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
|
||||
- [ ] `js-docs-*` markers intact and self-contained
|
||||
- [ ] `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`
|
||||
- [ ] Dark mode via `light-dark()` or all three dark selectors, behind `$enable-dark-mode`
|
||||
- [ ] 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
|
||||
- [ ] `lint:scss`, `check:tokens`, `lint:prettier`, `test:scss` clean
|
||||
- [ ] 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 |
|
||||
| **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.
|
||||
|
||||
|
||||
+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:
|
||||
# light-dark() Chrome 123, Firefox 120, Safari 17.5
|
||||
# @property Chrome 85, Firefox 128, Safari 16.4
|
||||
# :has() Chrome 105, Firefox 121, Safari 15.4
|
||||
# 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.
|
||||
Chrome >= 123
|
||||
Edge >= 123
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
"@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
|
||||
---
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
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 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'
|
||||
|
||||
type ComponentConfig = Record<string, never>
|
||||
@@ -134,9 +134,7 @@ class Autosize extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start autosize-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
Autosize.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_TOGGLE, Autosize)
|
||||
// js-docs-end autosize-init
|
||||
|
||||
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 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'
|
||||
|
||||
type ComponentConfig = {
|
||||
@@ -190,9 +191,7 @@ class Clipboard extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start clipboard-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
Clipboard.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_TOGGLE, Clipboard)
|
||||
// js-docs-end clipboard-init
|
||||
|
||||
export default Clipboard
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
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'
|
||||
|
||||
type CountUpFormat = 'number' | 'time'
|
||||
@@ -312,9 +312,7 @@ class CountUp extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start countup-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_COUNTUP)) {
|
||||
CountUp.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_COUNTUP, CountUp)
|
||||
// js-docs-end countup-init
|
||||
|
||||
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 EventHandler from './bootstrap/dom/event-handler'
|
||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||
import { initAll } from './bootstrap/util/component-functions'
|
||||
import { isDisabled } from './bootstrap/util/index'
|
||||
import type { ElementSelector } from './bootstrap/types'
|
||||
|
||||
@@ -106,6 +107,13 @@ const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
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
|
||||
*
|
||||
@@ -120,6 +128,14 @@ class Datepicker extends BaseComponent {
|
||||
declare _config: ComponentConfig
|
||||
_calendar: Calendar | null = null
|
||||
_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
|
||||
_isInline = false
|
||||
_boundInput: HTMLInputElement | null = null
|
||||
@@ -173,7 +189,9 @@ class Datepicker extends BaseComponent {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -182,10 +200,20 @@ class Datepicker extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
this._isShowing = true
|
||||
this._skipPluginShow = false
|
||||
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> {
|
||||
@@ -202,10 +230,16 @@ class Datepicker extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
this._calendar.hide()
|
||||
this._isShown = false
|
||||
// On a click on a button trigger the plugin's own listener runs next and
|
||||
// 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 {
|
||||
@@ -419,6 +453,58 @@ class Datepicker extends BaseComponent {
|
||||
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 {
|
||||
// The plugin uses 'system' for auto-detection, Bootstrap and Tabler use 'auto'
|
||||
const theme = this._getEffectiveTheme()
|
||||
@@ -440,18 +526,8 @@ class Datepicker extends BaseComponent {
|
||||
onInit: (self) => {
|
||||
this._syncThemeAttribute(self.context.mainElement)
|
||||
},
|
||||
onShow: () => {
|
||||
if (!this._calendar) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isShown = true
|
||||
this._syncThemeAttribute(this._calendar.context.mainElement)
|
||||
this._alignToPositionElement()
|
||||
},
|
||||
onHide: () => {
|
||||
this._isShown = false
|
||||
},
|
||||
onShow: () => this._handlePluginShow(),
|
||||
onHide: () => this._handlePluginHide(),
|
||||
}
|
||||
|
||||
// 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,
|
||||
// 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 eager = dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || (element as HTMLInputElement).value
|
||||
|
||||
if (eager) {
|
||||
Datepicker.getOrCreateInstance(element)
|
||||
}
|
||||
}
|
||||
return dataset.bsInline === 'true' || dataset.tblrInline === 'true' || 'bsSelectedDates' in dataset || 'tblrSelectedDates' in dataset || Boolean((element as HTMLInputElement).value)
|
||||
})
|
||||
// js-docs-end datepicker-init
|
||||
|
||||
export default Datepicker
|
||||
|
||||
Vendored
+6
-20
@@ -1,23 +1,9 @@
|
||||
// IMask (https://imask.js.org) is loaded separately, not bundled
|
||||
interface IMaskInstance {
|
||||
value: string
|
||||
unmaskedValue: string
|
||||
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
|
||||
}
|
||||
|
||||
// Plugins that are loaded separately, not bundled: IMask (https://imask.js.org),
|
||||
// SortableJS (https://sortablejs.github.io/Sortable/) and Vanilla Calendar Pro
|
||||
// (https://vanilla-calendar.pro). The instance types live in the component
|
||||
// modules, so they are part of the published `dist/types`.
|
||||
interface Window {
|
||||
// Vanilla Calendar Pro (https://vanilla-calendar.pro) is loaded separately, not bundled
|
||||
VanillaCalendarPro?: typeof import('vanilla-calendar-pro')
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => IMaskInstance
|
||||
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => SortableInstance
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => import('./input-mask').IMaskInstance
|
||||
Sortable?: new (element: HTMLElement, options?: Record<string, unknown>) => import('./sortable').SortableInstance
|
||||
}
|
||||
@@ -6,9 +6,18 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
// 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 = {
|
||||
mask: string
|
||||
lazy: boolean
|
||||
@@ -114,9 +123,8 @@ class InputMask extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start input-mask-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_MASK)) {
|
||||
InputMask.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_MASK, InputMask)
|
||||
// js-docs-end input-mask-init
|
||||
|
||||
export default InputMask
|
||||
export type { IMaskInstance }
|
||||
@@ -8,6 +8,7 @@
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
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'
|
||||
|
||||
type OtpInputType = 'numeric' | 'alphanumeric' | 'alpha'
|
||||
@@ -425,9 +426,7 @@ class OtpInput extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start otp-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
OtpInput.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_TOGGLE, OtpInput)
|
||||
// js-docs-end otp-init
|
||||
|
||||
export default OtpInput
|
||||
+11
-4
@@ -6,9 +6,17 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
// 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 ComponentConfigInput = Record<string, unknown>
|
||||
@@ -108,9 +116,8 @@ class Sortable extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start sortable-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_SORTABLE)) {
|
||||
Sortable.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_SORTABLE, Sortable)
|
||||
// js-docs-end sortable-init
|
||||
|
||||
export default Sortable
|
||||
export type { SortableInstance }
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
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'
|
||||
|
||||
type SparklineType = 'line' | 'bar' | 'circle' | 'tristate'
|
||||
@@ -603,9 +603,7 @@ class Sparkline extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start sparkline-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
Sparkline.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_TOGGLE, Sparkline)
|
||||
// js-docs-end sparkline-init
|
||||
|
||||
export default Sparkline
|
||||
@@ -8,6 +8,7 @@
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
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'
|
||||
|
||||
type StrengthLevel = 'weak' | 'fair' | 'good' | 'strong'
|
||||
@@ -270,9 +271,7 @@ class Strength extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start strength-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) {
|
||||
Strength.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_STRENGTH, Strength)
|
||||
// js-docs-end strength-init
|
||||
|
||||
export default Strength
|
||||
@@ -8,6 +8,7 @@
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
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'
|
||||
|
||||
type ComponentConfig = Record<string, never>
|
||||
@@ -160,9 +161,7 @@ class SwitchIcon extends BaseComponent {
|
||||
*/
|
||||
|
||||
// js-docs-start switch-icon-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
SwitchIcon.getOrCreateInstance(element)
|
||||
}
|
||||
initAll(SELECTOR_DATA_TOGGLE, SwitchIcon)
|
||||
// js-docs-end switch-icon-init
|
||||
|
||||
export default SwitchIcon
|
||||
@@ -27,6 +27,7 @@ describe('Datepicker', () => {
|
||||
})
|
||||
|
||||
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
|
||||
const tick = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 20))
|
||||
|
||||
describe('NAME', () => {
|
||||
it('should return plugin name', () => {
|
||||
@@ -243,5 +244,67 @@ describe('Datepicker', () => {
|
||||
expect(instance).not.toBeNull()
|
||||
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 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'
|
||||
|
||||
class DummyClass extends BaseComponent {
|
||||
@@ -145,4 +145,47 @@ describe('Component Functions', () => {
|
||||
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": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vanilla-calendar-pro": "^3.3.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vanilla-calendar-pro": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@melloware/coloris": "^0.25.0",
|
||||
"@types/node": "^26.4.0",
|
||||
|
||||
@@ -111,3 +111,10 @@
|
||||
// Kept from the old rule, so an inline element still gets the top margin.
|
||||
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-dark: #{$focus-ring-opacity-dark};
|
||||
--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-start root-form-validation-variables
|
||||
|
||||
@@ -145,7 +145,8 @@
|
||||
|
||||
// The knob replaces the masked check icon: a plain circle that slides.
|
||||
&::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);
|
||||
height: var(--form-switch-knob-size);
|
||||
margin-block: auto;
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
|
||||
// Runtime counterpart of `adjust-lightness()`: relative color syntax, so the
|
||||
// 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) {
|
||||
$delta: math.div($amount, 100%);
|
||||
$sign: if(sass($delta < 0): '-'; else: '+');
|
||||
|
||||
@@ -66,11 +66,14 @@
|
||||
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) {
|
||||
@if $color != null {
|
||||
outline: var(--focus-ring-width) solid #{$color};
|
||||
} @else {
|
||||
outline: var(--focus-ring);
|
||||
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||
}
|
||||
@if $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-danger",
|
||||
"--tblr-focus-ring",
|
||||
"--tblr-focus-ring-color",
|
||||
"--tblr-focus-ring-offset",
|
||||
"--tblr-focus-ring-opacity",
|
||||
"--tblr-focus-ring-opacity-dark",
|
||||
"--tblr-focus-ring-width",
|
||||
"--tblr-font-sans-serif",
|
||||
"--tblr-font-size-h3",
|
||||
"--tblr-font-size-h5",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
outline: var(--focus-ring);
|
||||
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@
|
||||
}
|
||||
@include expect() {
|
||||
.t {
|
||||
outline: var(--focus-ring);
|
||||
outline: var(--focus-ring, var(--focus-ring-width) solid var(--focus-ring-color));
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
.alert-link {
|
||||
font-weight: $alert-link-font-weight;
|
||||
color: var(--alert-link-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--alert-link-color);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-dismissible {
|
||||
|
||||
@@ -405,9 +405,6 @@ Card optinos
|
||||
.card-options {
|
||||
--card-options-top: #{$card-options-top};
|
||||
--card-options-inset: #{$card-options-inset};
|
||||
position: absolute;
|
||||
inset-inline-end: var(--card-options-inset);
|
||||
top: var(--card-options-top);
|
||||
display: flex;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
@@ -37,15 +37,6 @@
|
||||
font-family: inherit;
|
||||
font-size: var(--calendar-font-size);
|
||||
color: var(--calendar-color);
|
||||
color-scheme: light dark;
|
||||
|
||||
&[data-bs-theme='light'] {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
&[data-bs-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
|
||||
@@ -93,10 +93,11 @@ Form switch
|
||||
padding-inline-start: var(--form-switch-lg-padding-start);
|
||||
|
||||
.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);
|
||||
height: var(--form-switch-lg-height);
|
||||
margin-inline-start: var(--form-switch-lg-input-offset);
|
||||
background-size: var(--form-switch-lg-bg-size);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
|
||||
@@ -39,9 +39,12 @@ Feature|Used for|Chrome|Firefox|Safari
|
||||
`@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
|
||||
`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.
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
Have `.text-gray-*` classes in your markup|They color the text now - see [Gray text utilities](#gray-text-utilities)
|
||||
|
||||
## 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.
|
||||
- **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
|
||||
|
||||
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]
|
||||
---
|
||||
|
||||
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).
|
||||
|
||||
@@ -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
|
||||
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/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)
|
||||
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)
|
||||
@@ -100,7 +104,7 @@ Component|Attribute|Docs
|
||||
`tabler.Autosize`|`data-bs-toggle="autosize"`|[Autosize](/ui/plugins/autosize)
|
||||
`tabler.CountUp`|`data-countup`|[Countup](/ui/plugins/countup)
|
||||
`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)
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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:
|
||||
|
||||
- [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.
|
||||
- [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.
|
||||
- [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.
|
||||
@@ -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.
|
||||
- `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`.
|
||||
- `.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.
|
||||
- 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" />
|
||||
|
||||
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`:
|
||||
|
||||
<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
|
||||
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/maps/world.js"></script>`}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Card from '@ui/Card.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.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/plugins/sortable" />
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<h2 class="page-section-title">{sections[0]!.title}</h2>
|
||||
|
||||
@@ -500,6 +500,10 @@
|
||||
"title": "Social icons",
|
||||
"url": "/ui/plugins/social-icons"
|
||||
},
|
||||
{
|
||||
"title": "Sortable",
|
||||
"url": "/ui/plugins/sortable"
|
||||
},
|
||||
{
|
||||
"title": "Theme base colors",
|
||||
"url": "/ui/plugins/theme-base-colors"
|
||||
|
||||
Reference in new issue
Block a user