mirror of
https://github.com/tabler/tabler.git
synced 2026-08-05 19:03:18 +04:00
Merge branch 'dev' of https://github.com/tabler/tabler into goals-page
This commit is contained in:
@@ -57,8 +57,8 @@ Optional: `bootstrapLink: components/alerts/` when the component extends a Boots
|
||||
Import shared components after the frontmatter and wrap previews in `Example`:
|
||||
|
||||
```mdx
|
||||
import Example from '@shared/components/docs/Example.astro';
|
||||
import Alert from '@shared/components/Alert.astro';
|
||||
import Example from '@components/Example.astro';
|
||||
import Alert from '@ui/Alert.astro';
|
||||
|
||||
<Example>
|
||||
<Alert type="success" title="Wow!" description="Your account has been saved!" />
|
||||
|
||||
@@ -14,8 +14,8 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
- `core/` — `@tabler/core`: SCSS sources (`core/scss/`), JS, the distributable framework.
|
||||
- `preview/` — `@tabler/preview`: demo site built with Astro (pages in `preview/pages/*.astro`).
|
||||
- `docs/` — `@tabler/docs`: documentation site built with Astro (pages in `docs/pages/**/*.mdx`); docs-specific rules: `.agents/rules/docs.mdc`.
|
||||
- `shared/astro/` — Astro components, layouts, and lib shared by `preview` and `docs` (imported via the `@shared` alias).
|
||||
- `shared/data/` — JSON data used by pages (imported via the `@data` alias).
|
||||
- `shared/` — Astro components (`ui/`, `components/`, `layouts/`, `lib/`) shared by `preview` and `docs` via `@shared`; JSON data in `data/` via `@data`; plus `static/`.
|
||||
- `docs/components/` — docs-only Astro components (`Example`, menu, …) via `@components`.
|
||||
- Package manager: pnpm workspaces + turbo. Node.js >= 22.12.
|
||||
|
||||
## Language
|
||||
@@ -27,8 +27,8 @@ Shared instructions for all AI agents (Claude Code, Cursor, etc.). Canonical age
|
||||
|
||||
### Astro pages and components
|
||||
|
||||
- Pages are Astro components; reusable markup lives in `shared/astro/components/`.
|
||||
- Icons: use the shared component — `<Icon name="home" />` (import from `@shared/components/Icon.astro`).
|
||||
- Pages are Astro components; reusable markup lives in `shared/components/`.
|
||||
- Icons: use the shared component — `<Icon name="home" />` (import from `@ui/Icon.astro`).
|
||||
- Links between preview pages are relative: `./job-listing.html` (root-level pages).
|
||||
- Boolean HTML attributes in Astro: `selected={true}` renders a bare attribute, but some attributes (e.g. `multiple`) render `="true"` — use `multiple ? '' : undefined` when a bare attribute is required.
|
||||
|
||||
|
||||
@@ -38,7 +38,15 @@ From the repository root:
|
||||
|
||||
Use this to infer **intent**, **user-visible behavior**, and **risk**—not only filenames.
|
||||
|
||||
**Vercel preview URL:** after the branch is pushed, Vercel deploys a preview. Build the link from the branch name:
|
||||
**Existing PR content:** Before drafting, check whether a PR already exists for this branch (e.g. `gh pr view --json title,body,number,url` for the current branch, or a PR number/URL the user gave you). If one exists, read its current title and body first:
|
||||
|
||||
- Carry over any issue references it already contains — `Closes #N`, `Fixes #N`, `Resolves #N`, or a plain `#N` mention — into the new body. Put them in **Notes / rollout** (or, if the PR uses a dedicated `Issue`/`Closes` line, keep that same convention) so a regenerated description never silently drops the link to a tracked issue.
|
||||
- Don't assume the diff alone tells you which issue this closes — the existing PR body is often the only place that link is recorded.
|
||||
- If no PR exists yet, skip this step (there is nothing to carry over).
|
||||
|
||||
**Vercel preview URL:** only when the diff has **visual / UI changes** a reviewer can check in the browser (CSS, preview pages, docs examples, components, layout). Skip the preview URL for non-visual work (agent skills, CI, tooling, docs prose-only, lockfile, config with no rendered UI effect).
|
||||
|
||||
When a preview URL is needed and the branch is pushed, Vercel deploys a preview. Build the link from the branch name:
|
||||
|
||||
1. Take the current branch name from `git branch --show-current`.
|
||||
2. Replace `/` with `-`, **remove dots entirely** (do not replace them with dashes), and use lowercase (e.g. `feature/pricing-banner` → `feature-pricing-banner`, `update-icons-3.45.0` → `update-icons-3450`).
|
||||
@@ -67,7 +75,7 @@ Remove `money-back-guarantee` flag; show label on paid plans
|
||||
|
||||
## 4. Body (markdown template)
|
||||
|
||||
Output the body in a **second** fenced **`markdown`** block after the title block. Use this structure inside that block. Omit **Notes / rollout** if nothing applies. Do **not** add a separate `## Test plan` section unless the user explicitly asks for one—use **Preview** instead.
|
||||
Output the body in a **second** fenced **`markdown`** block after the title block. Use this structure inside that block. Omit **Notes / rollout** if nothing applies. Omit the **URL** line (or the whole **Preview** section) when there are no visual changes—do not link a Vercel preview in that case. Do **not** add a separate `## Test plan` section unless the user explicitly asks for one—use **Preview** (or a short “how to review” note under Summary) instead.
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
@@ -86,9 +94,9 @@ Output the body in a **second** fenced **`markdown`** block after the title bloc
|
||||
|
||||
**Summary bullets:** 1–4 bullets tying changes to product/engineering impact.
|
||||
|
||||
**Preview:** Always include when the branch is pushed (or note that preview is unavailable until push). Use the Vercel URL format from §2. Link the most relevant path(s)—homepage only if changes are global; otherwise deep-link (e.g. `/pricing`, `/blog/…`). **How to test** should be actionable: which page, which UI element or behavior changed, and what the reviewer should expect to see.
|
||||
**Preview:** Include a Vercel **URL** only when the change is visual and the branch is pushed (or note that preview is unavailable until push). If there are no visual changes, omit the preview link entirely—do not add a homepage or generic preview URL. When a URL is included, use the Vercel format from §2 and deep-link the most relevant path(s)—homepage only if changes are global. **How to test** should be actionable: which page, which UI element or behavior changed, and what the reviewer should expect to see. For non-visual PRs, say how to review the diff instead (e.g. which files to read).
|
||||
|
||||
**Notes:** Feature flags, env vars, backwards compatibility—only when evidenced in the diff or commit messages.
|
||||
**Notes:** Feature flags, env vars, backwards compatibility—only when evidenced in the diff or commit messages. Also include any issue reference carried over from an existing PR (see §2), e.g. `Closes #123`.
|
||||
|
||||
## 5. Language (simple English)
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
name: write-docs
|
||||
description: Write or update Tabler documentation pages in simple English using the current docs schema. Use when the user asks to create docs, edit docs, add new feature docs to an existing page, or standardize docs structure across any docs category.
|
||||
disable-model-invocation: true
|
||||
description: Write, update, or suggest Tabler documentation pages in simple English using the current docs schema. Use whenever the user asks to create docs, edit docs, add new feature docs to an existing page, or standardize docs structure across any docs category. Also consult this skill proactively — without being asked — whenever a new visual UI component (shared/ui/*.astro), plugin, or other user-facing feature has just been added or changed and has no matching, up-to-date page under docs/pages/**, since undocumented components are easy to forget about.
|
||||
---
|
||||
|
||||
# Write Tabler Docs
|
||||
|
||||
Write docs directly in files (do not stop at draft mode) and follow the current Tabler documentation schema.
|
||||
Follow the current Tabler documentation schema. How far to go depends on how the skill was triggered — see section 1.
|
||||
|
||||
## 1. Scope and behavior
|
||||
|
||||
@@ -15,6 +14,11 @@ Write docs directly in files (do not stop at draft mode) and follow the current
|
||||
- Create new pages when needed.
|
||||
- Use simple English in all prose.
|
||||
|
||||
### Direct request vs. proactive suggestion
|
||||
|
||||
- If the user explicitly asked for docs (e.g. "document this", "write docs for X", "update the badge page"), write directly in the files — do not stop at draft mode.
|
||||
- If this skill triggered on its own because something was built or changed without matching docs, don't start editing files unprompted. Point out what's missing in one or two sentences (which component/plugin, which page it would live under) and ask whether to write it. Proceed to write directly, per the rule above, only once the user says go.
|
||||
|
||||
## 2. Required language style (simple English)
|
||||
|
||||
- Use short sentences and common words.
|
||||
@@ -66,7 +70,7 @@ Guidance by page type:
|
||||
For visual examples, use the shared `Example` component (import after the frontmatter):
|
||||
|
||||
```mdx
|
||||
import Example from '@shared/components/docs/Example.astro';
|
||||
import Example from '@components/Example.astro';
|
||||
|
||||
<Example>
|
||||
<button class="btn btn-primary">Primary button</button>
|
||||
@@ -76,32 +80,54 @@ import Example from '@shared/components/docs/Example.astro';
|
||||
- Add 1-2 short sentences before each preview block to explain what the preview shows.
|
||||
- Use props when useful: `hideCode`, `centered`, `vertical`, `raw`, `column`, `bg`, `height`, `codeOnly`.
|
||||
- For a cleaner displayed snippet than the rendered preview, pass `code={...}`.
|
||||
- For icons and other shared components inside examples, import them from `@shared/components/` (for example `<Icon name="plus" />`).
|
||||
- For icons and other shared components inside examples, import them from `@ui/` (for example `<Icon name="plus" />`).
|
||||
- Raw HTML in the `Example` slot is reserialized by MDX — keep markup lines attached to tags to avoid stray `<p>` wrapping.
|
||||
|
||||
## 6. Workflow for each request
|
||||
## 6. Analyzing the component or plugin source
|
||||
|
||||
1. Identify target file(s) in `docs/pages/**`.
|
||||
Prose and example markup are only as accurate as their source. Before writing examples, ground them in the actual implementation rather than in guesses or in what a similar-looking page happens to show — nearby `.mdx` pages are a good style reference but can drift out of sync with the code.
|
||||
|
||||
- **UI components** live at `shared/ui/<ComponentName>.astro`. Read its `interface Props` block: each field (and its JSDoc comment, when present) is the authoritative list of supported options, defaults, and edge cases — for example `shared/ui/Badge.astro` documents `scale`, `light`, `icon`, and `personId` this way.
|
||||
- **Plugins and utilities** are often CSS-class-driven rather than a single Astro component. Check `core/scss/**/_<name>.scss` (or `core/scss/tabler-<name>.scss`) for the class variants that actually exist, and `shared/data/<name>.json` when the plugin is data-driven (e.g. `flags`, `payments`, `social-icons`).
|
||||
- **Real usage** in `preview/pages/**` renders the finished HTML and often exercises more combinations in practice than the docs page does — a useful cross-check for realistic examples.
|
||||
- Turn what you find into `<Example>` blocks that reflect real, valid prop/class combinations rather than invented markup.
|
||||
|
||||
## 7. Registering new pages in the docs menu
|
||||
|
||||
`docs/pages/**` is not scanned automatically to build navigation — the sidebar tree is frozen in `shared/data/docs.json` (see the comment in `docs/components/DocsMenu.astro`). A new leaf page with no entry there exists but is unreachable from the docs site.
|
||||
|
||||
- Only touch `docs.json` for genuinely **new** pages. Editing an existing page needs no menu change.
|
||||
- Find the matching section in the `menu` array by directory: `docs/pages/ui/components/*` → the `"Components"` entry under `"Tabler UI"`, `docs/pages/ui/plugins/*` → `"Plugins"`, `docs/pages/icons/libraries/*` → `"Libraries"` under `"Tabler Icons"`, and so on — the section `title`/`url` pairs mirror the `docs/pages/ui/*` and `docs/pages/icons/*` subdirectory names.
|
||||
- Add `{ "title": "<Title Case name>", "url": "/<matching>/<slug>/" }` to that section's `children`, in the same alphabetical position its neighbors already follow.
|
||||
- The `title` and `url` must match the new page's frontmatter `title` and its file path exactly, or the sidebar entry will point at the wrong place.
|
||||
|
||||
## 8. Workflow for each request
|
||||
|
||||
1. Identify target file(s) in `docs/pages/**` (or determine none exist yet — see section 7).
|
||||
2. Read the target page and 2-3 nearby pages in the same category to match tone and conventions.
|
||||
3. Apply the schema from section 4.
|
||||
4. Write/update the page directly in file(s).
|
||||
5. Keep only required frontmatter by default.
|
||||
6. Ensure prose is in simple English.
|
||||
7. Verify heading hierarchy (`##` then `###`) and snippet validity.
|
||||
3. Read the underlying component/plugin source per section 6 so examples are accurate, not guessed.
|
||||
4. Apply the schema from section 4.
|
||||
5. Write/update the page directly in file(s).
|
||||
6. If the page is new, add its entry to `shared/data/docs.json` per section 7.
|
||||
7. Keep only required frontmatter by default.
|
||||
8. Ensure prose is in simple English.
|
||||
9. Verify heading hierarchy (`##` then `###`) and snippet validity.
|
||||
|
||||
## 7. Rules while updating existing docs
|
||||
## 9. Rules while updating existing docs
|
||||
|
||||
- Preserve valid existing content that is still correct.
|
||||
- Add new functionality docs as focused new subsection(s), usually under `Variants`, `Usage`, or `Examples`.
|
||||
- Remove or rewrite only conflicting or outdated text.
|
||||
- Keep naming and terms consistent across the page.
|
||||
|
||||
## 8. Quality checklist before finishing
|
||||
## 10. Quality checklist before finishing
|
||||
|
||||
- [ ] Uses simple English.
|
||||
- [ ] Examples reflect the real Props/classes found in the source (section 6), not guesses.
|
||||
- [ ] Frontmatter uses static YAML.
|
||||
- [ ] Default frontmatter contains only `title`, `summary`, `description`, `layout` (unless user requested extra keys).
|
||||
- [ ] Follows schema and heading hierarchy.
|
||||
- [ ] Examples use the `Example` component pattern where applicable.
|
||||
- [ ] Accessibility section exists for interactive UI docs.
|
||||
- [ ] New pages have a matching entry in `shared/data/docs.json` (section 7); edits to existing pages don't touch it.
|
||||
- [ ] No mention of changeset reminders unless user asks.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Single-pass CSS pipeline shared by @tabler/core and @tabler/preview.
|
||||
//
|
||||
// Replaces the `sass` + `postcss` + `cleancss` CLI chain: each output file used
|
||||
// to be written twice (sass first, then postcss rewriting it in place), which
|
||||
// briefly exposed un-prefixed CSS on disk and made file watchers (dev-server
|
||||
// live reload) fire once per write burst. Here everything runs in-process and
|
||||
// each output file is written exactly once, fully processed.
|
||||
//
|
||||
// All options mirror the replaced CLI invocations — the output was verified
|
||||
// byte-identical (css, rtl, min and their source maps), bar the deliberate
|
||||
// breaks:afterComment deviation documented on minify() below:
|
||||
// - sass --no-source-map --load-path=node_modules --style expanded
|
||||
// - postcss (autoprefixer cascade:false, rtlcss for --rtl, external map with
|
||||
// annotation + sourcesContent — the former core/.build/postcss.config.mjs)
|
||||
// - cleancss -O1 --format breakWith=lf --with-rebase --source-map
|
||||
// --source-map-inline-sources --batch --batch-suffix ".min"
|
||||
//
|
||||
// --banner replaces the former core/.build/add-banner.ts, which ran as a
|
||||
// separate step *after* minification and rewrote the finished files in place.
|
||||
// Prepending the 6-line license comment there shifted every rule down without
|
||||
// touching the already-written .map, so all mappings pointed 7 lines too high
|
||||
// (#2766). Here the banner is part of the css before any map is generated, so
|
||||
// postcss and clean-css both account for it.
|
||||
//
|
||||
// Usage: tsx ../.build/build-css.ts <scssDir> <outDir> [--rtl] [--minify] [--banner]
|
||||
// (cwd = the package)
|
||||
/// <reference path="./modules.d.ts" />
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { EOL } from 'node:os'
|
||||
import { basename, join, resolve } from 'node:path'
|
||||
import { compile as compileSass } from 'sass'
|
||||
import postcss, { type Result } from 'postcss'
|
||||
import autoprefixer from 'autoprefixer'
|
||||
import rtlcss from 'rtlcss'
|
||||
import CleanCSS from 'clean-css'
|
||||
import { addBanner } from '../shared/banner/index.mjs'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const flags = args.filter((arg) => arg.startsWith('--'))
|
||||
const [scssDir, outDir] = args.filter((arg) => !arg.startsWith('--'))
|
||||
if (!scssDir || !outDir) {
|
||||
console.error('usage: tsx build-css.ts <scssDir> <outDir> [--rtl] [--minify] [--banner]')
|
||||
process.exit(1)
|
||||
}
|
||||
const withRtl = flags.includes('--rtl')
|
||||
const withMinify = flags.includes('--minify')
|
||||
const withBanner = flags.includes('--banner')
|
||||
|
||||
const mapOptions = { inline: false, annotation: true, sourcesContent: true }
|
||||
const written: string[] = []
|
||||
const pendingWrites: { file: string; content: string }[] = []
|
||||
|
||||
// Skipping identical content keeps file watchers quiet for outputs a given scss
|
||||
// edit did not actually change (the dev servers full-reload on these writes).
|
||||
function queueWrite(file: string, content: string) {
|
||||
if (existsSync(file) && readFileSync(file, 'utf8') === content) return
|
||||
pendingWrites.push({ file, content })
|
||||
}
|
||||
|
||||
// sass + autoprefixer for one entry: scss/x.scss → outDir/x.css (+ .map)
|
||||
async function compile(entry: string): Promise<{ outFile: string; result: Result }> {
|
||||
const outFile = join(outDir, `${basename(entry, '.scss')}.css`)
|
||||
const compiled = compileSass(join(scssDir, entry), { loadPaths: ['node_modules'], style: 'expanded' })
|
||||
// The sass CLI ends its output files with a newline; the JS API's css string
|
||||
// does not. Keep the byte-identical CLI behavior (the annotation comment then
|
||||
// lands after a blank line, exactly like `postcss --replace` produced).
|
||||
const css = `${compiled.css}\n`
|
||||
// Banner goes in before postcss runs, so the map it generates already counts
|
||||
// the banner's lines — and so does the rtl map chained onto it below.
|
||||
const input = withBanner ? addBanner(css, outFile) : css
|
||||
const result = await postcss([autoprefixer({ cascade: false })]).process(input, {
|
||||
from: outFile,
|
||||
to: outFile,
|
||||
map: mapOptions,
|
||||
})
|
||||
queueWrite(outFile, result.css)
|
||||
if (result.map) queueWrite(`${outFile}.map`, result.map.toString())
|
||||
written.push(outFile)
|
||||
return { outFile, result }
|
||||
}
|
||||
|
||||
// rtlcss over the prefixed output: outDir/x.css → outDir/x.rtl.css (+ .map)
|
||||
async function rtl(entry: string, outFile: string, base: Result): Promise<void> {
|
||||
const rtlFile = join(outDir, `${basename(entry, '.scss')}.rtl.css`)
|
||||
// Same input the CLI read from disk: the prefixed css including its
|
||||
// sourceMappingURL annotation, so postcss picks up the previous map.
|
||||
// The previous map is not on disk yet (writes are buffered) — pass it in.
|
||||
const result = await postcss([autoprefixer({ cascade: false }), rtlcss()]).process(base.css, {
|
||||
from: outFile,
|
||||
to: rtlFile,
|
||||
map: { ...mapOptions, prev: base.map.toString() },
|
||||
})
|
||||
queueWrite(rtlFile, result.css)
|
||||
if (result.map) queueWrite(`${rtlFile}.map`, result.map.toString())
|
||||
written.push(rtlFile)
|
||||
}
|
||||
|
||||
// All compile work is done before the first write: compiles take seconds while
|
||||
// writes take milliseconds, so watchers see one tight burst instead of writes
|
||||
// spread across the whole build (which would need a long reload debounce).
|
||||
function flushWrites(): void {
|
||||
for (const { file, content } of pendingWrites) {
|
||||
writeFileSync(file, content)
|
||||
console.log(`build-css: ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
// clean-css over every produced file: outDir/x.css → outDir/x.min.css (+ .map).
|
||||
// Mirrors clean-css-cli's option coercion and batch output naming/annotation
|
||||
// (see the clean-css-cli package's index.js).
|
||||
//
|
||||
// breaks:afterComment is the one deviation: clean-css counts the lines a kept
|
||||
// `/*! … */` comment spans but not the `*/` it leaves on the last one, so every
|
||||
// mapping on that line comes out `'*/'.length` columns short. Ending the line
|
||||
// after the comment puts the css at column 0 — exactly where the map says.
|
||||
async function minify(files: string[]): Promise<void> {
|
||||
const minified = await new CleanCSS({
|
||||
batch: true,
|
||||
format: 'breakWith=lf;breaks:afterComment=on',
|
||||
inline: 'local',
|
||||
level: { 1: true },
|
||||
rebase: true,
|
||||
rebaseTo: resolve(outDir),
|
||||
returnPromise: true,
|
||||
sourceMap: true,
|
||||
sourceMapInlineSources: true,
|
||||
}).minify(files)
|
||||
for (const inputFile of files) {
|
||||
const fileResult = minified[inputFile]
|
||||
if (!fileResult) throw new Error(`build-css: no minify result for ${inputFile}`)
|
||||
if (fileResult.errors.length > 0) throw new Error(fileResult.errors.join('\n'))
|
||||
for (const warning of fileResult.warnings) console.warn(`build-css: ${warning}`)
|
||||
const minFile = inputFile.replace(/\.css$/, '.min.css')
|
||||
writeFileSync(minFile, `${fileResult.styles}${EOL}/*# sourceMappingURL=${basename(minFile)}.map */`)
|
||||
writeFileSync(`${minFile}.map`, fileResult.sourceMap.toString())
|
||||
console.log(`build-css: ${minFile}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapped in a function because tsx compiles root-level .ts as CJS, where
|
||||
// top-level await is unavailable.
|
||||
async function main() {
|
||||
const entries = readdirSync(scssDir).filter((file) => file.endsWith('.scss') && !file.startsWith('_'))
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const { outFile, result } = await compile(entry)
|
||||
if (withRtl) await rtl(entry, outFile, result)
|
||||
}
|
||||
flushWrites()
|
||||
if (withMinify) await minify(written)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
// Shared Astro integration that rebuilds a package's public/ directory from
|
||||
// source and generated workspace assets (the Eleventy passthrough-copy
|
||||
// equivalent), then keeps it in sync during dev. Used by @tabler/docs and
|
||||
// @tabler/preview — each passes its own manifest from astro.config.mjs.
|
||||
//
|
||||
// Runs inside Astro's own lifecycle (astro:config:done) instead of a standalone
|
||||
// pre-script, so the copy is ordered by Astro for both dev and build — mirrors
|
||||
// Bootstrap's site/src/libs/astro.ts integration. Cross-package ordering (core
|
||||
// before preview before docs) still comes from the turbo dev graph.
|
||||
import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs'
|
||||
import { dirname, join, relative, sep } from 'node:path'
|
||||
|
||||
// Structural stand-ins for the astro/vite types used here. The real `astro`
|
||||
// package is not resolvable from the repo root (it lives in docs/ and preview/
|
||||
// node_modules), and `astro check` verifies the produced object against the
|
||||
// real AstroIntegration type at the usage site in each astro.config.mjs anyway.
|
||||
interface Logger {
|
||||
info(message: string): void
|
||||
warn(message: string): void
|
||||
}
|
||||
interface DevServer {
|
||||
watcher: {
|
||||
add(paths: string[]): void
|
||||
on(event: string, callback: (file: string) => void): void
|
||||
}
|
||||
hot: { send(payload: { type: string }): void }
|
||||
}
|
||||
interface Integration {
|
||||
name: string
|
||||
hooks: {
|
||||
'astro:config:setup'?: (options: { command: string }) => void
|
||||
'astro:config:done'?: (options: { logger: Logger }) => void
|
||||
'astro:server:setup'?: (options: { server: DevServer; logger: Logger }) => void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A directory (or single file) to copy into publicDir. `requiredFile` guards
|
||||
* against a present-but-stale source; `required: false` only warns when the
|
||||
* source is missing; `allowDestinationFallback` keeps the existing destination
|
||||
* when the source disappears mid-copy (e.g. @tabler/core rebuilding
|
||||
* concurrently).
|
||||
*/
|
||||
interface CopyEntry {
|
||||
from: string
|
||||
to: string
|
||||
label: string
|
||||
required?: boolean
|
||||
requiredFile?: string
|
||||
allowDestinationFallback?: boolean
|
||||
}
|
||||
|
||||
interface CopyAssetsOptions {
|
||||
/** repo root, for log-friendly relative paths */
|
||||
repo: string
|
||||
/** the package's public/ directory (fully generated) */
|
||||
publicDir: string
|
||||
copies: CopyEntry[]
|
||||
/** generated source dirs to live-sync into publicDir while `astro dev` runs */
|
||||
syncDirs?: { from: string; to: string }[]
|
||||
/** dirs written to directly by watchers — already in place, only trigger a browser reload */
|
||||
reloadDirs?: string[]
|
||||
}
|
||||
|
||||
export function copyAssets({ repo, publicDir, copies, syncDirs = [], reloadDirs = [] }: CopyAssetsOptions): Integration {
|
||||
let command: string
|
||||
|
||||
function rebuildPublicDir(logger: Logger) {
|
||||
// Always start from a clean public/ (mirrors Bootstrap's own docs
|
||||
// integration): running this twice in a row, or without a prior
|
||||
// `pnpm run clean`, must never accumulate stale/nested content —
|
||||
// see preview/.build/vite.config.mts for the unbounded-growth story.
|
||||
rmSync(publicDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
||||
mkdirSync(publicDir, { recursive: true })
|
||||
|
||||
for (const { from, to, label, required = true, requiredFile, allowDestinationFallback } of copies) {
|
||||
if (!existsSync(from) || (requiredFile && !existsSync(requiredFile))) {
|
||||
const message = `copy-assets: missing ${requiredFile ?? from}`
|
||||
if (required) throw new Error(`${message} — build ${label} first`)
|
||||
logger.warn(`${message} (skipped — build ${label} to get it)`)
|
||||
continue
|
||||
}
|
||||
mkdirSync(dirname(to), { recursive: true })
|
||||
if (statSync(from).isFile()) {
|
||||
copyFileSync(from, to)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
// dereference: sources may be symlinks (e.g. preview/static → shared/static)
|
||||
cpSync(from, to, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: (src) => !src.includes('/.vscode') && !src.includes('\\.vscode'),
|
||||
})
|
||||
} catch (error) {
|
||||
// In turbo dev, @tabler/core can clean dist while we copy it.
|
||||
// If fallback is allowed and destination exists, keep current assets.
|
||||
if (allowDestinationFallback && error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT' && existsSync(to)) {
|
||||
logger.warn(`copy-assets: source changed during copy ${from} (using existing ${to})`)
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
logger.info('public/ rebuilt from workspace assets')
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'copy-assets',
|
||||
hooks: {
|
||||
'astro:config:setup': (options) => {
|
||||
command = options.command
|
||||
},
|
||||
'astro:config:done': ({ logger }) => {
|
||||
// `astro check`/`astro sync` (command 'sync') runs these hooks too, but
|
||||
// only needs types — public/ is irrelevant there and CI's type-check
|
||||
// job has no built workspace assets to copy.
|
||||
if (command === 'sync') return
|
||||
rebuildPublicDir(logger)
|
||||
},
|
||||
'astro:server:setup': ({ server, logger }) => {
|
||||
if (command !== 'dev') return
|
||||
// The copy above runs once at startup, so edits rebuilt into the source
|
||||
// dirs during `pnpm run dev` (e.g. core/dist by core's watchers) would
|
||||
// never reach the served public/ copy. Watch them, sync changed files
|
||||
// into public/, and trigger a browser reload.
|
||||
server.watcher.add([...syncDirs.map((dir) => dir.from), ...reloadDirs])
|
||||
let reloadTimer: ReturnType<typeof setTimeout> | undefined
|
||||
const scheduleReload = (file: string) => {
|
||||
// Source maps piggyback on their css/js file's reload. build-css.ts
|
||||
// buffers all writes into one tight burst, so a short quiet window is
|
||||
// enough to coalesce a whole rebuild into a single reload.
|
||||
if (file.endsWith('.map')) return
|
||||
clearTimeout(reloadTimer)
|
||||
reloadTimer = setTimeout(() => {
|
||||
server.hot.send({ type: 'full-reload' })
|
||||
logger.info(`reloaded after change in ${relative(repo, file)}`)
|
||||
}, 250)
|
||||
}
|
||||
const sync = (file: string) => {
|
||||
for (const { from, to } of syncDirs) {
|
||||
if (!file.startsWith(from + sep)) continue
|
||||
const dest = join(to, relative(from, file))
|
||||
mkdirSync(dirname(dest), { recursive: true })
|
||||
copyFileSync(file, dest)
|
||||
scheduleReload(file)
|
||||
return
|
||||
}
|
||||
if (reloadDirs.some((dir) => file.startsWith(dir + sep))) scheduleReload(file)
|
||||
}
|
||||
server.watcher.on('add', sync)
|
||||
server.watcher.on('change', sync)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// Minimal ambient typings for build-tool dependencies that ship no types.
|
||||
// Only the surface used by .build/build-css.ts is declared.
|
||||
declare module 'rtlcss' {
|
||||
import type { Plugin } from 'postcss'
|
||||
export default function rtlcss(config?: unknown): Plugin
|
||||
}
|
||||
|
||||
declare module 'clean-css' {
|
||||
interface MinifyResult {
|
||||
styles: string
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
sourceMap: { toString(): string }
|
||||
}
|
||||
export default class CleanCSS {
|
||||
constructor(options: Record<string, unknown>)
|
||||
minify(input: string[]): Promise<Record<string, MinifyResult>>
|
||||
}
|
||||
}
|
||||
+40
-51
@@ -8,72 +8,61 @@ import * as prettier from 'prettier'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const docs: string[] = sync(join(__dirname, '..', 'docs', '**', '*.md'))
|
||||
const docs: string[] = sync(join(__dirname, '..', 'docs', 'pages', '**', '*.mdx'))
|
||||
|
||||
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
|
||||
}
|
||||
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)]
|
||||
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))
|
||||
)
|
||||
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])
|
||||
})
|
||||
let result = str
|
||||
matches.forEach((match: RegExpMatchArray, i: number) => {
|
||||
result = result.replace(match[0], replacements[i])
|
||||
})
|
||||
|
||||
return result
|
||||
return result
|
||||
}
|
||||
|
||||
async function processFiles(): Promise<void> {
|
||||
for (const file of docs) {
|
||||
const oldContent = readFileSync(file, 'utf8')
|
||||
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)
|
||||
// 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, '')
|
||||
// remove empty lines
|
||||
formattedHtml = formattedHtml.replace(/^\s*[\r\n]/gm, '')
|
||||
|
||||
return m1 + formattedHtml.trim() + '\n' + m4
|
||||
}
|
||||
return m.trim()
|
||||
}
|
||||
)
|
||||
return m1 + formattedHtml.trim() + '\n' + m4
|
||||
}
|
||||
return m.trim()
|
||||
})
|
||||
|
||||
if (content !== oldContent) {
|
||||
writeFileSync(file, content, 'utf8')
|
||||
console.log(`Reformatted ${file}`)
|
||||
}
|
||||
}
|
||||
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)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error processing files:', errorMessage)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,74 +3,63 @@ 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'
|
||||
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
|
||||
}
|
||||
}
|
||||
export function createViteConfig({ entry, name, fileName, formats, outDir, banner, minify = false }: CreateViteConfigOptions): UserConfig {
|
||||
// Vite 8 (Rolldown) always emits const bindings and no longer accepts the
|
||||
// Rollup-only generatedCode.constBindings option
|
||||
const rollupOutput: { banner?: string } = {}
|
||||
|
||||
// Add banner if provided
|
||||
if (banner) {
|
||||
rollupOutput.banner = banner
|
||||
}
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const config: UserConfig = {
|
||||
// These are library (JS bundle) builds, not app builds — Vite's default
|
||||
// behavior of copying <root>/public into outDir on every build is not wanted
|
||||
// here (and in @tabler/preview, where public/ is itself generated from this
|
||||
// same outDir, caused unbounded growth across repeated builds).
|
||||
publicDir: false,
|
||||
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)
|
||||
return defineConfig(config)
|
||||
}
|
||||
|
||||
|
||||
+6
-20
@@ -9,38 +9,24 @@ import { readFileSync } from 'node:fs'
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
interface PackageJson {
|
||||
version: string
|
||||
[key: string]: unknown
|
||||
version: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const pkg: PackageJson = JSON.parse(
|
||||
readFileSync(path.join(__dirname, '../core', 'package.json'), 'utf8')
|
||||
)
|
||||
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, '../shared/static', 'og.png'),
|
||||
'.',
|
||||
'preview.png'
|
||||
)
|
||||
zip.addLocalFile(path.join(__dirname, '../shared/static', 'og.png'), '.', 'preview.png')
|
||||
|
||||
zip.addFile(
|
||||
'documentation.url',
|
||||
Buffer.from('[InternetShortcut]\nURL = https://tabler.io/docs')
|
||||
)
|
||||
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`
|
||||
)
|
||||
const outputZipPath = path.join(__dirname, '../packages-zip', `tabler-${pkg.version}.zip`)
|
||||
|
||||
// Write the zip file
|
||||
zip.writeZip(outputZipPath)
|
||||
|
||||
console.log(`Zipped folder to ${outputZipPath}`)
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Updated the `active-users-2` card chart include to use `chart-id="active-users-2"` with `height="11"` for a more compact layout.
|
||||
Updated the `active-users-2` card chart to use `chart-id="active-users-2"` with `height="11"` for a more compact layout.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
---
|
||||
|
||||
Added `.text-gray-*` utility classes (`.text-gray-50` through `.text-gray-950`) alongside the existing `.bg-gray-*` and `.text-gray-*-fg` utilities.
|
||||
@@ -2,5 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added new `card-gradients.html` page showcasing various gradient card styles and components.
|
||||
|
||||
Added new `card-gradients` page showcasing various gradient card styles and components.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added color palette to signing component.
|
||||
Added a color palette to the signature pad component for selecting the pen color.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Replaced hardcoded `rem` and `px` spacing, sizing, and offset values with SCSS variables across icon, avatar, badge, button, card, alert, empty state, ribbon, status, dropdown, form, nav, pagination, progress, steps, and tag components for easier theme customization.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated the contributing guide, README and Docker setup for the Astro-based development toolchain.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Added new `dashboard-crm.html` page with reusable CRM cards.
|
||||
Added new `dashboard-crm` page with reusable CRM cards.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated dev servers to run on fixed ports: `3000` for preview and `3010` for docs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed documentation formatting issues: heading hierarchy, missing image alt texts and broken list structure across docs pages.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added `sitemap.xml` and `robots.txt` for the docs site, with a `sitemap` Eleventy collection and fixed docs layout title rendering outside production.
|
||||
Added `sitemap.xml` and `robots.txt` endpoints for the docs site and fixed docs layout title rendering outside production.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Update Tabler Icons to v3.35.0
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Updated preview and docs Astro code to use shared `date-format`, `string-format`, `pseudo-random`, and `include-args` helpers instead of duplicated inline Liquid filter ports.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.card-header` background-color to respect the `--tblr-card-cap-bg` CSS variable instead of being silently overridden by a `background: transparent` shorthand.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.card-tabs .nav-tabs` sharing `z-index` with `.dropdown-menu`, which made dropdowns opened over card tabs render behind them.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed barely visible checkbox and radio borders in dark theme by using `$input-border-color` instead of translucent border color for `.form-check-input`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed disabled form control background in dark theme by introducing `--tblr-bg-forms-disabled` (gray-800) so disabled inputs are visually distinct from enabled ones.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Fixed the `Plugins` card on the docs homepage linking to a 404 `/plugins` route instead of `/ui/plugins`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.input-icon` inline-start padding for `.form-select` by updating the selector from `:not(:last-child)` to `:not(:first-child)`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.form-select` keeping its default box-shadow inside `.input-group` by adding it to the existing `.form-control`/`.btn` box-shadow reset.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `--tblr-gray-*-fg` token generation to map directly to `--tblr-gray-*` instead of contrast-based fallbacks that could resolve to white.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed transform animations (`.icon-pulse`, `.icon-tada`, `.icon-rotate`) not working on webfont icons by adding `display: inline-block` and `transform-origin: center` to `.icon`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `sm` and `lg` size consistency for form controls, buttons, and input groups by aligning `line-height` and computed height variables to resolve [#2456](https://github.com/tabler/tabler/issues/2456).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed `.steps` horizontal overflow on small screens by enabling scrollable overflow below the `sm` breakpoint.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
"@tabler/docs": patch
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Unified local and CI quality gates: added root-level `lint-prettier`/`format-prettier` scripts covering `core`, `preview`, and `shared`, wired `check` to run lint and type-check together, and updated the lint workflow to run both. Reformatted the covered files with Prettier.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added framework integration guides for Laravel, React, Next.js, Vue, Angular, Nuxt, Symfony, Django, Rails, SvelteKit, and Astro.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
Use `.nvmrc` for Node version in CI workflows
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Updated icons to v3.34.1 with 75 new icons.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Updated root color tokens to use CSS `light-dark()` so paired light and dark values live in one `:root` declaration instead of separate dark-mode overrides.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Migrated all physical CSS direction properties to logical properties (`padding-inline`, `margin-inline`, `inset-inline`, `float: inline-start`/`inline-end`, `text-align: start/end`) for improved RTL support and reduced RTL stylesheet generation overhead.
|
||||
|
||||
Directional transforms, which have no logical equivalent, now use a `--tblr-dir` direction multiplier (`1` in LTR, `-1` under `[dir="rtl"]`), e.g. `translateX(calc(var(--tblr-dir) * 4px))`. These declarations are identical in `tabler.css` and `tabler.rtl.css` — the flip happens at runtime via the `dir` attribute, so RTL transforms now also work when using plain `tabler.css` with `dir="rtl"`.
|
||||
@@ -1,8 +1,5 @@
|
||||
---
|
||||
"@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.
|
||||
|
||||
Migrated the core build system from Rollup to Vite. 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.
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
"@tabler/preview": minor
|
||||
---
|
||||
|
||||
Refactored navbar-side component by consolidating separate include files (apps, language, notifications, theme, user) into a single `navbar-side.html` file for better maintainability.
|
||||
|
||||
Refactored the navbar-side component and reorganized its apps, language, notifications, theme and user sections for better maintainability.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Fixed Vercel deployment to serve `error-404.html` as the custom 404 page.
|
||||
@@ -3,4 +3,4 @@
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Added `Progress Step` component documentation and updated `ui/progress-steps.html` formatting for cleaner rendered output.
|
||||
Added `Progress Step` component documentation and cleaned up the progress steps preview markup for cleaner rendered output.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/core": major
|
||||
"@tabler/preview": patch
|
||||
---
|
||||
|
||||
Removed `@hotwired/turbo` integration, including `.turbo-progress-bar` styles and the Turbo loader preview demo.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Removed the unused `bootstrapLink` front matter field from `DocsLayout`, `DocsMdxLayout`, and all docs pages.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@tabler/core': major
|
||||
---
|
||||
|
||||
Remove unused SCSS `!default` variables (deprecated Bootstrap leftovers such as `$variable-prefix`, `$alert-*-scale`, accordion icon/button variables, `$popover-arrow-*`, numbered `$font-size-*` / `$line-height-*` scales and others). Configuring any of the removed variables via `@use ... with (...)` now raises a Sass error — they had no effect on the compiled CSS before. The color shade ladders (`$blue-100`…`$cyan-900`) and per-color maps (`$blues`…`$cyans`) are kept as configurable API. Compiled CSS output is unchanged.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed the `caret()` mixin by restoring `$caret-width` to `0.36em`, which had shrunk to `0.3em` in a previous SCSS refactor.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
---
|
||||
|
||||
Fixed oversized `dist/libs` by copying only the runtime files each library declares in `libs.json`, plus the assets HugeRTE loads at runtime and every upstream license file.
|
||||
@@ -1,27 +1,30 @@
|
||||
---
|
||||
name: astro-components
|
||||
description: Builds new Astro components, pages, and layouts for the Tabler monorepo (preview and docs packages, shared/astro library). Use when creating or extending demo pages, UI components, cards, layouts, or docs examples. Knows the project's component conventions, script/modal architecture, and shared building blocks.
|
||||
description: Builds new Astro components, pages, and layouts for the Tabler monorepo (preview and docs packages, shared library). Use when creating or extending demo pages, UI components, cards, layouts, or docs examples. Knows the project's component conventions, script/modal architecture, and shared building blocks.
|
||||
---
|
||||
|
||||
You are a specialist in building Astro components and pages for Tabler.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `shared/astro/` is the single component library used by both site packages:
|
||||
`components/` (UI + cards + parts), `layouts/`, `lib/`, `data/docs.json`.
|
||||
- `shared/` is the single component library used by both site packages:
|
||||
`ui/`, `components/` (cards + parts), `layouts/`, `lib/`, plus `data/` and `static/`.
|
||||
- `preview/` (`@tabler/preview`) — demo site; pages in `preview/pages/*.astro`
|
||||
(`srcDir: '.'`, `build.format: 'file'` → `foo.astro` → `/foo.html`).
|
||||
- `docs/` (`@tabler/docs`) — documentation; pages in `docs/pages/**/*.mdx`
|
||||
(`build.format: 'directory'`; docs writing rules: `.agents/rules/docs.mdc`).
|
||||
- Aliases (vite + tsconfig, per package): `@shared` → `../shared/astro`,
|
||||
`@data` → `../shared/data`, `@pages` → the package's pages dir.
|
||||
Inside `shared/astro`, use relative imports; from shared code to
|
||||
Docs-only components live in `docs/components/` (`@components` alias).
|
||||
- Aliases (vite + tsconfig, per package): `@shared` → `../shared`, `@ui` → `../shared/ui`,
|
||||
`@data` → `../shared/data`, `@components` → package components
|
||||
(`preview`: `shared/components`, `docs`: `docs/components`),
|
||||
`@pages` → the package's pages dir.
|
||||
Inside `shared`, use relative imports; from shared code to
|
||||
package-specific files use the package aliases. Any bare npm import used in
|
||||
`shared/astro` must be declared in `shared/astro/package.json`.
|
||||
`shared` must be declared in `shared/package.json`.
|
||||
|
||||
## Component conventions
|
||||
|
||||
- Components are `.astro` only; helper logic goes to `shared/astro/lib/*.ts`.
|
||||
- Components are `.astro` only; helper logic goes to `shared/lib/*.ts`.
|
||||
- TypeScript frontmatter: `interface Props` + destructuring with defaults.
|
||||
- Props are camelCase; the `class` prop stays `class`
|
||||
(destructure as `class: className`).
|
||||
@@ -43,13 +46,13 @@ You are a specialist in building Astro components and pages for Tabler.
|
||||
Read the `Props` interface of a component before using it; extend components
|
||||
additively instead of creating parallel variants.
|
||||
|
||||
- `@shared/components/Icon.astro` — `<Icon name="eye" size="sm" class="..." />`.
|
||||
- `@shared/components/Button.astro` — full button API (color, outline, ghost,
|
||||
- `@ui/Icon.astro` — `<Icon name="eye" size="sm" class="..." />`.
|
||||
- `@ui/Button.astro` — full button API (color, outline, ghost,
|
||||
size, icon, iconOnly, dismiss, loading, modalId, ...).
|
||||
- `@shared/components/Chart.astro` + `@shared/lib/chart-script.ts` — ApexCharts
|
||||
- `@ui/Chart.astro` + `@shared/lib/chart-script.ts` — ApexCharts
|
||||
engine driven by `@data/charts.json`. Never hand-write chart configs; add
|
||||
fields to `chart-script.ts` if a new chart needs them.
|
||||
- `@shared/components/ui/*` — Avatar, Badge, Progress, Pagination, Flag,
|
||||
- `@ui/*` — Avatar, Badge, Progress, Pagination, Flag,
|
||||
Dropdown, Table, Steps, Nav, Spinner, and ~45 more.
|
||||
- `@shared/components/cards/*` — dashboard/demo cards.
|
||||
- Layouts in `@shared/layouts/`: `BaseLayout` (head, assets, theme settings),
|
||||
@@ -57,21 +60,19 @@ additively instead of creating parallel variants.
|
||||
wrapper/container classes, page header), `SingleLayout` (auth pages),
|
||||
`ErrorLayout`, `SettingsLayout`, `MarketingLayout`, `PayLayout`,
|
||||
`DocsLayout`/`DocsMdxLayout` (docs).
|
||||
- Docs examples: `@shared/components/docs/Example.astro` (slot or `html` prop;
|
||||
- Docs examples: `@components/Example.astro` (slot or `html` prop;
|
||||
props: hideCode, code, centered, vertical, column, raw, bg, height, codeOnly).
|
||||
|
||||
## Page scripts and modals
|
||||
|
||||
- Register per-page scripts with `addPageScript()` and modals with
|
||||
`addPageModal()` (`@shared/lib/page-scripts.ts` / `page-modals.ts`).
|
||||
- Capture markup with `CaptureScript` / `CaptureModal` (HTML in the slot, not
|
||||
template strings). They register via `addPageScript()` / `addPageModal()`
|
||||
(`@shared/lib/page-scripts.ts` / `page-modals.ts`).
|
||||
Wrap at the call site, e.g. `<CaptureModal><Modal …>…</Modal></CaptureModal>`.
|
||||
Registration MUST be synchronous in the component frontmatter (before the
|
||||
first `await`) — Astro renders siblings concurrently, and a registration
|
||||
after `await Astro.slots.render()` loses the race against the drain in
|
||||
`PageScripts`/`PageModals` (emitted by `BaseLayout`).
|
||||
- Script-emitting components also render `<InlineScript code={script} />`.
|
||||
Its behavior is chosen per package by the vite define
|
||||
`import.meta.env.INLINE_PAGE_SCRIPTS`: preview drains registered scripts at
|
||||
the end of the page; docs inlines them next to the example.
|
||||
`PageScripts`/`PageModals` (emitted by `BaseLayout` / `DocsLayout`).
|
||||
- Third-party page libraries: list names in the layout's `pageLibs` prop —
|
||||
resolved via `@tabler/core/libs.json` (a full `http` URL in there is emitted
|
||||
verbatim; `head: true` libs go into `<head>`).
|
||||
@@ -89,7 +90,7 @@ additively instead of creating parallel variants.
|
||||
|
||||
1. Create `preview/pages/<name>.astro`; pick the layout (usually
|
||||
`DefaultLayout`) and pass `title`, menu/page-header props, `pageLibs`.
|
||||
2. Compose from existing components; add new ones to `shared/astro/components/`.
|
||||
2. Compose from existing components; add new ones to `shared/components/`.
|
||||
3. Build and verify: `pnpm --filter @tabler/preview build` (output in
|
||||
`preview/dist/<name>.html`), or `astro dev` for live preview.
|
||||
4. If the page should appear in navigation, update `@data` menu sources.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Generate PR title and description, use ALL changes in current branch vs dev branch. Use simple english, title should be no longer than 70 chars. Generate title and description in markdown. If issue numer are in branch name add reference in PR to this issue on Github.
|
||||
@@ -1,3 +1,8 @@
|
||||
demo/
|
||||
.git/
|
||||
.github/
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/.astro/
|
||||
packages-zip/
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
days-before-issue-stale: 360
|
||||
days-before-issue-close: 14
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
@@ -25,5 +25,8 @@ jobs:
|
||||
- name: Install pnpm dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint Markdown
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Type Check
|
||||
run: pnpm run type-check
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -52,3 +52,4 @@ coverage/
|
||||
__screenshots__/
|
||||
__pycache__/
|
||||
.claude/launch.json
|
||||
.claude/settings.local.json
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./.markdownlint.json",
|
||||
"MD013": false,
|
||||
"MD024": {
|
||||
"siblings_only": true
|
||||
},
|
||||
"MD041": false
|
||||
}
|
||||
+26
-1
@@ -2,4 +2,29 @@ dist
|
||||
.tmp
|
||||
.vscode
|
||||
.cache
|
||||
node_modules
|
||||
node_modules
|
||||
# generated / build outputs
|
||||
.astro
|
||||
coverage
|
||||
demo
|
||||
packages-zip
|
||||
preview/public
|
||||
preview/tmp-assets
|
||||
docs/public
|
||||
|
||||
# prettier-plugin-astro cannot parse HTML comments or <script> tags inside
|
||||
# JSX expressions ({cond && (<Fragment><!-- ... --><script>...)}) — these files
|
||||
# use those constructs on purpose (BEGIN/END markers emitted into demo HTML,
|
||||
# conditional inline scripts). Re-check when the plugin gains support.
|
||||
docs/layouts/DocsLayout.astro
|
||||
shared/components/cards/Card.astro
|
||||
shared/components/layout/PageHeader.astro
|
||||
shared/components/navbar/Navbar.astro
|
||||
shared/ui/Chart.astro
|
||||
shared/ui/ChartSparkline.astro
|
||||
shared/ui/Datepicker.astro
|
||||
shared/ui/InlinePlayer.astro
|
||||
shared/ui/Map.astro
|
||||
shared/ui/MapVector.astro
|
||||
shared/ui/Range.astro
|
||||
shared/ui/Select.astro
|
||||
|
||||
@@ -9,12 +9,19 @@
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "all",
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.scss",
|
||||
"options": {
|
||||
"parser": "scss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.astro",
|
||||
"options": {
|
||||
"parser": "astro"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"]
|
||||
}
|
||||
Vendored
+12
-10
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"**/.idea/": true
|
||||
},
|
||||
"explorerExclude.backup": {}
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"**/.idea/": true
|
||||
},
|
||||
"explorerExclude.backup": {},
|
||||
"typescript.tsdk": "preview/node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Contributing to Tabler
|
||||
|
||||
Thank you for wanting to contribute! This is a short overview - the full guide lives in the documentation: [How to Contribute](https://docs.tabler.io/ui/getting-started/how-to-contribute/).
|
||||
|
||||
## Quick start
|
||||
|
||||
Requirements: Node.js 22.12+ and [pnpm](https://pnpm.io/).
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
This starts the preview website at [http://localhost:3000](http://localhost:3000) and the documentation at [http://localhost:3010](http://localhost:3010), both with live reload.
|
||||
|
||||
## Where things live
|
||||
|
||||
- `core/` - the framework: SCSS (`core/scss/`) and JavaScript (`core/js/`)
|
||||
- `preview/` - demo website (Astro, pages in `preview/pages/*.astro`)
|
||||
- `docs/` - documentation website (Astro, pages in `docs/pages/**/*.mdx`)
|
||||
- `shared/` - components and layouts shared by preview and docs (`@shared` alias), plus `data/` and `static/`
|
||||
- `shared/data/` - JSON data for demo pages (`@data` alias)
|
||||
|
||||
Do not edit `dist/` folders - they are generated by the build.
|
||||
|
||||
## Before you open a pull request
|
||||
|
||||
1. **Branch off `dev`** and use the branch naming convention: `<type>/<short-description>`, for example `feat/gh-123-add-stepper-component` or `fix/markdown-table-overflow`. Allowed types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `build`, `ci`, `perf`, `style`, `revert`.
|
||||
2. **Build and test**: `pnpm run build` must pass. For docs changes, also run `pnpm run lint`.
|
||||
3. **Add a changeset** if your change affects any package: `pnpm exec changeset`. Use one sentence starting with `Added`, `Updated`, `Fixed` or `Removed`, with backticks for code tokens (for example `` Added `.btn-ghost` variant for buttons. ``).
|
||||
4. **Write the PR in English**: a concise title in present tense (max ~72 chars, no trailing period) and a short description that explains why the change is needed.
|
||||
|
||||
## Code style
|
||||
|
||||
- Use Bootstrap 5 conventions and Tabler's CSS custom properties pattern: `--#{$prefix}component-property`.
|
||||
- Write documentation in simple English: short sentences, common words, direct instructions.
|
||||
- Ensure changes work in all supported browsers (see the [browser support docs](https://docs.tabler.io/ui/getting-started/browser-support/)).
|
||||
|
||||
## Code of conduct
|
||||
|
||||
By participating, you agree to follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
|
||||
+12
-14
@@ -1,20 +1,18 @@
|
||||
FROM ruby:3.2-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
ADD _config.yml /app/
|
||||
ADD _config_prod.yml /app/
|
||||
ADD package.json /app/
|
||||
ADD pnpm-lock.yaml /app/
|
||||
ADD gulpfile.js /app/
|
||||
|
||||
RUN apk add --virtual build-dependencies build-base npm
|
||||
RUN apk upgrade
|
||||
RUN npm i -g pnpm
|
||||
RUN pnpm install
|
||||
# pnpm via corepack, pinned by the "packageManager" field in package.json
|
||||
RUN corepack enable
|
||||
|
||||
# website
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# preview website
|
||||
EXPOSE 3000
|
||||
# website management (browser auto reload)
|
||||
EXPOSE 3001
|
||||
# run tabler
|
||||
# documentation website
|
||||
EXPOSE 3010
|
||||
|
||||
# run tabler dev servers
|
||||
ENTRYPOINT [ "pnpm", "run", "start" ]
|
||||
|
||||
@@ -49,7 +49,7 @@ Tabler is fully responsive and compatible with all modern browsers. Thanks to it
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
We've created this admin panel for everyone who wants to create templates based on our pre-made components. Our mission is to deliver a user-friendly, clear and easy administration panel that can be used by both simple websites and sophisticated systems. The only requirement is basic HTML and CSS (and some [Liquid](https://github.com/Shopify/liquid/wiki)) knowledge — as a reward, you'll be able to manage and visualize different types of data in the easiest possible way!
|
||||
We've created this admin panel for everyone who wants to create templates based on our pre-made components. Our mission is to deliver a user-friendly, clear and easy administration panel that can be used by both simple websites and sophisticated systems. The only requirement is basic HTML and CSS knowledge — as a reward, you'll be able to manage and visualize different types of data in the easiest possible way!
|
||||
|
||||
* **Responsive:** With the support for mobile, tablet and desktop displays, it doesn’t matter what device you’re using. Tabler is responsive in all major browsers.
|
||||
* **Cross Browser:** Our theme works perfectly with the latest Chrome, Firefox+, Safari, Opera, Edge and mobile browsers. We work hard to provide continuous support for them.
|
||||
@@ -114,7 +114,7 @@ If you **do** wish to edit the source code after downloading it, for example to
|
||||
|
||||
### Installing and running development tools directly
|
||||
|
||||
1. [Install Node.js](https://nodejs.org/download/), which we use to manage our dependencies.
|
||||
1. [Install Node.js](https://nodejs.org/download/) (version 22.12 or newer), which we use to manage our dependencies.
|
||||
2. [Install pnpm](https://pnpm.io/installation) (We recommend either by [Using Corepack](https://pnpm.io/installation#using-corepack) or by [Using npm](https://pnpm.io/installation#using-npm))
|
||||
3. From the root `/tabler` directory where you downloaded the Tabler source files, run installation on the command line:
|
||||
```sh
|
||||
@@ -128,11 +128,10 @@ pnpm run start
|
||||
Changes to most of the source files of Tabler core, preview and docs will rebuild the application and refresh the page.
|
||||
|
||||
**Note**:
|
||||
If you wish to perform a one-off build without auto-refresh on any changes, you can run:
|
||||
If you wish to perform a one-off production build without a dev server, you can run:
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
You can open [http://localhost:3001](http://localhost:3001) to configure the Web server.
|
||||
|
||||
|
||||
### Installing and running development tools with Docker
|
||||
@@ -145,15 +144,15 @@ Here is an example of how to use this image:
|
||||
```sh
|
||||
docker build -t tabler .
|
||||
```
|
||||
2. Run the tabler image. The following command mounts the `src` directory into the container, exposes port 3000 to browse the website locally, and exposes port 3001 to automatically sync changes:
|
||||
2. Run the tabler image. The following command mounts the source directories into the container, exposes port 3000 to browse the preview website and port 3010 to browse the documentation website:
|
||||
```sh
|
||||
docker run -p 3000:3000 -p 3001:3001 -v $(pwd)/src:/app/src tabler
|
||||
docker run -p 3000:3000 -p 3010:3010 -v $(pwd)/core:/app/core -v $(pwd)/preview:/app/preview -v $(pwd)/docs:/app/docs -v $(pwd)/shared:/app/shared tabler
|
||||
```
|
||||
3. Open your browser to [http://localhost:3000](http://localhost:3000). Edit anything in the `src/` folder and watch your browser refresh the page after it has been rebuilt.
|
||||
3. Open your browser to [http://localhost:3000](http://localhost:3000) (preview) or [http://localhost:3010](http://localhost:3010) (documentation). Edit anything in the `core/`, `preview/`, `docs/` or `shared/` folders and watch your browser refresh the page after it has been rebuilt.
|
||||
|
||||
**Docker Compose**
|
||||
|
||||
You can also use the docker compose config from this repo. From the root `/tabler` directory where you downloaded the Tabler source files, use `docker compose build && docker compose up` or `docker compose up --build` to build and start the container. Edit anything in the `src/` folder the same way as with plain docker and access the same URLs and ports in your browser.
|
||||
You can also use the docker compose config from this repo. From the root `/tabler` directory where you downloaded the Tabler source files, use `docker compose build && docker compose up` or `docker compose up --build` to build and start the container. Edit anything in the `core/`, `preview/`, `docs/` or `shared/` folders the same way as with plain docker and access the same URLs and ports in your browser.
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const styles: string[] = sync(join(__dirname, '..', 'dist', 'css', '*.css'))
|
||||
|
||||
interface Plugins {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const plugins: Plugins = {
|
||||
'tabler-flags': 'Flags',
|
||||
'tabler-flags.rtl': 'Flags RTL',
|
||||
'tabler-marketing': 'Marketing',
|
||||
'tabler-marketing.rtl': 'Marketing RTL',
|
||||
'tabler-payments': 'Payments',
|
||||
'tabler-payments.rtl': 'Payments RTL',
|
||||
'tabler-socials': 'Socials',
|
||||
'tabler-socials.rtl': 'Socials RTL',
|
||||
'tabler-vendors': 'Vendors',
|
||||
'tabler-vendors.rtl': 'Vendors RTL',
|
||||
}
|
||||
|
||||
styles.forEach((file: string) => {
|
||||
const content = readFileSync(file, 'utf8')
|
||||
const filename = basename(file)
|
||||
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: string, m1: string) => {
|
||||
return `${m1}\n${bannerText}\n`
|
||||
})
|
||||
} else {
|
||||
newContent = `${bannerText}\n${content}`
|
||||
}
|
||||
|
||||
writeFileSync(file, newContent, 'utf8')
|
||||
})
|
||||
|
||||
@@ -12,73 +12,72 @@ 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>()
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = variableRegex.exec(content)) !== null) {
|
||||
const varName = match[1]
|
||||
variables.add(varName)
|
||||
}
|
||||
|
||||
return variables
|
||||
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 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`)
|
||||
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)
|
||||
}
|
||||
}
|
||||
// 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()
|
||||
// Sort alphabetically
|
||||
missingInTabler.sort()
|
||||
|
||||
console.log('='.repeat(60))
|
||||
console.log(`Variables in Bootstrap that are missing in Tabler: ${missingInTabler.length}`)
|
||||
console.log('='.repeat(60))
|
||||
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}`)
|
||||
})
|
||||
}
|
||||
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))
|
||||
// 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()
|
||||
compareVariables()
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error during analysis:', errorMessage)
|
||||
process.exit(1)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error('Error during analysis:', errorMessage)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
+48
-19
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { globSync } from 'glob'
|
||||
import { emptyDirSync, copySync } from 'fs-extra/esm'
|
||||
import libs from '../libs.json' with { type: 'json' }
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -9,14 +10,15 @@ import { join, dirname } from 'node:path'
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
interface LibConfig {
|
||||
npm?: string
|
||||
js?: string[]
|
||||
css?: string[]
|
||||
head?: boolean
|
||||
npm?: string
|
||||
js?: string[]
|
||||
css?: string[]
|
||||
extra?: string[]
|
||||
head?: boolean
|
||||
}
|
||||
|
||||
interface Libs {
|
||||
[key: string]: LibConfig
|
||||
[key: string]: LibConfig
|
||||
}
|
||||
|
||||
const libsData = libs as Libs
|
||||
@@ -24,22 +26,49 @@ const libsData = libs as Libs
|
||||
emptyDirSync(join(__dirname, '..', 'dist/libs'))
|
||||
|
||||
for (const name in libsData) {
|
||||
const { npm } = libsData[name]
|
||||
const { npm, js, css, extra } = libsData[name]
|
||||
|
||||
if (npm) {
|
||||
const from = join(__dirname, '..', `node_modules/${npm}`)
|
||||
const to = join(__dirname, '..', `dist/libs/${npm}`)
|
||||
if (npm) {
|
||||
const pkgDir = join(__dirname, '..', `node_modules/${npm}`)
|
||||
const to = join(__dirname, '..', `dist/libs/${npm}`)
|
||||
|
||||
// create dir in dist/libs
|
||||
if (!existsSync(to)) {
|
||||
mkdirSync(to, { recursive: true })
|
||||
}
|
||||
// create dir in dist/libs
|
||||
if (!existsSync(to)) {
|
||||
mkdirSync(to, { recursive: true })
|
||||
}
|
||||
|
||||
copySync(from, to, {
|
||||
dereference: true,
|
||||
})
|
||||
const files = [...(js || []), ...(css || [])]
|
||||
|
||||
console.log(`Successfully copied ${npm}`)
|
||||
}
|
||||
for (const pattern of extra || []) {
|
||||
const matches = globSync(pattern, { cwd: pkgDir, nodir: true, posix: true })
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`copy-libs: ${name} — "${pattern}" matched no files in ${npm}`)
|
||||
}
|
||||
files.push(...matches)
|
||||
}
|
||||
|
||||
if (files) {
|
||||
for (const file of files) {
|
||||
copySync(join(pkgDir, file), join(to, file), {
|
||||
dereference: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ship each upstream license alongside its files — we redistribute the
|
||||
// built bundles, and MIT/BSD/Apache all require the notice to travel with them.
|
||||
const licenses = globSync('{license,licence,copying}*', { cwd: pkgDir, nodir: true, nocase: true })
|
||||
|
||||
if (licenses.length === 0) {
|
||||
console.warn(`Warning: no license file found for ${npm}`)
|
||||
}
|
||||
|
||||
for (const file of licenses) {
|
||||
copySync(join(pkgDir, file), join(to, file), {
|
||||
dereference: true,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`Successfully copied ${npm}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+94
-95
@@ -8,114 +8,113 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const configFile = path.join(__dirname, '../../shared/data/sri.json')
|
||||
|
||||
interface FileConfig {
|
||||
file: string
|
||||
configPropertyName: string
|
||||
file: string
|
||||
configPropertyName: string
|
||||
}
|
||||
|
||||
const files: FileConfig[] = [
|
||||
{
|
||||
file: 'dist/css/tabler.min.css',
|
||||
configPropertyName: 'css'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler.rtl.min.css',
|
||||
configPropertyName: 'css-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-flags.min.css',
|
||||
configPropertyName: 'css-flags'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-flags.rtl.min.css',
|
||||
configPropertyName: 'css-flags-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-marketing.min.css',
|
||||
configPropertyName: 'css-marketing'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-marketing.rtl.min.css',
|
||||
configPropertyName: 'css-marketing-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-payments.min.css',
|
||||
configPropertyName: 'css-payments'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-payments.rtl.min.css',
|
||||
configPropertyName: 'css-payments-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-props.min.css',
|
||||
configPropertyName: 'css-props'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-props.rtl.min.css',
|
||||
configPropertyName: 'css-props-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-themes.min.css',
|
||||
configPropertyName: 'css-themes'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-themes.rtl.min.css',
|
||||
configPropertyName: 'css-themes-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-socials.min.css',
|
||||
configPropertyName: 'css-socials'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-socials.rtl.min.css',
|
||||
configPropertyName: 'css-socials-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-vendors.min.css',
|
||||
configPropertyName: 'css-vendors'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-vendors.rtl.min.css',
|
||||
configPropertyName: 'css-vendors-rtl'
|
||||
},
|
||||
{
|
||||
file: 'dist/js/tabler.min.js',
|
||||
configPropertyName: 'js'
|
||||
},
|
||||
{
|
||||
file: 'dist/js/tabler-theme.min.js',
|
||||
configPropertyName: 'js-theme'
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler.min.css',
|
||||
configPropertyName: 'css',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler.rtl.min.css',
|
||||
configPropertyName: 'css-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-flags.min.css',
|
||||
configPropertyName: 'css-flags',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-flags.rtl.min.css',
|
||||
configPropertyName: 'css-flags-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-marketing.min.css',
|
||||
configPropertyName: 'css-marketing',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-marketing.rtl.min.css',
|
||||
configPropertyName: 'css-marketing-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-payments.min.css',
|
||||
configPropertyName: 'css-payments',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-payments.rtl.min.css',
|
||||
configPropertyName: 'css-payments-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-props.min.css',
|
||||
configPropertyName: 'css-props',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-props.rtl.min.css',
|
||||
configPropertyName: 'css-props-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-themes.min.css',
|
||||
configPropertyName: 'css-themes',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-themes.rtl.min.css',
|
||||
configPropertyName: 'css-themes-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-socials.min.css',
|
||||
configPropertyName: 'css-socials',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-socials.rtl.min.css',
|
||||
configPropertyName: 'css-socials-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-vendors.min.css',
|
||||
configPropertyName: 'css-vendors',
|
||||
},
|
||||
{
|
||||
file: 'dist/css/tabler-vendors.rtl.min.css',
|
||||
configPropertyName: 'css-vendors-rtl',
|
||||
},
|
||||
{
|
||||
file: 'dist/js/tabler.min.js',
|
||||
configPropertyName: 'js',
|
||||
},
|
||||
{
|
||||
file: 'dist/js/tabler-theme.min.js',
|
||||
configPropertyName: 'js-theme',
|
||||
},
|
||||
]
|
||||
|
||||
function generateSRI(): void {
|
||||
const sriData: Record<string, string> = {}
|
||||
const sriData: Record<string, string> = {}
|
||||
|
||||
for (const { file, configPropertyName } of files) {
|
||||
try {
|
||||
const filePath = path.join(__dirname, '..', file)
|
||||
const data = readFileSync(filePath, 'utf8')
|
||||
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}`
|
||||
const algorithm = 'sha384'
|
||||
const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64')
|
||||
const integrity = `${algorithm}-${hash}`
|
||||
|
||||
console.log(`${configPropertyName}: ${integrity}`)
|
||||
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
|
||||
}
|
||||
}
|
||||
sriData[configPropertyName] = integrity
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error processing ${file}:`, errorMessage)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(configFile, JSON.stringify(sriData, null, 2) + '\n', 'utf8')
|
||||
writeFileSync(configFile, JSON.stringify(sriData, null, 2) + '\n', 'utf8')
|
||||
}
|
||||
|
||||
try {
|
||||
generateSRI()
|
||||
generateSRI()
|
||||
} catch (error) {
|
||||
console.error('Failed to generate SRI:', error)
|
||||
process.exit(1)
|
||||
console.error('Failed to generate SRI:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
+17
-18
@@ -12,7 +12,7 @@ const toDir = join(__dirname, '..', 'fonts')
|
||||
|
||||
// Create fonts directory if it doesn't exist
|
||||
if (!existsSync(toDir)) {
|
||||
mkdirSync(toDir, { recursive: true })
|
||||
mkdirSync(toDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Copy geist-mono fonts
|
||||
@@ -20,17 +20,17 @@ const monoFrom = join(fromDir, 'geist-mono')
|
||||
const monoTo = join(toDir, 'geist-mono')
|
||||
|
||||
if (existsSync(monoFrom)) {
|
||||
if (!existsSync(monoTo)) {
|
||||
mkdirSync(monoTo, { recursive: true })
|
||||
}
|
||||
if (!existsSync(monoTo)) {
|
||||
mkdirSync(monoTo, { recursive: true })
|
||||
}
|
||||
|
||||
copySync(monoFrom, monoTo, {
|
||||
dereference: true,
|
||||
})
|
||||
copySync(monoFrom, monoTo, {
|
||||
dereference: true,
|
||||
})
|
||||
|
||||
console.log(`Successfully copied geist-mono fonts`)
|
||||
console.log(`Successfully copied geist-mono fonts`)
|
||||
} else {
|
||||
console.warn(`Warning: geist-mono fonts not found at ${monoFrom}`)
|
||||
console.warn(`Warning: geist-mono fonts not found at ${monoFrom}`)
|
||||
}
|
||||
|
||||
// Copy geist-sans fonts
|
||||
@@ -38,16 +38,15 @@ const sansFrom = join(fromDir, 'geist-sans')
|
||||
const sansTo = join(toDir, 'geist-sans')
|
||||
|
||||
if (existsSync(sansFrom)) {
|
||||
if (!existsSync(sansTo)) {
|
||||
mkdirSync(sansTo, { recursive: true })
|
||||
}
|
||||
if (!existsSync(sansTo)) {
|
||||
mkdirSync(sansTo, { recursive: true })
|
||||
}
|
||||
|
||||
copySync(sansFrom, sansTo, {
|
||||
dereference: true,
|
||||
})
|
||||
copySync(sansFrom, sansTo, {
|
||||
dereference: true,
|
||||
})
|
||||
|
||||
console.log(`Successfully copied geist-sans fonts`)
|
||||
console.log(`Successfully copied geist-sans fonts`)
|
||||
} else {
|
||||
console.warn(`Warning: geist-sans fonts not found at ${sansFrom}`)
|
||||
console.warn(`Warning: geist-sans fonts not found at ${sansFrom}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default context => {
|
||||
return {
|
||||
map: {
|
||||
inline: false,
|
||||
annotation: true,
|
||||
sourcesContent: true
|
||||
},
|
||||
plugins: {
|
||||
autoprefixer: {
|
||||
cascade: false
|
||||
},
|
||||
rtlcss: context.env === 'RTL'
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -16,15 +16,14 @@ const entryPath = path.resolve(__dirname, `../js/${entryFile}`)
|
||||
const entry = `${entryPath}.ts`
|
||||
|
||||
export default createViteConfig({
|
||||
entry: entry,
|
||||
name: libraryName,
|
||||
fileName: (format) => {
|
||||
const esmSuffix = format === 'es' ? '.esm' : ''
|
||||
return `${baseName}${esmSuffix}.js`
|
||||
},
|
||||
formats: ['es', 'umd'],
|
||||
outDir: path.resolve(__dirname, '../dist/js'),
|
||||
banner: bannerText,
|
||||
minify: false
|
||||
entry: entry,
|
||||
name: libraryName,
|
||||
fileName: (format) => {
|
||||
const esmSuffix = format === 'es' ? '.esm' : ''
|
||||
return `${baseName}${esmSuffix}.js`
|
||||
},
|
||||
formats: ['es', 'umd'],
|
||||
outDir: path.resolve(__dirname, '../dist/js'),
|
||||
banner: bannerText,
|
||||
minify: false,
|
||||
})
|
||||
|
||||
|
||||
@@ -38,5 +38,5 @@ export const bootstrap = {
|
||||
ScrollSpy,
|
||||
Tab,
|
||||
Toast,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class BaseComponent extends Config {
|
||||
EventHandler.off(this._element, ctor.EVENT_KEY)
|
||||
|
||||
for (const propertyName of Object.getOwnPropertyNames(this)) {
|
||||
(this as Record<string, unknown>)[propertyName] = null
|
||||
;(this as Record<string, unknown>)[propertyName] = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,7 @@ import BaseComponent from './base-component'
|
||||
import EventHandler from './dom/event-handler'
|
||||
import Manipulator from './dom/manipulator'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
import {
|
||||
getNextActiveElement,
|
||||
isRTL,
|
||||
isVisible,
|
||||
reflow,
|
||||
triggerTransitionEnd
|
||||
} from './util/index'
|
||||
import { getNextActiveElement, isRTL, isVisible, reflow, triggerTransitionEnd } from './util/index'
|
||||
import Swipe from './util/swipe'
|
||||
import type { ComponentConfig, ComponentConfigType } from './types'
|
||||
|
||||
@@ -60,7 +54,7 @@ const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"], [data-tblr-ride="carousel
|
||||
|
||||
const KEY_TO_DIRECTION: Record<string, string> = {
|
||||
[ARROW_LEFT_KEY]: DIRECTION_RIGHT,
|
||||
[ARROW_RIGHT_KEY]: DIRECTION_LEFT
|
||||
[ARROW_RIGHT_KEY]: DIRECTION_LEFT,
|
||||
}
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
@@ -69,7 +63,7 @@ const Default: ComponentConfig = {
|
||||
pause: 'hover',
|
||||
ride: false,
|
||||
touch: true,
|
||||
wrap: true
|
||||
wrap: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -78,7 +72,7 @@ const DefaultType: ComponentConfigType = {
|
||||
pause: '(string|boolean)',
|
||||
ride: '(boolean|string)',
|
||||
touch: 'boolean',
|
||||
wrap: 'boolean'
|
||||
wrap: 'boolean',
|
||||
}
|
||||
|
||||
class Carousel extends BaseComponent {
|
||||
@@ -230,7 +224,7 @@ class Carousel extends BaseComponent {
|
||||
const swipeConfig = {
|
||||
leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
|
||||
rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),
|
||||
endCallback: endCallBack
|
||||
endCallback: endCallBack,
|
||||
}
|
||||
|
||||
this._swipeHelper = new Swipe(this._element, swipeConfig)
|
||||
@@ -289,7 +283,7 @@ class Carousel extends BaseComponent {
|
||||
|
||||
const activeElement = this._getActive()
|
||||
const isNext = order === ORDER_NEXT
|
||||
const nextElement = element || getNextActiveElement(this._getItems(), activeElement!, isNext, this._config.wrap as boolean) as HTMLElement
|
||||
const nextElement = element || (getNextActiveElement(this._getItems(), activeElement!, isNext, this._config.wrap as boolean) as HTMLElement)
|
||||
|
||||
if (nextElement === activeElement) {
|
||||
return
|
||||
@@ -302,7 +296,7 @@ class Carousel extends BaseComponent {
|
||||
relatedTarget: nextElement,
|
||||
direction: this._orderToDirection(order),
|
||||
from: this._getItemIndex(activeElement!),
|
||||
to: nextElementIndex
|
||||
to: nextElementIndex,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"], [data-tblr-toggle="co
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
parent: null,
|
||||
toggle: true
|
||||
toggle: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
parent: '(null|element)',
|
||||
toggle: 'boolean'
|
||||
toggle: 'boolean',
|
||||
}
|
||||
|
||||
class Collapse extends BaseComponent {
|
||||
@@ -59,8 +59,7 @@ class Collapse extends BaseComponent {
|
||||
|
||||
for (const elem of toggleList) {
|
||||
const selector = SelectorEngine.getSelectorFromElement(elem)
|
||||
const filterElement = SelectorEngine.find(selector!)
|
||||
.filter(foundElement => foundElement === this._element)
|
||||
const filterElement = SelectorEngine.find(selector!).filter((foundElement) => foundElement === this._element)
|
||||
|
||||
if (selector !== null && filterElement.length) {
|
||||
this._triggerArray.push(elem)
|
||||
@@ -107,8 +106,8 @@ class Collapse extends BaseComponent {
|
||||
|
||||
if (this._config.parent) {
|
||||
activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES)
|
||||
.filter(element => element !== this._element)
|
||||
.map(element => Collapse.getOrCreateInstance(element, { toggle: false }) as Collapse)
|
||||
.filter((element) => element !== this._element)
|
||||
.map((element) => Collapse.getOrCreateInstance(element, { toggle: false }) as Collapse)
|
||||
}
|
||||
|
||||
if (activeChildren.length && activeChildren[0]._isTransitioning) {
|
||||
@@ -225,7 +224,7 @@ class Collapse extends BaseComponent {
|
||||
|
||||
_getFirstLevelChildren(selector: string): HTMLElement[] {
|
||||
const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent as HTMLElement)
|
||||
return SelectorEngine.find(selector, this._config.parent as HTMLElement).filter(element => !children.includes(element))
|
||||
return SelectorEngine.find(selector, this._config.parent as HTMLElement).filter((element) => !children.includes(element))
|
||||
}
|
||||
|
||||
_addAriaAndCollapsedClass(triggerArray: HTMLElement[], isOpen: boolean): void {
|
||||
@@ -246,7 +245,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
|
||||
}
|
||||
|
||||
for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {
|
||||
(Collapse.getOrCreateInstance(element, { toggle: false }) as Collapse).toggle()
|
||||
;(Collapse.getOrCreateInstance(element, { toggle: false }) as Collapse).toggle()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const Data = {
|
||||
if (instanceMap.size === 0) {
|
||||
elementMap.delete(element)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Data
|
||||
|
||||
@@ -26,7 +26,7 @@ const eventRegistry: Record<string | number, Record<string, Record<string | numb
|
||||
let uidEvent = 1
|
||||
const customEvents: Record<string, string> = {
|
||||
mouseenter: 'mouseover',
|
||||
mouseleave: 'mouseout'
|
||||
mouseleave: 'mouseout',
|
||||
}
|
||||
|
||||
const nativeEvents = new Set([
|
||||
@@ -75,7 +75,7 @@ const nativeEvents = new Set([
|
||||
'readystatechange',
|
||||
'error',
|
||||
'abort',
|
||||
'scroll'
|
||||
'scroll',
|
||||
])
|
||||
|
||||
function makeEventUid(element: EventableElement | EventCallback, uid?: string): string | number {
|
||||
@@ -125,20 +125,11 @@ function bootstrapDelegationHandler(element: EventTarget, selector: string, fn:
|
||||
} as BootstrapHandler
|
||||
}
|
||||
|
||||
function findHandler(
|
||||
events: Record<string | number, BootstrapHandler>,
|
||||
callable: EventCallback,
|
||||
delegationSelector: string | null = null
|
||||
): BootstrapHandler | undefined {
|
||||
return Object.values(events)
|
||||
.find(event => event.callable === callable && event.delegationSelector === delegationSelector)
|
||||
function findHandler(events: Record<string | number, BootstrapHandler>, callable: EventCallback, delegationSelector: string | null = null): BootstrapHandler | undefined {
|
||||
return Object.values(events).find((event) => event.callable === callable && event.delegationSelector === delegationSelector)
|
||||
}
|
||||
|
||||
function normalizeParameters(
|
||||
originalTypeEvent: string,
|
||||
handler: string | EventCallback | undefined,
|
||||
delegationFunction: EventCallback | undefined
|
||||
): [boolean, EventCallback, string] {
|
||||
function normalizeParameters(originalTypeEvent: string, handler: string | EventCallback | undefined, delegationFunction: EventCallback | undefined): [boolean, EventCallback, string] {
|
||||
const isDelegated = typeof handler === 'string'
|
||||
const callable = isDelegated ? delegationFunction! : (handler || delegationFunction)!
|
||||
let typeEvent = getTypeEvent(originalTypeEvent)
|
||||
@@ -150,13 +141,7 @@ function normalizeParameters(
|
||||
return [isDelegated, callable, typeEvent]
|
||||
}
|
||||
|
||||
function addHandler(
|
||||
element: EventTarget | null,
|
||||
originalTypeEvent: string,
|
||||
handler: string | EventCallback | undefined,
|
||||
delegationFunction: EventCallback | undefined,
|
||||
oneOff: boolean
|
||||
): void {
|
||||
function addHandler(element: EventTarget | null, originalTypeEvent: string, handler: string | EventCallback | undefined, delegationFunction: EventCallback | undefined, oneOff: boolean): void {
|
||||
if (typeof originalTypeEvent !== 'string' || !element) {
|
||||
return
|
||||
}
|
||||
@@ -178,7 +163,7 @@ function addHandler(
|
||||
|
||||
const events = getElementEvents(element)
|
||||
const handlers = events[typeEvent] || (events[typeEvent] = {})
|
||||
const previousFunction = findHandler(handlers, callable, isDelegated ? handler as string : null)
|
||||
const previousFunction = findHandler(handlers, callable, isDelegated ? (handler as string) : null)
|
||||
|
||||
if (previousFunction) {
|
||||
previousFunction.oneOff = previousFunction.oneOff && oneOff
|
||||
@@ -187,11 +172,9 @@ function addHandler(
|
||||
}
|
||||
|
||||
const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))
|
||||
const fn: BootstrapHandler = isDelegated ?
|
||||
bootstrapDelegationHandler(element, handler as string, callable) :
|
||||
bootstrapHandler(element, callable)
|
||||
const fn: BootstrapHandler = isDelegated ? bootstrapDelegationHandler(element, handler as string, callable) : bootstrapHandler(element, callable)
|
||||
|
||||
fn.delegationSelector = isDelegated ? handler as string : null
|
||||
fn.delegationSelector = isDelegated ? (handler as string) : null
|
||||
fn.callable = callable
|
||||
fn.oneOff = oneOff
|
||||
fn.uidEvent = uid
|
||||
@@ -200,13 +183,7 @@ function addHandler(
|
||||
element.addEventListener(typeEvent, fn, isDelegated)
|
||||
}
|
||||
|
||||
function removeHandler(
|
||||
element: EventTarget,
|
||||
events: Record<string, Record<string | number, BootstrapHandler>>,
|
||||
typeEvent: string,
|
||||
handler: EventCallback,
|
||||
delegationSelector?: string | null
|
||||
): void {
|
||||
function removeHandler(element: EventTarget, events: Record<string, Record<string | number, BootstrapHandler>>, typeEvent: string, handler: EventCallback, delegationSelector?: string | null): void {
|
||||
const fn = findHandler(events[typeEvent], handler, delegationSelector ?? null)
|
||||
|
||||
if (!fn) {
|
||||
@@ -217,12 +194,7 @@ function removeHandler(
|
||||
delete events[typeEvent][fn.uidEvent!]
|
||||
}
|
||||
|
||||
function removeNamespacedHandlers(
|
||||
element: EventTarget,
|
||||
events: Record<string, Record<string | number, BootstrapHandler>>,
|
||||
typeEvent: string,
|
||||
namespace: string
|
||||
): void {
|
||||
function removeNamespacedHandlers(element: EventTarget, events: Record<string, Record<string | number, BootstrapHandler>>, typeEvent: string, namespace: string): void {
|
||||
const storeElementEvent = events[typeEvent] || {}
|
||||
|
||||
for (const [handlerKey, event] of Object.entries(storeElementEvent)) {
|
||||
@@ -262,7 +234,7 @@ const EventHandler = {
|
||||
return
|
||||
}
|
||||
|
||||
removeHandler(element, events, typeEvent, callable, isDelegated ? handler as string : null)
|
||||
removeHandler(element, events, typeEvent, callable, isDelegated ? (handler as string) : null)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,19 +263,19 @@ const EventHandler = {
|
||||
element.dispatchEvent(evt)
|
||||
|
||||
return evt
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function hydrateObj<T extends object>(obj: T, meta: Record<string, unknown> = {}): T {
|
||||
for (const [key, value] of Object.entries(meta)) {
|
||||
try {
|
||||
(obj as Record<string, unknown>)[key] = value
|
||||
;(obj as Record<string, unknown>)[key] = value
|
||||
} catch {
|
||||
Object.defineProperty(obj, key, {
|
||||
configurable: true,
|
||||
get() {
|
||||
return value
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ function normalizeData(value: string): DataValue {
|
||||
}
|
||||
|
||||
function normalizeDataKey(key: string): string {
|
||||
return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`)
|
||||
return key.replace(/[A-Z]/g, (chr) => `-${chr.toLowerCase()}`)
|
||||
}
|
||||
|
||||
const PREFIXES = ['tblr', 'bs'] as const
|
||||
@@ -60,7 +60,7 @@ const Manipulator = {
|
||||
const attributes: Record<string, DataValue> = {}
|
||||
|
||||
for (const prefix of PREFIXES) {
|
||||
const keys = Object.keys(element.dataset).filter(key => key.startsWith(prefix) && !key.startsWith(`${prefix}Config`))
|
||||
const keys = Object.keys(element.dataset).filter((key) => key.startsWith(prefix) && !key.startsWith(`${prefix}Config`))
|
||||
|
||||
for (const key of keys) {
|
||||
let pureKey = key.replace(new RegExp(`^${prefix}`), '')
|
||||
@@ -83,7 +83,7 @@ const Manipulator = {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Manipulator
|
||||
|
||||
@@ -24,7 +24,12 @@ const getSelector = (element: HTMLElement): string | null => {
|
||||
selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
|
||||
}
|
||||
|
||||
return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null
|
||||
return selector
|
||||
? selector
|
||||
.split(',')
|
||||
.map((sel) => parseSelector(sel))
|
||||
.join(',')
|
||||
: null
|
||||
}
|
||||
|
||||
const SelectorEngine = {
|
||||
@@ -37,7 +42,7 @@ const SelectorEngine = {
|
||||
},
|
||||
|
||||
children(element: HTMLElement, selector: string): HTMLElement[] {
|
||||
return Array.from(element.children).filter(child => child.matches(selector)) as HTMLElement[]
|
||||
return Array.from(element.children).filter((child) => child.matches(selector)) as HTMLElement[]
|
||||
},
|
||||
|
||||
parents(element: HTMLElement, selector: string): HTMLElement[] {
|
||||
@@ -81,18 +86,9 @@ const SelectorEngine = {
|
||||
},
|
||||
|
||||
focusableChildren(element: HTMLElement): HTMLElement[] {
|
||||
const focusables = [
|
||||
'a',
|
||||
'button',
|
||||
'input',
|
||||
'textarea',
|
||||
'select',
|
||||
'details',
|
||||
'[tabindex]',
|
||||
'[contenteditable="true"]'
|
||||
].map(selector => `${selector}:not([tabindex^="-"])`).join(',')
|
||||
const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map((selector) => `${selector}:not([tabindex^="-"])`).join(',')
|
||||
|
||||
return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el))
|
||||
return this.find(focusables, element).filter((el) => !isDisabled(el) && isVisible(el))
|
||||
},
|
||||
|
||||
getSelectorFromElement(element: HTMLElement): string | null {
|
||||
@@ -115,7 +111,7 @@ const SelectorEngine = {
|
||||
const selector = getSelector(element)
|
||||
|
||||
return selector ? SelectorEngine.find(selector) : []
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default SelectorEngine
|
||||
|
||||
@@ -10,16 +10,7 @@ import BaseComponent from './base-component'
|
||||
import EventHandler from './dom/event-handler'
|
||||
import Manipulator from './dom/manipulator'
|
||||
import SelectorEngine from './dom/selector-engine'
|
||||
import {
|
||||
execute,
|
||||
getElement,
|
||||
getNextActiveElement,
|
||||
isDisabled,
|
||||
isElement,
|
||||
isRTL,
|
||||
isVisible,
|
||||
noop
|
||||
} from './util/index'
|
||||
import { execute, getElement, getNextActiveElement, isDisabled, isElement, isRTL, isVisible, noop } from './util/index'
|
||||
import type { ComponentConfig, ComponentConfigType } from './types'
|
||||
|
||||
const NAME = 'dropdown'
|
||||
@@ -70,7 +61,7 @@ const Default: ComponentConfig = {
|
||||
display: 'dynamic',
|
||||
offset: [0, 2],
|
||||
popperConfig: null,
|
||||
reference: 'toggle'
|
||||
reference: 'toggle',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -79,7 +70,7 @@ const DefaultType: ComponentConfigType = {
|
||||
display: 'string',
|
||||
offset: '(array|string|function)',
|
||||
popperConfig: '(null|object|function)',
|
||||
reference: '(string|element|object)'
|
||||
reference: '(string|element|object)',
|
||||
}
|
||||
|
||||
class Dropdown extends BaseComponent {
|
||||
@@ -93,9 +84,7 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
this._popper = null
|
||||
this._parent = this._element.parentNode as HTMLElement
|
||||
this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
|
||||
SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
|
||||
SelectorEngine.findOne(SELECTOR_MENU, this._parent)!
|
||||
this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent)!
|
||||
this._inNavbar = this._detectNavbar()
|
||||
}
|
||||
|
||||
@@ -121,7 +110,7 @@ class Dropdown extends BaseComponent {
|
||||
}
|
||||
|
||||
const relatedTarget = {
|
||||
relatedTarget: this._element
|
||||
relatedTarget: this._element,
|
||||
}
|
||||
|
||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
|
||||
@@ -152,7 +141,7 @@ class Dropdown extends BaseComponent {
|
||||
}
|
||||
|
||||
const relatedTarget = {
|
||||
relatedTarget: this._element
|
||||
relatedTarget: this._element,
|
||||
}
|
||||
|
||||
this._completeHide(relatedTarget)
|
||||
@@ -199,9 +188,7 @@ class Dropdown extends BaseComponent {
|
||||
_getConfig(config: Partial<ComponentConfig>): ComponentConfig {
|
||||
config = super._getConfig(config)
|
||||
|
||||
if (typeof config.reference === 'object' && !isElement(config.reference) &&
|
||||
typeof (config.reference as any).getBoundingClientRect !== 'function'
|
||||
) {
|
||||
if (typeof config.reference === 'object' && !isElement(config.reference) && typeof (config.reference as any).getBoundingClientRect !== 'function') {
|
||||
throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
|
||||
}
|
||||
|
||||
@@ -210,7 +197,7 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
_createPopper(): void {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)')
|
||||
throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)")
|
||||
}
|
||||
|
||||
let referenceElement: HTMLElement | Popper.VirtualElement = this._element
|
||||
@@ -267,7 +254,7 @@ class Dropdown extends BaseComponent {
|
||||
const { offset } = this._config
|
||||
|
||||
if (typeof offset === 'string') {
|
||||
return offset.split(',').map(value => Number.parseInt(value, 10))
|
||||
return offset.split(',').map((value) => Number.parseInt(value, 10))
|
||||
}
|
||||
|
||||
if (typeof offset === 'function') {
|
||||
@@ -280,37 +267,41 @@ class Dropdown extends BaseComponent {
|
||||
_getPopperConfig(): Partial<Popper.Options> {
|
||||
const defaultBsPopperConfig: Partial<Popper.Options> = {
|
||||
placement: this._getPlacement() as Popper.Placement,
|
||||
modifiers: [{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: this._config.boundary
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: this._getOffset()
|
||||
}
|
||||
}]
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: this._config.boundary,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: this._getOffset(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (this._inNavbar || this._config.display === 'static') {
|
||||
Manipulator.setDataAttribute(this._menu, 'popper', 'static')
|
||||
defaultBsPopperConfig.modifiers = [{
|
||||
name: 'applyStyles',
|
||||
enabled: false
|
||||
}]
|
||||
defaultBsPopperConfig.modifiers = [
|
||||
{
|
||||
name: 'applyStyles',
|
||||
enabled: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const popperConfig = execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...(typeof popperConfig === 'object' && popperConfig !== null ? popperConfig : {})
|
||||
...(typeof popperConfig === 'object' && popperConfig !== null ? popperConfig : {}),
|
||||
}
|
||||
}
|
||||
|
||||
_selectMenuItem({ key, target }: { key: string; target: HTMLElement }): void {
|
||||
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
|
||||
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter((element) => isVisible(element))
|
||||
|
||||
if (!items.length) {
|
||||
return
|
||||
@@ -334,11 +325,7 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
const composedPath = event.composedPath()
|
||||
const isMenuTarget = composedPath.includes(context._menu)
|
||||
if (
|
||||
composedPath.includes(context._element) ||
|
||||
(context._config.autoClose === 'inside' && !isMenuTarget) ||
|
||||
(context._config.autoClose === 'outside' && isMenuTarget)
|
||||
) {
|
||||
if (composedPath.includes(context._element) || (context._config.autoClose === 'inside' && !isMenuTarget) || (context._config.autoClose === 'outside' && isMenuTarget)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -371,11 +358,7 @@ class Dropdown extends BaseComponent {
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?
|
||||
this :
|
||||
(SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||
|
||||
SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||
|
||||
SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, (event as any).delegateTarget.parentNode))
|
||||
const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, (event as any).delegateTarget.parentNode)
|
||||
|
||||
const instance = Dropdown.getOrCreateInstance(getToggleButton!) as Dropdown
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import SelectorEngine from './dom/selector-engine'
|
||||
import Backdrop from './util/backdrop'
|
||||
import { enableDismissTrigger } from './util/component-functions'
|
||||
import FocusTrap from './util/focustrap'
|
||||
import {
|
||||
isRTL, isVisible, reflow
|
||||
} from './util/index'
|
||||
import { isRTL, isVisible, reflow } from './util/index'
|
||||
import ScrollBarHelper from './util/scrollbar'
|
||||
|
||||
/**
|
||||
@@ -58,13 +56,13 @@ interface ComponentConfigType {
|
||||
const Default: ComponentConfig = {
|
||||
backdrop: true,
|
||||
focus: true,
|
||||
keyboard: true
|
||||
keyboard: true,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
backdrop: '(boolean|string)',
|
||||
focus: 'boolean',
|
||||
keyboard: 'boolean'
|
||||
keyboard: 'boolean',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +112,7 @@ class Modal extends BaseComponent {
|
||||
}
|
||||
|
||||
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
|
||||
relatedTarget
|
||||
relatedTarget,
|
||||
})
|
||||
|
||||
if (showEvent.defaultPrevented) {
|
||||
@@ -170,13 +168,13 @@ class Modal extends BaseComponent {
|
||||
_initializeBackDrop(): Backdrop {
|
||||
return new Backdrop({
|
||||
isVisible: Boolean(this._config.backdrop),
|
||||
isAnimated: this._isAnimated()
|
||||
isAnimated: this._isAnimated(),
|
||||
})
|
||||
}
|
||||
|
||||
_initializeFocusTrap(): FocusTrap {
|
||||
return new FocusTrap({
|
||||
trapElement: this._element
|
||||
trapElement: this._element,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,7 +205,7 @@ class Modal extends BaseComponent {
|
||||
|
||||
this._isTransitioning = false
|
||||
EventHandler.trigger(this._element, EVENT_SHOWN, {
|
||||
relatedTarget
|
||||
relatedTarget,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,13 @@ interface ComponentConfigType {
|
||||
const Default: ComponentConfig = {
|
||||
backdrop: true,
|
||||
keyboard: true,
|
||||
scroll: false
|
||||
scroll: false,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
backdrop: '(boolean|string)',
|
||||
keyboard: 'boolean',
|
||||
scroll: 'boolean'
|
||||
scroll: 'boolean',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,13 +186,13 @@ class Offcanvas extends BaseComponent {
|
||||
isVisible: isBackdropVisible,
|
||||
isAnimated: true,
|
||||
rootElement: this._element.parentNode as HTMLElement,
|
||||
clickCallback: isBackdropVisible ? clickCallback : null
|
||||
clickCallback: isBackdropVisible ? clickCallback : null,
|
||||
})
|
||||
}
|
||||
|
||||
_initializeFocusTrap(): FocusTrap {
|
||||
return new FocusTrap({
|
||||
trapElement: this._element
|
||||
trapElement: this._element,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -29,17 +29,13 @@ const Default: ComponentConfig = {
|
||||
content: '',
|
||||
offset: [0, 8],
|
||||
placement: 'right',
|
||||
template: '<div class="popover" role="tooltip">' +
|
||||
'<div class="popover-arrow"></div>' +
|
||||
'<h3 class="popover-header"></h3>' +
|
||||
'<div class="popover-body"></div>' +
|
||||
'</div>',
|
||||
trigger: 'click'
|
||||
template: '<div class="popover" role="tooltip">' + '<div class="popover-arrow"></div>' + '<h3 class="popover-header"></h3>' + '<div class="popover-body"></div>' + '</div>',
|
||||
trigger: 'click',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
...Tooltip.DefaultType,
|
||||
content: '(null|string|element|function)'
|
||||
content: '(null|string|element|function)',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +62,7 @@ class Popover extends Tooltip {
|
||||
_getContentForTemplate(): Record<string, any> {
|
||||
return {
|
||||
[SELECTOR_TITLE]: this._getTitle(),
|
||||
[SELECTOR_CONTENT]: this._getContent()
|
||||
[SELECTOR_CONTENT]: this._getContent(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const Default: ComponentConfig = {
|
||||
rootMargin: '0px 0px -25%',
|
||||
smoothScroll: false,
|
||||
target: null,
|
||||
threshold: [0.1, 0.5, 1]
|
||||
threshold: [0.1, 0.5, 1],
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -46,7 +46,7 @@ const DefaultType: ComponentConfigType = {
|
||||
rootMargin: 'string',
|
||||
smoothScroll: 'boolean',
|
||||
target: 'element',
|
||||
threshold: 'array'
|
||||
threshold: 'array',
|
||||
}
|
||||
|
||||
class ScrollSpy extends BaseComponent {
|
||||
@@ -70,7 +70,7 @@ class ScrollSpy extends BaseComponent {
|
||||
this._observer = null
|
||||
this._previousScrollData = {
|
||||
visibleEntryTop: 0,
|
||||
parentScrollTop: 0
|
||||
parentScrollTop: 0,
|
||||
}
|
||||
this.refresh()
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class ScrollSpy extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
(root as HTMLElement).scrollTop = height
|
||||
;(root as HTMLElement).scrollTop = height
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -146,10 +146,10 @@ class ScrollSpy extends BaseComponent {
|
||||
const options: IntersectionObserverInit = {
|
||||
root: this._rootElement,
|
||||
threshold: this._config.threshold as number[],
|
||||
rootMargin: this._config.rootMargin as string
|
||||
rootMargin: this._config.rootMargin as string,
|
||||
}
|
||||
|
||||
return new IntersectionObserver(entries => this._observerCallback(entries), options)
|
||||
return new IntersectionObserver((entries) => this._observerCallback(entries), options)
|
||||
}
|
||||
|
||||
_observerCallback(entries: IntersectionObserverEntry[]): void {
|
||||
@@ -222,8 +222,7 @@ class ScrollSpy extends BaseComponent {
|
||||
|
||||
_activateParents(target: HTMLElement): void {
|
||||
if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
|
||||
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)!)!
|
||||
.classList.add(CLASS_NAME_ACTIVE)
|
||||
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)!)!.classList.add(CLASS_NAME_ACTIVE)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -74,9 +74,7 @@ class Tab extends BaseComponent {
|
||||
|
||||
const active = this._getActiveElem()
|
||||
|
||||
const hideEvent = active ?
|
||||
EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem }) :
|
||||
null
|
||||
const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem }) : null
|
||||
|
||||
const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { relatedTarget: active })
|
||||
|
||||
@@ -107,7 +105,7 @@ class Tab extends BaseComponent {
|
||||
element.setAttribute('aria-selected', 'true')
|
||||
this._toggleDropDown(element, true)
|
||||
EventHandler.trigger(element, EVENT_SHOWN, {
|
||||
relatedTarget: relatedElem
|
||||
relatedTarget: relatedElem,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,14 +138,14 @@ class Tab extends BaseComponent {
|
||||
}
|
||||
|
||||
_keydown(event: KeyboardEvent): void {
|
||||
if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key))) {
|
||||
if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
const children = this._getChildren().filter(element => !isDisabled(element))
|
||||
const children = this._getChildren().filter((element) => !isDisabled(element))
|
||||
let nextActiveElement: HTMLElement | undefined
|
||||
|
||||
if ([HOME_KEY, END_KEY].includes(event.key)) {
|
||||
@@ -168,7 +166,7 @@ class Tab extends BaseComponent {
|
||||
}
|
||||
|
||||
_getActiveElem(): HTMLElement | null {
|
||||
return this._getChildren().find(child => this._elemIsActive(child)) || null
|
||||
return this._getChildren().find((child) => this._elemIsActive(child)) || null
|
||||
}
|
||||
|
||||
_setInitialAttributes(parent: HTMLElement, children: HTMLElement[]): void {
|
||||
|
||||
@@ -32,13 +32,13 @@ const CLASS_NAME_SHOWING = 'showing'
|
||||
const DefaultType: ComponentConfigType = {
|
||||
animation: 'boolean',
|
||||
autohide: 'boolean',
|
||||
delay: 'number'
|
||||
delay: 'number',
|
||||
}
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
delay: 5000,
|
||||
}
|
||||
|
||||
class Toast extends BaseComponent {
|
||||
|
||||
@@ -58,7 +58,7 @@ const AttachmentMap: Record<string, string> = {
|
||||
TOP: 'top',
|
||||
RIGHT: isRTL() ? 'left' : 'right',
|
||||
BOTTOM: 'bottom',
|
||||
LEFT: isRTL() ? 'right' : 'left'
|
||||
LEFT: isRTL() ? 'right' : 'left',
|
||||
}
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
@@ -76,12 +76,9 @@ const Default: ComponentConfig = {
|
||||
sanitize: true,
|
||||
sanitizeFn: null,
|
||||
selector: false,
|
||||
template: '<div class="tooltip" role="tooltip">' +
|
||||
'<div class="tooltip-arrow"></div>' +
|
||||
'<div class="tooltip-inner"></div>' +
|
||||
'</div>',
|
||||
template: '<div class="tooltip" role="tooltip">' + '<div class="tooltip-arrow"></div>' + '<div class="tooltip-inner"></div>' + '</div>',
|
||||
title: '',
|
||||
trigger: 'hover focus'
|
||||
trigger: 'hover focus',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -101,7 +98,7 @@ const DefaultType: ComponentConfigType = {
|
||||
selector: '(string|boolean)',
|
||||
template: 'string',
|
||||
title: '(string|element|function)',
|
||||
trigger: 'string'
|
||||
trigger: 'string',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +118,7 @@ class Tooltip extends BaseComponent {
|
||||
|
||||
constructor(element: HTMLElement | string, config?: Partial<ComponentConfig>) {
|
||||
if (typeof Popper === 'undefined') {
|
||||
throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)')
|
||||
throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)")
|
||||
}
|
||||
|
||||
super(element, config)
|
||||
@@ -187,9 +184,7 @@ class Tooltip extends BaseComponent {
|
||||
EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler)
|
||||
|
||||
if (this._element.getAttribute('data-bs-original-title') || this._element.getAttribute('data-tblr-original-title')) {
|
||||
this._element.setAttribute('title',
|
||||
this._element.getAttribute('data-bs-original-title') ||
|
||||
this._element.getAttribute('data-tblr-original-title') || '')
|
||||
this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title') || this._element.getAttribute('data-tblr-original-title') || '')
|
||||
}
|
||||
|
||||
this._disposePopper()
|
||||
@@ -343,7 +338,7 @@ class Tooltip extends BaseComponent {
|
||||
this._templateFactory = new TemplateFactory({
|
||||
...this._config,
|
||||
content,
|
||||
extraClass: this._resolvePossibleFunction(this._config.customClass)
|
||||
extraClass: this._resolvePossibleFunction(this._config.customClass),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,14 +347,12 @@ class Tooltip extends BaseComponent {
|
||||
|
||||
_getContentForTemplate(): Record<string, any> {
|
||||
return {
|
||||
[SELECTOR_TOOLTIP_INNER]: this._getTitle()
|
||||
[SELECTOR_TOOLTIP_INNER]: this._getTitle(),
|
||||
}
|
||||
}
|
||||
|
||||
_getTitle(): string {
|
||||
return this._resolvePossibleFunction(this._config.title) ||
|
||||
this._element.getAttribute('data-bs-original-title') ||
|
||||
this._element.getAttribute('data-tblr-original-title') || ''
|
||||
return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title') || this._element.getAttribute('data-tblr-original-title') || ''
|
||||
}
|
||||
|
||||
_initializeOnDelegatedTarget(event: Event & { delegateTarget?: HTMLElement }): Tooltip {
|
||||
@@ -405,26 +398,26 @@ class Tooltip extends BaseComponent {
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: this._config.fallbackPlacements
|
||||
}
|
||||
fallbackPlacements: this._config.fallbackPlacements,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: this._getOffset()
|
||||
}
|
||||
offset: this._getOffset(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: this._config.boundary
|
||||
}
|
||||
boundary: this._config.boundary,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: `.${(this.constructor as typeof Tooltip).NAME}-arrow`
|
||||
}
|
||||
element: `.${(this.constructor as typeof Tooltip).NAME}-arrow`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preSetPlacement',
|
||||
@@ -432,15 +425,15 @@ class Tooltip extends BaseComponent {
|
||||
phase: 'beforeMain',
|
||||
fn: (data: any) => {
|
||||
this._getTipElement()!.setAttribute('data-popper-placement', data.state.placement)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const popperConfig = execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
|
||||
return {
|
||||
...defaultBsPopperConfig,
|
||||
...(typeof popperConfig === 'object' && popperConfig !== null ? popperConfig : {})
|
||||
...(typeof popperConfig === 'object' && popperConfig !== null ? popperConfig : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,12 +448,8 @@ class Tooltip extends BaseComponent {
|
||||
context.toggle()
|
||||
})
|
||||
} else if (trigger !== TRIGGER_MANUAL) {
|
||||
const eventIn = trigger === TRIGGER_HOVER ?
|
||||
(this.constructor as typeof Tooltip).eventName(EVENT_MOUSEENTER) :
|
||||
(this.constructor as typeof Tooltip).eventName(EVENT_FOCUSIN)
|
||||
const eventOut = trigger === TRIGGER_HOVER ?
|
||||
(this.constructor as typeof Tooltip).eventName(EVENT_MOUSELEAVE) :
|
||||
(this.constructor as typeof Tooltip).eventName(EVENT_FOCUSOUT)
|
||||
const eventIn = trigger === TRIGGER_HOVER ? (this.constructor as typeof Tooltip).eventName(EVENT_MOUSEENTER) : (this.constructor as typeof Tooltip).eventName(EVENT_FOCUSIN)
|
||||
const eventOut = trigger === TRIGGER_HOVER ? (this.constructor as typeof Tooltip).eventName(EVENT_MOUSELEAVE) : (this.constructor as typeof Tooltip).eventName(EVENT_FOCUSOUT)
|
||||
|
||||
EventHandler.on(this._element, eventIn, this._config.selector, (event: Event) => {
|
||||
const context = this._initializeOnDelegatedTarget(event)
|
||||
@@ -469,8 +458,7 @@ class Tooltip extends BaseComponent {
|
||||
})
|
||||
EventHandler.on(this._element, eventOut, this._config.selector, (event: Event & { relatedTarget?: HTMLElement }) => {
|
||||
const context = this._initializeOnDelegatedTarget(event)
|
||||
context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] =
|
||||
context._element.contains(event.relatedTarget as Node)
|
||||
context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget as Node)
|
||||
|
||||
context._leave()
|
||||
})
|
||||
@@ -550,7 +538,7 @@ class Tooltip extends BaseComponent {
|
||||
|
||||
config = {
|
||||
...dataAttributes,
|
||||
...(typeof config === 'object' && config ? config : {})
|
||||
...(typeof config === 'object' && config ? config : {}),
|
||||
}
|
||||
config = this._mergeConfigObj(config)
|
||||
config = this._configAfterMerge(config)
|
||||
@@ -564,7 +552,7 @@ class Tooltip extends BaseComponent {
|
||||
if (typeof config.delay === 'number') {
|
||||
config.delay = {
|
||||
show: config.delay,
|
||||
hide: config.delay
|
||||
hide: config.delay,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
import EventHandler from '../dom/event-handler.js'
|
||||
import Config from './config'
|
||||
import {
|
||||
execute, executeAfterTransition, getElement, reflow
|
||||
} from './index'
|
||||
import { execute, executeAfterTransition, getElement, reflow } from './index'
|
||||
import type { ComponentConfig, ComponentConfigType } from '../types'
|
||||
|
||||
const NAME = 'backdrop'
|
||||
@@ -30,7 +28,7 @@ const Default: BackdropConfig = {
|
||||
clickCallback: null,
|
||||
isAnimated: false,
|
||||
isVisible: true,
|
||||
rootElement: 'body'
|
||||
rootElement: 'body',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -38,7 +36,7 @@ const DefaultType: ComponentConfigType = {
|
||||
clickCallback: '(function|null)',
|
||||
isAnimated: 'boolean',
|
||||
isVisible: 'boolean',
|
||||
rootElement: '(element|string)'
|
||||
rootElement: '(element|string)',
|
||||
}
|
||||
|
||||
class Backdrop extends Config {
|
||||
|
||||
@@ -36,6 +36,4 @@ const enableDismissTrigger = (component: DismissibleComponent, method = 'hide'):
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
enableDismissTrigger
|
||||
}
|
||||
export { enableDismissTrigger }
|
||||
|
||||
@@ -41,7 +41,7 @@ class Config {
|
||||
...ctor.Default,
|
||||
...(typeof jsonConfig === 'object' ? jsonConfig : {}),
|
||||
...(isElement(element) ? Manipulator.getDataAttributes(element!) : {}),
|
||||
...(typeof config === 'object' ? config : {})
|
||||
...(typeof config === 'object' ? config : {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ class Config {
|
||||
const valueType = isElement(value) ? 'element' : toType(value)
|
||||
|
||||
if (!new RegExp(expectedTypes).test(valueType)) {
|
||||
throw new TypeError(
|
||||
`${ctor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
|
||||
)
|
||||
throw new TypeError(`${ctor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ interface FocusTrapConfig {
|
||||
|
||||
const Default: FocusTrapConfig = {
|
||||
autofocus: true,
|
||||
trapElement: null
|
||||
trapElement: null,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
autofocus: 'boolean',
|
||||
trapElement: 'element'
|
||||
trapElement: 'element',
|
||||
}
|
||||
|
||||
class FocusTrap extends Config {
|
||||
|
||||
@@ -22,7 +22,10 @@ const toType = (object: unknown): string => {
|
||||
return `${object}`
|
||||
}
|
||||
|
||||
return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)![1].toLowerCase()
|
||||
return Object.prototype.toString
|
||||
.call(object)
|
||||
.match(/\s([a-z]+)/i)![1]
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
const getUID = (prefix: string): string => {
|
||||
@@ -203,21 +206,4 @@ const getNextActiveElement = <T>(list: T[], activeElement: T, shouldGetNext: boo
|
||||
return list[Math.max(0, Math.min(index, listLength - 1))]
|
||||
}
|
||||
|
||||
export {
|
||||
execute,
|
||||
executeAfterTransition,
|
||||
findShadowRoot,
|
||||
getElement,
|
||||
getNextActiveElement,
|
||||
getTransitionDurationFromElement,
|
||||
getUID,
|
||||
isDisabled,
|
||||
isElement,
|
||||
isRTL,
|
||||
isVisible,
|
||||
noop,
|
||||
parseSelector,
|
||||
reflow,
|
||||
triggerTransitionEnd,
|
||||
toType
|
||||
}
|
||||
export { execute, executeAfterTransition, findShadowRoot, getElement, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, parseSelector, reflow, triggerTransitionEnd, toType }
|
||||
|
||||
@@ -12,51 +12,42 @@ const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
|
||||
|
||||
export const DefaultAllowlist: AllowList = {
|
||||
'*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
|
||||
a: ['target', 'href', 'title', 'rel'],
|
||||
area: [],
|
||||
b: [],
|
||||
br: [],
|
||||
col: [],
|
||||
code: [],
|
||||
dd: [],
|
||||
div: [],
|
||||
dl: [],
|
||||
dt: [],
|
||||
em: [],
|
||||
hr: [],
|
||||
h1: [],
|
||||
h2: [],
|
||||
h3: [],
|
||||
h4: [],
|
||||
h5: [],
|
||||
h6: [],
|
||||
i: [],
|
||||
img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],
|
||||
li: [],
|
||||
ol: [],
|
||||
p: [],
|
||||
pre: [],
|
||||
s: [],
|
||||
small: [],
|
||||
span: [],
|
||||
sub: [],
|
||||
sup: [],
|
||||
strong: [],
|
||||
u: [],
|
||||
ul: []
|
||||
'a': ['target', 'href', 'title', 'rel'],
|
||||
'area': [],
|
||||
'b': [],
|
||||
'br': [],
|
||||
'col': [],
|
||||
'code': [],
|
||||
'dd': [],
|
||||
'div': [],
|
||||
'dl': [],
|
||||
'dt': [],
|
||||
'em': [],
|
||||
'hr': [],
|
||||
'h1': [],
|
||||
'h2': [],
|
||||
'h3': [],
|
||||
'h4': [],
|
||||
'h5': [],
|
||||
'h6': [],
|
||||
'i': [],
|
||||
'img': ['src', 'srcset', 'alt', 'title', 'width', 'height'],
|
||||
'li': [],
|
||||
'ol': [],
|
||||
'p': [],
|
||||
'pre': [],
|
||||
's': [],
|
||||
'small': [],
|
||||
'span': [],
|
||||
'sub': [],
|
||||
'sup': [],
|
||||
'strong': [],
|
||||
'u': [],
|
||||
'ul': [],
|
||||
}
|
||||
// js-docs-end allow-list
|
||||
|
||||
const uriAttributes = new Set([
|
||||
'background',
|
||||
'cite',
|
||||
'href',
|
||||
'itemtype',
|
||||
'longdesc',
|
||||
'poster',
|
||||
'src',
|
||||
'xlink:href'
|
||||
])
|
||||
const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href'])
|
||||
|
||||
const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i
|
||||
|
||||
@@ -71,8 +62,7 @@ const allowedAttribute = (attribute: Attr, allowedAttributeList: (string | RegEx
|
||||
return true
|
||||
}
|
||||
|
||||
return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp)
|
||||
.some(regex => (regex as RegExp).test(attributeName))
|
||||
return allowedAttributeList.filter((attributeRegex) => attributeRegex instanceof RegExp).some((regex) => (regex as RegExp).test(attributeName))
|
||||
}
|
||||
|
||||
export function sanitizeHtml(unsafeHtml: string, allowList: AllowList, sanitizeFunction?: SanitizeFn): string {
|
||||
|
||||
@@ -29,9 +29,9 @@ class ScrollBarHelper {
|
||||
hide(): void {
|
||||
const width = this.getWidth()
|
||||
this._disableOverFlow()
|
||||
this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width)
|
||||
this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width)
|
||||
this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width)
|
||||
this._setElementAttributes(this._element, PROPERTY_PADDING, (calculatedValue) => calculatedValue + width)
|
||||
this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, (calculatedValue) => calculatedValue + width)
|
||||
this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, (calculatedValue) => calculatedValue - width)
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
|
||||
@@ -31,13 +31,13 @@ interface SwipeConfig {
|
||||
const Default: SwipeConfig = {
|
||||
endCallback: null,
|
||||
leftCallback: null,
|
||||
rightCallback: null
|
||||
rightCallback: null,
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
endCallback: '(function|null)',
|
||||
leftCallback: '(function|null)',
|
||||
rightCallback: '(function|null)'
|
||||
rightCallback: '(function|null)',
|
||||
}
|
||||
|
||||
class Swipe extends Config {
|
||||
@@ -97,9 +97,7 @@ class Swipe extends Config {
|
||||
}
|
||||
|
||||
_move(event: Event): void {
|
||||
this._deltaX = (event as TouchEvent).touches && (event as TouchEvent).touches.length > 1 ?
|
||||
0 :
|
||||
(event as TouchEvent).touches[0].clientX - this._deltaX
|
||||
this._deltaX = (event as TouchEvent).touches && (event as TouchEvent).touches.length > 1 ? 0 : (event as TouchEvent).touches[0].clientX - this._deltaX
|
||||
}
|
||||
|
||||
_handleSwipe(): void {
|
||||
|
||||
@@ -30,7 +30,7 @@ const Default: TemplateFactoryConfig = {
|
||||
html: false,
|
||||
sanitize: true,
|
||||
sanitizeFn: null,
|
||||
template: '<div></div>'
|
||||
template: '<div></div>',
|
||||
}
|
||||
|
||||
const DefaultType: ComponentConfigType = {
|
||||
@@ -40,12 +40,12 @@ const DefaultType: ComponentConfigType = {
|
||||
html: 'boolean',
|
||||
sanitize: 'boolean',
|
||||
sanitizeFn: '(null|function)',
|
||||
template: 'string'
|
||||
template: 'string',
|
||||
}
|
||||
|
||||
const DefaultContentType: ComponentConfigType = {
|
||||
entry: '(string|element|function|null)',
|
||||
selector: '(string|element)'
|
||||
selector: '(string|element)',
|
||||
}
|
||||
|
||||
class TemplateFactory extends Config {
|
||||
@@ -70,7 +70,7 @@ class TemplateFactory extends Config {
|
||||
|
||||
getContent(): unknown[] {
|
||||
return Object.values(this._config.content)
|
||||
.map(config => this._resolvePossibleFunction(config))
|
||||
.map((config) => this._resolvePossibleFunction(config))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user