mirror of
https://github.com/tabler/tabler.git
synced 2026-01-25 12:26:30 +00:00
Compare commits
38 Commits
dev-layout
...
dev-js-lib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224de12414 | ||
|
|
da69e147fc | ||
|
|
087794dc49 | ||
|
|
d0e92a3fe0 | ||
|
|
1d4c1fa016 | ||
|
|
276fe61996 | ||
|
|
4abc069959 | ||
|
|
01b8208227 | ||
|
|
d3a358fec9 | ||
|
|
4ef9dbde51 | ||
|
|
ebcfa18060 | ||
|
|
96168a826c | ||
|
|
c3e6aa1bd3 | ||
|
|
8e3cddb70f | ||
|
|
857988dd44 | ||
|
|
a24d5cab13 | ||
|
|
0c654c61f0 | ||
|
|
8e73f57140 | ||
|
|
f0fb9c66c0 | ||
|
|
29d9d4b5df | ||
|
|
84c31d1383 | ||
|
|
41fd82b388 | ||
|
|
abac36c580 | ||
|
|
301e77898c | ||
|
|
a14425792b | ||
|
|
48dbd1ed1b | ||
|
|
ee8875deb6 | ||
|
|
c0a93b8611 | ||
|
|
42081245b4 | ||
|
|
d56e1a2bac | ||
|
|
c6e8879bb6 | ||
|
|
a811fdb662 | ||
|
|
63a35a849c | ||
|
|
94e1a95ffb | ||
|
|
83ec6f8bcc | ||
|
|
e3d86c519b | ||
|
|
f9d6076014 | ||
|
|
f264470d8f |
@@ -1,6 +1,10 @@
|
||||
>= 1%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
>= 0.5%
|
||||
last 2 major versions
|
||||
not dead
|
||||
safari >= 15.4
|
||||
iOS >= 15.4
|
||||
Chrome >= 120
|
||||
Firefox >= 121
|
||||
iOS >= 15.6
|
||||
Safari >= 15.6
|
||||
not Explorer <= 11
|
||||
Samsung >= 23
|
||||
not kaios <= 2.5
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob';
|
||||
import * as prettier from "prettier";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const docs = sync(join(__dirname, '..', 'docs', '**', '*.md'))
|
||||
|
||||
async function formatHTML(htmlString) {
|
||||
try {
|
||||
const formattedHtml = await prettier.format(htmlString, {
|
||||
parser: "html",
|
||||
printWidth: 100,
|
||||
});
|
||||
return formattedHtml;
|
||||
} catch (error) {
|
||||
console.error("Error formatting HTML:", error);
|
||||
return htmlString; // Return original in case of an error
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceAsync(str, regex, asyncFn) {
|
||||
const matches = [...str.matchAll(regex)];
|
||||
|
||||
const replacements = await Promise.all(
|
||||
matches.map(async (match) => asyncFn(...match))
|
||||
);
|
||||
|
||||
let result = str;
|
||||
matches.forEach((match, i) => {
|
||||
result = result.replace(match[0], replacements[i]);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const file of docs) {
|
||||
const oldContent = readFileSync(file, 'utf8')
|
||||
|
||||
// get codeblocks from markdown
|
||||
const content = await replaceAsync(oldContent, /(```([a-z0-9]+).*?\n)(.*?)(```)/gs, async (m, m1, m2, m3, m4) => {
|
||||
if (m2 === 'html') {
|
||||
m3 = await formatHTML(m3);
|
||||
|
||||
// remove empty lines
|
||||
m3 = m3.replace(/^\s*[\r\n]/gm, '');
|
||||
|
||||
return m1 + m3.trim() + "\n" + m4;
|
||||
}
|
||||
return m.trim();
|
||||
})
|
||||
|
||||
if (content !== oldContent) {
|
||||
writeFileSync(file, content, 'utf8')
|
||||
console.log(`Reformatted ${file}`)
|
||||
}
|
||||
}
|
||||
79
.build/reformat-mdx.ts
Normal file
79
.build/reformat-mdx.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob'
|
||||
import * as prettier from 'prettier'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const docs: string[] = sync(join(__dirname, '..', 'docs', '**', '*.md'))
|
||||
|
||||
async function formatHTML(htmlString: string): Promise<string> {
|
||||
try {
|
||||
const formattedHtml = await prettier.format(htmlString, {
|
||||
parser: 'html',
|
||||
printWidth: 100,
|
||||
})
|
||||
return formattedHtml
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error formatting HTML:', errorMessage)
|
||||
return htmlString // Return original in case of an error
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceAsync(
|
||||
str: string,
|
||||
regex: RegExp,
|
||||
asyncFn: (...args: string[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const matches = [...str.matchAll(regex)]
|
||||
|
||||
const replacements = await Promise.all(
|
||||
matches.map(async (match: RegExpMatchArray) => asyncFn(...match))
|
||||
)
|
||||
|
||||
let result = str
|
||||
matches.forEach((match: RegExpMatchArray, i: number) => {
|
||||
result = result.replace(match[0], replacements[i])
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function processFiles(): Promise<void> {
|
||||
for (const file of docs) {
|
||||
const oldContent = readFileSync(file, 'utf8')
|
||||
|
||||
// get codeblocks from markdown
|
||||
const content = await replaceAsync(
|
||||
oldContent,
|
||||
/(```([a-z0-9]+).*?\n)(.*?)(```)/gs,
|
||||
async (m: string, m1: string, m2: string, m3: string, m4: string) => {
|
||||
if (m2 === 'html') {
|
||||
let formattedHtml = await formatHTML(m3)
|
||||
|
||||
// remove empty lines
|
||||
formattedHtml = formattedHtml.replace(/^\s*[\r\n]/gm, '')
|
||||
|
||||
return m1 + formattedHtml.trim() + '\n' + m4
|
||||
}
|
||||
return m.trim()
|
||||
}
|
||||
)
|
||||
|
||||
if (content !== oldContent) {
|
||||
writeFileSync(file, content, 'utf8')
|
||||
console.log(`Reformatted ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processFiles().catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error processing files:', errorMessage)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
76
.build/vite.config.helper.ts
Normal file
76
.build/vite.config.helper.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, type UserConfig } from 'vite'
|
||||
|
||||
interface CreateViteConfigOptions {
|
||||
entry: string
|
||||
name?: string
|
||||
fileName: string | ((format: string) => string)
|
||||
formats: ('es' | 'umd' | 'iife' | 'cjs')[]
|
||||
outDir: string
|
||||
banner?: string
|
||||
minify?: boolean | 'esbuild'
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Vite configuration for building libraries
|
||||
*/
|
||||
export function createViteConfig({
|
||||
entry,
|
||||
name,
|
||||
fileName,
|
||||
formats,
|
||||
outDir,
|
||||
banner,
|
||||
minify = false
|
||||
}: CreateViteConfigOptions): UserConfig {
|
||||
const rollupOutput: {
|
||||
generatedCode: {
|
||||
constBindings: boolean
|
||||
}
|
||||
banner?: string
|
||||
} = {
|
||||
generatedCode: {
|
||||
constBindings: true
|
||||
}
|
||||
}
|
||||
|
||||
// Add banner if provided
|
||||
if (banner) {
|
||||
rollupOutput.banner = banner
|
||||
}
|
||||
|
||||
const config: UserConfig = {
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(entry),
|
||||
name: name,
|
||||
fileName: typeof fileName === 'function' ? fileName : () => fileName,
|
||||
formats: formats
|
||||
},
|
||||
outDir: path.resolve(outDir),
|
||||
emptyOutDir: false,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: rollupOutput
|
||||
},
|
||||
target: 'es2015',
|
||||
minify: minify
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
esbuild: {
|
||||
target: 'es2015',
|
||||
tsconfigRaw: {
|
||||
compilerOptions: {
|
||||
module: 'ES2020',
|
||||
target: 'ES2015'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defineConfig(config)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import AdmZip from 'adm-zip';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
// Get __dirname in ESM
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(path.join(__dirname, '../core', 'package.json'), 'utf8')
|
||||
)
|
||||
|
||||
// Create zip instance and add folder
|
||||
const zip = new AdmZip();
|
||||
zip.addLocalFolder(path.join(__dirname, '../preview/dist'), 'dashboard');
|
||||
|
||||
zip.addLocalFile(path.join(__dirname, '../preview/static', 'og.png'), '.', 'preview.png');
|
||||
|
||||
zip.addFile("documentation.url", Buffer.from("[InternetShortcut]\nURL = https://tabler.io/docs"));
|
||||
|
||||
|
||||
// Folder to zip and output path
|
||||
const outputZipPath = path.join(__dirname, '../packages-zip', `tabler-${pkg.version}.zip`);
|
||||
|
||||
// Write the zip file
|
||||
zip.writeZip(outputZipPath);
|
||||
|
||||
console.log(`Zipped folder to ${outputZipPath}`);
|
||||
46
.build/zip-package.ts
Normal file
46
.build/zip-package.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import AdmZip from 'adm-zip'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
// Get __dirname in ESM
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
interface PackageJson {
|
||||
version: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const pkg: PackageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, '../core', 'package.json'), 'utf8')
|
||||
)
|
||||
|
||||
// Create zip instance and add folder
|
||||
const zip = new AdmZip()
|
||||
zip.addLocalFolder(path.join(__dirname, '../preview/dist'), 'dashboard')
|
||||
|
||||
zip.addLocalFile(
|
||||
path.join(__dirname, '../preview/static', 'og.png'),
|
||||
'.',
|
||||
'preview.png'
|
||||
)
|
||||
|
||||
zip.addFile(
|
||||
'documentation.url',
|
||||
Buffer.from('[InternetShortcut]\nURL = https://tabler.io/docs')
|
||||
)
|
||||
|
||||
// Folder to zip and output path
|
||||
const outputZipPath = path.join(
|
||||
__dirname,
|
||||
'../packages-zip',
|
||||
`tabler-${pkg.version}.zip`
|
||||
)
|
||||
|
||||
// Write the zip file
|
||||
zip.writeZip(outputZipPath)
|
||||
|
||||
console.log(`Zipped folder to ${outputZipPath}`)
|
||||
|
||||
6
.changeset/migrate-rgba-to-color-mix.md
Normal file
6
.changeset/migrate-rgba-to-color-mix.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Migrated `rgba()` functions to modern CSS color functions (`color-mix()` and `color-transparent()`) for better browser support and cleaner code. Replaced `rgba(var(--#{$prefix}*-rgb), ...)` with `color-mix(in srgb, var(--#{$prefix}*) ..., transparent)`, static percentage `color-mix()` with `color-transparent()`, and `rgba($variable, ...)` with `color-transparent($variable, ...)`.
|
||||
|
||||
8
.changeset/migrate-rollup-to-vite.md
Normal file
8
.changeset/migrate-rollup-to-vite.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Migrated build system from Rollup to Vite across all packages. Replaced `rollup.config.mjs` with `vite.config.mjs` and updated build scripts to use `vite build` instead of `rollup`. Build outputs remain identical (UMD and ESM formats) with no breaking changes for end users.
|
||||
|
||||
7
.changeset/silly-crabs-walk.md
Normal file
7
.changeset/silly-crabs-walk.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added Driver.js library integration and Tour demo page for interactive product tours and onboarding guides.
|
||||
|
||||
6
.changeset/update-icons-3.36.1.md
Normal file
6
.changeset/update-icons-3.36.1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Updated `@tabler/icons` to v3.36.1.
|
||||
|
||||
7
.changeset/upgrade-apexcharts.md
Normal file
7
.changeset/upgrade-apexcharts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Upgraded `apexcharts` from `3.54.1` to `5.3.6` and added CSS variables (`--chart-{id}-color-{index}`) for dynamic chart colors to fix compatibility with the new version.
|
||||
|
||||
68
.github/workflows/argos.yml
vendored
68
.github/workflows/argos.yml
vendored
@@ -1,68 +0,0 @@
|
||||
name: Argos Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
paths:
|
||||
- 'preview/**/*.js'
|
||||
- 'preview/**/*.html'
|
||||
- 'preview/**/*.scss'
|
||||
- 'core/**/*.js'
|
||||
- 'core/**/*.scss'
|
||||
|
||||
env:
|
||||
NODE: 20
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
# if: github.event.pull_request.draft == false
|
||||
if: false
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get installed Playwright version
|
||||
id: playwright-version
|
||||
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: pnpm run playwright
|
||||
42
.github/workflows/build.yml
vendored
Normal file
42
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
|
||||
env:
|
||||
NODE: 22
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: node --version
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
6
.github/workflows/bundlewatch.yml
vendored
6
.github/workflows/bundlewatch.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_COLOR: 2
|
||||
NODE: 20
|
||||
NODE: 22
|
||||
|
||||
jobs:
|
||||
bundlewatch:
|
||||
@@ -17,10 +17,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
32
.github/workflows/lint.yml
vendored
Normal file
32
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
|
||||
env:
|
||||
NODE: 22
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint Markdown
|
||||
run: pnpm run lint
|
||||
2
.github/workflows/lockfiles.yaml
vendored
2
.github/workflows/lockfiles.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Verify lock file integrity
|
||||
steps:
|
||||
- name: Clone Tabler
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Prevent lock file change
|
||||
uses: xalvarez/prevent-file-change-action@v3
|
||||
with:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
pull-requests: write # to create pull request
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: Test build
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
|
||||
env:
|
||||
NODE: 20
|
||||
NODE: 22
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
@@ -38,5 +38,5 @@ jobs:
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
|
||||
45
.github/workflows/type-check.yml
vendored
Normal file
45
.github/workflows/type-check.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Type Check
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE: 20
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
type-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install PNPM
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "${{ env.NODE }}"
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run type-check
|
||||
run: pnpm run type-check
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -36,4 +36,14 @@ package-lock.json
|
||||
demo/
|
||||
dist/
|
||||
packages-zip/
|
||||
.env
|
||||
.env
|
||||
sri.json
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
.tsbuildinfo
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
**/coverage/
|
||||
*.lcov
|
||||
61
.markdownlint.json
Normal file
61
.markdownlint.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"default": true,
|
||||
"MD001": {
|
||||
"level": 2
|
||||
},
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
"MD004": {
|
||||
"style": "dash"
|
||||
},
|
||||
"MD007": {
|
||||
"indent": 2
|
||||
},
|
||||
"MD009": {
|
||||
"br_spaces": 2
|
||||
},
|
||||
"MD010": false,
|
||||
"MD012": {
|
||||
"maximum": 2
|
||||
},
|
||||
"MD013": {
|
||||
"line_length": 120,
|
||||
"code_blocks": false,
|
||||
"tables": false,
|
||||
"headings": false,
|
||||
"headings_line_length": 120
|
||||
},
|
||||
"MD022": true,
|
||||
"MD025": {
|
||||
"front_matter_title": ""
|
||||
},
|
||||
"MD026": {
|
||||
"punctuation": ".,;:!"
|
||||
},
|
||||
"MD030": {
|
||||
"ul_single": 1,
|
||||
"ul_multi": 1,
|
||||
"ol_single": 1,
|
||||
"ol_multi": 1
|
||||
},
|
||||
"MD031": true,
|
||||
"MD032": true,
|
||||
"MD033": false,
|
||||
"MD034": false,
|
||||
"MD035": {
|
||||
"style": "---"
|
||||
},
|
||||
"MD036": false,
|
||||
"MD037": true,
|
||||
"MD038": true,
|
||||
"MD039": true,
|
||||
"MD040": true,
|
||||
"MD041": {
|
||||
"front_matter_title": ""
|
||||
},
|
||||
"MD046": {
|
||||
"style": "fenced"
|
||||
},
|
||||
"MD047": true
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2025 The Tabler Authors
|
||||
Copyright (c) 2018-2026 The Tabler Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join, dirname, basename } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { sync } from 'glob';
|
||||
import banner from '../../shared/banner/index.mjs';
|
||||
import { sync } from 'glob'
|
||||
import banner from '../../shared/banner/index.mjs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const styles = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
|
||||
const styles: string[] = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
|
||||
|
||||
const plugins = {
|
||||
interface Plugins {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const plugins: Plugins = {
|
||||
'tabler-flags': 'Flags',
|
||||
'tabler-flags.rtl': 'Flags RTL',
|
||||
'tabler-marketing': 'Marketing',
|
||||
@@ -25,22 +27,24 @@ const plugins = {
|
||||
'tabler-vendors.rtl': 'Vendors RTL',
|
||||
}
|
||||
|
||||
styles.forEach((file, i) => {
|
||||
styles.forEach((file: string) => {
|
||||
const content = readFileSync(file, 'utf8')
|
||||
const filename = basename(file)
|
||||
const pluginKey = Object.keys(plugins).find(plugin => filename.includes(plugin))
|
||||
const plugin = plugins[pluginKey]
|
||||
const pluginKey = Object.keys(plugins).find((plugin: string) => filename.includes(plugin))
|
||||
const plugin = pluginKey ? plugins[pluginKey] : undefined
|
||||
const regex = /^(@charset ['"][a-zA-Z0-9-]+['"];?)\n?/i
|
||||
|
||||
let newContent = ''
|
||||
const bannerText = banner(plugin)
|
||||
|
||||
if (content.match(regex)) {
|
||||
newContent = content.replace(regex, (m, m1) => {
|
||||
return `${m1}\n${banner(plugin)}\n`
|
||||
newContent = content.replace(regex, (m: string, m1: string) => {
|
||||
return `${m1}\n${bannerText}\n`
|
||||
})
|
||||
} else {
|
||||
newContent = `${banner(plugin)}\n${content}`
|
||||
newContent = `${bannerText}\n${content}`
|
||||
}
|
||||
|
||||
writeFileSync(file, newContent, 'utf8')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get __dirname in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// File paths (relative to core/.build directory)
|
||||
const bootstrapPath = path.join(__dirname, '../node_modules/bootstrap/scss/_variables.scss');
|
||||
const tablerPath = path.join(__dirname, '../scss/_variables.scss');
|
||||
|
||||
// Function to extract variable names from SCSS file
|
||||
function extractVariables(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const variables = new Set();
|
||||
|
||||
// Regex to find SCSS variables
|
||||
// Looks for patterns like: $variable-name: value
|
||||
// Includes variables in maps and lists
|
||||
const variableRegex = /\$([a-zA-Z0-9_-]+)\s*[:=]/g;
|
||||
|
||||
let match;
|
||||
while ((match = variableRegex.exec(content)) !== null) {
|
||||
const varName = match[1];
|
||||
variables.add(varName);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
// Main function
|
||||
function compareVariables() {
|
||||
console.log('Analyzing Bootstrap variables...');
|
||||
const bootstrapVars = extractVariables(bootstrapPath);
|
||||
console.log(`Found ${bootstrapVars.size} variables in Bootstrap\n`);
|
||||
|
||||
console.log('Analyzing Tabler variables...');
|
||||
const tablerVars = extractVariables(tablerPath);
|
||||
console.log(`Found ${tablerVars.size} variables in Tabler\n`);
|
||||
|
||||
// Find variables that are in Bootstrap but not in Tabler
|
||||
const missingInTabler = [];
|
||||
for (const varName of bootstrapVars) {
|
||||
if (!tablerVars.has(varName)) {
|
||||
missingInTabler.push(varName);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
missingInTabler.sort();
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Variables in Bootstrap that are missing in Tabler: ${missingInTabler.length}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
if (missingInTabler.length === 0) {
|
||||
console.log('All Bootstrap variables are present in Tabler!');
|
||||
} else {
|
||||
console.log('\nList of missing variables:\n');
|
||||
missingInTabler.forEach((varName, index) => {
|
||||
console.log(`${(index + 1).toString().padStart(4)}. $${varName}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Optionally: show statistics
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('Statistics:');
|
||||
console.log(` Bootstrap: ${bootstrapVars.size} variables`);
|
||||
console.log(` Tabler: ${tablerVars.size} variables`);
|
||||
console.log(` Missing: ${missingInTabler.length} variables`);
|
||||
console.log(` Coverage: ${((1 - missingInTabler.length / bootstrapVars.size) * 100).toFixed(1)}%`);
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
try {
|
||||
compareVariables();
|
||||
} catch (error) {
|
||||
console.error('Error during analysis:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
84
core/.build/compare-variables.ts
Normal file
84
core/.build/compare-variables.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Get __dirname in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// File paths (relative to core/.build directory)
|
||||
const bootstrapPath = path.join(__dirname, '../node_modules/bootstrap/scss/_variables.scss')
|
||||
const tablerPath = path.join(__dirname, '../scss/_variables.scss')
|
||||
|
||||
// Function to extract variable names from SCSS file
|
||||
function extractVariables(filePath: string): Set<string> {
|
||||
const content = readFileSync(filePath, 'utf8')
|
||||
const variables = new Set<string>()
|
||||
|
||||
// Regex to find SCSS variables
|
||||
// Looks for patterns like: $variable-name: value
|
||||
// Includes variables in maps and lists
|
||||
const variableRegex = /\$([a-zA-Z0-9_-]+)\s*[:=]/g
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = variableRegex.exec(content)) !== null) {
|
||||
const varName = match[1]
|
||||
variables.add(varName)
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
// Main function
|
||||
function compareVariables(): void {
|
||||
console.log('Analyzing Bootstrap variables...')
|
||||
const bootstrapVars = extractVariables(bootstrapPath)
|
||||
console.log(`Found ${bootstrapVars.size} variables in Bootstrap\n`)
|
||||
|
||||
console.log('Analyzing Tabler variables...')
|
||||
const tablerVars = extractVariables(tablerPath)
|
||||
console.log(`Found ${tablerVars.size} variables in Tabler\n`)
|
||||
|
||||
// Find variables that are in Bootstrap but not in Tabler
|
||||
const missingInTabler: string[] = []
|
||||
for (const varName of bootstrapVars) {
|
||||
if (!tablerVars.has(varName)) {
|
||||
missingInTabler.push(varName)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically
|
||||
missingInTabler.sort()
|
||||
|
||||
console.log('='.repeat(60))
|
||||
console.log(`Variables in Bootstrap that are missing in Tabler: ${missingInTabler.length}`)
|
||||
console.log('='.repeat(60))
|
||||
|
||||
if (missingInTabler.length === 0) {
|
||||
console.log('All Bootstrap variables are present in Tabler!')
|
||||
} else {
|
||||
console.log('\nList of missing variables:\n')
|
||||
missingInTabler.forEach((varName: string, index: number) => {
|
||||
console.log(`${(index + 1).toString().padStart(4)}. $${varName}`)
|
||||
})
|
||||
}
|
||||
|
||||
// Optionally: show statistics
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('Statistics:')
|
||||
console.log(` Bootstrap: ${bootstrapVars.size} variables`)
|
||||
console.log(` Tabler: ${tablerVars.size} variables`)
|
||||
console.log(` Missing: ${missingInTabler.length} variables`)
|
||||
console.log(` Coverage: ${((1 - missingInTabler.length / bootstrapVars.size) * 100).toFixed(1)}%`)
|
||||
console.log('='.repeat(60))
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
try {
|
||||
compareVariables()
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error during analysis:', errorMessage)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
import { existsSync, mkdirSync, lstatSync } from 'fs'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { emptyDirSync, copySync } from 'fs-extra/esm'
|
||||
import libs from '../libs.json' with { type: 'json' }
|
||||
import { fileURLToPath } from 'url'
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join, dirname } from 'node:path'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
interface LibConfig {
|
||||
npm?: string
|
||||
js?: string[]
|
||||
css?: string[]
|
||||
head?: boolean
|
||||
}
|
||||
|
||||
interface Libs {
|
||||
[key: string]: LibConfig
|
||||
}
|
||||
|
||||
const libsData = libs as Libs
|
||||
|
||||
emptyDirSync(join(__dirname, '..', 'dist/libs'))
|
||||
|
||||
for(const name in libs) {
|
||||
const { npm } = libs[name]
|
||||
for (const name in libsData) {
|
||||
const { npm } = libsData[name]
|
||||
|
||||
if (npm) {
|
||||
const from = join(__dirname, '..', `node_modules/${npm}`)
|
||||
@@ -23,11 +34,12 @@ for(const name in libs) {
|
||||
if (!existsSync(to)) {
|
||||
mkdirSync(to, { recursive: true })
|
||||
}
|
||||
|
||||
|
||||
copySync(from, to, {
|
||||
dereference: true,
|
||||
})
|
||||
|
||||
|
||||
console.log(`Successfully copied ${npm}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const sh = require('shelljs');
|
||||
import * as crypto from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
sh.config.fatal = true
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const configFile = path.join(__dirname, '../../shared/data/sri.json')
|
||||
|
||||
const files = [
|
||||
interface FileConfig {
|
||||
file: string
|
||||
configPropertyName: string
|
||||
}
|
||||
|
||||
const files: FileConfig[] = [
|
||||
{
|
||||
file: 'dist/css/tabler.min.css',
|
||||
configPropertyName: 'css'
|
||||
@@ -80,28 +85,37 @@ const files = [
|
||||
file: 'dist/js/tabler-theme.min.js',
|
||||
configPropertyName: 'js-theme'
|
||||
},
|
||||
// {
|
||||
// file: 'dist/preview/css/demo.min.css',
|
||||
// configPropertyName: 'demo-css'
|
||||
// },
|
||||
// {
|
||||
// file: 'dist/preview/js/demo.min.js',
|
||||
// configPropertyName: 'demo-js'
|
||||
// },
|
||||
]
|
||||
|
||||
for (const { file, configPropertyName } of files) {
|
||||
fs.readFile(path.join(__dirname, '..', file), 'utf8', (error, data) => {
|
||||
if (error) {
|
||||
function generateSRI(): void {
|
||||
const sriData: Record<string, string> = {}
|
||||
|
||||
for (const { file, configPropertyName } of files) {
|
||||
try {
|
||||
const filePath = path.join(__dirname, '..', file)
|
||||
const data = readFileSync(filePath, 'utf8')
|
||||
|
||||
const algorithm = 'sha384'
|
||||
const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64')
|
||||
const integrity = `${algorithm}-${hash}`
|
||||
|
||||
console.log(`${configPropertyName}: ${integrity}`)
|
||||
|
||||
sriData[configPropertyName] = integrity
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error processing ${file}:`, errorMessage)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const algorithm = 'sha384'
|
||||
const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64')
|
||||
const integrity = `${algorithm}-${hash}`
|
||||
writeFileSync(configFile, JSON.stringify(sriData, null, 2) + '\n', 'utf8')
|
||||
}
|
||||
|
||||
console.log(`${configPropertyName}: ${integrity}`)
|
||||
try {
|
||||
generateSRI()
|
||||
} catch (error) {
|
||||
console.error('Failed to generate SRI:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
sh.sed('-i', new RegExp(`^(\\s+"${configPropertyName}":\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { copySync } from 'fs-extra/esm'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join, dirname } from 'node:path'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
@@ -25,11 +23,11 @@ if (existsSync(monoFrom)) {
|
||||
if (!existsSync(monoTo)) {
|
||||
mkdirSync(monoTo, { recursive: true })
|
||||
}
|
||||
|
||||
|
||||
copySync(monoFrom, monoTo, {
|
||||
dereference: true,
|
||||
})
|
||||
|
||||
|
||||
console.log(`Successfully copied geist-mono fonts`)
|
||||
} else {
|
||||
console.warn(`Warning: geist-mono fonts not found at ${monoFrom}`)
|
||||
@@ -43,11 +41,11 @@ if (existsSync(sansFrom)) {
|
||||
if (!existsSync(sansTo)) {
|
||||
mkdirSync(sansTo, { recursive: true })
|
||||
}
|
||||
|
||||
|
||||
copySync(sansFrom, sansTo, {
|
||||
dereference: true,
|
||||
})
|
||||
|
||||
|
||||
console.log(`Successfully copied geist-sans fonts`)
|
||||
} else {
|
||||
console.warn(`Warning: geist-sans fonts not found at ${sansFrom}`)
|
||||
@@ -1,47 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { babel } from '@rollup/plugin-babel'
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import banner from '../../shared/banner/index.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const ESM = process.env.ESM === 'true'
|
||||
const THEME = process.env.THEME === 'true'
|
||||
|
||||
const external = []
|
||||
const plugins = [
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
babelHelpers: 'bundled'
|
||||
})
|
||||
]
|
||||
|
||||
plugins.push(
|
||||
replace({
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
preventAssignment: true
|
||||
}),
|
||||
nodeResolve()
|
||||
)
|
||||
|
||||
const destinationFile = `tabler${THEME ? '-theme' : ''}${ESM ? '.esm' : ''}`
|
||||
const rollupConfig = {
|
||||
input: path.resolve(__dirname, `../js/tabler${THEME ? '-theme' : ''}.js`),
|
||||
output: {
|
||||
banner: banner(),
|
||||
file: path.resolve(__dirname, `../dist/js/${destinationFile}.js`),
|
||||
format: ESM ? 'esm' : 'umd',
|
||||
generatedCode: 'es2015'
|
||||
},
|
||||
external,
|
||||
plugins
|
||||
}
|
||||
|
||||
if (!ESM) {
|
||||
rollupConfig.output.name = `tabler${THEME ? '-theme' : ''}`
|
||||
}
|
||||
|
||||
export default rollupConfig
|
||||
33
core/.build/vite.config.mts
Normal file
33
core/.build/vite.config.mts
Normal file
@@ -0,0 +1,33 @@
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { createViteConfig } from '../../.build/vite.config.helper'
|
||||
import getBanner from '../../shared/banner/index.mjs'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const ESM = process.env.ESM === 'true'
|
||||
const THEME = process.env.THEME === 'true'
|
||||
|
||||
const MINIFY = process.env.MINIFY === 'true'
|
||||
const destinationFile = `tabler${THEME ? '-theme' : ''}${ESM ? '.esm' : ''}`
|
||||
const entryFile = `tabler${THEME ? '-theme' : ''}`
|
||||
const libraryName = `tabler${THEME ? '-theme' : ''}`
|
||||
|
||||
const bannerText = getBanner()
|
||||
|
||||
// Try .ts first, fallback to .js for gradual migration
|
||||
const entryPath = path.resolve(__dirname, `../js/${entryFile}`)
|
||||
const entry = existsSync(`${entryPath}.ts`) ? `${entryPath}.ts` : `${entryPath}.js`
|
||||
|
||||
export default createViteConfig({
|
||||
entry: entry,
|
||||
name: ESM ? undefined : libraryName,
|
||||
fileName: () => MINIFY ? `${destinationFile}.min.js` : `${destinationFile}.js`,
|
||||
formats: [ESM ? 'es' : 'umd'],
|
||||
outDir: path.resolve(__dirname, '../dist/js'),
|
||||
banner: bannerText,
|
||||
minify: MINIFY ? true : false
|
||||
})
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a2640e2: Add Playwright configuration and visual regression tests
|
||||
- d3ae77c: Enable `scrollSpy` in `countup` module
|
||||
- bd3d959: Refactor SCSS files to replace divide function with calc
|
||||
- cb278c7: Add Segmented Control component
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="16" fill="none"><mask id="a" width="21" height="16" x="0" y="0" mask-type="alpha" maskUnits="userSpaceOnUse"><path fill="#fff" d="M.001.927h20v15h-20z"/></mask><g mask="url(#a)"><path fill="#F7FCFF" fill-rule="evenodd" d="M.001.927v15h20v-15h-20Z" clip-rule="evenodd"/><mask id="b" width="21" height="16" x="0" y="0" mask-type="alpha" maskUnits="userSpaceOnUse"><path fill="#fff" fill-rule="evenodd" d="M.001.927v15h20v-15h-20Z" clip-rule="evenodd"/></mask><g fill-rule="evenodd" clip-rule="evenodd" mask="url(#b)"><path fill="#3D58DB" d="M.001.927v15h20v-15h-20Z"/><path fill="#FFD018" d="m9.407 3.137-.14.818L10 3.57l.735.386-.14-.818.594-.64h-.821L10 1.695l-.367.804h-.822l.595.639Zm0 10.855-.14.819.734-.387.735.387-.14-.819.594-.639h-.821L10 12.55l-.367.804h-.822l.595.64ZM3.484 9.438l.14-.818-.594-.64h.822l.367-.803.367.804h.822l-.595.639.14.818-.734-.386-.735.386Zm1.352 1.77-.14.818.734-.386.735.386-.14-.818.594-.64h-.821l-.368-.803-.367.804H4.24l.595.639Zm9.009.818.14-.818-.595-.64h.822l.367-.803.368.804h.821l-.594.639.14.818-.735-.386-.734.386Zm-9.01-6.062-.14.818.735-.386.735.386-.14-.818.594-.639h-.821l-.368-.804-.367.804H4.24l.595.64Zm9.01.818.14-.818-.595-.639h.822l.367-.804.368.804h.821l-.594.64.14.817-.735-.386-.734.386ZM6.66 13.29l-.14.819.735-.387.734.386-.14-.818.595-.639h-.822l-.367-.804-.368.804h-.821l.594.64Zm5.418.819.14-.819-.594-.639h.821l.367-.804.368.804h.821l-.594.64.14.817-.735-.386-.734.386ZM6.52 4.666l.735-.387.734.387-.14-.818.595-.64h-.822l-.367-.804-.368.804h-.821l.594.64-.14.818Zm5.558 0 .14-.818-.594-.64h.821l.367-.804.368.804h.821l-.594.64.14.818-.735-.387-.734.387Zm3.062 3.879-.14.818.735-.386.735.386-.14-.818.593-.64h-.82l-.368-.803-.368.804h-.821l.594.639Z"/></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" fill="none" viewBox="0 0 20 15"><path fill="#3d58db" fill-rule="evenodd" d="M0 0v15h20V0z" clip-rule="evenodd"/><path fill="#ffd018" fill-rule="evenodd" d="m9.407 2.442-.14.818.733-.385.735.386-.14-.818.594-.64h-.821L10 1l-.367.804h-.822zm0 10.855-.14.819.734-.387.735.387-.14-.819.594-.639h-.821L10 11.855l-.367.804h-.822l.595.64zM3.484 8.743l.14-.818-.594-.64h.822l.367-.803.367.804h.822l-.595.639.14.818-.734-.386zm1.352 1.77-.14.818.734-.386.735.386-.14-.818.594-.64h-.821L5.43 9.07l-.367.804H4.24zm9.009.818.14-.818-.595-.64h.822l.367-.803.368.804h.821l-.594.639.14.818-.735-.386zm-9.01-6.062-.14.818.735-.386.735.386-.14-.818.594-.639h-.821l-.368-.804-.367.804H4.24zm9.01.818.14-.818-.595-.639h.822l.367-.804.368.804h.821l-.594.64.14.817-.735-.386zM6.66 12.595l-.14.819.735-.387.734.386-.14-.818.595-.639h-.822l-.367-.804-.368.804h-.821zm5.418.819.14-.819-.594-.639h.821l.367-.804.368.804h.821l-.594.64.14.817-.735-.386zM6.52 3.971l.735-.387.734.387-.14-.818.595-.64h-.822l-.367-.804-.368.804h-.821l.594.64zm5.558 0 .14-.818-.594-.64h.821l.367-.804.368.804h.821l-.594.64.14.818-.735-.387zM15.14 7.85l-.14.818.735-.386.735.386-.14-.818.593-.64h-.82l-.368-.803-.368.804h-.821z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,8 +0,0 @@
|
||||
// Autosize plugin
|
||||
const elements = document.querySelectorAll('[data-bs-toggle="autosize"]')
|
||||
|
||||
if (elements.length) {
|
||||
elements.forEach(function (element) {
|
||||
window.autosize && window.autosize(element)
|
||||
})
|
||||
}
|
||||
107
core/js/src/autosize.ts
Normal file
107
core/js/src/autosize.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import autosize from 'autosize'
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler autosize.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'autosize'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_AUTOSIZE = '[data-bs-autosize]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Autosize {
|
||||
private element: HTMLElement | HTMLTextAreaElement
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(element: HTMLElement | HTMLTextAreaElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize autosize on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
autosize(this.element)
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update autosize (useful when content changes programmatically)
|
||||
*/
|
||||
update(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
autosize.update(this.element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy autosize instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
autosize.destroy(this.element)
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement | HTMLTextAreaElement): Autosize | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement | HTMLTextAreaElement): Autosize {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement | HTMLTextAreaElement, Autosize>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const autosizeTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_AUTOSIZE))
|
||||
autosizeTriggerList.map(function (autosizeTriggerEl: HTMLElement) {
|
||||
const instance = Autosize.getOrCreateInstance(autosizeTriggerEl)
|
||||
elementMap.set(autosizeTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default Autosize
|
||||
20
core/js/src/bootstrap.js
vendored
20
core/js/src/bootstrap.js
vendored
@@ -1,20 +0,0 @@
|
||||
export * as Popper from '@popperjs/core'
|
||||
|
||||
// Export all Bootstrap components directly for consistent usage
|
||||
export {
|
||||
Alert,
|
||||
Button,
|
||||
Carousel,
|
||||
Collapse,
|
||||
Dropdown,
|
||||
Modal,
|
||||
Offcanvas,
|
||||
Popover,
|
||||
ScrollSpy,
|
||||
Tab,
|
||||
Toast,
|
||||
Tooltip
|
||||
} from 'bootstrap'
|
||||
|
||||
// Re-export everything as namespace for backward compatibility
|
||||
export * as bootstrap from 'bootstrap'
|
||||
2
core/js/src/clipboard.ts
Normal file
2
core/js/src/clipboard.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Backward compatibility alias (old file name).
|
||||
export { default } from './copy'
|
||||
26
core/js/src/collapse.ts
Normal file
26
core/js/src/collapse.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Collapse } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core collapse
|
||||
*/
|
||||
const collapseTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="collapse"]'))
|
||||
collapseTriggerList.map(function (collapseTriggerEl: HTMLElement) {
|
||||
const target = collapseTriggerEl.getAttribute('data-bs-target') || collapseTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapseEl = document.querySelector<HTMLElement>(target)
|
||||
if (collapseEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const collapse = new Collapse(collapseEl, {
|
||||
toggle: false,
|
||||
})
|
||||
|
||||
collapseTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
collapse.toggle()
|
||||
})
|
||||
})
|
||||
334
core/js/src/copy.ts
Normal file
334
core/js/src/copy.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler copy.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import { Tooltip } from 'bootstrap'
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
const NAME = 'copy'
|
||||
const DATA_KEY = `bs.${NAME}`
|
||||
|
||||
type CopyConfig = {
|
||||
timeout: number
|
||||
reset: boolean
|
||||
extend: boolean
|
||||
recopy: boolean
|
||||
feedback: 'swap' | 'tooltip'
|
||||
tooltip: string
|
||||
tooltipPlacement: string
|
||||
}
|
||||
|
||||
const Default: CopyConfig = {
|
||||
timeout: 1500,
|
||||
reset: true,
|
||||
extend: true,
|
||||
recopy: true,
|
||||
feedback: 'swap',
|
||||
tooltip: 'Copied!',
|
||||
tooltipPlacement: 'top',
|
||||
}
|
||||
|
||||
const Selector = {
|
||||
toggle: '[data-bs-toggle="copy"]',
|
||||
default: '[data-bs-copy-default]',
|
||||
success: '[data-bs-copy-success]',
|
||||
} as const
|
||||
|
||||
const Event = {
|
||||
COPY: `copy.bs.${NAME}`,
|
||||
COPIED: `copied.bs.${NAME}`,
|
||||
COPYFAIL: `copyfail.bs.${NAME}`,
|
||||
RESET: `reset.bs.${NAME}`,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
class Copy {
|
||||
static NAME = NAME
|
||||
static DATA_KEY = DATA_KEY
|
||||
static Default = Default
|
||||
static Selector = Selector
|
||||
static Event = Event
|
||||
|
||||
private _element: HTMLElement | null
|
||||
private _timer: number | null = null
|
||||
private _isCopied: boolean = false
|
||||
private _config: CopyConfig
|
||||
private _tooltipInstance: any = null
|
||||
|
||||
constructor(element: HTMLElement, config: Partial<CopyConfig> = {}) {
|
||||
this._element = element
|
||||
this._config = { ...Default, ...this._getConfig(), ...config }
|
||||
}
|
||||
|
||||
static getInstance(element?: HTMLElement | null): Copy | null {
|
||||
return (element as any)?.[DATA_KEY] ?? null
|
||||
}
|
||||
|
||||
static getOrCreateInstance(element?: HTMLElement | null, config?: Partial<CopyConfig>): Copy | null {
|
||||
if (!element) return null
|
||||
;(element as any)[DATA_KEY] ??= new Copy(element, config)
|
||||
return (element as any)[DATA_KEY]
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = null
|
||||
this._disposeTooltip()
|
||||
|
||||
if (this._element) {
|
||||
delete (this._element as any)[DATA_KEY]
|
||||
}
|
||||
|
||||
this._element = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API: manual reset
|
||||
*/
|
||||
reset(): void {
|
||||
if (!this._element) return
|
||||
if (!this._isCopied) return
|
||||
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = null
|
||||
|
||||
this._isCopied = false
|
||||
this._applyFeedback(false)
|
||||
this._trigger(Event.RESET, { manual: true })
|
||||
}
|
||||
|
||||
async copy(): Promise<boolean> {
|
||||
if (!this._element) return false
|
||||
|
||||
// Already "Copied" and recopy disabled — just extend timer or noop
|
||||
if (this._isCopied && !this._config.recopy) {
|
||||
if (this._config.reset && this._config.extend) this._armResetTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
const text = this._getText()
|
||||
const target = this._getTarget()
|
||||
|
||||
const before = this._trigger(Event.COPY, { text, target })
|
||||
if (before.defaultPrevented) return false
|
||||
|
||||
if (text == null) {
|
||||
this._trigger(Event.COPYFAIL, { text: null, target, reason: 'no-text' })
|
||||
return false
|
||||
}
|
||||
|
||||
const ok = await this._writeText(text)
|
||||
if (!ok) {
|
||||
this._trigger(Event.COPYFAIL, { text, target, reason: 'clipboard-failed' })
|
||||
return false
|
||||
}
|
||||
|
||||
this._isCopied = true
|
||||
this._applyFeedback(true)
|
||||
this._trigger(Event.COPIED, { text, target })
|
||||
|
||||
if (this._config.reset) this._armResetTimer()
|
||||
return true
|
||||
}
|
||||
|
||||
private _getConfig(): Partial<CopyConfig> {
|
||||
if (!this._element) return {}
|
||||
|
||||
const d = this._element.dataset
|
||||
const num = (v: string | undefined) => {
|
||||
if (v == null || v === '') return undefined
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
const bool = (v: string | undefined) => (v != null ? v !== 'false' : undefined)
|
||||
const str = (v: string | undefined) => (v != null && v !== '' ? v : undefined)
|
||||
|
||||
const config: Partial<CopyConfig> = {}
|
||||
|
||||
const timeout = num(d.bsCopyTimeout)
|
||||
const reset = bool(d.bsCopyReset)
|
||||
const extend = bool(d.bsCopyExtend)
|
||||
const recopy = bool(d.bsCopyRecopy)
|
||||
const feedback = str(d.bsCopyFeedback)
|
||||
const tooltip = str(d.bsCopyTooltip)
|
||||
const tooltipPlacement = str(d.bsCopyTooltipPlacement)
|
||||
|
||||
if (timeout !== undefined) config.timeout = timeout
|
||||
if (reset !== undefined) config.reset = reset
|
||||
if (extend !== undefined) config.extend = extend
|
||||
if (recopy !== undefined) config.recopy = recopy
|
||||
if (feedback === 'swap' || feedback === 'tooltip') config.feedback = feedback
|
||||
if (tooltip !== undefined) config.tooltip = tooltip
|
||||
if (tooltipPlacement !== undefined) config.tooltipPlacement = tooltipPlacement
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private _getTarget(): Element | null {
|
||||
if (!this._element) return null
|
||||
const selector = this._element.dataset.bsCopyTarget
|
||||
return selector ? document.querySelector(selector) : null
|
||||
}
|
||||
|
||||
private _getText(): string | null {
|
||||
if (!this._element) return null
|
||||
|
||||
const d = this._element.dataset
|
||||
if (d.bsCopyText) return d.bsCopyText
|
||||
|
||||
const target = this._getTarget()
|
||||
if (!target) return null
|
||||
|
||||
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return target.value
|
||||
return target.textContent?.trim() ?? ''
|
||||
}
|
||||
|
||||
private async _writeText(text: string): Promise<boolean> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch {
|
||||
// ignore and fallback
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.setAttribute('readonly', '')
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
const ok = document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private _armResetTimer(): void {
|
||||
if (!this._element) return
|
||||
|
||||
if (!this._config.extend && this._timer) return
|
||||
if (this._timer) clearTimeout(this._timer)
|
||||
this._timer = window.setTimeout(() => {
|
||||
this._isCopied = false
|
||||
this._applyFeedback(false)
|
||||
this._trigger(Event.RESET, { manual: false })
|
||||
}, this._config.timeout)
|
||||
}
|
||||
|
||||
private _applyFeedback(isCopied: boolean): void {
|
||||
if (!this._element) return
|
||||
|
||||
const mode = this._config.feedback
|
||||
|
||||
// Tooltip mode: use Bootstrap Tooltip (manual trigger)
|
||||
if (mode === 'tooltip') {
|
||||
if (isCopied) this._showTooltip()
|
||||
else this._hideTooltip()
|
||||
return
|
||||
}
|
||||
|
||||
this._swapText(isCopied)
|
||||
}
|
||||
|
||||
private _swapText(isCopied: boolean): void {
|
||||
if (!this._element) return
|
||||
|
||||
const def = this._element.querySelector<HTMLElement>(Selector.default)
|
||||
const success = this._element.querySelector<HTMLElement>(Selector.success)
|
||||
|
||||
if (def) {
|
||||
def.hidden = isCopied
|
||||
def.classList.toggle('d-none', isCopied)
|
||||
}
|
||||
if (success) {
|
||||
success.hidden = !isCopied
|
||||
success.classList.toggle('d-none', !isCopied)
|
||||
}
|
||||
|
||||
this._element.classList.toggle('is-copied', isCopied)
|
||||
if (isCopied) this._element.setAttribute('aria-label', 'Copied')
|
||||
else this._element.removeAttribute('aria-label')
|
||||
}
|
||||
|
||||
private _showTooltip(): void {
|
||||
if (!this._element) return
|
||||
|
||||
if (!this._tooltipInstance) {
|
||||
this._element.setAttribute('data-bs-title', this._config.tooltip)
|
||||
this._tooltipInstance = Tooltip.getOrCreateInstance(this._element, {
|
||||
trigger: 'manual',
|
||||
placement: this._config.tooltipPlacement,
|
||||
})
|
||||
} else {
|
||||
this._element.setAttribute('data-bs-title', this._config.tooltip)
|
||||
// Bootstrap 5.3+ supports setContent(); use it when available
|
||||
if (typeof this._tooltipInstance.setContent === 'function') {
|
||||
this._tooltipInstance.setContent({ '.tooltip-inner': this._config.tooltip })
|
||||
}
|
||||
}
|
||||
|
||||
this._tooltipInstance.show()
|
||||
}
|
||||
|
||||
private _hideTooltip(): void {
|
||||
this._tooltipInstance?.hide()
|
||||
}
|
||||
|
||||
private _disposeTooltip(): void {
|
||||
if (this._tooltipInstance) {
|
||||
this._tooltipInstance.dispose()
|
||||
this._tooltipInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
private _trigger(type: string, detail: unknown): CustomEvent {
|
||||
if (!this._element) {
|
||||
return new CustomEvent(type, { bubbles: true, cancelable: false, detail })
|
||||
}
|
||||
|
||||
const cancelable = type === Event.COPY
|
||||
const event = new CustomEvent(type, { bubbles: true, cancelable, detail })
|
||||
this._element.dispatchEvent(event)
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API — click
|
||||
*/
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as Element | null
|
||||
const trigger = target?.closest?.(Selector.toggle) as HTMLElement | null
|
||||
if (!trigger) return
|
||||
e.preventDefault()
|
||||
Copy.getOrCreateInstance(trigger)?.copy()
|
||||
})
|
||||
|
||||
/**
|
||||
* Data API — Enter/Space
|
||||
*/
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const ke = e as KeyboardEvent
|
||||
if (ke.key !== 'Enter' && ke.key !== ' ' && ke.key !== 'Spacebar') return
|
||||
|
||||
const target = ke.target as Element | null
|
||||
const trigger = target?.closest?.(Selector.toggle) as HTMLElement | null
|
||||
if (!trigger) return
|
||||
|
||||
ke.preventDefault()
|
||||
Copy.getOrCreateInstance(trigger)?.copy()
|
||||
})
|
||||
|
||||
export default Copy
|
||||
@@ -1,25 +0,0 @@
|
||||
const elements = document.querySelectorAll('[data-countup]')
|
||||
|
||||
if (elements.length) {
|
||||
elements.forEach(function (element) {
|
||||
let options = {}
|
||||
try {
|
||||
const dataOptions = element.getAttribute('data-countup') ? JSON.parse(element.getAttribute('data-countup')) : {}
|
||||
options = Object.assign(
|
||||
{
|
||||
enableScrollSpy: true,
|
||||
},
|
||||
dataOptions,
|
||||
)
|
||||
} catch (error) {}
|
||||
|
||||
const value = parseInt(element.innerHTML, 10)
|
||||
|
||||
if (window.countUp && window.countUp.CountUp) {
|
||||
const countUp = new window.countUp.CountUp(element, value, options)
|
||||
if (!countUp.error) {
|
||||
countUp.start()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
139
core/js/src/countup.ts
Normal file
139
core/js/src/countup.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { CountUp } from 'countup.js'
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler countup.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'countup'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_COUNTUP = '[data-countup]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Countup {
|
||||
private element: HTMLElement
|
||||
private countUpInstance: CountUp | null = null
|
||||
private initialized: boolean = false
|
||||
private options: Record<string, any> = {}
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize countup on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse options from data attribute
|
||||
let options: Record<string, any> = {}
|
||||
try {
|
||||
const dataOptions = this.element.getAttribute('data-countup') ? JSON.parse(this.element.getAttribute('data-countup')!) : {}
|
||||
options = Object.assign(
|
||||
{
|
||||
enableScrollSpy: true,
|
||||
},
|
||||
dataOptions,
|
||||
)
|
||||
} catch (error) {
|
||||
// ignore invalid JSON
|
||||
}
|
||||
|
||||
this.options = options
|
||||
|
||||
const value = parseInt(this.element.innerHTML, 10)
|
||||
|
||||
if (!isNaN(value)) {
|
||||
this.countUpInstance = new CountUp(this.element, value, options)
|
||||
if (!this.countUpInstance.error) {
|
||||
this.countUpInstance.start()
|
||||
this.initialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update countup (restart animation)
|
||||
*/
|
||||
update(): void {
|
||||
if (!this.initialized || !this.countUpInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = parseInt(this.element.innerHTML, 10)
|
||||
if (!isNaN(value)) {
|
||||
this.countUpInstance = new CountUp(this.element, value, this.options)
|
||||
if (!this.countUpInstance.error) {
|
||||
this.countUpInstance.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy countup instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// CountUp doesn't have a destroy method, so we just reset state
|
||||
this.countUpInstance = null
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement): Countup | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement): Countup {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement, Countup>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const countupTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_COUNTUP))
|
||||
countupTriggerList.map(function (countupTriggerEl: HTMLElement) {
|
||||
const instance = Countup.getOrCreateInstance(countupTriggerEl)
|
||||
elementMap.set(countupTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default Countup
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Dropdown } from './bootstrap'
|
||||
|
||||
/*
|
||||
Core dropdowns
|
||||
*/
|
||||
let dropdownTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="dropdown"]'))
|
||||
dropdownTriggerList.map(function (dropdownTriggerEl) {
|
||||
let options = {
|
||||
boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.querySelector('.btn') : 'clippingParents',
|
||||
}
|
||||
return new Dropdown(dropdownTriggerEl, options)
|
||||
})
|
||||
12
core/js/src/dropdown.ts
Normal file
12
core/js/src/dropdown.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Dropdown } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core dropdowns
|
||||
*/
|
||||
const dropdownTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]'))
|
||||
dropdownTriggerList.map(function (dropdownTriggerEl: HTMLElement) {
|
||||
const options = {
|
||||
boundary: dropdownTriggerEl.getAttribute('data-bs-boundary') === 'viewport' ? document.querySelector('.btn') : 'clippingParents',
|
||||
}
|
||||
return new Dropdown(dropdownTriggerEl, options)
|
||||
})
|
||||
29
core/js/src/global.d.ts
vendored
Normal file
29
core/js/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// Global type declarations for window properties
|
||||
|
||||
interface Window {
|
||||
autosize?: (element: HTMLElement | HTMLTextAreaElement) => void
|
||||
countUp?: {
|
||||
CountUp: new (target: HTMLElement, endVal: number, options?: any) => {
|
||||
error: boolean
|
||||
start: () => void
|
||||
}
|
||||
}
|
||||
IMask?: new (element: HTMLElement, options: { mask: string; lazy?: boolean }) => any
|
||||
Sortable?: new (element: HTMLElement, options?: any) => any
|
||||
Tabler?: {
|
||||
setTheme: (value: string) => void
|
||||
setPrimary: (value: string) => void
|
||||
setBase: (value: string) => void
|
||||
setFont: (value: string) => void
|
||||
setRadius: (value: string) => void
|
||||
reset: () => void
|
||||
getConfig: () => {
|
||||
theme: string
|
||||
'theme-base': string
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Input mask plugin
|
||||
|
||||
var maskElementList = [].slice.call(document.querySelectorAll('[data-mask]'))
|
||||
maskElementList.map(function (maskEl) {
|
||||
window.IMask &&
|
||||
new window.IMask(maskEl, {
|
||||
mask: maskEl.dataset.mask,
|
||||
lazy: maskEl.dataset['mask-visible'] === 'true',
|
||||
})
|
||||
})
|
||||
153
core/js/src/input-mask.ts
Normal file
153
core/js/src/input-mask.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import IMask, { type InputMask as IMaskInputMask } from 'imask'
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler input-mask.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'input-mask'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const SELECTOR_DATA_MASK = '[data-mask]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class InputMask {
|
||||
private element: HTMLElement | HTMLInputElement
|
||||
private maskInstance: IMaskInputMask<any> | null = null
|
||||
private initialized: boolean = false
|
||||
private options: Record<string, any> = {}
|
||||
|
||||
constructor(element: HTMLElement | HTMLInputElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize input mask on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
const mask = this.element.getAttribute('data-mask')
|
||||
if (!mask) {
|
||||
return
|
||||
}
|
||||
|
||||
const options: Record<string, any> = {
|
||||
mask: mask,
|
||||
lazy: this.element.getAttribute('data-mask-visible') === 'true',
|
||||
}
|
||||
|
||||
this.options = options
|
||||
|
||||
this.maskInstance = IMask(this.element, options)
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update input mask (useful when mask changes programmatically)
|
||||
*/
|
||||
update(mask?: string, options?: Partial<Record<string, any>>): void {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
const newOptions: Record<string, any> = {
|
||||
...this.options,
|
||||
...options,
|
||||
}
|
||||
|
||||
if (mask) {
|
||||
newOptions.mask = mask
|
||||
}
|
||||
|
||||
this.maskInstance.updateOptions(newOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current masked value
|
||||
*/
|
||||
getValue(): string {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.maskInstance.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current unmasked value
|
||||
*/
|
||||
getUnmaskedValue(): string {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.maskInstance.unmaskedValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy input mask instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized || !this.maskInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
this.maskInstance.destroy()
|
||||
this.maskInstance = null
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement | HTMLInputElement): InputMask | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement | HTMLInputElement): InputMask {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement | HTMLInputElement, InputMask>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const inputMaskTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_MASK))
|
||||
inputMaskTriggerList.map(function (inputMaskTriggerEl: HTMLElement) {
|
||||
const instance = InputMask.getOrCreateInstance(inputMaskTriggerEl)
|
||||
elementMap.set(inputMaskTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default InputMask
|
||||
24
core/js/src/modal.ts
Normal file
24
core/js/src/modal.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Modal } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core modals
|
||||
*/
|
||||
const modalTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="modal"]'))
|
||||
modalTriggerList.map(function (modalTriggerEl: HTMLElement) {
|
||||
const target = modalTriggerEl.getAttribute('data-bs-target') || modalTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const modalEl = document.querySelector<HTMLElement>(target)
|
||||
if (modalEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const modal = new Modal(modalEl)
|
||||
|
||||
modalTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
modal.show()
|
||||
})
|
||||
})
|
||||
24
core/js/src/offcanvas.ts
Normal file
24
core/js/src/offcanvas.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Offcanvas } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core offcanvas
|
||||
*/
|
||||
const offcanvasTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="offcanvas"]'))
|
||||
offcanvasTriggerList.map(function (offcanvasTriggerEl: HTMLElement) {
|
||||
const target = offcanvasTriggerEl.getAttribute('data-bs-target') || offcanvasTriggerEl.getAttribute('href')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const offcanvasEl = document.querySelector<HTMLElement>(target)
|
||||
if (offcanvasEl === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const offcanvas = new Offcanvas(offcanvasEl)
|
||||
|
||||
offcanvasTriggerEl.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
offcanvas.show()
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Popover } from './bootstrap'
|
||||
import { Popover } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Core popovers
|
||||
*/
|
||||
let popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
let options = {
|
||||
const popoverTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="popover"]'))
|
||||
popoverTriggerList.map(function (popoverTriggerEl: HTMLElement) {
|
||||
const options = {
|
||||
delay: { show: 50, hide: 50 },
|
||||
html: popoverTriggerEl.getAttribute('data-bs-html') === 'true',
|
||||
placement: popoverTriggerEl.getAttribute('data-bs-placement') ?? 'auto',
|
||||
@@ -2,11 +2,11 @@
|
||||
// Initializes Sortable on elements marked with [data-sortable]
|
||||
// Allows options via JSON in data attribute: data-sortable='{"animation":150}'
|
||||
|
||||
const sortableElements = document.querySelectorAll('[data-sortable]')
|
||||
const sortableElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>('[data-sortable]')
|
||||
|
||||
if (sortableElements.length) {
|
||||
sortableElements.forEach(function (element) {
|
||||
let options = {}
|
||||
sortableElements.forEach(function (element: HTMLElement) {
|
||||
let options: Record<string, any> = {}
|
||||
|
||||
try {
|
||||
const rawOptions = element.getAttribute('data-sortable')
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
Switch icons
|
||||
*/
|
||||
let switchesTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="switch-icon"]'))
|
||||
switchesTriggerList.map(function (switchTriggerEl) {
|
||||
switchTriggerEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
switchTriggerEl.classList.toggle('active')
|
||||
})
|
||||
})
|
||||
147
core/js/src/switch-icon.ts
Normal file
147
core/js/src/switch-icon.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler switch-icon.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'switch-icon'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
const CLASS_NAME_ACTIVE = 'active'
|
||||
|
||||
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="switch-icon"]'
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class SwitchIcon {
|
||||
private element: HTMLElement
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(element: HTMLElement) {
|
||||
this.element = element
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize switch-icon on the element
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._setListeners()
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle active state
|
||||
*/
|
||||
toggle(): void {
|
||||
this.element.classList.toggle(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show (activate) switch-icon
|
||||
*/
|
||||
show(): void {
|
||||
this.element.classList.add(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide (deactivate) switch-icon
|
||||
*/
|
||||
hide(): void {
|
||||
this.element.classList.remove(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if switch-icon is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.element.classList.contains(CLASS_NAME_ACTIVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy switch-icon instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._removeListeners()
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Private
|
||||
/**
|
||||
* Set event listeners
|
||||
*/
|
||||
private _setListeners(): void {
|
||||
this.element.addEventListener('click', this._handleClick)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event listeners
|
||||
*/
|
||||
private _removeListeners(): void {
|
||||
this.element.removeEventListener('click', this._handleClick)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click event
|
||||
*/
|
||||
private _handleClick = (e: MouseEvent): void => {
|
||||
e.stopPropagation()
|
||||
this.toggle()
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get instance from element
|
||||
*/
|
||||
static getInstance(element: HTMLElement): SwitchIcon | null {
|
||||
return elementMap.get(element) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(element: HTMLElement): SwitchIcon {
|
||||
return this.getInstance(element) || new this(element)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance storage
|
||||
*/
|
||||
const elementMap = new WeakMap<HTMLElement, SwitchIcon>()
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const switchIconTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>(SELECTOR_DATA_TOGGLE))
|
||||
switchIconTriggerList.map(function (switchIconTriggerEl: HTMLElement) {
|
||||
const instance = SwitchIcon.getOrCreateInstance(switchIconTriggerEl)
|
||||
elementMap.set(switchIconTriggerEl, instance)
|
||||
instance.init()
|
||||
return instance
|
||||
})
|
||||
|
||||
export default SwitchIcon
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Tab } from './bootstrap'
|
||||
|
||||
export const EnableActivationTabsFromLocationHash = () => {
|
||||
const locationHash = window.location.hash
|
||||
|
||||
if (locationHash) {
|
||||
const tabsList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tab"]'))
|
||||
const matchedTabs = tabsList.filter((tab) => tab.hash === locationHash)
|
||||
|
||||
matchedTabs.map((tab) => {
|
||||
new Tab(tab).show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
EnableActivationTabsFromLocationHash()
|
||||
16
core/js/src/tab.ts
Normal file
16
core/js/src/tab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Tab } from 'bootstrap'
|
||||
|
||||
export const EnableActivationTabsFromLocationHash = (): void => {
|
||||
const locationHash: string = window.location.hash
|
||||
|
||||
if (locationHash) {
|
||||
const tabsList: HTMLAnchorElement[] = [].slice.call(document.querySelectorAll<HTMLAnchorElement>('[data-bs-toggle="tab"]'))
|
||||
const matchedTabs = tabsList.filter((tab: HTMLAnchorElement) => tab.hash === locationHash)
|
||||
|
||||
matchedTabs.map((tab: HTMLAnchorElement) => {
|
||||
new Tab(tab).show()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
EnableActivationTabsFromLocationHash()
|
||||
@@ -1,12 +1,12 @@
|
||||
export const prefix = 'tblr-'
|
||||
export const prefix: string = 'tblr-'
|
||||
|
||||
export const hexToRgba = (hex, opacity) => {
|
||||
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
|
||||
}
|
||||
|
||||
export const getColor = (color, opacity = 1) => {
|
||||
export const getColor = (color: string, opacity: number = 1): string | null => {
|
||||
const c = getComputedStyle(document.body).getPropertyValue(`--${prefix}${color}`).trim()
|
||||
|
||||
if (opacity !== 1) {
|
||||
222
core/js/src/theme.ts
Normal file
222
core/js/src/theme.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler theme.js
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'theme'
|
||||
const DATA_KEY = `tblr.${NAME}`
|
||||
|
||||
interface ThemeConfig {
|
||||
'theme': string
|
||||
'theme-base': string
|
||||
'theme-font': string
|
||||
'theme-primary': string
|
||||
'theme-radius': string
|
||||
}
|
||||
|
||||
const DEFAULT_THEME_CONFIG: ThemeConfig = {
|
||||
'theme': 'light',
|
||||
'theme-base': 'gray',
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
}
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*/
|
||||
|
||||
class Theme {
|
||||
private initialized: boolean = false
|
||||
private config: ThemeConfig
|
||||
|
||||
constructor(config?: Partial<ThemeConfig>) {
|
||||
this.config = { ...DEFAULT_THEME_CONFIG, ...config }
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get NAME() {
|
||||
return NAME
|
||||
}
|
||||
|
||||
static get DATA_KEY() {
|
||||
return DATA_KEY
|
||||
}
|
||||
|
||||
// Public
|
||||
/**
|
||||
* Initialize theme
|
||||
*/
|
||||
init(): void {
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this._applyTheme()
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update theme configuration
|
||||
*/
|
||||
update(config: Partial<ThemeConfig>): void {
|
||||
this.config = { ...this.config, ...config }
|
||||
this._applyTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme (light/dark)
|
||||
*/
|
||||
setTheme(value: string): void {
|
||||
this.update({ theme: value })
|
||||
this._updateURL('theme', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set primary color
|
||||
*/
|
||||
setPrimary(value: string): void {
|
||||
this.update({ 'theme-primary': value })
|
||||
this._updateURL('theme-primary', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme base
|
||||
*/
|
||||
setBase(value: string): void {
|
||||
this.update({ 'theme-base': value })
|
||||
this._updateURL('theme-base', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set font family
|
||||
*/
|
||||
setFont(value: string): void {
|
||||
this.update({ 'theme-font': value })
|
||||
this._updateURL('theme-font', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set border radius
|
||||
*/
|
||||
setRadius(value: string): void {
|
||||
this.update({ 'theme-radius': value })
|
||||
this._updateURL('theme-radius', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all theme settings to default
|
||||
*/
|
||||
reset(): void {
|
||||
this.config = { ...DEFAULT_THEME_CONFIG }
|
||||
this._applyTheme()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
for (const key in DEFAULT_THEME_CONFIG) {
|
||||
url.searchParams.delete(key)
|
||||
localStorage.removeItem('tabler-' + key)
|
||||
}
|
||||
window.history.pushState({}, '', url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme configuration
|
||||
*/
|
||||
getConfig(): ThemeConfig {
|
||||
const config: ThemeConfig = { ...DEFAULT_THEME_CONFIG }
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
for (const key in DEFAULT_THEME_CONFIG) {
|
||||
const stored = localStorage.getItem('tabler-' + key)
|
||||
if (stored) {
|
||||
config[key as keyof ThemeConfig] = stored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose theme instance
|
||||
*/
|
||||
dispose(): void {
|
||||
if (!this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
// Private
|
||||
/**
|
||||
* Update URL with theme parameter
|
||||
*/
|
||||
private _updateURL(key: string, value: string): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
if (value === DEFAULT_THEME_CONFIG[key as keyof ThemeConfig]) {
|
||||
url.searchParams.delete(key)
|
||||
} else {
|
||||
url.searchParams.set(key, value)
|
||||
}
|
||||
window.history.pushState({}, '', url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to document
|
||||
*/
|
||||
private _applyTheme(): void {
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams: URLSearchParams, prop: string): string | null => searchParams.get(prop),
|
||||
})
|
||||
|
||||
for (const key in this.config) {
|
||||
const param = params[key]
|
||||
let selectedValue: string
|
||||
|
||||
if (!!param) {
|
||||
localStorage.setItem('tabler-' + key, param)
|
||||
selectedValue = param
|
||||
} else {
|
||||
const storedTheme = localStorage.getItem('tabler-' + key)
|
||||
selectedValue = storedTheme ? storedTheme : this.config[key as keyof ThemeConfig]
|
||||
}
|
||||
|
||||
if (selectedValue !== this.config[key as keyof ThemeConfig]) {
|
||||
document.documentElement.setAttribute('data-bs-' + key, selectedValue)
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-bs-' + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static
|
||||
/**
|
||||
* Get or create instance
|
||||
*/
|
||||
static getOrCreateInstance(config?: Partial<ThemeConfig>): Theme {
|
||||
if (!Theme.instance) {
|
||||
Theme.instance = new Theme(config)
|
||||
}
|
||||
return Theme.instance
|
||||
}
|
||||
|
||||
private static instance: Theme | null = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
const theme = Theme.getOrCreateInstance()
|
||||
theme.init()
|
||||
|
||||
export default Theme
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Toast } from './bootstrap'
|
||||
|
||||
/*
|
||||
Toasts
|
||||
*/
|
||||
let toastsTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="toast"]'))
|
||||
toastsTriggerList.map(function (toastTriggerEl) {
|
||||
if (!toastTriggerEl.hasAttribute('data-bs-target')) {
|
||||
return
|
||||
}
|
||||
|
||||
const toastEl = new Toast(toastTriggerEl.getAttribute('data-bs-target'))
|
||||
|
||||
toastTriggerEl.addEventListener('click', () => {
|
||||
toastEl.show()
|
||||
})
|
||||
})
|
||||
18
core/js/src/toast.ts
Normal file
18
core/js/src/toast.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Toast } from 'bootstrap'
|
||||
|
||||
/*
|
||||
Toasts
|
||||
*/
|
||||
const toastsTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="toast"]'))
|
||||
toastsTriggerList.map(function (toastTriggerEl: HTMLElement) {
|
||||
const target = toastTriggerEl.getAttribute('data-bs-target')
|
||||
if (target === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const toastEl = new Toast(target)
|
||||
|
||||
toastTriggerEl.addEventListener('click', () => {
|
||||
toastEl.show()
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Tooltip } from './bootstrap'
|
||||
|
||||
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
let options = {
|
||||
delay: { show: 50, hide: 50 },
|
||||
html: tooltipTriggerEl.getAttribute('data-bs-html') === 'true',
|
||||
placement: tooltipTriggerEl.getAttribute('data-bs-placement') ?? 'auto',
|
||||
}
|
||||
return new Tooltip(tooltipTriggerEl, options)
|
||||
})
|
||||
11
core/js/src/tooltip.ts
Normal file
11
core/js/src/tooltip.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Tooltip } from 'bootstrap'
|
||||
|
||||
const tooltipTriggerList: HTMLElement[] = [].slice.call(document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]'))
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl: HTMLElement) {
|
||||
const options = {
|
||||
delay: { show: 50, hide: 50 },
|
||||
html: tooltipTriggerEl.getAttribute('data-bs-html') === 'true',
|
||||
placement: tooltipTriggerEl.getAttribute('data-bs-placement') ?? 'auto',
|
||||
}
|
||||
return new Tooltip(tooltipTriggerEl, options)
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* demo-theme is specifically loaded right after the body and not deferred
|
||||
* to ensure we switch to the chosen dark/light theme as fast as possible.
|
||||
* This will prevent any flashes of the light theme (default) before switching.
|
||||
*/
|
||||
const themeConfig = {
|
||||
'theme': 'light',
|
||||
'theme-base': 'gray',
|
||||
'theme-font': 'sans-serif',
|
||||
'theme-primary': 'blue',
|
||||
'theme-radius': '1',
|
||||
}
|
||||
|
||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
})
|
||||
|
||||
for (const key in themeConfig) {
|
||||
const param = params[key]
|
||||
let selectedValue
|
||||
|
||||
if (!!param) {
|
||||
localStorage.setItem('tabler-' + key, param)
|
||||
selectedValue = param
|
||||
} else {
|
||||
const storedTheme = localStorage.getItem('tabler-' + key)
|
||||
selectedValue = storedTheme ? storedTheme : themeConfig[key]
|
||||
}
|
||||
|
||||
if (selectedValue !== themeConfig[key]) {
|
||||
document.documentElement.setAttribute('data-bs-' + key, selectedValue)
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-bs-' + key)
|
||||
}
|
||||
}
|
||||
7
core/js/tabler-theme.ts
Normal file
7
core/js/tabler-theme.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Theme initialization entry point
|
||||
* This file is specifically loaded right after the body and not deferred
|
||||
* to ensure we switch to the chosen dark/light theme as fast as possible.
|
||||
* This will prevent any flashes of the light theme (default) before switching.
|
||||
*/
|
||||
import './src/theme'
|
||||
@@ -1,16 +0,0 @@
|
||||
import './src/autosize'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
import './src/dropdown'
|
||||
import './src/tooltip'
|
||||
import './src/popover'
|
||||
import './src/switch-icon'
|
||||
import './src/tab'
|
||||
import './src/toast'
|
||||
import './src/sortable'
|
||||
|
||||
// Re-export everything from bootstrap.js (single source of truth)
|
||||
export * from './src/bootstrap'
|
||||
|
||||
// Re-export tabler namespace
|
||||
export * as tabler from './src/tabler'
|
||||
56
core/js/tabler.ts
Normal file
56
core/js/tabler.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import './src/dropdown'
|
||||
import './src/tooltip'
|
||||
import './src/popover'
|
||||
import './src/tab'
|
||||
import './src/toast'
|
||||
import './src/modal'
|
||||
import './src/collapse'
|
||||
import './src/offcanvas'
|
||||
import './src/sortable'
|
||||
import './src/switch-icon'
|
||||
import './src/autosize'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
import './src/copy'
|
||||
import Theme from './src/theme'
|
||||
|
||||
// Export Popper
|
||||
export * as Popper from '@popperjs/core'
|
||||
|
||||
// Export all Bootstrap components directly for consistent usage
|
||||
export { Alert, Button, Carousel, Collapse, Dropdown, Modal, Offcanvas, Popover, ScrollSpy, Tab, Toast, Tooltip } from 'bootstrap'
|
||||
|
||||
// Export custom Tabler components
|
||||
export { default as Autosize } from './src/autosize'
|
||||
export { default as SwitchIcon } from './src/switch-icon'
|
||||
export { default as Countup } from './src/countup'
|
||||
export { default as InputMask } from './src/input-mask'
|
||||
export { default as Copy } from './src/copy'
|
||||
export { default as Clipboard } from './src/clipboard'
|
||||
|
||||
// Re-export everything as namespace for backward compatibility
|
||||
export * as bootstrap from 'bootstrap'
|
||||
|
||||
// Re-export tabler namespace
|
||||
export * as tabler from './src/tabler'
|
||||
|
||||
// Create global Tabler API object
|
||||
const themeInstance = Theme.getOrCreateInstance()
|
||||
|
||||
const Tabler = {
|
||||
setTheme: (value: string) => themeInstance.setTheme(value),
|
||||
setPrimary: (value: string) => themeInstance.setPrimary(value),
|
||||
setBase: (value: string) => themeInstance.setBase(value),
|
||||
setFont: (value: string) => themeInstance.setFont(value),
|
||||
setRadius: (value: string) => themeInstance.setRadius(value),
|
||||
reset: () => themeInstance.reset(),
|
||||
getConfig: () => themeInstance.getConfig(),
|
||||
}
|
||||
|
||||
// Export Tabler API
|
||||
export { Tabler }
|
||||
|
||||
// Make Tabler available globally on window object
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as any).Tabler = Tabler
|
||||
}
|
||||
168
core/js/tests/README.md
Normal file
168
core/js/tests/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Tabler JavaScript Tests
|
||||
|
||||
## How does Tabler's test suite work?
|
||||
|
||||
Tabler uses **Vitest** for unit testing. Each plugin has a file dedicated to its tests in `tests/unit/<plugin-name>.spec.ts`.
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run the unit test suite, run:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
To run tests in watch mode:
|
||||
```bash
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
To run tests with UI:
|
||||
```bash
|
||||
pnpm test:ui
|
||||
```
|
||||
|
||||
To run tests with coverage:
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
## How do I add a new unit test?
|
||||
|
||||
1. Locate and open the file dedicated to the plugin which you need to add tests to (`tests/unit/<plugin-name>.spec.ts`).
|
||||
2. Review the Vitest API Documentation and use the existing tests as references for how to structure your new tests.
|
||||
3. Write the necessary unit test(s) for the new or revised functionality.
|
||||
4. Run `pnpm test` to see the results of your newly-added test(s).
|
||||
|
||||
**Note:** Your new unit tests should fail before your changes are applied to the plugin, and should pass after your changes are applied to the plugin.
|
||||
|
||||
## What should a unit test look like?
|
||||
|
||||
- Each test should have a unique name clearly stating what unit is being tested.
|
||||
- Each test should be in the corresponding `describe` block.
|
||||
- Each test should test only one unit per test, although one test can include several assertions. Create multiple tests for multiple units of functionality.
|
||||
- Each test should use `expect` to ensure something is expected.
|
||||
- Each test should follow the project's JavaScript Code Guidelines.
|
||||
|
||||
## Code Coverage
|
||||
|
||||
We're aiming for at least 90% test coverage for our code. To ensure your changes meet or exceed this limit, run:
|
||||
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
This will generate coverage reports in multiple formats:
|
||||
- **Text**: Displayed in terminal
|
||||
- **HTML**: Open `coverage/index.html` in your browser for detailed coverage report
|
||||
- **JSON**: `coverage/coverage-final.json` for programmatic access
|
||||
- **LCOV**: `coverage/lcov.info` for CI/CD integration
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
The following thresholds are enforced (minimum 90%):
|
||||
- Lines: 90%
|
||||
- Functions: 90%
|
||||
- Branches: 90%
|
||||
- Statements: 90%
|
||||
|
||||
If coverage falls below these thresholds, tests will fail.
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests follow Bootstrap's test structure pattern:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import PluginName from '../../src/plugin-name'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('PluginName', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Example Tests
|
||||
|
||||
### Synchronous test
|
||||
|
||||
```typescript
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return this instance', () => {
|
||||
fixtureEl.innerHTML = '<div></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const instance = new PluginName(divEl)
|
||||
|
||||
expect(PluginName.getInstance(divEl)).toEqual(instance)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Asynchronous test
|
||||
|
||||
```typescript
|
||||
it('should show a tooltip without the animation', async () => {
|
||||
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'
|
||||
const tooltipEl = fixtureEl.querySelector('a') as HTMLElement
|
||||
const tooltip = new Tooltip(tooltipEl, {
|
||||
animation: false
|
||||
})
|
||||
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
tooltipEl.addEventListener('shown.bs.tooltip', () => {
|
||||
const tip = document.querySelector('.tooltip')
|
||||
|
||||
expect(tip).not.toBeNull()
|
||||
expect(tip?.classList.contains('fade')).toBe(false)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
tooltip.show()
|
||||
await promise
|
||||
})
|
||||
```
|
||||
|
||||
## Fixture Helper
|
||||
|
||||
Use the fixture helper to manage test DOM elements:
|
||||
|
||||
```typescript
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('MyPlugin', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
it('should work', () => {
|
||||
fixtureEl.innerHTML = '<div class="test"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
// Your test code here
|
||||
})
|
||||
})
|
||||
```
|
||||
36
core/js/tests/helpers/fixture.ts
Normal file
36
core/js/tests/helpers/fixture.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Test fixture helper
|
||||
* Similar to Bootstrap's fixtureEl pattern
|
||||
*/
|
||||
|
||||
let fixtureContainer: HTMLDivElement | null = null
|
||||
|
||||
/**
|
||||
* Get or create fixture container
|
||||
*/
|
||||
export function getFixture(): HTMLDivElement {
|
||||
if (!fixtureContainer) {
|
||||
fixtureContainer = document.createElement('div')
|
||||
fixtureContainer.id = 'test-fixture'
|
||||
document.body.appendChild(fixtureContainer)
|
||||
}
|
||||
return fixtureContainer
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear fixture container
|
||||
*/
|
||||
export function clearFixture(): void {
|
||||
const fixture = getFixture()
|
||||
fixture.innerHTML = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove fixture container
|
||||
*/
|
||||
export function removeFixture(): void {
|
||||
if (fixtureContainer && fixtureContainer.parentNode) {
|
||||
fixtureContainer.parentNode.removeChild(fixtureContainer)
|
||||
fixtureContainer = null
|
||||
}
|
||||
}
|
||||
13
core/js/tests/helpers/setup.ts
Normal file
13
core/js/tests/helpers/setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { afterEach } from 'vitest'
|
||||
import { clearFixture } from './fixture'
|
||||
|
||||
/**
|
||||
* Setup file for Vitest tests
|
||||
* This file runs before each test file
|
||||
*
|
||||
* Note: jsdom automatically cleans up the DOM between tests,
|
||||
* but we clear fixture for consistency with Bootstrap's test pattern
|
||||
*/
|
||||
afterEach(() => {
|
||||
clearFixture()
|
||||
})
|
||||
346
core/js/tests/unit/clipboard.spec.ts
Normal file
346
core/js/tests/unit/clipboard.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { Tooltip } from 'bootstrap'
|
||||
import Copy from '../../src/copy'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
describe('Copy', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
let clipboardWriteText: ReturnType<typeof vi.fn>
|
||||
let originalClipboardDescriptor: PropertyDescriptor | undefined
|
||||
let originalExecCommand: any
|
||||
|
||||
const setNavigatorClipboard = (writeTextImpl: (text: string) => Promise<void>) => {
|
||||
originalClipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 'clipboard')
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: writeTextImpl },
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
|
||||
clipboardWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
setNavigatorClipboard(clipboardWriteText)
|
||||
|
||||
originalExecCommand = (document as any).execCommand
|
||||
;(document as any).execCommand = vi.fn().mockReturnValue(false)
|
||||
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
|
||||
if (originalClipboardDescriptor) {
|
||||
Object.defineProperty(navigator, 'clipboard', originalClipboardDescriptor)
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete (navigator as any).clipboard
|
||||
}
|
||||
|
||||
;(document as any).execCommand = originalExecCommand
|
||||
})
|
||||
|
||||
describe('getInstance / getOrCreateInstance', () => {
|
||||
it('should return null when instance does not exist', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
expect(Copy.getInstance(buttonEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should create and store instance on element', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance1 = Copy.getOrCreateInstance(buttonEl)
|
||||
expect(instance1).toBeInstanceOf(Copy)
|
||||
expect(Copy.getInstance(buttonEl)).toBe(instance1)
|
||||
|
||||
const instance2 = Copy.getOrCreateInstance(buttonEl)
|
||||
expect(instance2).toBe(instance1)
|
||||
})
|
||||
|
||||
it('dispose should remove stored instance', () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="x"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
expect(Copy.getInstance(buttonEl)).toBe(instance)
|
||||
|
||||
instance.dispose()
|
||||
expect(Copy.getInstance(buttonEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy()', () => {
|
||||
it('should copy text from data-bs-copy-text and set copied state', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const def = buttonEl.querySelector('[data-bs-copy-default]') as HTMLElement
|
||||
const success = buttonEl.querySelector('[data-bs-copy-success]') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith('hello')
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
expect(buttonEl.getAttribute('aria-label')).toBe('Copied')
|
||||
expect(def.hidden).toBe(true)
|
||||
expect(success.hidden).toBe(false)
|
||||
})
|
||||
|
||||
it('should fire cancelable COPY event and respect preventDefault()', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
buttonEl.addEventListener(Copy.Event.COPY, (e) => e.preventDefault())
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(clipboardWriteText).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should emit COPYFAIL when there is no text', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onFail = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.COPYFAIL, onFail as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(onFail).toHaveBeenCalledTimes(1)
|
||||
const ev = onFail.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.reason).toBe('no-text')
|
||||
})
|
||||
|
||||
it('should emit COPYFAIL when clipboard write fails', async () => {
|
||||
clipboardWriteText.mockRejectedValueOnce(new Error('nope'))
|
||||
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onFail = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.COPYFAIL, onFail as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl)!
|
||||
const ok = await instance.copy()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(onFail).toHaveBeenCalledTimes(1)
|
||||
const ev = onFail.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.reason).toBe('clipboard-failed')
|
||||
})
|
||||
|
||||
it('should reset automatically after timeout when reset=true', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const def = buttonEl.querySelector('[data-bs-copy-default]') as HTMLElement
|
||||
const success = buttonEl.querySelector('[data-bs-copy-success]') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000 })!
|
||||
await instance.copy()
|
||||
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
vi.advanceTimersByTime(999)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
expect(buttonEl.getAttribute('aria-label')).toBeNull()
|
||||
expect(def.hidden).toBe(false)
|
||||
expect(success.hidden).toBe(true)
|
||||
})
|
||||
|
||||
it('should extend timeout without recopy when recopy=false and extend=true', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000, recopy: false, extend: true, reset: true })!
|
||||
|
||||
await instance.copy()
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
await instance.copy()
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(1)
|
||||
|
||||
// 600ms after the second click: should still be copied (timer extended)
|
||||
vi.advanceTimersByTime(600)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
// 401ms more (total 1001ms after second click): should reset
|
||||
vi.advanceTimersByTime(401)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
})
|
||||
|
||||
it('should recopy when recopy=true (default)', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { reset: false })!
|
||||
|
||||
await instance.copy()
|
||||
await instance.copy()
|
||||
|
||||
expect(clipboardWriteText).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should not extend timeout when extend=false and timer is already running', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { timeout: 1000, recopy: false, extend: false, reset: true })!
|
||||
|
||||
await instance.copy()
|
||||
vi.advanceTimersByTime(500)
|
||||
await instance.copy() // should NOT extend existing timer
|
||||
|
||||
vi.advanceTimersByTime(501) // total 1001ms from first copy
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset()', () => {
|
||||
it('should manually reset and emit RESET with manual=true', async () => {
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const onReset = vi.fn()
|
||||
buttonEl.addEventListener(Copy.Event.RESET, onReset as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { reset: false })!
|
||||
await instance.copy()
|
||||
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
|
||||
instance.reset()
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
expect(onReset).toHaveBeenCalledTimes(1)
|
||||
const ev = onReset.mock.calls[0][0] as CustomEvent
|
||||
expect(ev.detail.manual).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data API', () => {
|
||||
it('should copy on click via document listener', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
const span = buttonEl.querySelector('span') as HTMLElement
|
||||
|
||||
span.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
|
||||
// allow async copy() chain to complete (Data API doesn't await)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith('hello')
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('feedback: tooltip', () => {
|
||||
it('should show/hide bootstrap tooltip when available', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const show = vi.fn()
|
||||
const hide = vi.fn()
|
||||
const dispose = vi.fn()
|
||||
const getOrCreateInstance = vi.spyOn(Tooltip, 'getOrCreateInstance').mockReturnValue({ show, hide, dispose } as any)
|
||||
|
||||
fixtureEl.innerHTML = '<button data-bs-toggle="copy" data-bs-copy-text="hello"></button>'
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, {
|
||||
feedback: 'tooltip',
|
||||
tooltip: 'Copied!',
|
||||
tooltipPlacement: 'top',
|
||||
timeout: 500,
|
||||
reset: true,
|
||||
})!
|
||||
|
||||
await instance.copy()
|
||||
expect(getOrCreateInstance).toHaveBeenCalledTimes(1)
|
||||
expect(show).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
expect(hide).toHaveBeenCalledTimes(1)
|
||||
|
||||
instance.dispose()
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
getOrCreateInstance.mockRestore()
|
||||
})
|
||||
|
||||
it('should not swap when tooltip mode is enabled', async () => {
|
||||
fixtureEl.innerHTML = `
|
||||
<button data-bs-toggle="copy" data-bs-copy-text="hello">
|
||||
<span data-bs-copy-default>Default</span>
|
||||
<span data-bs-copy-success hidden>Success</span>
|
||||
</button>
|
||||
`
|
||||
const buttonEl = fixtureEl.querySelector('button') as HTMLElement
|
||||
|
||||
const show = vi.fn()
|
||||
const hide = vi.fn()
|
||||
const dispose = vi.fn()
|
||||
const getOrCreateInstance = vi.spyOn(Tooltip, 'getOrCreateInstance').mockReturnValue({ show, hide, dispose } as any)
|
||||
|
||||
const instance = Copy.getOrCreateInstance(buttonEl, { feedback: 'tooltip', reset: false })!
|
||||
await instance.copy()
|
||||
|
||||
// tooltip mode uses Tooltip feedback and does not rely on swap markup
|
||||
expect(show).toHaveBeenCalledTimes(1)
|
||||
expect(buttonEl.classList.contains('is-copied')).toBe(false)
|
||||
|
||||
instance.dispose()
|
||||
getOrCreateInstance.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('static properties', () => {
|
||||
it('should have correct NAME', () => {
|
||||
expect(Copy.NAME).toBe('copy')
|
||||
})
|
||||
|
||||
it('should have correct DATA_KEY', () => {
|
||||
expect(Copy.DATA_KEY).toBe('bs.copy')
|
||||
})
|
||||
})
|
||||
})
|
||||
204
core/js/tests/unit/switch-icon.spec.ts
Normal file
204
core/js/tests/unit/switch-icon.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import SwitchIcon from '../../src/switch-icon'
|
||||
import { getFixture, clearFixture } from '../helpers/fixture'
|
||||
|
||||
/**
|
||||
* Unit tests for SwitchIcon plugin
|
||||
* Following Bootstrap's test structure pattern
|
||||
*/
|
||||
|
||||
describe('SwitchIcon', () => {
|
||||
let fixtureEl: HTMLDivElement
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureEl = getFixture()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
describe('getInstance', () => {
|
||||
it('should return null if there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return instance when created via Data API', () => {
|
||||
// Simulate Data API behavior - element with data attribute triggers initialization
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
// Manually trigger Data API-like behavior
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(divEl)
|
||||
// Note: In real Data API, elementMap.set() is called, but getOrCreateInstance doesn't do that
|
||||
// So we test that getInstance returns null when instance is not in map
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOrCreateInstance', () => {
|
||||
it('should return new instance when there is no instance', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
|
||||
const switchIcon = SwitchIcon.getOrCreateInstance(divEl)
|
||||
|
||||
expect(switchIcon).toBeInstanceOf(SwitchIcon)
|
||||
// Note: getOrCreateInstance doesn't add to elementMap, only Data API does
|
||||
expect(SwitchIcon.getInstance(divEl)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return new instance each time (not stored in map)', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon1 = SwitchIcon.getOrCreateInstance(divEl)
|
||||
const switchIcon2 = SwitchIcon.getOrCreateInstance(divEl)
|
||||
|
||||
// Both are new instances because they're not stored in elementMap
|
||||
// getOrCreateInstance creates new instance if not found in map
|
||||
expect(switchIcon1).toBeInstanceOf(SwitchIcon)
|
||||
expect(switchIcon2).toBeInstanceOf(SwitchIcon)
|
||||
// They are different instances because elementMap is not used by getOrCreateInstance
|
||||
expect(switchIcon1).not.toEqual(switchIcon2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should toggle active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
switchIcon.toggle()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
switchIcon.toggle()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('show', () => {
|
||||
it('should add active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(divEl.classList.contains('active')).toBe(false)
|
||||
|
||||
switchIcon.show()
|
||||
expect(divEl.classList.contains('active')).toBe(true)
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hide', () => {
|
||||
it('should remove active class', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon" class="active"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
expect(divEl.classList.contains('active')).toBe(true)
|
||||
|
||||
switchIcon.hide()
|
||||
expect(divEl.classList.contains('active')).toBe(false)
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
it('should initialize and set event listeners', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
// Simulate click
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('should not initialize twice', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
const firstClick = () => {
|
||||
divEl.click()
|
||||
}
|
||||
firstClick()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
// Reset
|
||||
switchIcon.hide()
|
||||
switchIcon.init() // Should not add duplicate listeners
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should remove event listeners', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
switchIcon.dispose()
|
||||
|
||||
// Click should not toggle after dispose
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('should do nothing if not initialized', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
switchIcon.dispose()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('click event', () => {
|
||||
it('should toggle on click after init', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
|
||||
switchIcon.init()
|
||||
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(true)
|
||||
|
||||
divEl.click()
|
||||
expect(switchIcon.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('should stop propagation', () => {
|
||||
fixtureEl.innerHTML = '<div data-bs-toggle="switch-icon"></div>'
|
||||
const divEl = fixtureEl.querySelector('div') as HTMLElement
|
||||
const switchIcon = new SwitchIcon(divEl)
|
||||
let parentClicked = false
|
||||
|
||||
fixtureEl.addEventListener('click', () => {
|
||||
parentClicked = true
|
||||
})
|
||||
|
||||
switchIcon.init()
|
||||
divEl.click()
|
||||
|
||||
expect(parentClicked).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
64
core/js/tests/unit/tabler.test.ts
Normal file
64
core/js/tests/unit/tabler.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { hexToRgba, getColor, prefix } from '../../src/tabler'
|
||||
|
||||
describe('tabler', () => {
|
||||
describe('prefix', () => {
|
||||
it('should have correct prefix value', () => {
|
||||
expect(prefix).toBe('tblr-')
|
||||
})
|
||||
})
|
||||
|
||||
describe('hexToRgba', () => {
|
||||
it('should convert hex color to rgba string', () => {
|
||||
expect(hexToRgba('#ff0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
expect(hexToRgba('#00ff00', 0.5)).toBe('rgba(0, 255, 0, 0.5)')
|
||||
expect(hexToRgba('#0000ff', 0.8)).toBe('rgba(0, 0, 255, 0.8)')
|
||||
})
|
||||
|
||||
it('should handle hex without #', () => {
|
||||
expect(hexToRgba('ff0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
})
|
||||
|
||||
it('should handle uppercase hex', () => {
|
||||
expect(hexToRgba('#FF0000', 1)).toBe('rgba(255, 0, 0, 1)')
|
||||
})
|
||||
|
||||
it('should return null for invalid hex', () => {
|
||||
expect(hexToRgba('invalid', 1)).toBeNull()
|
||||
expect(hexToRgba('#gg0000', 1)).toBeNull()
|
||||
expect(hexToRgba('', 1)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getColor', () => {
|
||||
let originalBody: HTMLBodyElement
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original body
|
||||
originalBody = document.body
|
||||
|
||||
// Set test CSS variable value
|
||||
document.body.style.setProperty(`--${prefix}primary`, '#ff0000')
|
||||
})
|
||||
|
||||
it('should get color from CSS variable', () => {
|
||||
const color = getColor('primary')
|
||||
expect(color).toBe('#ff0000')
|
||||
})
|
||||
|
||||
it('should return null if color variable does not exist', () => {
|
||||
const color = getColor('nonexistent')
|
||||
expect(color).toBe('')
|
||||
})
|
||||
|
||||
it('should convert to rgba when opacity is provided', () => {
|
||||
const color = getColor('primary', 0.5)
|
||||
expect(color).toBe('rgba(255, 0, 0, 0.5)')
|
||||
})
|
||||
|
||||
it('should return hex color when opacity is 1', () => {
|
||||
const color = getColor('primary', 1)
|
||||
expect(color).toBe('#ff0000')
|
||||
})
|
||||
})
|
||||
})
|
||||
20
core/js/tests/vitest.config.ts
Normal file
20
core/js/tests/vitest.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./helpers/setup.ts'],
|
||||
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, '../src')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -166,5 +166,14 @@
|
||||
"dist/turbo.es2017-umd.js"
|
||||
],
|
||||
"head": true
|
||||
},
|
||||
"driver.js": {
|
||||
"npm": "driver.js",
|
||||
"js": [
|
||||
"dist/driver.js.iife.js"
|
||||
],
|
||||
"css": [
|
||||
"dist/driver.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,46 @@
|
||||
"homepage": "https://tabler.io",
|
||||
"scripts": {
|
||||
"dev": "pnpm run clean && pnpm run copy && pnpm run watch",
|
||||
"build": "pnpm run clean && pnpm run css && pnpm run js && pnpm run copy && pnpm run generate-sri",
|
||||
"build": "pnpm run clean && pnpm run build-assets && pnpm run copy && pnpm run generate-sri",
|
||||
"build-assets": "concurrently \"pnpm run css\" \"pnpm run js\"",
|
||||
"clean": "shx rm -rf dist demo",
|
||||
"css": "pnpm run css-compile && pnpm run css-prefix && pnpm run css-rtl && pnpm run css-minify && pnpm run css-banner",
|
||||
"css-compile": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/css/",
|
||||
"css-banner": "node .build/add-banner.mjs",
|
||||
"css": "pnpm run css-build && pnpm run css-prefix && pnpm run css-rtl && pnpm run css-minify && pnpm run css-banner",
|
||||
"css-build": "sass --no-source-map --load-path=node_modules --style expanded scss/:dist/css/",
|
||||
"css-banner": "tsx .build/add-banner.ts",
|
||||
"css-prefix": "postcss --config .build/postcss.config.mjs --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"",
|
||||
"css-rtl": "cross-env NODE_ENV=RTL postcss --config .build/postcss.config.mjs --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"",
|
||||
"css-minify": "pnpm run css-minify-main && pnpm run css-minify-rtl",
|
||||
"css-minify": "concurrently \"pnpm run css-minify-main\" \"pnpm run css-minify-rtl\"",
|
||||
"css-minify-main": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"",
|
||||
"css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"",
|
||||
"css-lint": "pnpm run css-lint-variables",
|
||||
"css-lint-variables": "find-unused-sass-variables scss/ node_modules/bootstrap/scss/",
|
||||
"js": "pnpm run js-compile && pnpm run js-minify",
|
||||
"js-compile": "pnpm run js-compile-standalone && pnpm run js-compile-standalone-esm && pnpm run js-compile-theme && pnpm run js-compile-theme-esm",
|
||||
"js-compile-theme-esm": "rollup --environment THEME:true --environment ESM:true --config .build/rollup.config.mjs --sourcemap",
|
||||
"js-compile-theme": "rollup --environment THEME:true --config .build/rollup.config.mjs --sourcemap",
|
||||
"js-compile-standalone": "rollup --config .build/rollup.config.mjs --sourcemap",
|
||||
"js-compile-standalone-esm": "rollup --environment ESM:true --config .build/rollup.config.mjs --sourcemap",
|
||||
"js-minify": "pnpm run js-minify-standalone && pnpm run js-minify-standalone-esm && pnpm run js-minify-theme && pnpm run js-minify-theme-esm",
|
||||
"js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.js.map,includeSources,url=tabler.min.js.map\" --output dist/js/tabler.min.js dist/js/tabler.js",
|
||||
"js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler.esm.js.map,includeSources,url=tabler.esm.min.js.map\" --output dist/js/tabler.esm.min.js dist/js/tabler.esm.js",
|
||||
"js-minify-theme": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.js.map,includeSources,url=tabler-theme.min.js.map\" --output dist/js/tabler-theme.min.js dist/js/tabler-theme.js",
|
||||
"js-minify-theme-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/tabler-theme.esm.js.map,includeSources,url=tabler-theme.esm.min.js.map\" --output dist/js/tabler-theme.esm.min.js dist/js/tabler-theme.esm.js",
|
||||
"copy": "pnpm run copy-img && pnpm run copy-libs && pnpm run copy-fonts",
|
||||
"js": "pnpm run js-build && pnpm run js-build-min",
|
||||
"js-build": "concurrently \"pnpm run js-build-standalone\" \"pnpm run js-build-standalone-esm\" \"pnpm run js-build-theme\" \"pnpm run js-build-theme-esm\"",
|
||||
"js-build-theme-esm": "cross-env THEME=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-theme": "cross-env THEME=true vite build --config .build/vite.config.mts",
|
||||
"js-build-standalone": "vite build --config .build/vite.config.mts",
|
||||
"js-build-standalone-esm": "cross-env ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min": "concurrently \"pnpm run js-build-min-standalone\" \"pnpm run js-build-min-standalone-esm\" \"pnpm run js-build-min-theme\" \"pnpm run js-build-min-theme-esm\"",
|
||||
"js-build-min-standalone": "cross-env MINIFY=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-standalone-esm": "cross-env MINIFY=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-theme": "cross-env MINIFY=true THEME=true vite build --config .build/vite.config.mts",
|
||||
"js-build-min-theme-esm": "cross-env MINIFY=true THEME=true ESM=true vite build --config .build/vite.config.mts",
|
||||
"copy": "concurrently \"pnpm run copy-img\" \"pnpm run copy-libs\" \"pnpm run copy-fonts\"",
|
||||
"copy-img": "shx mkdir -p dist/img && shx cp -rf img/* dist/img",
|
||||
"copy-libs": "node .build/copy-libs.mjs",
|
||||
"copy-libs": "tsx .build/copy-libs.ts",
|
||||
"copy-fonts": "shx mkdir -p dist/fonts && shx cp -rf fonts/* dist/fonts",
|
||||
"import-fonts": "node .build/import-fonts.mjs",
|
||||
"import-fonts": "tsx .build/import-fonts.ts",
|
||||
"watch": "concurrently \"pnpm run watch-css\" \"pnpm run watch-js\"",
|
||||
"watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css-compile && pnpm run css-prefix\"",
|
||||
"watch-js": "nodemon --watch js/ --ext js --exec \"pnpm run js-compile\"",
|
||||
"watch-css": "nodemon --watch scss/ --ext scss --exec \"pnpm run css-build && pnpm run css-prefix\"",
|
||||
"watch-js": "nodemon --watch js/ --ext ts,js --exec \"pnpm run js-build\"",
|
||||
"bundlewatch": "bundlewatch",
|
||||
"generate-sri": "node .build/generate-sri.js",
|
||||
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.js\" --cache",
|
||||
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.js\" --cache"
|
||||
"generate-sri": "tsx .build/generate-sri.ts",
|
||||
"format:check": "prettier --check \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"format:write": "prettier --write \"scss/**/*.scss\" \"js/**/*.{js,ts}\" --cache",
|
||||
"type-check": "tsc --noEmit",
|
||||
"test": "vitest run --config js/tests/vitest.config.ts",
|
||||
"test:watch": "vitest --config js/tests/vitest.config.ts",
|
||||
"test:ui": "vitest --ui --config js/tests/vitest.config.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -69,7 +74,7 @@
|
||||
"files": [
|
||||
"docs/**/*",
|
||||
"dist/**/*",
|
||||
"js/**/*.{js,map}",
|
||||
"js/**/*.{ts,js,map}",
|
||||
"img/**/*.{svg}",
|
||||
"scss/**/*.scss",
|
||||
"libs.json"
|
||||
@@ -155,11 +160,16 @@
|
||||
"devDependencies": {
|
||||
"@hotwired/turbo": "^8.0.18",
|
||||
"@melloware/coloris": "^0.25.0",
|
||||
"apexcharts": "3.54.1",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@vitest/coverage-v8": "2.1.9",
|
||||
"@vitest/ui": "^2.0.0",
|
||||
"apexcharts": "^5.3.6",
|
||||
"autosize": "^6.0.1",
|
||||
"choices.js": "^11.1.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"countup.js": "^2.9.0",
|
||||
"driver.js": "^1.0.0",
|
||||
"dropzone": "^6.0.0-beta.2",
|
||||
"find-unused-sass-variables": "^6.1.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
@@ -168,6 +178,7 @@
|
||||
"geist": "^1.5.1",
|
||||
"hugerte": "^1.0.9",
|
||||
"imask": "^7.6.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsvectormap": "^1.7.0",
|
||||
"list.js": "^2.3.1",
|
||||
"litepicker": "^2.0.12",
|
||||
@@ -177,7 +188,9 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"star-rating.js": "^4.3.1",
|
||||
"tom-select": "^2.4.3",
|
||||
"typed.js": "^2.1.0"
|
||||
"typed.js": "^2.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
/** Theme colors */
|
||||
@each $name, $color in map.merge($theme-colors, $social-colors) {
|
||||
@debug contrast-ratio($color, white), $name, $min-contrast-ratio;
|
||||
--#{$prefix}#{$name}: #{$color};
|
||||
--#{$prefix}#{$name}-rgb: #{to-rgb($color)};
|
||||
--#{$prefix}#{$name}-fg: #{if(contrast-ratio($color) > $min-contrast-ratio, var(--#{$prefix}light), var(--#{$prefix}dark))};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,12 +33,7 @@ $enable-deprecation-messages: true !default;
|
||||
$enable-important-utilities: true !default;
|
||||
|
||||
// Escaped Characters
|
||||
$escaped-characters: (
|
||||
('<', '%3c'),
|
||||
('>', '%3e'),
|
||||
('#', '%23'),
|
||||
('(', '%28'),
|
||||
(')', '%29')) !default;
|
||||
$escaped-characters: (('<', '%3c'), ('>', '%3e'), ('#', '%23'), ('(', '%28'), (')', '%29')) !default;
|
||||
|
||||
// Dark Mode
|
||||
$color-mode-type: data !default;
|
||||
@@ -560,11 +555,6 @@ $text-secondary-opacity: 0.7 !default;
|
||||
$text-secondary-light-opacity: 0.4 !default;
|
||||
$text-secondary-dark-opacity: 0.8 !default;
|
||||
|
||||
$border-opacity: 0.16 !default;
|
||||
$border-light-opacity: 0.08 !default;
|
||||
$border-dark-opacity: 0.24 !default;
|
||||
$border-active-opacity: 0.58 !default;
|
||||
|
||||
$bg-surface: var(--#{$prefix}white) !default;
|
||||
$bg-surface-secondary: var(--#{$prefix}gray-100) !default;
|
||||
$bg-surface-tertiary: var(--#{$prefix}gray-50) !default;
|
||||
@@ -572,12 +562,8 @@ $bg-surface-dark: var(--#{$prefix}dark) !default;
|
||||
|
||||
$body-text-align: null !default;
|
||||
$body-bg: $gray-50 !default;
|
||||
$body-color: $dark !default;
|
||||
$body-color: $gray-800 !default;
|
||||
$body-emphasis-color: $gray-700 !default;
|
||||
$body-secondary-color: rgba($body-color, 0.75) !default;
|
||||
$body-secondary-bg: $gray-200 !default;
|
||||
$body-tertiary-color: rgba($body-color, 0.5) !default;
|
||||
$body-tertiary-bg: $gray-100 !default;
|
||||
|
||||
$color-contrast-dark: $body-color !default;
|
||||
$color-contrast-light: $light !default;
|
||||
@@ -587,20 +573,27 @@ $text-secondary: $gray-500 !default;
|
||||
$text-secondary-light: $gray-400 !default;
|
||||
$text-secondary-dark: $gray-600 !default;
|
||||
|
||||
$border-color: $gray-200 !default;
|
||||
$border-color-translucent: rgba(4, 32, 69, 0.1);
|
||||
$border-light-color: var(--#{$prefix}gray-200) !default;
|
||||
$border-light-opacity: 4.7% !default;
|
||||
$border-light-color-translucent: color-mix(in srgb, var(--#{$prefix}gray-800) #{$border-light-opacity}, transparent) !default;
|
||||
|
||||
$border-dark-color: $gray-400 !default;
|
||||
$border-dark-color-translucent: rgba(4, 32, 69, 0.27);
|
||||
$border-color: var(--#{$prefix}gray-200) !default;
|
||||
$border-opacity: 11.9% !default;
|
||||
$border-color-translucent: color-mix(in srgb, var(--#{$prefix}gray-800) #{$border-opacity}, transparent) !default;
|
||||
|
||||
$border-active-color: color.mix($text-secondary, #ffffff, math.percentage($border-active-opacity)) !default;
|
||||
$border-active-color-translucent: rgba($text-secondary, $border-active-opacity) !default;
|
||||
$border-dark-color: var(--#{$prefix}gray-300) !default;
|
||||
$border-dark-opacity: 20.7% !default;
|
||||
$border-dark-color-translucent: color-mix(in srgb, var(--#{$prefix}gray-800) #{$border-dark-opacity}, transparent) !default;
|
||||
|
||||
$active-bg: rgba(var(--#{$prefix}primary-rgb), 0.04) !default;
|
||||
$border-active-color: var(--#{$prefix}gray-400) !default;
|
||||
$border-active-opacity: 44.8% !default;
|
||||
$border-active-color-translucent: color-mix(in srgb, var(--#{$prefix}gray-800) #{$border-active-opacity}, transparent) !default;
|
||||
|
||||
$active-bg: color-transparent(var(--#{$prefix}primary), 0.04) !default;
|
||||
$active-color: var(--#{$prefix}primary) !default;
|
||||
$active-border-color: var(--#{$prefix}primary) !default;
|
||||
|
||||
$hover-bg: rgba(var(--#{$prefix}secondary-rgb), 0.08) !default;
|
||||
$hover-bg: color-transparent(var(--#{$prefix}secondary), 0.08) !default;
|
||||
|
||||
$disabled-bg: var(--#{$prefix}bg-surface-secondary) !default;
|
||||
$disabled-color: color-transparent(var(--#{$prefix}body-color), 0.4) !default;
|
||||
@@ -880,7 +873,7 @@ $avatar-sizes: (
|
||||
brand-size: 2rem,
|
||||
),
|
||||
) !default;
|
||||
$avatar-border-radius: var(--#{$prefix}border-radius) !default;
|
||||
$avatar-border-radius: var(--#{$prefix}border-radius-pill) !default;
|
||||
$avatar-font-size: $h4-font-size !default;
|
||||
$avatar-box-shadow: var(--#{$prefix}shadow-border) !default;
|
||||
$avatar-list-spacing: -0.5;
|
||||
@@ -988,14 +981,14 @@ $aspect-ratios: (
|
||||
) !default;
|
||||
|
||||
// Shadows
|
||||
$box-shadow: rgba(var(--#{$prefix}body-color-rgb), 0.04) 0 2px 4px 0 !default;
|
||||
$box-shadow: color-transparent(var(--#{$prefix}body-color), 0.04) 0 2px 4px 0 !default;
|
||||
$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075) !default;
|
||||
$box-shadow-lg: 0 1rem 3rem rgba($black, 0.175) !default;
|
||||
$box-shadow-transparent: 0 0 0 0 transparent !default;
|
||||
$box-shadow-border: inset 0 0 0 1px var(--#{$prefix}border-color-translucent) !default;
|
||||
$box-shadow-input: 0 1px 1px rgba(var(--#{$prefix}body-color-rgb), 0.06) !default;
|
||||
$box-shadow-card: 0 0 4px rgba(var(--#{$prefix}body-color-rgb), 0.04) !default;
|
||||
$box-shadow-card-hover: rgba(var(--#{$prefix}body-color-rgb), 0.16) 0 2px 16px 0 !default;
|
||||
$box-shadow-input: 0 1px 1px color-transparent(var(--#{$prefix}body-color), 0.06) !default;
|
||||
$box-shadow-card: 0px 1px 3px rgba(0, 0, 0, 0.08) !default;
|
||||
$box-shadow-card-hover: color-transparent(var(--#{$prefix}body-color), 0.16) 0 2px 16px 0 !default;
|
||||
$box-shadow-dropdown:
|
||||
0 16px 24px 2px rgba(0, 0, 0, 0.07),
|
||||
0 6px 30px 5px rgba(0, 0, 0, 0.06),
|
||||
@@ -1020,7 +1013,7 @@ $component-active-bg: $primary !default;
|
||||
// Focus
|
||||
$focus-ring-width: 0.25rem !default;
|
||||
$focus-ring-opacity: 0.25 !default;
|
||||
$focus-ring-color: rgba(var(--#{$prefix}primary-rgb), $focus-ring-opacity) !default;
|
||||
$focus-ring-color: color-mix(in srgb, var(--#{$prefix}primary) #{percentage($focus-ring-opacity)}, transparent) !default;
|
||||
$focus-ring-blur: 0 !default;
|
||||
$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default;
|
||||
|
||||
@@ -1166,9 +1159,9 @@ $btn-padding-y-lg: $input-btn-padding-y-lg !default;
|
||||
$btn-padding-x-lg: $input-btn-padding-x-lg !default;
|
||||
|
||||
// Inputs
|
||||
$input-bg: var(--#{$prefix}body-bg) !default;
|
||||
$input-bg: var(--#{$prefix}bg-forms) !default;
|
||||
$input-disabled-color: null !default;
|
||||
$input-disabled-bg: var(--#{$prefix}secondary-bg) !default;
|
||||
$input-disabled-bg: var(--#{$prefix}bg-surface-secondary) !default;
|
||||
$input-disabled-border-color: null !default;
|
||||
|
||||
$input-height: null !default;
|
||||
@@ -1517,9 +1510,9 @@ $navbar-light-active-color: var(--#{$prefix}body-color) !default;
|
||||
$navbar-light-hover-color: var(--#{$prefix}body-color) !default;
|
||||
$navbar-light-disabled-color: var(--#{$prefix}disabled-color) !default;
|
||||
$navbar-light-active-bg: rgba(0, 0, 0, 0.2) !default;
|
||||
$navbar-light-icon-color: rgba($body-color, 0.75) !default;
|
||||
$navbar-light-icon-color: color-transparent(var(--#{$prefix}body-color), 0.75) !default;
|
||||
$navbar-light-toggler-icon-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'><path stroke='#{$navbar-light-icon-color}' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/></svg>") !default;
|
||||
$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), 0.15) !default;
|
||||
$navbar-light-toggler-border-color: color-transparent(var(--#{$prefix}emphasis-color), 0.15) !default;
|
||||
$navbar-light-brand-hover-color: $navbar-light-active-color !default;
|
||||
|
||||
$navbar-dark-color: rgba($white, $text-secondary-opacity) !default;
|
||||
@@ -1694,7 +1687,7 @@ $table-active-bg: var(--#{$prefix}active-bg) !default;
|
||||
|
||||
$table-hover-color: $table-color !default;
|
||||
$table-hover-bg-factor: 0.075 !default;
|
||||
$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default;
|
||||
$table-hover-bg: color-mix(in srgb, var(--#{$prefix}emphasis-color) #{percentage($table-hover-bg-factor)}, transparent) !default;
|
||||
|
||||
$table-caption-color: var(--#{$prefix}secondary-color) !default;
|
||||
|
||||
@@ -1729,7 +1722,7 @@ $toast-box-shadow: var(--#{$prefix}box-shadow) !default;
|
||||
$toast-spacing: $container-padding-x !default;
|
||||
|
||||
$toast-header-color: var(--#{$prefix}gray-500) !default;
|
||||
$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), 0.85) !default;
|
||||
$toast-header-background-color: color-transparent(var(--#{$prefix}body-bg), 0.85) !default;
|
||||
$toast-header-border-color: $toast-border-color !default;
|
||||
|
||||
// Tracking
|
||||
@@ -1774,8 +1767,6 @@ $list-group-action-active-color: var(--#{$prefix}body-color) !default;
|
||||
$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-bg: var(--#{$prefix}bg-forms) !default;
|
||||
$input-disabled-bg: $disabled-bg !default;
|
||||
$input-border-color: var(--#{$prefix}border-color) !default;
|
||||
$input-border-color-translucent: var(--#{$prefix}border-color-translucent) !default;
|
||||
@@ -1865,16 +1856,16 @@ $form-select-font-size-lg: $input-font-size-lg !default;
|
||||
$form-select-border-radius-lg: $input-border-radius-lg !default;
|
||||
$form-select-transition: $input-transition !default;
|
||||
|
||||
$form-switch-color: rgba($black, 0.25) !default;
|
||||
$form-switch-color: white !default;
|
||||
$form-switch-width: 2rem !default;
|
||||
$form-switch-height: 1.25rem !default;
|
||||
$form-switch-padding-start: $form-switch-width + 0.5rem !default;
|
||||
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$border-color}'/></svg>") !default;
|
||||
$form-switch-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-color}'/></svg>") !default;
|
||||
$form-switch-border-radius: $form-switch-width !default;
|
||||
$form-switch-transition: background-position 0.15s ease-in-out !default;
|
||||
$form-switch-focus-color: $input-focus-border-color !default;
|
||||
$form-switch-focus-color: white !default;
|
||||
$form-switch-focus-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-focus-color}'/></svg>") !default;
|
||||
$form-switch-checked-color: $component-active-color !default;
|
||||
$form-switch-checked-color: white !default;
|
||||
$form-switch-checked-bg-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#{$form-switch-checked-color}'/></svg>") !default;
|
||||
$form-switch-checked-bg-position: right center !default;
|
||||
$form-switch-bg-size: auto !default;
|
||||
@@ -1960,7 +1951,7 @@ $form-validation-states: (
|
||||
'icon': $form-feedback-icon-valid,
|
||||
'tooltip-color': #fff,
|
||||
'tooltip-bg-color': var(--#{$prefix}success),
|
||||
'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity),
|
||||
'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width color-mix(in srgb, var(--#{$prefix}success) #{percentage($input-btn-focus-color-opacity)}, transparent),
|
||||
'border-color': var(--#{$prefix}form-valid-border-color),
|
||||
),
|
||||
'invalid': (
|
||||
@@ -1968,7 +1959,7 @@ $form-validation-states: (
|
||||
'icon': $form-feedback-icon-invalid,
|
||||
'tooltip-color': #fff,
|
||||
'tooltip-bg-color': var(--#{$prefix}danger),
|
||||
'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity),
|
||||
'focus-box-shadow': 0 0 $input-btn-focus-blur $input-focus-width color-mix(in srgb, var(--#{$prefix}danger) #{percentage($input-btn-focus-color-opacity)}, transparent),
|
||||
'border-color': var(--#{$prefix}form-invalid-border-color),
|
||||
),
|
||||
) !default;
|
||||
|
||||
@@ -1,184 +1,203 @@
|
||||
// Geist Sans Font Family
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Thin.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Thin.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-UltraLight.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraLight.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Light.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Light.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Regular.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Regular.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Medium.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Medium.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-SemiBold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-SemiBold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Bold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Bold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Black.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Black.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Black.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Black.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-UltraBlack.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraBlack.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraBlack.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-UltraBlack.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
// Geist Sans Variable Font
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('#{$assets-base}/fonts/geist-sans/Geist-Variable.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Variable.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-sans/Geist-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
// Geist Mono Font Family
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Thin.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Thin.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraLight.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraLight.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Light.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Light.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Regular.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Regular.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Medium.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Medium.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-SemiBold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-SemiBold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Bold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Bold.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Black.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Black.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Black.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Black.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraBlack.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraBlack.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraBlack.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-UltraBlack.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
// Geist Mono Variable Font
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('#{$assets-base}/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-family: 'Geist Mono';
|
||||
src:
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2'),
|
||||
url('#{$assets-base}/fonts/geist-mono/GeistMono-Variable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
content: '';
|
||||
}
|
||||
|
||||
>* {
|
||||
> * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
@@ -141,4 +141,4 @@
|
||||
outline: 0;
|
||||
// By default, there is no `--bs-focus-ring-x`, `--bs-focus-ring-y`, or `--bs-focus-ring-blur`, but we provide CSS variables with fallbacks to initial `0` values
|
||||
box-shadow: var(--#{$prefix}focus-ring-x, 0) var(--#{$prefix}focus-ring-y, 0) var(--#{$prefix}focus-ring-blur, 0) var(--#{$prefix}focus-ring-width) var(--#{$prefix}focus-ring-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
.nav-item.active:after {
|
||||
border-bottom-width: 0;
|
||||
border-inline-start-width: 3px;
|
||||
inset-inline-end: auto;
|
||||
inset-inline-end: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ Navbar
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
inset-inline-end: 0.5rem;
|
||||
inset-inline-end: 0.5rem;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
}
|
||||
@@ -151,8 +151,8 @@ Navbar
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: -0.25rem;
|
||||
border: 0 var(--#{$prefix}border-style) var(--#{$prefix}navbar-active-border-color);
|
||||
border-bottom-width: 2px;
|
||||
@@ -235,7 +235,7 @@ Navbar toggler
|
||||
border-radius: inherit;
|
||||
background: inherit;
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
@include transition(inherit);
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ Navbar vertical
|
||||
width: $sidebar-width;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
z-index: $zindex-fixed;
|
||||
align-items: start;
|
||||
@@ -323,8 +323,8 @@ Navbar vertical
|
||||
|
||||
&.navbar-right,
|
||||
&.navbar-end {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -384,8 +384,8 @@ Navbar vertical
|
||||
height: $navbar-overlap-height;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
background: inherit;
|
||||
z-index: -1;
|
||||
box-shadow: inherit;
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
background-image: $overlay-gradient;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
--#{$prefix}border-color-translucent: #{$border-color-translucent};
|
||||
--#{$prefix}border-dark-color: #{$border-dark-color};
|
||||
--#{$prefix}border-dark-color-translucent: #{$border-dark-color-translucent};
|
||||
--#{$prefix}border-light-color: #{$border-light-color};
|
||||
--#{$prefix}border-light-color-translucent: #{$border-light-color-translucent};
|
||||
--#{$prefix}border-active-color: #{$border-active-color};
|
||||
--#{$prefix}border-active-color-translucent: #{$border-active-color-translucent};
|
||||
|
||||
--#{$prefix}icon-color: #{$icon-color};
|
||||
|
||||
|
||||
@@ -144,12 +144,12 @@
|
||||
// stylelint-disable scss/dollar-variable-pattern
|
||||
@function rgba-css-var($identifier, $target) {
|
||||
@if $identifier == 'body' and $target == 'bg' {
|
||||
@return rgba(var(--#{$prefix}#{$identifier}-bg-rgb), var(--#{$prefix}#{$target}-opacity));
|
||||
@return color-mix(in srgb, var(--#{$prefix}#{$identifier}-bg) calc(var(--#{$prefix}#{$target}-opacity) * 100%), transparent);
|
||||
}
|
||||
@if $identifier == 'body' and $target == 'text' {
|
||||
@return rgba(var(--#{$prefix}#{$identifier}-color-rgb), var(--#{$prefix}#{$target}-opacity));
|
||||
@return color-mix(in srgb, var(--#{$prefix}#{$identifier}-color) calc(var(--#{$prefix}#{$target}-opacity) * 100%), transparent);
|
||||
} @else {
|
||||
@return rgba(var(--#{$prefix}#{$identifier}-rgb), var(--#{$prefix}#{$target}-opacity));
|
||||
@return color-mix(in srgb, var(--#{$prefix}#{$identifier}) calc(var(--#{$prefix}#{$target}-opacity) * 100%), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +59,10 @@
|
||||
|
||||
@mixin focus-ring($show-border: false) {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 $focus-ring-blur $focus-ring-width rgba(var(--#{$prefix}primary-rgb), 0.25);
|
||||
box-shadow: 0 0 $focus-ring-blur $focus-ring-width color-transparent(var(--#{$prefix}primary), 0.25);
|
||||
|
||||
@if ($show-border) {
|
||||
border-color: rgba(var(--#{$prefix}primary-rgb), 0.25);
|
||||
border-color: color-transparent(var(--#{$prefix}primary), 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,17 +15,17 @@
|
||||
}
|
||||
|
||||
[data-bs-theme-base='gray'] {
|
||||
--#{$prefix}gray-50: #f9fafb;
|
||||
--#{$prefix}gray-100: #f3f4f6;
|
||||
--#{$prefix}gray-200: #e5e7eb;
|
||||
--#{$prefix}gray-300: #d1d5db;
|
||||
--#{$prefix}gray-400: #9ca3af;
|
||||
--#{$prefix}gray-500: #6b7280;
|
||||
--#{$prefix}gray-600: #4b5563;
|
||||
--#{$prefix}gray-700: #374151;
|
||||
--#{$prefix}gray-800: #1f2937;
|
||||
--#{$prefix}gray-900: #111827;
|
||||
--#{$prefix}gray-950: #030712;
|
||||
--#{$prefix}gray-50: $gray-50;
|
||||
--#{$prefix}gray-100: $gray-100;
|
||||
--#{$prefix}gray-200: $gray-200;
|
||||
--#{$prefix}gray-300: $gray-300;
|
||||
--#{$prefix}gray-400: $gray-400;
|
||||
--#{$prefix}gray-500: $gray-500;
|
||||
--#{$prefix}gray-600: $gray-600;
|
||||
--#{$prefix}gray-700: $gray-700;
|
||||
--#{$prefix}gray-800: $gray-800;
|
||||
--#{$prefix}gray-900: $gray-900;
|
||||
--#{$prefix}gray-950: $gray-950;
|
||||
}
|
||||
|
||||
[data-bs-theme-base='zinc'] {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
.alert {
|
||||
--#{$prefix}alert-color: var(--#{$prefix}body-color);
|
||||
--#{$prefix}alert-bg: #{color-transparent(var(--#{$prefix}alert-color), 0.1)};
|
||||
--#{$prefix}alert-variant-color: var(--#{$prefix}body-color);
|
||||
--#{$prefix}alert-color: var(--#{$prefix}alert-variant-color);
|
||||
--#{$prefix}alert-bg: #{color-transparent(var(--#{$prefix}alert-variant-color), 0.16, var(--#{$prefix}bg-surface))};
|
||||
--#{$prefix}alert-padding-x: #{$alert-padding-x};
|
||||
--#{$prefix}alert-padding-y: #{$alert-padding-y};
|
||||
--#{$prefix}alert-margin-bottom: #{$alert-margin-bottom};
|
||||
--#{$prefix}alert-border-color: #{color-transparent(var(--#{$prefix}alert-color), 0.2)};
|
||||
--#{$prefix}alert-border-color: #{color-transparent(var(--#{$prefix}alert-variant-color), 0.2, var(--#{$prefix}bg-surface))};
|
||||
--#{$prefix}alert-border-color: var(--#{$prefix}border-color);
|
||||
--#{$prefix}alert-border: var(--#{$prefix}border-width) solid var(--#{$prefix}alert-border-color);
|
||||
--#{$prefix}alert-border-radius: var(--#{$prefix}border-radius);
|
||||
--#{$prefix}alert-link-color: inherit;
|
||||
@@ -16,6 +18,8 @@
|
||||
background-color: color-mix(in srgb, var(--#{$prefix}alert-bg), var(--#{$prefix}bg-surface));
|
||||
border-radius: var(--#{$prefix}alert-border-radius);
|
||||
border: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}alert-border-color);
|
||||
box-shadow: var(--#{$prefix}box-shadow);
|
||||
color: var(--#{$prefix}alert-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
@@ -66,15 +70,14 @@
|
||||
.btn-close {
|
||||
position: absolute;
|
||||
top: calc(var(--#{$prefix}alert-padding-x) / 2 - 1px);
|
||||
inset-inline-end: calc(var(--#{$prefix}alert-padding-y) / 2 - 1px);
|
||||
inset-inline-end: calc(var(--#{$prefix}alert-padding-y) / 2 - 1px);
|
||||
z-index: 1;
|
||||
padding: calc(var(--#{$prefix}alert-padding-y) * 1.25) var(--#{$prefix}alert-padding-x);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-important {
|
||||
border-color: var(--#{$prefix}alert-color);
|
||||
background-color: var(--#{$prefix}alert-color);
|
||||
background-color: var(--#{$prefix}alert-variant-color);
|
||||
color: var(--#{$prefix}white);
|
||||
|
||||
.alert-description {
|
||||
@@ -93,6 +96,6 @@
|
||||
|
||||
@each $name, $color in $theme-colors {
|
||||
.alert-#{$name} {
|
||||
--#{$prefix}alert-color: var(--#{$prefix}#{$name});
|
||||
--#{$prefix}alert-variant-color: var(--#{$prefix}#{$name});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
--#{$prefix}avatar-font-size: #{$avatar-font-size};
|
||||
--#{$prefix}avatar-icon-size: #{$avatar-icon-size};
|
||||
--#{$prefix}avatar-brand-size: #{$avatar-brand-size};
|
||||
--#{$prefix}avatar-border-radius: #{$avatar-border-radius};
|
||||
position: relative;
|
||||
width: var(--#{$prefix}avatar-size);
|
||||
height: var(--#{$prefix}avatar-size);
|
||||
@@ -24,7 +25,7 @@
|
||||
vertical-align: bottom;
|
||||
user-select: none;
|
||||
background: var(--#{$prefix}avatar-bg) no-repeat center/cover;
|
||||
border-radius: $avatar-border-radius;
|
||||
border-radius: var(--#{$prefix}avatar-border-radius);
|
||||
box-shadow: var(--#{$prefix}avatar-box-shadow);
|
||||
transition:
|
||||
color $transition-time,
|
||||
@@ -38,7 +39,7 @@
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 0;
|
||||
border-radius: $border-radius-pill;
|
||||
box-shadow: 0 0 0 calc(var(--#{$prefix}avatar-status-size) / 4) $card-bg;
|
||||
@@ -58,6 +59,10 @@
|
||||
border-radius: $border-radius-pill;
|
||||
}
|
||||
|
||||
.avatar-square {
|
||||
border-radius: var(--#{$prefix}border-radius);
|
||||
}
|
||||
|
||||
@each $avatar-size, $size in $avatar-sizes {
|
||||
.avatar-#{$avatar-size} {
|
||||
--#{$prefix}avatar-size: #{map.get($size, size)};
|
||||
@@ -66,14 +71,14 @@
|
||||
--#{$prefix}avatar-icon-size: #{map.get($size, icon-size)};
|
||||
--#{$prefix}avatar-brand-size: #{map.get($size, brand-size)};
|
||||
|
||||
@if map.has-key($size, border-radius) {
|
||||
border-radius: map.get($size, border-radius);
|
||||
}
|
||||
|
||||
.badge:empty {
|
||||
width: map.get($size, status-size);
|
||||
height: map.get($size, status-size);
|
||||
}
|
||||
|
||||
&.avatar-square {
|
||||
--#{$prefix}avatar-border-radius: #{map.get($size, border-radius)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,10 +101,13 @@
|
||||
--#{$prefix}list-gap: 0;
|
||||
|
||||
.avatar {
|
||||
margin-inline-end: calc(#{$avatar-list-spacing} * var(--#{$prefix}avatar-size)) !important;
|
||||
box-shadow:
|
||||
var(--#{$prefix}avatar-box-shadow),
|
||||
0 0 0 2px var(--#{$prefix}card-bg, var(--#{$prefix}bg-surface));
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-inline-start: calc(#{$avatar-list-spacing} * var(--#{$prefix}avatar-size)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@
|
||||
//
|
||||
// Button color variations
|
||||
//
|
||||
.btn-ghost {
|
||||
--#{$prefix}btn-bg: transparent;
|
||||
--#{$prefix}btn-border-color: transparent;
|
||||
--#{$prefix}btn-box-shadow: none;
|
||||
--#{$prefix}btn-hover-bg: var(--#{$prefix}bg-surface-secondary);
|
||||
--#{$prefix}btn-hover-border-color: transparent;
|
||||
--#{$prefix}btn-hover-color: var(--#{$prefix}body-color);
|
||||
}
|
||||
|
||||
@each $color, $value in map.merge($theme-colors, $social-colors) {
|
||||
.btn-#{$color} {
|
||||
@if $color == 'dark' {
|
||||
@@ -114,15 +123,6 @@
|
||||
--#{$prefix}btn-disabled-border-color: var(--#{$prefix}#{$color});
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
--#{$prefix}btn-bg: transparent;
|
||||
--#{$prefix}btn-border-color: transparent;
|
||||
--#{$prefix}btn-box-shadow: none;
|
||||
--#{$prefix}btn-hover-bg: var(--#{$prefix}bg-surface-secondary);
|
||||
--#{$prefix}btn-hover-border-color: transparent;
|
||||
--#{$prefix}btn-hover-color: var(--#{$prefix}body-color);
|
||||
}
|
||||
|
||||
.btn-ghost-#{$color},
|
||||
.btn-ghost.btn-#{$color} {
|
||||
--#{$prefix}btn-color: var(--#{$prefix}#{$color});
|
||||
@@ -250,7 +250,7 @@
|
||||
position: absolute;
|
||||
width: var(--#{$prefix}btn-icon-size);
|
||||
height: var(--#{$prefix}btn-icon-size);
|
||||
inset-inline-start: calc(50% - var(--#{$prefix}btn-icon-size) / 2);
|
||||
inset-inline-start: calc(50% - var(--#{$prefix}btn-icon-size) / 2);
|
||||
top: calc(50% - var(--#{$prefix}btn-icon-size) / 2);
|
||||
animation: spinner-border 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
height: 1.4rem;
|
||||
content: '';
|
||||
background: rgba(var(--#{$prefix}primary-rgb), 0.1);
|
||||
background: color-transparent(var(--#{$prefix}primary), 0.1);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -95,10 +95,10 @@
|
||||
}
|
||||
|
||||
&.range-start:before {
|
||||
inset-inline-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
}
|
||||
|
||||
&.range-end:before {
|
||||
inset-inline-end: 50%;
|
||||
inset-inline-end: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
// Card borderless
|
||||
.card-borderless {
|
||||
|
||||
&,
|
||||
.card-header,
|
||||
.card-footer {
|
||||
@@ -48,6 +47,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Card dashed
|
||||
.card-dashed {
|
||||
border: var(--#{$prefix}border-width) dashed var(--#{$prefix}border-color);
|
||||
}
|
||||
|
||||
// Card transparent
|
||||
.card-transparent {
|
||||
background: transparent;
|
||||
border: var(--#{$prefix}border-width) dashed var(--#{$prefix}border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// Card stamp
|
||||
.card-stamp {
|
||||
--#{$prefix}stamp-size: 7rem;
|
||||
@@ -141,7 +152,7 @@
|
||||
background: $active-bg;
|
||||
}
|
||||
|
||||
&+& {
|
||||
& + & {
|
||||
border-inline-start: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color);
|
||||
}
|
||||
}
|
||||
@@ -340,17 +351,17 @@ Stacked card
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-sm>& {
|
||||
.card-sm > & {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-md>& {
|
||||
.card-md > & {
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-lg>& {
|
||||
.card-lg > & {
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -364,7 +375,7 @@ Stacked card
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&+& {
|
||||
& + & {
|
||||
border-top: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color);
|
||||
}
|
||||
}
|
||||
@@ -427,7 +438,6 @@ Card table
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
tr {
|
||||
|
||||
td,
|
||||
th {
|
||||
&:first-child {
|
||||
@@ -456,11 +466,11 @@ Card table
|
||||
tfoot {
|
||||
&:last-child {
|
||||
tr:last-child {
|
||||
>*:last-child {
|
||||
> *:last-child {
|
||||
border-end-end-radius: calc(var(--#{$prefix}card-border-radius) - var(--#{$prefix}card-border-width));
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
> *:first-child {
|
||||
border-end-start-radius: calc(var(--#{$prefix}card-border-radius) - var(--#{$prefix}card-border-width));
|
||||
}
|
||||
}
|
||||
@@ -496,7 +506,7 @@ Card table
|
||||
}
|
||||
}
|
||||
|
||||
.card-body+& {
|
||||
.card-body + & {
|
||||
border-top: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}table-border-color);
|
||||
}
|
||||
}
|
||||
@@ -541,7 +551,7 @@ Card avatar
|
||||
Card list group
|
||||
*/
|
||||
.card-list-group {
|
||||
.card-body+& {
|
||||
.card-body + & {
|
||||
border-top: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color);
|
||||
}
|
||||
|
||||
@@ -600,7 +610,7 @@ Card list group
|
||||
}
|
||||
}
|
||||
|
||||
+.nav-item {
|
||||
+ .nav-item {
|
||||
margin-inline-start: calc(-1 * #{$card-border-width});
|
||||
}
|
||||
}
|
||||
@@ -640,7 +650,7 @@ Card list group
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
.nav-tabs+.tab-content .card {
|
||||
.nav-tabs + .tab-content .card {
|
||||
border-end-start-radius: var(--#{$prefix}card-border-radius);
|
||||
border-start-start-radius: 0;
|
||||
}
|
||||
@@ -654,7 +664,6 @@ Card note
|
||||
--#{$prefix}card-border-color: #fff1c9;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Card gradient
|
||||
*/
|
||||
@@ -663,10 +672,10 @@ Card gradient
|
||||
--#{$prefix}card-gradient-opacity: 86%;
|
||||
--#{$prefix}card-gradient: var(--tblr-primary), var(--tblr-primary);
|
||||
|
||||
background: radial-gradient(ellipse at center, var(--#{$prefix}card-bg) 0%, color-mix(in srgb, var(--#{$prefix}card-bg) 0%, transparent) 80%) border-box,
|
||||
linear-gradient(var(--#{$prefix}card-gradient-direction), color-mix(in srgb, var(--#{$prefix}card-bg) var(--#{$prefix}card-gradient-opacity), transparent) 0%, var(--#{$prefix}card-bg) 40%) border-box,
|
||||
linear-gradient(calc(270deg + var(--#{$prefix}card-gradient-direction)), var(--#{$prefix}card-gradient)) border-box;
|
||||
|
||||
background:
|
||||
radial-gradient(ellipse at center, var(--#{$prefix}card-bg) 0%, color-mix(in srgb, var(--#{$prefix}card-bg) 0%, transparent) 80%) border-box,
|
||||
linear-gradient(var(--#{$prefix}card-gradient-direction), color-mix(in srgb, var(--#{$prefix}card-bg) var(--#{$prefix}card-gradient-opacity), transparent) 0%, var(--#{$prefix}card-bg) 40%) border-box,
|
||||
linear-gradient(calc(270deg + var(--#{$prefix}card-gradient-direction)), var(--#{$prefix}card-gradient)) border-box;
|
||||
}
|
||||
|
||||
@each $name, $color in map.merge($colors, $theme-colors) {
|
||||
@@ -676,14 +685,7 @@ Card gradient
|
||||
}
|
||||
|
||||
.card-gradient-rainbow {
|
||||
--#{$prefix}card-gradient: #78C5D6,
|
||||
#459BA8,
|
||||
#79C267,
|
||||
#C5D647,
|
||||
#F5D63D,
|
||||
#F08B33,
|
||||
#E868A2,
|
||||
#BE61A5;
|
||||
--#{$prefix}card-gradient: #78c5d6, #459ba8, #79c267, #c5d647, #f5d63d, #f08b33, #e868a2, #be61a5;
|
||||
}
|
||||
|
||||
.card-gradient-sun {
|
||||
@@ -695,7 +697,7 @@ Card gradient
|
||||
}
|
||||
|
||||
.card-gradient-ocean {
|
||||
--#{$prefix}card-gradient: #1CB5E0, #000851;
|
||||
--#{$prefix}card-gradient: #1cb5e0, #000851;
|
||||
}
|
||||
|
||||
.card-gradient-mellow {
|
||||
@@ -703,7 +705,7 @@ Card gradient
|
||||
}
|
||||
|
||||
.card-gradient-disco {
|
||||
--#{$prefix}card-gradient: #FC466B, #3F5EFB;
|
||||
--#{$prefix}card-gradient: #fc466b, #3f5efb;
|
||||
}
|
||||
|
||||
.card-gradient-psychedelic {
|
||||
@@ -715,7 +717,7 @@ Card gradient
|
||||
}
|
||||
|
||||
.card-gradient-gold {
|
||||
--#{$prefix}card-gradient: #9d4100, #bf7122, #f59f00, #FFD700;
|
||||
--#{$prefix}card-gradient: #9d4100, #bf7122, #f59f00, #ffd700;
|
||||
}
|
||||
|
||||
.card-gradient-animated {
|
||||
@@ -732,4 +734,4 @@ Card gradient
|
||||
|
||||
.card-gradient-start {
|
||||
--#{$prefix}card-gradient-direction: 90deg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -0.25rem;
|
||||
inset-inline-start: 0.75rem;
|
||||
inset-inline-start: 0.75rem;
|
||||
display: block;
|
||||
background: inherit;
|
||||
width: 14px;
|
||||
@@ -90,8 +90,8 @@
|
||||
|
||||
&.dropdown-menu-end {
|
||||
&:before {
|
||||
inset-inline-end: 0.75rem;
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 0.75rem;
|
||||
inset-inline-start: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: '';
|
||||
@@ -33,8 +33,8 @@ Dimmer
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
display: none;
|
||||
margin: 0 auto;
|
||||
transform: translateY(-50%);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> .btn-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-end: 0;
|
||||
width: $modal-header-height;
|
||||
height: $modal-header-height;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@keyframes progress-indeterminate {
|
||||
0% {
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-start: -35%;
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-start: -35%;
|
||||
}
|
||||
|
||||
100%,
|
||||
60% {
|
||||
inset-inline-end: -90%;
|
||||
inset-inline-start: 100%;
|
||||
inset-inline-end: -90%;
|
||||
inset-inline-start: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ Progress bar
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
content: '';
|
||||
background-color: inherit;
|
||||
will-change: left, right;
|
||||
@@ -93,6 +93,8 @@ Progressbg
|
||||
.progressbg-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: 100%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
&.bg-#{$color}-lt {
|
||||
border-color: rgba(var(--#{$prefix}#{$color}-rgb), 0.1) !important;
|
||||
border-color: color-transparent(var(--#{$prefix}#{$color}), 0.1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
&:before {
|
||||
top: 0;
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-end: 100%;
|
||||
bottom: auto;
|
||||
border-color: inherit;
|
||||
border-top-color: transparent;
|
||||
@@ -73,13 +73,13 @@
|
||||
}
|
||||
|
||||
&.ribbon-start {
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 0.75rem;
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 0.75rem;
|
||||
|
||||
&:before {
|
||||
top: 0;
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-start: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@
|
||||
&:before {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
border-color: inherit;
|
||||
border-top-color: transparent;
|
||||
border-inline-start-color: transparent;
|
||||
@@ -111,7 +111,7 @@
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-end: 100%;
|
||||
inset-inline-end: 100%;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
@@ -127,8 +127,8 @@
|
||||
padding-inline-end: 0.5rem;
|
||||
|
||||
&:after {
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 100%;
|
||||
inset-inline-end: auto;
|
||||
inset-inline-start: 100%;
|
||||
border-inline-end-color: transparent;
|
||||
|
||||
border-inline-end-width: 0.5rem;
|
||||
@@ -144,8 +144,8 @@
|
||||
|
||||
&:after {
|
||||
top: 100%;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: 0;
|
||||
border-color: inherit;
|
||||
border-width: 1rem;
|
||||
border-top-width: 0;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
padding: 0.25rem 0.75rem;
|
||||
gap: 0.5rem;
|
||||
color: var(--#{$prefix}status-color);
|
||||
background: rgba(var(--#{$prefix}status-color-rgb), 0.1);
|
||||
background: color-transparent(var(--#{$prefix}status-color), 0.1);
|
||||
font-size: $font-size-base;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
&:not(:last-child):after {
|
||||
position: absolute;
|
||||
inset-inline-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
width: 100%;
|
||||
content: '';
|
||||
transform: translateY(-50%);
|
||||
@@ -67,7 +67,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
z-index: 1;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
@@ -137,7 +137,7 @@
|
||||
|
||||
&:before {
|
||||
top: var(--#{$prefix}steps-dot-offset);
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
content: '';
|
||||
transform: translateX(-50%);
|
||||
top: var(--#{$prefix}steps-dot-offset);
|
||||
inset-inline-start: calc(var(--#{$prefix}steps-dot-size) * 0.5);
|
||||
inset-inline-start: calc(var(--#{$prefix}steps-dot-size) * 0.5);
|
||||
width: var(--#{$prefix}steps-border-width);
|
||||
height: calc(100% + 1rem);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
.switch-icon-b {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-start: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: var(--#{$prefix}timeline-icon-size);
|
||||
inset-inline-start: calc(var(--#{$prefix}timeline-icon-size) / 2);
|
||||
inset-inline-start: calc(var(--#{$prefix}timeline-icon-size) / 2);
|
||||
bottom: calc(-1 * var(--#{$prefix}page-padding));
|
||||
width: var(--#{$prefix}border-width);
|
||||
background-color: var(--#{$prefix}border-color);
|
||||
|
||||
@@ -82,4 +82,4 @@ Form switch
|
||||
.form-check-label {
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user