Add ESLint with typescript-eslint and fix its findings (#3256)

This commit is contained in:
Paweł Kuna authored and Bartek committed 2026-10-02 11:32:45 +02:00
1 parent 0e90f8fd16
commit 6394af0a59
40 files changed
+935 -339

No files matched your search

+3 -3
View File
@@ -27,7 +27,7 @@ import { pkgName, releasedPackage } from './released-package'
const fixtureDir = '.build/compat-fixture'
const packageArg = process.argv.indexOf('--package')
const coreDir = packageArg > -1 ? process.argv[packageArg + 1]! : 'core'
const coreDir = packageArg > -1 ? process.argv[packageArg + 1] : 'core'
const MIME: Record<string, string> = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.svg': 'image/svg+xml', '.map': 'application/json' }
@@ -61,7 +61,7 @@ function sassResults(pkg: string): Result {
function serve(pkg: string): Promise<Server> {
const server = createServer((request, response) => {
const path = decodeURIComponent((request.url ?? '/').split('?')[0]!)
const path = decodeURIComponent((request.url ?? '/').split('?')[0])
const file = path === '/' ? resolve(fixtureDir, 'page.html') : resolve(pkg, `.${path}`)
if (!existsSync(file) || !statSync(file).isFile()) {
response.writeHead(404).end()
@@ -86,7 +86,7 @@ async function pageResults(pkg: string, page: Page): Promise<Result> {
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' })
await page.addStyleTag({ content: '*, *::before, *::after { transition: none !important; }' })
const style = (selector: string, property: string): Promise<string> => page.evaluate(([s, p]) => getComputedStyle(document.querySelector(s!)!).getPropertyValue(p!), [selector, property])
const style = (selector: string, property: string): Promise<string> => page.evaluate(([s, p]) => getComputedStyle(document.querySelector(s)!).getPropertyValue(p), [selector, property])
results['page: no script error and no 404'] = problems.length === 0 ? 'yes' : problems.join('; ')
results['page: rgba(var(--tblr-primary-rgb), .5) paints'] = await style('#highlight', 'background-color')
+8 -8
View File
@@ -48,7 +48,7 @@ function walk(root: string, dir = root): string[] {
})
}
const matches = (source: string, pattern: RegExp): Set<string> => new Set([...source.matchAll(pattern)].map((match) => match[1]!))
const matches = (source: string, pattern: RegExp): Set<string> => new Set([...source.matchAll(pattern)].map((match) => match[1]))
// --- css -------------------------------------------------------------------
@@ -106,7 +106,7 @@ function splitParams(source: string | undefined): string[] {
}
params.push(current)
// `$color: rgba($white, 0.15)` → `$color`; `$args...` → `$args...`
return params.map((param) => param.trim().split(':')[0]!.trim()).filter(Boolean)
return params.map((param) => param.trim().split(':')[0].trim()).filter(Boolean)
}
function sassSurface(root: string): { variables: Set<string>; callables: Map<string, Callable> } {
@@ -117,8 +117,8 @@ function sassSurface(root: string): { variables: Set<string>; callables: Map<str
const source = readFileSync(join(root, 'scss', file), 'utf8').replace(/\/\/.*$/gm, '')
for (const name of matches(source, sassVariable)) variables.add(name)
for (const [, kind, name, params] of source.matchAll(sassCallable)) {
if (name!.startsWith('_') || name!.startsWith('-')) continue // private by convention
callables.set(`${kind} ${name}`, { kind: kind!, name: name!, params: splitParams(params) })
if (name.startsWith('_') || name.startsWith('-')) continue // private by convention
callables.set(`${kind} ${name}`, { kind: kind, name: name, params: splitParams(params) })
}
}
return { variables, callables }
@@ -160,7 +160,7 @@ function sassTypes(root: string): Map<string, string> {
}
}`
const { css } = compileString(source, { loadPaths: [root, 'node_modules'], logger: { warn() {}, debug() {} } })
return new Map([...css.matchAll(/--([\w-]+): (\w+);/g)].map((match) => [match[1]!, match[2]!]))
return new Map([...css.matchAll(/--([\w-]+): (\w+);/g)].map((match) => [match[1], match[2]]))
}
function sassTypeBreaks(released: string, current: string): string[] {
@@ -175,7 +175,7 @@ function esmExports(file: string): Set<string> {
const source = readFileSync(file, 'utf8')
const names = new Set<string>()
for (const [, list] of source.matchAll(/^export\s*\{([^}]*)\}/gm)) {
for (const entry of list!.split(',')) {
for (const entry of list.split(',')) {
const name = entry
.trim()
.split(/\s+as\s+/)
@@ -220,7 +220,7 @@ async function main() {
const lines = readFileSync(baselineFile, 'utf8').split('\n')
const againstIndex = lines.findIndex((line) => againstPattern.test(line))
if (againstIndex === -1) throw new Error(`${baselineFile} has no \`# against: <version>\` line`)
const against = againstPattern.exec(lines[againstIndex]!)![1]!
const against = againstPattern.exec(lines[againstIndex])![1]
if (reset) {
writeFileSync(baselineFile, [...lines.slice(0, againstIndex), `# against: ${version}`, ''].join('\n'))
@@ -233,7 +233,7 @@ async function main() {
const baseline = new Map<string, boolean>() // key → accepted
for (const line of stale ? [] : lines) {
const [entry, comment = ''] = line.split(' # ')
const key = entry!.trim()
const key = entry.trim()
if (!key || key.startsWith('#')) continue
baseline.set(key, comment.trim().startsWith('accepted:'))
}
+4 -4
View File
@@ -52,7 +52,7 @@ const CONSOLE_IGNORE = [/GL Driver Message/, /Google Maps JavaScript API/, /allo
type PageReport = { url: string; errors: string[]; warnings: string[] }
const resolveFile = (root: string, urlPath: string): string | null => {
const clean = decodeURIComponent(urlPath.split('?')[0]!.split('#')[0]!)
const clean = decodeURIComponent(urlPath.split('?')[0].split('#')[0])
// `/x/` and `/x/index.html` also resolve to x.html: Astro's file format
// collapses x/index.astro to x.html and the hosting serves it under both.
const collapsed = clean.replace(/\/(index\.html)?$/, '')
@@ -87,7 +87,7 @@ const serve = (root: string): Promise<{ server: Server; base: string }> =>
const sitemapPaths = async (base: string, dir?: string): Promise<string[]> => {
const xml = dir ? readFileSync(join(dir, 'sitemap.xml'), 'utf8') : await (await fetch(`${base}/sitemap.xml`)).text()
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]!.replace(/^https?:\/\/[^/]+/, '')).filter((p) => p.startsWith('/'))
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].replace(/^https?:\/\/[^/]+/, '')).filter((p) => p.startsWith('/'))
}
// Runs in the page as plain JavaScript (a string, so no bundler helpers such as
@@ -162,14 +162,14 @@ const crawl = async (browser: Browser, base: string, paths: string[]) => {
await page.goto(base + path, { waitUntil: 'load', timeout: 60_000 })
await page.waitForTimeout(500)
if (onSite()) {
const r = (await page.evaluate(INSPECT)) as Inspection
const r = await page.evaluate<Inspection>(INSPECT)
for (const id of r.duplicateIds) if (!VENDOR_ID_PREFIXES.some((p) => id.startsWith(p))) report.errors.push(`duplicate id "${id}"`)
for (const t of r.missingTargets) report.errors.push(`no element for ${t}`)
for (const n of r.nested) report.errors.push(`nested interactive: ${n}`)
for (const src of r.imgNoAlt) report.warnings.push(`img without alt: ${src}`)
for (const html of r.unnamed) report.warnings.push(`no accessible name: ${html}`)
for (const href of r.links) {
const key = href.split(/[?#]/)[0]!
const key = href.split(/[?#]/)[0]
if (!links.has(key)) links.set(key, path)
}
}
+7 -7
View File
@@ -23,8 +23,8 @@ const htmlTargets: string[] = []
const cssDirs: string[] = []
let updateBaseline = false
for (let i = 0; i < args.length; i++) {
if (args[i] === '--html') htmlTargets.push(args[++i]!)
else if (args[i] === '--css') cssDirs.push(args[++i]!)
if (args[i] === '--html') htmlTargets.push(args[++i])
else if (args[i] === '--css') cssDirs.push(args[++i])
else if (args[i] === '--update-baseline') updateBaseline = true
}
if (htmlTargets.length === 0 || cssDirs.length === 0) {
@@ -41,14 +41,14 @@ const walk = (dir: string, ext: string): string[] =>
const defined = new Set<string>()
for (const dir of cssDirs) {
for (const file of walk(resolve(dir), '.css')) {
for (const m of readFileSync(file, 'utf8').matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) defined.add(m[1]!)
for (const m of readFileSync(file, 'utf8').matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) defined.add(m[1])
}
}
const pagesOf = async (target: string): Promise<{ path: string; html: string }[]> => {
if (/^https?:/.test(target)) {
const xml = await (await fetch(`${target}/sitemap.xml`)).text()
const paths = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]!.replace(/^https?:\/\/[^/]+/, ''))
const paths = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].replace(/^https?:\/\/[^/]+/, ''))
const pages = []
for (const path of paths) pages.push({ path, html: await (await fetch(target + path)).text() })
return pages
@@ -56,7 +56,7 @@ const pagesOf = async (target: string): Promise<{ path: string; html: string }[]
const root = resolve(target)
const xml = readFileSync(join(root, 'sitemap.xml'), 'utf8')
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => {
const path = m[1]!.replace(/^https?:\/\/[^/]+/, '')
const path = m[1].replace(/^https?:\/\/[^/]+/, '')
const file = [join(root, path), join(root, `${path}.html`), join(root, path, 'index.html')].find((f) => existsSync(f) && statSync(f).isFile())
return { path, html: file ? readFileSync(file, 'utf8') : '' }
})
@@ -70,9 +70,9 @@ const main = async () => {
pageCount++
// Page-level <style> blocks define classes too (demo pages style their own markup).
const inline = new Set<string>()
for (const block of html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) for (const m of block[1]!.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) inline.add(m[1]!)
for (const block of html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) for (const m of block[1].matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) inline.add(m[1])
for (const m of html.matchAll(/class="([^"]*)"/g)) {
for (const cls of m[1]!.split(/\s+/).filter(Boolean)) {
for (const cls of m[1].split(/\s+/).filter(Boolean)) {
if (defined.has(cls) || inline.has(cls)) continue
if (!used.has(cls)) used.set(cls, new Set())
used.get(cls)!.add(`${target}${path}`)
+4 -1
View File
@@ -98,4 +98,7 @@ const main = async () => {
}
}
main()
main().catch((error) => {
console.error(error)
process.exit(1)
})
+5 -2
View File
@@ -61,7 +61,7 @@ const main = async () => {
try {
pages.set(name, await fetchPage(url))
} catch (error) {
failures.push(`${url} — ${error instanceof Error ? error.message : error}`)
failures.push(`${url} — ${error instanceof Error ? error.message : String(error)}`)
}
}
}),
@@ -130,4 +130,7 @@ const main = async () => {
console.log(`✓ All ${pages.size} pages are byte-identical to the baseline.`)
}
main()
main().catch((error) => {
console.error(error)
process.exit(1)
})
+4 -4
View File
@@ -113,7 +113,7 @@ function lineBalance(line: string): number {
let depth = 0
for (const m of line.matchAll(/<(\/)?([a-zA-Z][\w.-]*)((?:"[^"]*"|'[^']*'|[^<>"'])*?)(\/)?>/g)) {
if (m[1]) depth--
else if (!m[4] && !voidTags.has(m[2]!.toLowerCase())) depth++
else if (!m[4] && !voidTags.has(m[2].toLowerCase())) depth++
}
return depth
}
@@ -121,7 +121,7 @@ function lineBalance(line: string): number {
const hasText = (line: string): boolean => line.replace(/<[^<>]*>/g, '').trim().length > 0
/** The tags of a fragment in order, for comparing markup before and after formatting. */
const tagSequence = (html: string): string => [...html.matchAll(/<(\/?)([a-zA-Z][\w.-]*)/g)].map((m) => `${m[1]}${m[2]!.toLowerCase()}`).join(',')
const tagSequence = (html: string): string => [...html.matchAll(/<(\/?)([a-zA-Z][\w.-]*)/g)].map((m) => `${m[1]}${m[2].toLowerCase()}`).join(',')
/**
* Merge raw-text lines back into their markup (see formatExamples). MDX parses
@@ -145,8 +145,8 @@ function glueTextLines(formatted: string): string {
else out.push(line)
// A text line can also land after a closing tag whose opener sits on an
// earlier line (label after a multi-line svg) — pull those lines up too.
while (out.length > 1 && hasText(out[out.length - 1]!) && lineBalance(out[out.length - 1]!) < 0) {
const merged = `${out[out.length - 2]} ${out[out.length - 1]!.trim()}`
while (out.length > 1 && hasText(out[out.length - 1]) && lineBalance(out[out.length - 1]) < 0) {
const merged = `${out[out.length - 2]} ${out[out.length - 1].trim()}`
out.splice(out.length - 2, 2, merged)
}
}
-1
View File
@@ -1,5 +1,4 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig, type UserConfig } from 'vite'
interface CreateViteConfigOptions {
+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": patch
"@tabler/docs": patch
"@tabler/preview": patch
---
Added ESLint with `typescript-eslint` and fixed its findings in `Datepicker`, `Dropdown`, `Tooltip` and `Sparkline`.
-10
View File
@@ -1,10 +0,0 @@
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "single"],
"no-var": ["error"]
},
"parserOptions": {
"ecmaVersion": 6
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ class Button extends BaseComponent {
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, (event: Event) => {
event.preventDefault()
const target = (event.target as HTMLElement)?.closest(SELECTOR_DATA_TOGGLE) as HTMLElement | null
const target = (event.target as HTMLElement)?.closest<HTMLElement>(SELECTOR_DATA_TOGGLE)
if (!target) {
return
}
+1 -1
View File
@@ -297,7 +297,7 @@ class Carousel extends BaseComponent {
const activeElement = this._getActive()
const isNext = order === ORDER_NEXT
const nextElement = element || (getNextActiveElement(this._getItems(), activeElement!, isNext, this._config.wrap as boolean) as HTMLElement)
const nextElement = element || getNextActiveElement<HTMLElement>(this._getItems(), activeElement!, isNext, this._config.wrap)
if (nextElement === activeElement) {
return
-1
View File
@@ -16,7 +16,6 @@ const Data = {
const instanceMap = elementMap.get(element)!
if (!instanceMap.has(key) && instanceMap.size !== 0) {
// eslint-disable-next-line no-console
console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)
return
}
+2 -2
View File
@@ -106,7 +106,7 @@ function bootstrapHandler(element: EventTarget, fn: EventCallback): BootstrapHan
}
return fn.apply(element as HTMLElement, [event])
} as BootstrapHandler
}
}
function bootstrapDelegationHandler(element: EventTarget, selector: string, fn: EventCallback): BootstrapHandler {
@@ -128,7 +128,7 @@ function bootstrapDelegationHandler(element: EventTarget, selector: string, fn:
return fn.apply(target as HTMLElement, [event])
}
}
} as BootstrapHandler
}
}
function findHandler(events: Record<string | number, BootstrapHandler>, callable: EventCallback, delegationSelector: string | null = null): BootstrapHandler | undefined {
+1 -1
View File
@@ -29,7 +29,7 @@ function normalizeData(value: string): DataValue {
}
try {
return JSON.parse(decodeURIComponent(value))
return JSON.parse(decodeURIComponent(value)) as DataValue
} catch {
return value
}
+11 -7
View File
@@ -123,7 +123,11 @@ class Dropdown extends BaseComponent {
}
toggle(): void {
this._isShown() ? this.hide() : this.show()
if (this._isShown()) {
this.hide()
} else {
this.show()
}
}
show(): void {
@@ -180,7 +184,7 @@ class Dropdown extends BaseComponent {
update(): void {
this._inNavbar = this._detectNavbar()
if (this._popper) {
this._popper.update()
void this._popper.update()
}
}
@@ -210,7 +214,7 @@ class Dropdown extends BaseComponent {
_getConfig(config?: ComponentConfigInput): ComponentConfig {
const merged = super._getConfig(config) as ComponentConfig
if (typeof merged.reference === 'object' && !isElement(merged.reference) && typeof (merged.reference as Popper.VirtualElement).getBoundingClientRect !== 'function') {
if (typeof merged.reference === 'object' && !isElement(merged.reference) && typeof merged.reference.getBoundingClientRect !== 'function') {
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
}
@@ -227,9 +231,9 @@ class Dropdown extends BaseComponent {
if (this._config.reference === 'parent') {
referenceElement = this._parent
} else if (isElement(this._config.reference)) {
referenceElement = getElement(this._config.reference as HTMLElement | string)!
referenceElement = getElement(this._config.reference)!
} else if (typeof this._config.reference === 'object') {
referenceElement = this._config.reference as Popper.VirtualElement
referenceElement = this._config.reference
}
const popperConfig = this._getPopperConfig()
@@ -383,7 +387,7 @@ class Dropdown extends BaseComponent {
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, (event as DelegatedEvent<KeyboardEvent>).delegateTarget.parentNode as Element)
const instance = Dropdown.getOrCreateInstance(getToggleButton!) as Dropdown
const instance = Dropdown.getOrCreateInstance(getToggleButton) as Dropdown
if (isUpOrDownEvent) {
event.stopPropagation()
@@ -395,7 +399,7 @@ class Dropdown extends BaseComponent {
if (instance._isShown()) {
event.stopPropagation()
instance.hide()
getToggleButton!.focus()
getToggleButton.focus()
}
}
+4 -4
View File
@@ -137,9 +137,9 @@ class ScrollSpy extends BaseComponent {
return
}
EventHandler.off(this._config.target as HTMLElement, EVENT_CLICK)
EventHandler.off(this._config.target, EVENT_CLICK)
EventHandler.on(this._config.target as HTMLElement, EVENT_CLICK, SELECTOR_TARGET_LINKS, (event: Event) => {
EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, (event: Event) => {
const observableSection = this._observableSections.get((event.target as HTMLAnchorElement).hash)
if (observableSection) {
event.preventDefault()
@@ -158,8 +158,8 @@ class ScrollSpy extends BaseComponent {
_getNewObserver(): IntersectionObserver {
const options: IntersectionObserverInit = {
root: this._rootElement,
threshold: this._config.threshold as number[],
rootMargin: this._config.rootMargin as string,
threshold: this._config.threshold,
rootMargin: this._config.rootMargin,
}
return new IntersectionObserver((entries) => this._observerCallback(entries), options)
+1 -1
View File
@@ -153,7 +153,7 @@ class Tab extends BaseComponent {
nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]
} else {
const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
nextActiveElement = getNextActiveElement(children, event.target as HTMLElement, isNext, true) as HTMLElement
nextActiveElement = getNextActiveElement<HTMLElement>(children, event.target as HTMLElement, isNext, true)
}
if (nextActiveElement) {
+4 -4
View File
@@ -138,7 +138,7 @@ class Tooltip extends BaseComponent {
declare _element: HTMLElement
declare _config: ComponentConfig
_isEnabled: boolean
_timeout: ReturnType<typeof setTimeout> | number
_timeout: ReturnType<typeof setTimeout>
_isHovered: boolean | null
_activeTrigger: Record<string, boolean>
_popper: Popper.Instance | null
@@ -259,7 +259,7 @@ class Tooltip extends BaseComponent {
this._popper = this._createPopper(tip)
tip!.classList.add(CLASS_NAME_SHOW)
tip.classList.add(CLASS_NAME_SHOW)
if ('ontouchstart' in document.documentElement) {
for (const element of Array.from(document.body.children)) {
@@ -322,7 +322,7 @@ class Tooltip extends BaseComponent {
update(): void {
if (this._popper) {
this._popper.update()
void this._popper.update()
}
}
@@ -511,7 +511,7 @@ class Tooltip extends BaseComponent {
return
}
if (!this._element.getAttribute('aria-label') && !this._element.textContent!.trim()) {
if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {
this._element.setAttribute('aria-label', title)
}
+2 -2
View File
@@ -34,13 +34,13 @@ class Config {
}
_mergeConfigObj(config?: ComponentConfig, element?: HTMLElement): ComponentConfig {
const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element!, 'config') : {}
const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}
const ctor = this.constructor as unknown as ConfigStatic
return {
...ctor.Default,
...(typeof jsonConfig === 'object' ? jsonConfig : {}),
...(isElement(element) ? Manipulator.getDataAttributes(element!) : {}),
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
...(typeof config === 'object' ? config : {}),
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ class FocusTrap extends Config {
const elements = SelectorEngine.focusableChildren(trapElement)
if (elements.length === 0) {
trapElement!.focus()
trapElement.focus()
} else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {
elements[elements.length - 1].focus()
} else {
+2 -2
View File
@@ -13,7 +13,7 @@ const TRANSITION_END = 'transitionend'
const parseSelector = (selector: string): string => {
if (selector && typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`)
selector = selector.replace(/#([^\s"#']+)/g, (_match: string, id: string) => `#${CSS.escape(id)}`)
}
return selector
@@ -166,7 +166,7 @@ const noop = (): void => {}
* @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
*/
const reflow = (element: HTMLElement): void => {
element.offsetHeight // eslint-disable-line no-unused-expressions
element.offsetHeight // eslint-disable-line @typescript-eslint/no-unused-expressions
}
const isRTL = (): boolean => document.documentElement.dir === 'rtl'
+1 -1
View File
@@ -62,7 +62,7 @@ const allowedAttribute = (attribute: Attr, allowedAttributeList: (string | RegEx
return true
}
return allowedAttributeList.filter((attributeRegex) => attributeRegex instanceof RegExp).some((regex) => (regex as RegExp).test(attributeName))
return allowedAttributeList.filter((attributeRegex) => attributeRegex instanceof RegExp).some((regex) => regex.test(attributeName))
}
export function sanitizeHtml(unsafeHtml: string, allowList: AllowList, sanitizeFunction?: SanitizeFn): string {
+2
View File
@@ -81,6 +81,8 @@ class ScrollBarHelper {
}
Manipulator.removeDataAttribute(element, styleProperty)
// The attribute holds the CSS value `_saveInitialAttribute()` stored
// eslint-disable-next-line @typescript-eslint/no-base-to-string
element.style.setProperty(styleProperty, String(value))
}
+3 -3
View File
@@ -273,7 +273,7 @@ const stage = {
}
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i]!
const p = this.particles[i]
const fadeFrom = height * (1 - p.emitter.config.fade)
p.sway += 0.05 * step
@@ -317,7 +317,7 @@ const stage = {
// is still in the air; `end` fires on its element and it leaves the stage.
_settle(now: number): void {
for (let i = this.emitters.length - 1; i >= 0; i--) {
const emitter = this.emitters[i]!
const emitter = this.emitters[i]
const pouring = !emitter.stopped && now - emitter.startedAt < emitter.config.duration
const airborne = this.particles.some((p) => p.emitter === emitter)
@@ -349,7 +349,7 @@ class Confetti extends BaseComponent {
_emitter: Emitter | null = null
constructor(element: ElementSelector, config?: ComponentConfigInput) {
super(element, config as BaseConfig | undefined)
super(element, config)
}
// Getters
+9 -6
View File
@@ -70,6 +70,7 @@ type WeekDayID = 0 | 1 | 2 | 3 | 4 | 5 | 6
*/
interface CalendarInstance {
// Loosely typed on purpose: the plugin's context is large and version-specific.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: Record<string, any>
init(): unknown
update(resetOptions?: object): unknown
@@ -258,6 +259,8 @@ class Datepicker extends BaseComponent {
this._isShowing = false
}
// `async` like `show()`, so a plugin error reaches the caller as a rejection
// eslint-disable-next-line @typescript-eslint/require-await
async hide(): Promise<void> {
if (this._config.inline) {
return // Inline calendars are always visible
@@ -512,7 +515,7 @@ class Datepicker extends BaseComponent {
return
}
this.hide()
void this.hide()
}
EventHandler.on(document, EVENT_FOCUSIN, this._onFocusIn)
@@ -665,7 +668,7 @@ class Datepicker extends BaseComponent {
if (shouldHide) {
window.clearTimeout(this._hideTimeout)
this._hideTimeout = window.setTimeout(() => this.hide(), HIDE_DELAY)
this._hideTimeout = window.setTimeout(() => void this.hide(), HIDE_DELAY)
}
}
@@ -699,7 +702,7 @@ class Datepicker extends BaseComponent {
}
if (dates.length === 1) {
return this._formatDate(dates[0]!)
return this._formatDate(dates[0])
}
// For date ranges, use en-dash; for multiple dates, use comma
@@ -732,7 +735,7 @@ class Datepicker extends BaseComponent {
this._calendar?.set({ selectedDates: this._selectedDates, ...this._monthOf(this._selectedDates[0]) })
}
_toIsoDate(value: DateAny | string): string {
_toIsoDate(value: Date | number | string): string {
if (typeof value === 'string' && DATE_PATTERN.test(value)) {
return value
}
@@ -772,7 +775,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
}
event.preventDefault()
;(Datepicker.getOrCreateInstance(this) as Datepicker).toggle()
void (Datepicker.getOrCreateInstance(this) as Datepicker).toggle()
})
EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function (this: HTMLElement) {
@@ -780,7 +783,7 @@ EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function
return
}
;(Datepicker.getOrCreateInstance(this) as Datepicker).show()
void (Datepicker.getOrCreateInstance(this) as Datepicker).show()
})
// Render on load what cannot wait for a focus or a click: inline calendars,
+1 -1
View File
@@ -12,7 +12,7 @@ export const prefix: string = 'tblr-'
export const hexToRgba = (hex: string, opacity: number): string | null => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? `rgba(${parseInt(result[1]!, 16)}, ${parseInt(result[2]!, 16)}, ${parseInt(result[3]!, 16)}, ${opacity})` : null
return result ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${opacity})` : null
}
export const getColor = (color: string, opacity: number = 1): string | null => {
+2 -2
View File
@@ -114,14 +114,14 @@ const clampSpan = (min: number, max: number): number => {
// Accepts "3,4,2" as well as a JSON array literal "[3,4,2]".
const parseNumberList = (input: unknown): number[] => {
const raw = String(input ?? '').trim()
const raw = typeof input === 'string' ? input.trim() : ''
if (!raw) {
return []
}
if (raw.startsWith('[')) {
try {
const arr = JSON.parse(raw)
const arr = JSON.parse(raw) as unknown
return Array.isArray(arr) ? arr.map(Number).filter(Number.isFinite) : []
} catch {
return []
+2 -1
View File
@@ -296,7 +296,7 @@ describe('Tab', () => {
fixtureEl.innerHTML = ['<ul class="nav">', ' <li class="nav-link" data-bs-toggle="tab"></li>', '</ul>'].join('')
const tabEl = fixtureEl.querySelector('.nav-link')!
const tab = new Tab(tabEl)
new Tab(tabEl)
const spyStop = vi.spyOn(Event.prototype, 'stopPropagation')
const spyPrevent = vi.spyOn(Event.prototype, 'preventDefault')
@@ -326,6 +326,7 @@ describe('Tab', () => {
tabEl1.dispatchEvent(keydown)
expect(spyFocus2).toHaveBeenCalled()
expect(spyFocus3).not.toHaveBeenCalled()
})
it('should handle left/up arrow', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
import { execute, executeAfterTransition, findShadowRoot, getElement, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, parseSelector, reflow, toType, triggerTransitionEnd } from '../../../src/bootstrap/util/index'
import { clearFixture, getFixture } from '../../helpers/fixture'
+1 -1
View File
@@ -286,7 +286,7 @@
"star-rating.js": "^4.3.1",
"tom-select": "^2.6.2",
"typed.js": "^3.0.0",
"typescript": "^7.0.2",
"typescript": "^6.0.3",
"vanilla-calendar-pro": "^3.4.0",
"vitest": "4.1.11"
}
+3 -3
View File
@@ -79,7 +79,7 @@ async function replaceAsync(input: string, pattern: RegExp, replacer: (match: Re
const matches = [...input.matchAll(pattern)]
if (!matches.length) return input
const replacements = await Promise.all(matches.map((match) => replacer(match as RegExpExecArray)))
const replacements = await Promise.all(matches.map((match) => replacer(match)))
let result = ''
let last = 0
@@ -105,7 +105,7 @@ async function resolveCodeDocs(tag: string): Promise<string> {
const load = kind.sources[`../../${file}`]
if (!load) return ''
let source = (await load()) as string
let source = await load()
if (kind.lang === 'scss') source = source.replaceAll(' !default', '')
const snippet = extractMarkedSnippet(source, kind.marker, name)
@@ -302,7 +302,7 @@ export function menuOrderedUrls(): string[] {
return parts.length ? `/${parts.join('/')}` : '/'
}
const walk = (nodes: MenuNode[]): string[] => nodes.flatMap((node) => [...(node.url ? [normalize(node.url)] : []), ...walk(node.children ?? [])])
return [...new Set(walk(docs.menu as MenuNode[]))]
return [...new Set(walk(docs.menu))]
}
/** Absolute in production, root-relative in dev — same rule as sitemap.xml.ts. */
+1 -1
View File
@@ -30,7 +30,7 @@ function sources() {
cachedSources ??= Promise.all(
Object.entries(scssSources)
.sort(([a], [b]) => a.localeCompare(b))
.map(async ([key, load]) => ({ path: key.replace('../../', ''), source: (await load()) as string })),
.map(async ([key, load]) => ({ path: key.replace('../../', ''), source: await load() })),
)
return cachedSources
}
+118
View File
@@ -0,0 +1,118 @@
import { fileURLToPath } from 'node:url'
import { includeIgnoreFile } from '@eslint/compat'
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import astro from 'eslint-plugin-astro'
import globals from 'globals'
// Same files Prettier formats, linted with type information
const SCRIPTS = ['core/js/**/*.{js,ts}', 'core/.build/**/*.{ts,mts,mjs}', '.build/**/*.{ts,mts}', 'screenshots/.build/**/*.ts', '{preview,docs,screenshots}/**/*.{mjs,mts,ts}', 'shared/**/*.{js,mjs,mts,ts}']
// Config files and SCSS tests that no tsconfig includes, linted without type information
const TOOLING = ['*.{js,mjs,cjs}', '.changeset/*.cjs', 'core/*.{mjs,mts}', 'core/scss/tests/**/*.mjs']
const ASTRO = ['{preview,docs,screenshots,shared}/**/*.astro']
// The `<script>` blocks of Astro files, which the Astro processor hands over as virtual files
const ASTRO_SCRIPTS = ['**/*.astro/*.js', '**/*.astro/*.ts']
// Dot directories are skipped by the `**/*` include of the package tsconfigs; `.build/tsconfig.json` covers them
const BUILD_SCRIPTS = ['.build/**', 'core/.build/**', 'screenshots/.build/**']
const unsafeRulesOff = {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
}
export default tseslint.config(
includeIgnoreFile(fileURLToPath(new URL('.gitignore', import.meta.url))),
{
ignores: ['.claude/**', '.cursor/**'],
},
{
files: [...SCRIPTS, ...TOOLING, ...ASTRO, ...ASTRO_SCRIPTS],
languageOptions: { globals: { ...globals.browser, ...globals.node } },
},
{
files: SCRIPTS,
ignores: ASTRO_SCRIPTS,
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: {
projectService: { allowDefaultProject: ['docs/pages/.well-known/*.ts', 'docs/pages/.well-known/*/*.ts', 'docs/pages/.well-known/*/*/*.ts', 'preview/.build/*.mjs'] },
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: [...TOOLING, ...ASTRO_SCRIPTS],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
},
{
files: [...SCRIPTS, ...TOOLING, ...ASTRO, ...ASTRO_SCRIPTS],
rules: {
'prefer-const': ['error', { destructuring: 'all' }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'none' }],
},
},
{
// Only the shipped bundle is held to them. Elsewhere `.astro` imports, plain `.mjs`
// files and JSON reach the type checker as `any`, so these rules report noise.
files: SCRIPTS,
ignores: ['core/js/src/**', ...ASTRO_SCRIPTS],
rules: unsafeRulesOff,
},
{
files: BUILD_SCRIPTS,
languageOptions: { parserOptions: { projectService: false, project: './.build/tsconfig.json' } },
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/triple-slash-reference': 'off',
},
},
{
// `core/tsconfig.json` leaves the specs out, so they are linted without type information
files: ['core/js/tests/**'],
extends: [tseslint.configs.disableTypeChecked],
rules: { '@typescript-eslint/no-explicit-any': 'off' },
},
{
// Bootstrap registers its static Data API handlers unbound, the same as upstream
files: ['core/js/src/bootstrap/**'],
rules: { '@typescript-eslint/unbound-method': 'off' },
},
{
// Ambient declarations of globals have to use `var`
files: ['**/*.d.ts'],
rules: { 'no-var': 'off' },
},
{
// Inline scripts ship as-is in the demo HTML: they keep the repo's init pattern
// (`a && b()`, `ready ? init() : on(...)`) and the ES5 style some of them use
files: ASTRO_SCRIPTS,
rules: {
'no-var': 'off',
'@typescript-eslint/no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-nocheck': 'allow-with-description' }],
},
},
...astro.configs.recommended,
{
files: ['**/*.astro/*.ts'],
languageOptions: { parser: tseslint.parser },
},
{
// Rules only: the configs' own parser setting would replace the Astro parser
files: ASTRO,
plugins: { '@typescript-eslint': tseslint.plugin },
languageOptions: { parserOptions: { parser: tseslint.parser, extraFileExtensions: ['.astro'] } },
rules: {
...js.configs.recommended.rules,
...Object.assign({}, ...tseslint.configs.recommended.map((config) => config.rules ?? {})),
// TypeScript already checks names in the frontmatter and the template
'no-undef': 'off',
// Demo components take loosely shaped demo data
'@typescript-eslint/no-explicit-any': 'off',
},
},
)
+11 -3
View File
@@ -34,8 +34,10 @@
"check:open-source": "tsx .build/check-open-source.ts",
"check:html": "tsx .build/check-html.ts",
"check:markup-classes": "tsx .build/check-markup-classes.ts",
"lint": "pnpm run lint:md && pnpm run check:docs-links && pnpm run lint:prettier && pnpm run lint:scss && pnpm run check:css-vars && pnpm run check:open-source && pnpm run check:tokens",
"lint:fix": "pnpm run lint:md:fix && pnpm run lint:scss:fix && pnpm run format:prettier",
"lint": "pnpm run lint:md && pnpm run check:docs-links && pnpm run lint:prettier && pnpm run lint:js && pnpm run lint:scss && pnpm run check:css-vars && pnpm run check:open-source && pnpm run check:tokens",
"lint:fix": "pnpm run lint:md:fix && pnpm run lint:js:fix && pnpm run lint:scss:fix && pnpm run format:prettier",
"lint:js": "eslint --cache --cache-location .cache/eslint/",
"lint:js:fix": "eslint --fix --cache --cache-location .cache/eslint/",
"lint:scss": "pnpm --filter @tabler/core lint",
"lint:scss:fix": "pnpm --filter @tabler/core lint:scss:fix",
"lint:md": "markdownlint --config .markdownlint-docs.json \"docs/content/**/*.mdx\"",
@@ -56,6 +58,8 @@
},
"devDependencies": {
"@changesets/cli": "^3.0.1",
"@eslint/compat": "^2.1.1",
"@eslint/js": "^10.0.1",
"@mdx-js/mdx": "^3.1.1",
"@stylistic/stylelint-config": "^2.0.0",
"@tabler/icons": "^3.48.0",
@@ -66,8 +70,11 @@
"clean-css": "^5.3.3",
"concurrently": "^10.0.5",
"cross-env": "^10.1.0",
"eslint": "^10.11.0",
"eslint-plugin-astro": "^3.2.1",
"github-slugger": "^2.0.0",
"glob": "^13.0.6",
"globals": "^17.12.0",
"js-beautify": "^2.0.3",
"markdownlint-cli": "^0.49.1",
"nodemon": "^3.1.14",
@@ -84,7 +91,8 @@
"terser": "^5.51.2",
"tsx": "^4.23.13",
"turbo": "2.10.12",
"typescript": "^7.0.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.71.0",
"vite": "^8.2.2"
}
}
+696 -237
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -134,8 +134,8 @@ async function main() {
await startPreviewServer()
// Interrupting the script (Ctrl+C) must not leave the preview server behind
// squatting on the port for the next run to collide with.
process.once('SIGINT', stopPreviewServer)
process.once('SIGTERM', stopPreviewServer)
process.once('SIGINT', () => void stopPreviewServer())
process.once('SIGTERM', () => void stopPreviewServer())
try {
const browser = await chromium.launch()
+8 -8
View File
@@ -145,9 +145,9 @@ function systemFfmpeg(): string | undefined {
function resample(frames: Frame[], end: number): Buffer[] {
const out: Buffer[] = []
let index = 0
for (let t = frames[0]!.time; t <= end; t += 1 / FPS) {
while (index + 1 < frames.length && frames[index + 1]!.time <= t) index++
out.push(frames[index]!.data)
for (let t = frames[0].time; t <= end; t += 1 / FPS) {
while (index + 1 < frames.length && frames[index + 1].time <= t) index++
out.push(frames[index].data)
}
return out
}
@@ -269,9 +269,9 @@ async function recordOne(page: Page, client: CDPSession, slug: string, theme: 'l
await moveCursor(page, { x: width / 2, y: height + 40 }, 0)
const frames: Frame[] = []
const onFrame = async (event: { data: string; sessionId: number }) => {
const onFrame = (event: { data: string; sessionId: number }) => {
frames.push({ data: Buffer.from(event.data, 'base64'), time: Date.now() / 1000 })
await client.send('Page.screencastFrameAck', { sessionId: event.sessionId }).catch(() => {})
void client.send('Page.screencastFrameAck', { sessionId: event.sessionId }).catch(() => {})
}
client.on('Page.screencastFrame', onFrame)
await client.send('Page.startScreencast', { format: 'jpeg', quality: 92, maxWidth: width, maxHeight: height, everyNthFrame: 1 })
@@ -303,8 +303,8 @@ async function main() {
console.log(`Starting preview server on port ${PORT}…`)
await startPreviewServer()
process.once('SIGINT', stopPreviewServer)
process.once('SIGTERM', stopPreviewServer)
process.once('SIGINT', () => void stopPreviewServer())
process.once('SIGTERM', () => void stopPreviewServer())
try {
const browser = await chromium.launch()
@@ -321,7 +321,7 @@ async function main() {
// RECORD_FRAMES="0.5,2,4" also writes those seconds as JPEG stills, for a quick look without a player.
for (const second of (process.env.RECORD_FRAMES ?? '').split(',').filter(Boolean).map(Number)) {
const frame = sampled[Math.min(sampled.length - 1, Math.round(second * FPS))]!
const frame = sampled[Math.min(sampled.length - 1, Math.round(second * FPS))]
writeFileSync(path.join(outDir, `${name}-${second}s.jpg`), frame)
}
+1 -1
View File
@@ -430,7 +430,7 @@ export function chartConfig(opts: { id: string; data: ChartData; height: number
}
if (data.datetime) {
const count = Array.isArray(series[0]?.data) ? (series[0].data as number[]).length : 0
const count = Array.isArray(series[0]?.data) ? series[0].data.length : 0
config.labels = datetimeLabels(data['start-date'] ?? '2020-06-20', count)
}
+1 -1
View File
@@ -29,7 +29,7 @@ export interface ThemePreset extends ThemeOption {
export interface ThemeSetting {
/** input name; a real switcher key, or the preset group's own name */
key: ThemeKey | string
key: string
/** fieldset legend */
legend: string
/** help text under the legend */