diff --git a/.github/workflows/update-bootstrap.yml b/.github/workflows/update-bootstrap.yml new file mode 100644 index 00000000..08b1fd7f --- /dev/null +++ b/.github/workflows/update-bootstrap.yml @@ -0,0 +1,64 @@ +name: Update Bootstrap + +# Daily pull of new dist files from upstream twbs/bootstrap. If the tracked +# branch has moved, re-import the assets, sync the version, verify the +# stylesheets still compile, and commit the result. + +on: + schedule: + - cron: '0 6 * * *' # every day at 06:00 UTC + workflow_dispatch: + inputs: + branch: + description: Upstream twbs/bootstrap branch to track + required: false + default: v6-dev + +permissions: + contents: write + +concurrency: + group: update-bootstrap + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/test/gemfiles/rails_7_0_dartsass.gemfile + UPSTREAM_BRANCH: ${{ github.event.inputs.branch || 'v6-dev' }} + steps: + - uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Import latest Bootstrap and sync version + id: update + run: | + bundle exec rake update"[$UPSTREAM_BRANCH]" + bundle exec rake sync_version"[$UPSTREAM_BRANCH]" + # Only consider the update's own outputs, never stray CI artifacts. + if [ -n "$(git status --porcelain -- assets lib/bootstrap/version.rb)" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "Already up to date with twbs/bootstrap@${UPSTREAM_BRANCH}." + fi + + - name: Verify the stylesheets still compile + if: steps.update.outputs.changed == 'true' + run: bundle exec rake debug + + - name: Commit and push + if: steps.update.outputs.changed == 'true' + run: | + sha=$(grep -oE "[0-9a-f]{40}" lib/bootstrap/version.rb | head -1) + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A -- assets lib/bootstrap/version.rb + git commit -m "Auto-update Bootstrap from twbs/bootstrap@${UPSTREAM_BRANCH} (${sha})" + git push diff --git a/.gitignore b/.gitignore index ec182536..c1df3a53 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,8 @@ Gemfile.lock .rvmrc .rbenv-version -# Ignore bundler config -/.bundle +# Ignore bundler config (in the repo root and under test/gemfiles/*) +.bundle/ /vendor/cache /vendor/bundle tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d48127..f439d9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ The changelog only includes changes specific to the RubyGem. The Bootstrap framework changes can be found in [the Releases section of twbs/bootstrap](https://github.com/twbs/bootstrap/releases). Release announcement posts on [the official Bootstrap blog](http://blog.getbootstrap.com) contain summaries of the most noteworthy changes made in each release of Bootstrap. +# 6.0.0.alpha1 + +First pre-release tracking Bootstrap 6 (upstream [`v6-dev`](https://github.com/twbs/bootstrap/tree/v6-dev)). **This is an alpha; expect breaking changes.** + +* **Sass module system.** Bootstrap 6 replaced `@import` with `@use`/`@forward`. + Import Bootstrap with `@use "bootstrap"` and customize variables via + `@use "bootstrap" with (...)`. See the [v5→v6 migration guide](https://github.com/twbs/bootstrap/blob/v6-dev/skills/bootstrap-v5-v6-migration/SKILL.md). +* **Dart Sass is required to compile the stylesheets.** LibSass/SassC + (`sassc-rails`) cannot compile the module system. `sassc-rails` remains a + supported Sass engine option for the gem, but Bootstrap 6's own stylesheets + will only compile under `dartsass-sprockets`, `dartsass-rails`, or + `cssbundling-rails`. +* The standalone `bootstrap-grid`, `bootstrap-reboot`, and `bootstrap-utilities` + Sass entry points were removed upstream; only `bootstrap` remains. +* **JavaScript is now ES-module only.** Bootstrap 6 removed the UMD bundle and + the `window.bootstrap` global, so the `bootstrap-sprockets` Sprockets manifest + and the `globalThis` shim are gone. Load Bootstrap via importmaps (see the + README). +* **Popper replaced by [Floating UI](https://floating-ui.com/).** The `popper_js` + runtime dependency was removed. The self-contained `bootstrap.bundle.{js,min.js}` + builds inline both `@floating-ui/dom` and `vanilla-calendar-pro`, so a single + importmap pin works with no extra dependencies (the recommended path). For + lighter-weight pinning of the non-bundled `bootstrap.{js,min.js}` or individual + component modules, a self-contained ESM build of `@floating-ui/dom` is also + vendored as `floating-ui.js`. + # 5.3.4 * Autoprefixer is now optional. diff --git a/Gemfile b/Gemfile index 9f4cb9a3..a7fb19d7 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source 'https://rubygems.org' gemspec group :development do - gem 'popper_js', '>= 1.12.3' gem 'dartsass-sprockets' end diff --git a/README.md b/README.md index 493a5c08..e3c552ad 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # Bootstrap Ruby Gem [](https://github.com/twbs/bootstrap-rubygem/actions/workflows/ci.yml) [](https://rubygems.org/gems/bootstrap) -[Bootstrap 5][bootstrap-home] ruby gem for Ruby on Rails (*Sprockets*/*Importmaps*) and Hanami (formerly Lotus). +[Bootstrap 6][bootstrap-home] ruby gem for Ruby on Rails (*Sprockets*/*Importmaps*) and Hanami (formerly Lotus). For Sass versions of Bootstrap 3 and 2 see [bootstrap-sass](https://github.com/twbs/bootstrap-sass) instead. +> **Bootstrap 6 (pre-release):** This is an alpha tracking the upstream +> [`v6-dev`](https://github.com/twbs/bootstrap/tree/v6-dev) branch. +> Bootstrap 6 moved its Sass to the [module system](https://sass-lang.com/documentation/at-rules/use) +> (`@use`/`@forward`), so its stylesheets require a **Dart Sass** engine to +> compile — LibSass/SassC (`sassc-rails`) cannot compile them. Its JavaScript is +> ES-module only and is loaded via importmaps. See the [CHANGELOG](CHANGELOG.md). +> For the previous stable release, use `gem 'bootstrap', '~> 5.3.8'`. + **Ruby on Rails Note**: Newer releases of Rails have added additional ways for assets to be processed. The `twbs/bootstrap-rubygem` is for use with Importmaps or Sprockets, but not Webpack. @@ -21,14 +29,16 @@ Please see the appropriate guide for your environment of choice: Add `bootstrap` to your Gemfile: ```ruby -gem 'bootstrap', '~> 5.3.8' +gem 'bootstrap', '~> 6.0.0.alpha1' ``` -This gem requires a Sass engine, so make sure you have **one** of these gems in your Gemfile: +This gem requires a Sass engine, so make sure you have **one** of these gems in your Gemfile. +Bootstrap 6 stylesheets use the Sass module system, so a **Dart Sass** engine is +required to compile them — `sassc-rails` (LibSass) can no longer compile Bootstrap: - [`dartsass-sprockets`](https://github.com/tablecheck/dartsass-sprockets): Dart Sass engine, recommended but only works for Ruby 2.6+ and Rails 5+ - [`dartsass-rails`](https://github.com/rails/dartsass-rails): Dart Sass engine, recommended for Rails projects that use Propshaft - [`cssbundling-rails`](https://github.com/rails/cssbundling-rails): External Sass engine -- [`sassc-rails`](https://github.com/sass/sassc-rails): SassC engine, deprecated but compatible with Ruby 2.3+ and Rails 4 +- [`sassc-rails`](https://github.com/sass/sassc-rails): SassC engine, deprecated and compatible with Ruby 2.3+ and Rails 4, but **cannot compile Bootstrap 6** stylesheets Also ensure that `sprockets-rails` is at least v2.3.2. @@ -37,14 +47,24 @@ If you are using Rails, add the `autoprefixer-rails` gem to your app and ensure `bundle install` and restart your server to make the files available through the pipeline. -Import Bootstrap styles in `app/assets/stylesheets/application.scss`: +Import Bootstrap styles in `app/assets/stylesheets/application.scss` with `@use` +(Bootstrap 6 no longer supports `@import`): + +```scss +@use "bootstrap"; +``` + +To customize Bootstrap's variables, configure them through the `@use ... with` +rule instead of setting globals before an `@import`: ```scss -// Custom bootstrap variables must be set or imported *before* bootstrap. -@import "bootstrap"; +@use "bootstrap" with ( + $primary: #c0ffee, + $enable-rounded: false +); ``` -The available variables can be found [here](assets/stylesheets/bootstrap/_variables.scss). +The available variables can be found in [`bootstrap/_config.scss`](assets/stylesheets/bootstrap/_config.scss). Make sure the file has `.scss` extension (or `.sass` for Sass syntax). If you have just generated a new Rails app, it may come with a `.css` file instead. If this file exists, it will be served instead of Sass, so rename it: @@ -53,57 +73,68 @@ it may come with a `.css` file instead. If this file exists, it will be served i $ mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss ``` -Then, remove all the `*= require` and `*= require_tree` statements from the Sass file. Instead, use `@import` to import Sass files. +Then, remove all the `*= require` and `*= require_tree` statements from the Sass file. Instead, use `@use` to import Sass files. Do not use `*= require` in Sass or your other stylesheets will not be able to access the Bootstrap mixins and variables. -Bootstrap JavaScript can optionally use jQuery. -If you're using Rails 5.1+, you can add the `jquery-rails` gem to your Gemfile: +### JavaScript -```ruby -gem 'jquery-rails' -``` +Bootstrap 6's JavaScript is **ES-module only** — there is no UMD bundle and no +`window.bootstrap` global, so the old Sprockets `//= require bootstrap-sprockets` +concatenation is gone. Load it through **importmaps** instead. -Bootstrap tooltips and popovers depend on [popper.js] for positioning. -The `bootstrap` gem already depends on the -[popper_js](https://github.com/glebm/popper_js-rubygem) gem. +Bootstrap 6 uses [Floating UI](https://floating-ui.com/) (`@floating-ui/dom`) +for positioning tooltips, popovers, and menus, replacing Popper. A self-contained +ESM build of `@floating-ui/dom` is **vendored in this gem** as `floating-ui.js`, +so you do not need an external dependency for it. #### Importmaps -You can pin either `bootstrap.js` or `bootstrap.min.js` in `config/importmap.rb` -as well as `popper.js`: +The simplest option is the self-contained **bundle** +(`bootstrap.bundle.min.js`), which inlines Floating UI and `vanilla-calendar-pro`, +so it needs no other pins. In `config/importmap.rb`: ```ruby -pin "bootstrap", to: "bootstrap.min.js", preload: true -pin "@popperjs/core", to: "popper.js", preload: true +pin "bootstrap", to: "bootstrap.bundle.min.js", preload: true ``` -Whichever files you pin will need to be added to `config.assets.precompile`: +Then import the components you need from your application's entrypoint: -```ruby -# config/initializers/assets.rb -Rails.application.config.assets.precompile += %w(bootstrap.min.js popper.js) +```js +// app/javascript/application.js +import { Tooltip } from "bootstrap" + +for (const el of document.querySelectorAll('[data-bs-toggle="tooltip"]')) { + new Tooltip(el) +} ``` -#### Sprockets +The data-attribute APIs (`data-bs-toggle`, etc.) work automatically once the +module is loaded. -Add Bootstrap dependencies and Bootstrap to your `application.js`: + +Lighter-weight pinning (without the bundle) -```js -//= require jquery3 -//= require popper -//= require bootstrap-sprockets +If you want to avoid the bundled Floating UI / Datepicker code, pin the +non-bundled `bootstrap.min.js` together with the gem's **vendored** +`@floating-ui/dom` build: + +```ruby +pin "bootstrap", to: "bootstrap.min.js", preload: true +pin "@floating-ui/dom", to: "floating-ui.js", preload: true ``` -While `bootstrap-sprockets` provides individual Bootstrap components -for ease of debugging, you may alternatively require -the concatenated `bootstrap` for faster compilation: +Individual components are also available as separate modules +(e.g. `pin "bootstrap/tooltip", to: "bootstrap/tooltip.js"`) for finer-grained +pinning. The [Datepicker](https://getbootstrap.com/) component additionally +depends on [`vanilla-calendar-pro`](https://www.npmjs.com/package/vanilla-calendar-pro), +which is **not** vendored — if you use it (or the non-bundled `bootstrap.min.js`, +which imports it), pin it from a CDN: -```js -//= require jquery3 -//= require popper -//= require bootstrap +```ruby +pin "vanilla-calendar-pro", to: "https://ga.jspm.io/npm:vanilla-calendar-pro@3.1.0/index.js" ``` + ### b. Other Ruby frameworks @@ -123,13 +154,11 @@ By default all of Bootstrap is imported. You can also import components explicitly. To start with a full list of modules copy [`_bootstrap.scss`](assets/stylesheets/_bootstrap.scss) file into your assets as `_bootstrap-custom.scss`. Then comment out components you do not want from `_bootstrap-custom`. -In the application Sass file, replace `@import 'bootstrap'` with: +In the application Sass file, replace `@use 'bootstrap'` with: ```scss -@import 'bootstrap-custom'; +@use 'bootstrap-custom'; ``` [bootstrap-home]: https://getbootstrap.com -[bootstrap-variables.scss]: https://github.com/twbs/bootstrap-rubygem/blob/master/templates/project/_bootstrap-variables.scss [autoprefixer]: https://github.com/ai/autoprefixer -[popper.js]: https://popper.js.org diff --git a/Rakefile b/Rakefile index 52402cce..d94b9a6a 100644 --- a/Rakefile +++ b/Rakefile @@ -58,7 +58,8 @@ task :debug do require './lib/bootstrap' require 'term/ansicolor' path = Bootstrap.stylesheets_path - %w(_bootstrap _bootstrap-reboot _bootstrap-grid).each do |file| + # Bootstrap 6 exposes a single `bootstrap` entry point. + %w(_bootstrap).each do |file| filename = "#{path}/#{file}.scss" css = if defined?(SassC::Engine) SassC::Engine.new(File.read(filename), filename: filename, syntax: :scss).render @@ -77,6 +78,22 @@ task :update, :branch do |t, args| Updater.new(branch: args[:branch]).update_bootstrap end +desc 'Update only bootstrap stylesheets from upstream (leaves JS untouched)' +task :update_scss, :branch do |t, args| + require './tasks/updater' + Updater.new(branch: args[:branch], skip_js: true).update_bootstrap +end + +desc 'Sync the gem VERSION to the upstream Bootstrap package.json version' +task :sync_version, :branch do |t, args| + require './tasks/updater' + # npm prerelease versions (e.g. 6.0.0-alpha1) map to RubyGems (6.0.0.alpha1). + gem_version = Updater.new(branch: args[:branch]).upstream_version.sub('-', '.') + path = 'lib/bootstrap/version.rb' + File.write(path, File.read(path).sub(/VERSION\s*=\s*'[^']*'/, "VERSION = '#{gem_version}'")) + $stderr.puts "VERSION set to #{gem_version}" +end + desc 'Start a dummy Rails app server' task :rails_server do require 'rack' diff --git a/assets/javascripts/bootstrap-global-this-define.js b/assets/javascripts/bootstrap-global-this-define.js deleted file mode 100644 index f3a3cc13..00000000 --- a/assets/javascripts/bootstrap-global-this-define.js +++ /dev/null @@ -1,6 +0,0 @@ -// Set a `globalThis` so that bootstrap components are defined on window.bootstrap instead of window. -window['bootstrap'] = { - "@popperjs/core": window.Popper, - _originalGlobalThis: window['globalThis'] -}; -window['globalThis'] = window['bootstrap']; diff --git a/assets/javascripts/bootstrap-global-this-undefine.js b/assets/javascripts/bootstrap-global-this-undefine.js deleted file mode 100644 index 6f96eb1d..00000000 --- a/assets/javascripts/bootstrap-global-this-undefine.js +++ /dev/null @@ -1,2 +0,0 @@ -window['globalThis'] = window['bootstrap']._originalGlobalThis; -window['bootstrap']._originalGlobalThis = null; diff --git a/assets/javascripts/bootstrap-sprockets.js b/assets/javascripts/bootstrap-sprockets.js deleted file mode 100644 index 8235e211..00000000 --- a/assets/javascripts/bootstrap-sprockets.js +++ /dev/null @@ -1,28 +0,0 @@ -//= require ./bootstrap-global-this-define -//= require ./bootstrap/dom/data -//= require ./bootstrap/util/index -//= require ./bootstrap/dom/event-handler -//= require ./bootstrap/dom/manipulator -//= require ./bootstrap/util/config -//= require ./bootstrap/base-component -//= require ./bootstrap/util/sanitizer -//= require ./bootstrap/dom/selector-engine -//= require ./bootstrap/util/template-factory -//= require ./bootstrap/tooltip -//= require ./bootstrap/popover -//= require ./bootstrap/util/component-functions -//= require ./bootstrap/alert -//= require ./bootstrap/toast -//= require ./bootstrap/button -//= require ./bootstrap/collapse -//= require ./bootstrap/util/scrollbar -//= require ./bootstrap/tab -//= require ./bootstrap/util/focustrap -//= require ./bootstrap/util/backdrop -//= require ./bootstrap/scrollspy -//= require ./bootstrap/modal -//= require ./bootstrap/offcanvas -//= require ./bootstrap/util/swipe -//= require ./bootstrap/carousel -//= require ./bootstrap/dropdown -//= require ./bootstrap-global-this-undefine diff --git a/assets/javascripts/bootstrap.bundle.js b/assets/javascripts/bootstrap.bundle.js new file mode 100644 index 00000000..ad126dcb --- /dev/null +++ b/assets/javascripts/bootstrap.bundle.js @@ -0,0 +1,9567 @@ +/*! + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const elementMap = new Map(); +const Data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; + } + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; + } + return null; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; + } + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; + } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); + + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); + } + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll', 'scrollend']); + +/** + * Private methods + */ + +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + return fn.apply(element, [event]); + }; +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); + } + } + }; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); + } + }; + }; + callable = wrapFunction(callable); + } + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; + } + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; + } + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; + const events = getElementEvents(element); + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); + return; + } + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } + } + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); + } + } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; + } +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; + } + }); + } + } + return obj; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +function normalizeData(value) { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; + } + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement$1 = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement$1(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement$1(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { + return false; + } + if (summary === null) { + return false; + } + } + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; + } + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { + return null; + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const isRTL$1 = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { + return; + } + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); + }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Class definition + */ + +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement$1(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement$1(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement$1(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); + } + } + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const VERSION = '6.0.0-alpha1'; + +/** + * Class definition + */ + +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; + } + this._element = element; + this._config = this._getConfig(config); + + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); + } + Data.set(this._element, this.constructor.DATA_KEY, this); + } + + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; + } + } + + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; + } + + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; + } + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; + } + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; + } + next = next.nextElementSibling; + } + return []; + }, + focusableChildren(element) { + 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)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); + } + for (const instance of instances) { + instance[method](); + } + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event + }); + }); +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$l = 'alert'; +const DATA_KEY$h = 'bs.alert'; +const EVENT_KEY$i = `.${DATA_KEY$h}`; +const EVENT_CLOSE = `close${EVENT_KEY$i}`; +const EVENT_CLOSED = `closed${EVENT_KEY$i}`; +const CLASS_NAME_FADE$4 = 'fade'; +const CLASS_NAME_SHOW$6 = 'show'; + +/** + * Class definition + */ + +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME$l; + } + + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; + } + this._element.classList.remove(CLASS_NAME_SHOW$6); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$4); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + } + + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Alert, 'close'); + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$k = 'button'; +const DATA_KEY$g = 'bs.button'; +const EVENT_KEY$h = `.${DATA_KEY$g}`; +const DATA_API_KEY$c = '.data-api'; +const CLASS_NAME_ACTIVE$4 = 'active'; +const SELECTOR_DATA_TOGGLE$a = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API$8 = `click${EVENT_KEY$h}${DATA_API_KEY$c}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME$k; + } + + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$4)); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$8, SELECTOR_DATA_TOGGLE$a, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE$a); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$j = 'carousel'; +const DATA_KEY$f = 'bs.carousel'; +const EVENT_KEY$g = `.${DATA_KEY$f}`; +const DATA_API_KEY$b = '.data-api'; +const ARROW_LEFT_KEY$2 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$2 = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY$g}`; +const EVENT_SLID = `slid${EVENT_KEY$g}`; +const EVENT_KEYDOWN$2 = `keydown${EVENT_KEY$g}`; +const EVENT_MOUSEENTER$2 = `mouseenter${EVENT_KEY$g}`; +const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$g}`; +const EVENT_POINTERDOWN$1 = `pointerdown${EVENT_KEY$g}`; +const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$g}${DATA_API_KEY$b}`; +const EVENT_CLICK_DATA_API$7 = `click${EVENT_KEY$g}${DATA_API_KEY$b}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE$3 = 'active'; +const CLASS_NAME_FADE$3 = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER$1 = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY$2]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY$2]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default$i = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType$i = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER$1, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); + } + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default$i; + } + static get DefaultType() { + return DefaultType$i; + } + static get NAME() { + return NAME$j; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); + } + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); + + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; + } + if (rawIndex < 0) { + this._loopTransition(false); + return; + } + } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + if (this._isFade()) { + this._fadeTo(targetIndex); + return; + } + + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); + } + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); + } + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + } + + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); + } + this._viewport.style.scrollSnapType = ''; + + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY$g); + super.dispose(); + } + + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default$i.ends; + } + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN$2, event => this._keydown(event)); + } + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER$2, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); + } + + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN$1, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { + this.next(); + } + } + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; + } + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); + } + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; + } + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); + } + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; + } + } + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; + } + + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); + } + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } + + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; + } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; + + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; + } + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; + } + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; + } + + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); + } + + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL$1() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE$3); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); + } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); + } + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL$1()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } + + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; + } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE$3, index === this._activeIndex); + } + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; + } + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true + }); + } + control.disabled = disabled; + } + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; + } + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE$3); + active.removeAttribute('aria-current'); + } + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE$3); + newActive.setAttribute('aria-current', 'true'); + } + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; + } + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; + } + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; + } + return index; + } + + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } + + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; + } + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL$1()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); + return; + } + this._scheduleAutoplay(upcoming); + }, interval); + } + + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { + return; + } + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); + return; + } + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { + return; + } + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); + } + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE$3); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); + + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$i = 'collapse'; +const DATA_KEY$e = 'bs.collapse'; +const EVENT_KEY$f = `.${DATA_KEY$e}`; +const DATA_API_KEY$a = '.data-api'; +const EVENT_SHOW$7 = `show${EVENT_KEY$f}`; +const EVENT_SHOWN$6 = `shown${EVENT_KEY$f}`; +const EVENT_HIDE$6 = `hide${EVENT_KEY$f}`; +const EVENT_HIDDEN$8 = `hidden${EVENT_KEY$f}`; +const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$f}${DATA_API_KEY$a}`; +const CLASS_NAME_SHOW$5 = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE$9 = '[data-bs-toggle="collapse"]'; +const Default$h = { + parent: null, + toggle: true +}; +const DefaultType$h = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$9); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); + } + } + + // Getters + static get Default() { + return Default$h; + } + static get DefaultType() { + return DefaultType$h; + } + static get NAME() { + return NAME$i; + } + + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); + } + } + show() { + if (this._isTransitioning || this._isShown()) { + return; + } + let activeChildren = []; + + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); + } + if (activeChildren.length && activeChildren[0]._isTransitioning) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$7); + if (startEvent.defaultPrevented) { + return; + } + for (const activeInstance of activeChildren) { + activeInstance.hide(); + } + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN$6); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); + } + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN$8); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } + + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW$5); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); + } + } + } + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; + } + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$9, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); + +/** + * Custom positioning reference element. + * @see https://floating-ui.com/docs/virtual-elements + */ + +const min = Math.min; +const max = Math.max; +const round = Math.round; +const floor = Math.floor; +const createCoords = v => ({ + x: v, + y: v +}); +const oppositeSideMap = { + left: 'right', + right: 'left', + bottom: 'top', + top: 'bottom' +}; +function clamp(start, value, end) { + return max(start, min(value, end)); +} +function evaluate(value, param) { + return typeof value === 'function' ? value(param) : value; +} +function getSide(placement) { + return placement.split('-')[0]; +} +function getAlignment(placement) { + return placement.split('-')[1]; +} +function getOppositeAxis(axis) { + return axis === 'x' ? 'y' : 'x'; +} +function getAxisLength(axis) { + return axis === 'y' ? 'height' : 'width'; +} +function getSideAxis(placement) { + const firstChar = placement[0]; + return firstChar === 't' || firstChar === 'b' ? 'y' : 'x'; +} +function getAlignmentAxis(placement) { + return getOppositeAxis(getSideAxis(placement)); +} +function getAlignmentSides(placement, rects, rtl) { + if (rtl === void 0) { + rtl = false; + } + const alignment = getAlignment(placement); + const alignmentAxis = getAlignmentAxis(placement); + const length = getAxisLength(alignmentAxis); + let mainAlignmentSide = alignmentAxis === 'x' ? alignment === (rtl ? 'end' : 'start') ? 'right' : 'left' : alignment === 'start' ? 'bottom' : 'top'; + if (rects.reference[length] > rects.floating[length]) { + mainAlignmentSide = getOppositePlacement(mainAlignmentSide); + } + return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)]; +} +function getExpandedPlacements(placement) { + const oppositePlacement = getOppositePlacement(placement); + return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)]; +} +function getOppositeAlignmentPlacement(placement) { + return placement.includes('start') ? placement.replace('start', 'end') : placement.replace('end', 'start'); +} +const lrPlacement = ['left', 'right']; +const rlPlacement = ['right', 'left']; +const tbPlacement = ['top', 'bottom']; +const btPlacement = ['bottom', 'top']; +function getSideList(side, isStart, rtl) { + switch (side) { + case 'top': + case 'bottom': + if (rtl) return isStart ? rlPlacement : lrPlacement; + return isStart ? lrPlacement : rlPlacement; + case 'left': + case 'right': + return isStart ? tbPlacement : btPlacement; + default: + return []; + } +} +function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) { + const alignment = getAlignment(placement); + let list = getSideList(getSide(placement), direction === 'start', rtl); + if (alignment) { + list = list.map(side => side + "-" + alignment); + if (flipAlignment) { + list = list.concat(list.map(getOppositeAlignmentPlacement)); + } + } + return list; +} +function getOppositePlacement(placement) { + const side = getSide(placement); + return oppositeSideMap[side] + placement.slice(side.length); +} +function expandPaddingObject(padding) { + return { + top: 0, + right: 0, + bottom: 0, + left: 0, + ...padding + }; +} +function getPaddingObject(padding) { + return typeof padding !== 'number' ? expandPaddingObject(padding) : { + top: padding, + right: padding, + bottom: padding, + left: padding + }; +} +function rectToClientRect(rect) { + const { + x, + y, + width, + height + } = rect; + return { + width, + height, + top: y, + left: x, + right: x + width, + bottom: y + height, + x, + y + }; +} + +function computeCoordsFromPlacement(_ref, placement, rtl) { + let { + reference, + floating + } = _ref; + const sideAxis = getSideAxis(placement); + const alignmentAxis = getAlignmentAxis(placement); + const alignLength = getAxisLength(alignmentAxis); + const side = getSide(placement); + const isVertical = sideAxis === 'y'; + const commonX = reference.x + reference.width / 2 - floating.width / 2; + const commonY = reference.y + reference.height / 2 - floating.height / 2; + const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2; + let coords; + switch (side) { + case 'top': + coords = { + x: commonX, + y: reference.y - floating.height + }; + break; + case 'bottom': + coords = { + x: commonX, + y: reference.y + reference.height + }; + break; + case 'right': + coords = { + x: reference.x + reference.width, + y: commonY + }; + break; + case 'left': + coords = { + x: reference.x - floating.width, + y: commonY + }; + break; + default: + coords = { + x: reference.x, + y: reference.y + }; + } + switch (getAlignment(placement)) { + case 'start': + coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1); + break; + case 'end': + coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1); + break; + } + return coords; +} + +/** + * Resolves with an object of overflow side offsets that determine how much the + * element is overflowing a given clipping boundary on each side. + * - positive = overflowing the boundary by that number of pixels + * - negative = how many pixels left before it will overflow + * - 0 = lies flush with the boundary + * @see https://floating-ui.com/docs/detectOverflow + */ +async function detectOverflow(state, options) { + var _await$platform$isEle; + if (options === void 0) { + options = {}; + } + const { + x, + y, + platform, + rects, + elements, + strategy + } = state; + const { + boundary = 'clippingAncestors', + rootBoundary = 'viewport', + elementContext = 'floating', + altBoundary = false, + padding = 0 + } = evaluate(options, state); + const paddingObject = getPaddingObject(padding); + const altContext = elementContext === 'floating' ? 'reference' : 'floating'; + const element = elements[altBoundary ? altContext : elementContext]; + const clippingClientRect = rectToClientRect(await platform.getClippingRect({ + element: ((_await$platform$isEle = await (platform.isElement == null ? void 0 : platform.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || (await (platform.getDocumentElement == null ? void 0 : platform.getDocumentElement(elements.floating))), + boundary, + rootBoundary, + strategy + })); + const rect = elementContext === 'floating' ? { + x, + y, + width: rects.floating.width, + height: rects.floating.height + } : rects.reference; + const offsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(elements.floating)); + const offsetScale = (await (platform.isElement == null ? void 0 : platform.isElement(offsetParent))) ? (await (platform.getScale == null ? void 0 : platform.getScale(offsetParent))) || { + x: 1, + y: 1 + } : { + x: 1, + y: 1 + }; + const elementClientRect = rectToClientRect(platform.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform.convertOffsetParentRelativeRectToViewportRelativeRect({ + elements, + rect, + offsetParent, + strategy + }) : rect); + return { + top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y, + bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y, + left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x, + right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x + }; +} + +// Maximum number of resets that can occur before bailing to avoid infinite reset loops. +const MAX_RESET_COUNT = 50; + +/** + * Computes the `x` and `y` coordinates that will place the floating element + * next to a given reference element. + * + * This export does not have any `platform` interface logic. You will need to + * write one for the platform you are using Floating UI with. + */ +const computePosition$1 = async (reference, floating, config) => { + const { + placement = 'bottom', + strategy = 'absolute', + middleware = [], + platform + } = config; + const platformWithDetectOverflow = platform.detectOverflow ? platform : { + ...platform, + detectOverflow + }; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(floating)); + let rects = await platform.getElementRects({ + reference, + floating, + strategy + }); + let { + x, + y + } = computeCoordsFromPlacement(rects, placement, rtl); + let statefulPlacement = placement; + let resetCount = 0; + const middlewareData = {}; + for (let i = 0; i < middleware.length; i++) { + const currentMiddleware = middleware[i]; + if (!currentMiddleware) { + continue; + } + const { + name, + fn + } = currentMiddleware; + const { + x: nextX, + y: nextY, + data, + reset + } = await fn({ + x, + y, + initialPlacement: placement, + placement: statefulPlacement, + strategy, + middlewareData, + rects, + platform: platformWithDetectOverflow, + elements: { + reference, + floating + } + }); + x = nextX != null ? nextX : x; + y = nextY != null ? nextY : y; + middlewareData[name] = { + ...middlewareData[name], + ...data + }; + if (reset && resetCount < MAX_RESET_COUNT) { + resetCount++; + if (typeof reset === 'object') { + if (reset.placement) { + statefulPlacement = reset.placement; + } + if (reset.rects) { + rects = reset.rects === true ? await platform.getElementRects({ + reference, + floating, + strategy + }) : reset.rects; + } + ({ + x, + y + } = computeCoordsFromPlacement(rects, statefulPlacement, rtl)); + } + i = -1; + } + } + return { + x, + y, + placement: statefulPlacement, + strategy, + middlewareData + }; +}; + +/** + * Provides data to position an inner element of the floating element so that it + * appears centered to the reference element. + * @see https://floating-ui.com/docs/arrow + */ +const arrow$1 = options => ({ + name: 'arrow', + options, + async fn(state) { + const { + x, + y, + placement, + rects, + platform, + elements, + middlewareData + } = state; + // Since `element` is required, we don't Partial<> the type. + const { + element, + padding = 0 + } = evaluate(options, state) || {}; + if (element == null) { + return {}; + } + const paddingObject = getPaddingObject(padding); + const coords = { + x, + y + }; + const axis = getAlignmentAxis(placement); + const length = getAxisLength(axis); + const arrowDimensions = await platform.getDimensions(element); + const isYAxis = axis === 'y'; + const minProp = isYAxis ? 'top' : 'left'; + const maxProp = isYAxis ? 'bottom' : 'right'; + const clientProp = isYAxis ? 'clientHeight' : 'clientWidth'; + const endDiff = rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]; + const startDiff = coords[axis] - rects.reference[axis]; + const arrowOffsetParent = await (platform.getOffsetParent == null ? void 0 : platform.getOffsetParent(element)); + let clientSize = arrowOffsetParent ? arrowOffsetParent[clientProp] : 0; + + // DOM platform can return `window` as the `offsetParent`. + if (!clientSize || !(await (platform.isElement == null ? void 0 : platform.isElement(arrowOffsetParent)))) { + clientSize = elements.floating[clientProp] || rects.floating[length]; + } + const centerToReference = endDiff / 2 - startDiff / 2; + + // If the padding is large enough that it causes the arrow to no longer be + // centered, modify the padding so that it is centered. + const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1; + const minPadding = min(paddingObject[minProp], largestPossiblePadding); + const maxPadding = min(paddingObject[maxProp], largestPossiblePadding); + + // Make sure the arrow doesn't overflow the floating element if the center + // point is outside the floating element's bounds. + const min$1 = minPadding; + const max = clientSize - arrowDimensions[length] - maxPadding; + const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference; + const offset = clamp(min$1, center, max); + + // If the reference is small enough that the arrow's padding causes it to + // to point to nothing for an aligned placement, adjust the offset of the + // floating element itself. To ensure `shift()` continues to take action, + // a single reset is performed when this is true. + const shouldAddOffset = !middlewareData.arrow && getAlignment(placement) != null && center !== offset && rects.reference[length] / 2 - (center < min$1 ? minPadding : maxPadding) - arrowDimensions[length] / 2 < 0; + const alignmentOffset = shouldAddOffset ? center < min$1 ? center - min$1 : center - max : 0; + return { + [axis]: coords[axis] + alignmentOffset, + data: { + [axis]: offset, + centerOffset: center - offset - alignmentOffset, + ...(shouldAddOffset && { + alignmentOffset + }) + }, + reset: shouldAddOffset + }; + } +}); + +/** + * Optimizes the visibility of the floating element by flipping the `placement` + * in order to keep it in view when the preferred placement(s) will overflow the + * clipping boundary. Alternative to `autoPlacement`. + * @see https://floating-ui.com/docs/flip + */ +const flip$1 = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'flip', + options, + async fn(state) { + var _middlewareData$arrow, _middlewareData$flip; + const { + placement, + middlewareData, + rects, + initialPlacement, + platform, + elements + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = true, + fallbackPlacements: specifiedFallbackPlacements, + fallbackStrategy = 'bestFit', + fallbackAxisSideDirection = 'none', + flipAlignment = true, + ...detectOverflowOptions + } = evaluate(options, state); + + // If a reset by the arrow was caused due to an alignment offset being + // added, we should skip any logic now since `flip()` has already done its + // work. + // https://github.com/floating-ui/floating-ui/issues/2549#issuecomment-1719601643 + if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + const side = getSide(placement); + const initialSideAxis = getSideAxis(initialPlacement); + const isBasePlacement = getSide(initialPlacement) === initialPlacement; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement)); + const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none'; + if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) { + fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl)); + } + const placements = [initialPlacement, ...fallbackPlacements]; + const overflow = await platform.detectOverflow(state, detectOverflowOptions); + const overflows = []; + let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || []; + if (checkMainAxis) { + overflows.push(overflow[side]); + } + if (checkCrossAxis) { + const sides = getAlignmentSides(placement, rects, rtl); + overflows.push(overflow[sides[0]], overflow[sides[1]]); + } + overflowsData = [...overflowsData, { + placement, + overflows + }]; + + // One or more sides is overflowing. + if (!overflows.every(side => side <= 0)) { + var _middlewareData$flip2, _overflowsData$filter; + const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1; + const nextPlacement = placements[nextIndex]; + if (nextPlacement) { + const ignoreCrossAxisOverflow = checkCrossAxis === 'alignment' ? initialSideAxis !== getSideAxis(nextPlacement) : false; + if (!ignoreCrossAxisOverflow || + // We leave the current main axis only if every placement on that axis + // overflows the main axis. + overflowsData.every(d => getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true)) { + // Try next placement and re-run the lifecycle. + return { + data: { + index: nextIndex, + overflows: overflowsData + }, + reset: { + placement: nextPlacement + } + }; + } + } + + // First, find the candidates that fit on the mainAxis side of overflow, + // then find the placement that fits the best on the main crossAxis side. + let resetPlacement = (_overflowsData$filter = overflowsData.filter(d => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement; + + // Otherwise fallback. + if (!resetPlacement) { + switch (fallbackStrategy) { + case 'bestFit': + { + var _overflowsData$filter2; + const placement = (_overflowsData$filter2 = overflowsData.filter(d => { + if (hasFallbackAxisSideDirection) { + const currentSideAxis = getSideAxis(d.placement); + return currentSideAxis === initialSideAxis || + // Create a bias to the `y` side axis due to horizontal + // reading directions favoring greater width. + currentSideAxis === 'y'; + } + return true; + }).map(d => [d.placement, d.overflows.filter(overflow => overflow > 0).reduce((acc, overflow) => acc + overflow, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0]; + if (placement) { + resetPlacement = placement; + } + break; + } + case 'initialPlacement': + resetPlacement = initialPlacement; + break; + } + } + if (placement !== resetPlacement) { + return { + reset: { + placement: resetPlacement + } + }; + } + } + return {}; + } + }; +}; + +const originSides = /*#__PURE__*/new Set(['left', 'top']); + +// For type backwards-compatibility, the `OffsetOptions` type was also +// Derivable. + +async function convertValueToCoords(state, options) { + const { + placement, + platform, + elements + } = state; + const rtl = await (platform.isRTL == null ? void 0 : platform.isRTL(elements.floating)); + const side = getSide(placement); + const alignment = getAlignment(placement); + const isVertical = getSideAxis(placement) === 'y'; + const mainAxisMulti = originSides.has(side) ? -1 : 1; + const crossAxisMulti = rtl && isVertical ? -1 : 1; + const rawValue = evaluate(options, state); + + // eslint-disable-next-line prefer-const + let { + mainAxis, + crossAxis, + alignmentAxis + } = typeof rawValue === 'number' ? { + mainAxis: rawValue, + crossAxis: 0, + alignmentAxis: null + } : { + mainAxis: rawValue.mainAxis || 0, + crossAxis: rawValue.crossAxis || 0, + alignmentAxis: rawValue.alignmentAxis + }; + if (alignment && typeof alignmentAxis === 'number') { + crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis; + } + return isVertical ? { + x: crossAxis * crossAxisMulti, + y: mainAxis * mainAxisMulti + } : { + x: mainAxis * mainAxisMulti, + y: crossAxis * crossAxisMulti + }; +} + +/** + * Modifies the placement by translating the floating element along the + * specified axes. + * A number (shorthand for `mainAxis` or distance), or an axes configuration + * object may be passed. + * @see https://floating-ui.com/docs/offset + */ +const offset$1 = function (options) { + if (options === void 0) { + options = 0; + } + return { + name: 'offset', + options, + async fn(state) { + var _middlewareData$offse, _middlewareData$arrow; + const { + x, + y, + placement, + middlewareData + } = state; + const diffCoords = await convertValueToCoords(state, options); + + // If the placement is the same and the arrow caused an alignment offset + // then we don't need to change the positioning coordinates. + if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) { + return {}; + } + return { + x: x + diffCoords.x, + y: y + diffCoords.y, + data: { + ...diffCoords, + placement + } + }; + } + }; +}; + +/** + * Optimizes the visibility of the floating element by shifting it in order to + * keep it in view when it will overflow the clipping boundary. + * @see https://floating-ui.com/docs/shift + */ +const shift$1 = function (options) { + if (options === void 0) { + options = {}; + } + return { + name: 'shift', + options, + async fn(state) { + const { + x, + y, + placement, + platform + } = state; + const { + mainAxis: checkMainAxis = true, + crossAxis: checkCrossAxis = false, + limiter = { + fn: _ref => { + let { + x, + y + } = _ref; + return { + x, + y + }; + } + }, + ...detectOverflowOptions + } = evaluate(options, state); + const coords = { + x, + y + }; + const overflow = await platform.detectOverflow(state, detectOverflowOptions); + const crossAxis = getSideAxis(getSide(placement)); + const mainAxis = getOppositeAxis(crossAxis); + let mainAxisCoord = coords[mainAxis]; + let crossAxisCoord = coords[crossAxis]; + if (checkMainAxis) { + const minSide = mainAxis === 'y' ? 'top' : 'left'; + const maxSide = mainAxis === 'y' ? 'bottom' : 'right'; + const min = mainAxisCoord + overflow[minSide]; + const max = mainAxisCoord - overflow[maxSide]; + mainAxisCoord = clamp(min, mainAxisCoord, max); + } + if (checkCrossAxis) { + const minSide = crossAxis === 'y' ? 'top' : 'left'; + const maxSide = crossAxis === 'y' ? 'bottom' : 'right'; + const min = crossAxisCoord + overflow[minSide]; + const max = crossAxisCoord - overflow[maxSide]; + crossAxisCoord = clamp(min, crossAxisCoord, max); + } + const limitedCoords = limiter.fn({ + ...state, + [mainAxis]: mainAxisCoord, + [crossAxis]: crossAxisCoord + }); + return { + ...limitedCoords, + data: { + x: limitedCoords.x - x, + y: limitedCoords.y - y, + enabled: { + [mainAxis]: checkMainAxis, + [crossAxis]: checkCrossAxis + } + } + }; + } + }; +}; + +function hasWindow() { + return typeof window !== 'undefined'; +} +function getNodeName(node) { + if (isNode(node)) { + return (node.nodeName || '').toLowerCase(); + } + // Mocked nodes in testing environments may not be instances of Node. By + // returning `#document` an infinite loop won't occur. + // https://github.com/floating-ui/floating-ui/issues/2317 + return '#document'; +} +function getWindow(node) { + var _node$ownerDocument; + return (node == null || (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window; +} +function getDocumentElement(node) { + var _ref; + return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement; +} +function isNode(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Node || value instanceof getWindow(value).Node; +} +function isElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof Element || value instanceof getWindow(value).Element; +} +function isHTMLElement(value) { + if (!hasWindow()) { + return false; + } + return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement; +} +function isShadowRoot(value) { + if (!hasWindow() || typeof ShadowRoot === 'undefined') { + return false; + } + return value instanceof ShadowRoot || value instanceof getWindow(value).ShadowRoot; +} +function isOverflowElement(element) { + const { + overflow, + overflowX, + overflowY, + display + } = getComputedStyle$1(element); + return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && display !== 'inline' && display !== 'contents'; +} +function isTableElement(element) { + return /^(table|td|th)$/.test(getNodeName(element)); +} +function isTopLayer(element) { + try { + if (element.matches(':popover-open')) { + return true; + } + } catch (_e) { + // no-op + } + try { + return element.matches(':modal'); + } catch (_e) { + return false; + } +} +const willChangeRe = /transform|translate|scale|rotate|perspective|filter/; +const containRe = /paint|layout|strict|content/; +const isNotNone = value => !!value && value !== 'none'; +let isWebKitValue; +function isContainingBlock(elementOrCss) { + const css = isElement(elementOrCss) ? getComputedStyle$1(elementOrCss) : elementOrCss; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + return isNotNone(css.transform) || isNotNone(css.translate) || isNotNone(css.scale) || isNotNone(css.rotate) || isNotNone(css.perspective) || !isWebKit() && (isNotNone(css.backdropFilter) || isNotNone(css.filter)) || willChangeRe.test(css.willChange || '') || containRe.test(css.contain || ''); +} +function getContainingBlock(element) { + let currentNode = getParentNode(element); + while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) { + if (isContainingBlock(currentNode)) { + return currentNode; + } else if (isTopLayer(currentNode)) { + return null; + } + currentNode = getParentNode(currentNode); + } + return null; +} +function isWebKit() { + if (isWebKitValue == null) { + isWebKitValue = typeof CSS !== 'undefined' && CSS.supports && CSS.supports('-webkit-backdrop-filter', 'none'); + } + return isWebKitValue; +} +function isLastTraversableNode(node) { + return /^(html|body|#document)$/.test(getNodeName(node)); +} +function getComputedStyle$1(element) { + return getWindow(element).getComputedStyle(element); +} +function getNodeScroll(element) { + if (isElement(element)) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; + } + return { + scrollLeft: element.scrollX, + scrollTop: element.scrollY + }; +} +function getParentNode(node) { + if (getNodeName(node) === 'html') { + return node; + } + const result = + // Step into the shadow DOM of the parent of a slotted node. + node.assignedSlot || + // DOM Element detected. + node.parentNode || + // ShadowRoot detected. + isShadowRoot(node) && node.host || + // Fallback. + getDocumentElement(node); + return isShadowRoot(result) ? result.host : result; +} +function getNearestOverflowAncestor(node) { + const parentNode = getParentNode(node); + if (isLastTraversableNode(parentNode)) { + return node.ownerDocument ? node.ownerDocument.body : node.body; + } + if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) { + return parentNode; + } + return getNearestOverflowAncestor(parentNode); +} +function getOverflowAncestors(node, list, traverseIframes) { + var _node$ownerDocument2; + if (list === void 0) { + list = []; + } + if (traverseIframes === void 0) { + traverseIframes = true; + } + const scrollableAncestor = getNearestOverflowAncestor(node); + const isBody = scrollableAncestor === ((_node$ownerDocument2 = node.ownerDocument) == null ? void 0 : _node$ownerDocument2.body); + const win = getWindow(scrollableAncestor); + if (isBody) { + const frameElement = getFrameElement(win); + return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : [], frameElement && traverseIframes ? getOverflowAncestors(frameElement) : []); + } else { + return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor, [], traverseIframes)); + } +} +function getFrameElement(win) { + return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; +} + +function getCssDimensions(element) { + const css = getComputedStyle$1(element); + // In testing environments, the `width` and `height` properties are empty + // strings for SVG elements, returning NaN. Fallback to `0` in this case. + let width = parseFloat(css.width) || 0; + let height = parseFloat(css.height) || 0; + const hasOffset = isHTMLElement(element); + const offsetWidth = hasOffset ? element.offsetWidth : width; + const offsetHeight = hasOffset ? element.offsetHeight : height; + const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight; + if (shouldFallback) { + width = offsetWidth; + height = offsetHeight; + } + return { + width, + height, + $: shouldFallback + }; +} + +function unwrapElement(element) { + return !isElement(element) ? element.contextElement : element; +} + +function getScale(element) { + const domElement = unwrapElement(element); + if (!isHTMLElement(domElement)) { + return createCoords(1); + } + const rect = domElement.getBoundingClientRect(); + const { + width, + height, + $ + } = getCssDimensions(domElement); + let x = ($ ? round(rect.width) : rect.width) / width; + let y = ($ ? round(rect.height) : rect.height) / height; + + // 0, NaN, or Infinity should always fallback to 1. + + if (!x || !Number.isFinite(x)) { + x = 1; + } + if (!y || !Number.isFinite(y)) { + y = 1; + } + return { + x, + y + }; +} + +const noOffsets = /*#__PURE__*/createCoords(0); +function getVisualOffsets(element) { + const win = getWindow(element); + if (!isWebKit() || !win.visualViewport) { + return noOffsets; + } + return { + x: win.visualViewport.offsetLeft, + y: win.visualViewport.offsetTop + }; +} +function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) { + if (isFixed === void 0) { + isFixed = false; + } + if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) { + return false; + } + return isFixed; +} + +function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) { + if (includeScale === void 0) { + includeScale = false; + } + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + const clientRect = element.getBoundingClientRect(); + const domElement = unwrapElement(element); + let scale = createCoords(1); + if (includeScale) { + if (offsetParent) { + if (isElement(offsetParent)) { + scale = getScale(offsetParent); + } + } else { + scale = getScale(element); + } + } + const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0); + let x = (clientRect.left + visualOffsets.x) / scale.x; + let y = (clientRect.top + visualOffsets.y) / scale.y; + let width = clientRect.width / scale.x; + let height = clientRect.height / scale.y; + if (domElement) { + const win = getWindow(domElement); + const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent; + let currentWin = win; + let currentIFrame = getFrameElement(currentWin); + while (currentIFrame && offsetParent && offsetWin !== currentWin) { + const iframeScale = getScale(currentIFrame); + const iframeRect = currentIFrame.getBoundingClientRect(); + const css = getComputedStyle$1(currentIFrame); + const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x; + const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y; + x *= iframeScale.x; + y *= iframeScale.y; + width *= iframeScale.x; + height *= iframeScale.y; + x += left; + y += top; + currentWin = getWindow(currentIFrame); + currentIFrame = getFrameElement(currentWin); + } + } + return rectToClientRect({ + width, + height, + x, + y + }); +} + +// If has a CSS width greater than the viewport, then this will be +// incorrect for RTL. +function getWindowScrollBarX(element, rect) { + const leftScroll = getNodeScroll(element).scrollLeft; + if (!rect) { + return getBoundingClientRect(getDocumentElement(element)).left + leftScroll; + } + return rect.left + leftScroll; +} + +function getHTMLOffset(documentElement, scroll) { + const htmlRect = documentElement.getBoundingClientRect(); + const x = htmlRect.left + scroll.scrollLeft - getWindowScrollBarX(documentElement, htmlRect); + const y = htmlRect.top + scroll.scrollTop; + return { + x, + y + }; +} + +function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) { + let { + elements, + rect, + offsetParent, + strategy + } = _ref; + const isFixed = strategy === 'fixed'; + const documentElement = getDocumentElement(offsetParent); + const topLayer = elements ? isTopLayer(elements.floating) : false; + if (offsetParent === documentElement || topLayer && isFixed) { + return rect; + } + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + let scale = createCoords(1); + const offsets = createCoords(0); + const isOffsetParentAnElement = isHTMLElement(offsetParent); + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isOffsetParentAnElement) { + const offsetRect = getBoundingClientRect(offsetParent); + scale = getScale(offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); + return { + width: rect.width * scale.x, + height: rect.height * scale.y, + x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x + htmlOffset.x, + y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y + htmlOffset.y + }; +} + +function getClientRects(element) { + return Array.from(element.getClientRects()); +} + +// Gets the entire size of the scrollable document area, even extending outside +// of the `` and `` rect bounds if horizontally scrollable. +function getDocumentRect(element) { + const html = getDocumentElement(element); + const scroll = getNodeScroll(element); + const body = element.ownerDocument.body; + const width = max(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth); + const height = max(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight); + let x = -scroll.scrollLeft + getWindowScrollBarX(element); + const y = -scroll.scrollTop; + if (getComputedStyle$1(body).direction === 'rtl') { + x += max(html.clientWidth, body.clientWidth) - width; + } + return { + width, + height, + x, + y + }; +} + +// Safety check: ensure the scrollbar space is reasonable in case this +// calculation is affected by unusual styles. +// Most scrollbars leave 15-18px of space. +const SCROLLBAR_MAX = 25; +function getViewportRect(element, strategy) { + const win = getWindow(element); + const html = getDocumentElement(element); + const visualViewport = win.visualViewport; + let width = html.clientWidth; + let height = html.clientHeight; + let x = 0; + let y = 0; + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + const visualViewportBased = isWebKit(); + if (!visualViewportBased || visualViewportBased && strategy === 'fixed') { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + const windowScrollbarX = getWindowScrollBarX(html); + // `overflow: hidden` + `scrollbar-gutter: stable` reduces the + // visual width of the but this is not considered in the size + // of `html.clientWidth`. + if (windowScrollbarX <= 0) { + const doc = html.ownerDocument; + const body = doc.body; + const bodyStyles = getComputedStyle(body); + const bodyMarginInline = doc.compatMode === 'CSS1Compat' ? parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight) || 0 : 0; + const clippingStableScrollbarWidth = Math.abs(html.clientWidth - body.clientWidth - bodyMarginInline); + if (clippingStableScrollbarWidth <= SCROLLBAR_MAX) { + width -= clippingStableScrollbarWidth; + } + } else if (windowScrollbarX <= SCROLLBAR_MAX) { + // If the scrollbar is on the left, the width needs to be extended + // by the scrollbar amount so there isn't extra space on the right. + width += windowScrollbarX; + } + return { + width, + height, + x, + y + }; +} + +// Returns the inner client rect, subtracting scrollbars if present. +function getInnerBoundingClientRect(element, strategy) { + const clientRect = getBoundingClientRect(element, true, strategy === 'fixed'); + const top = clientRect.top + element.clientTop; + const left = clientRect.left + element.clientLeft; + const scale = isHTMLElement(element) ? getScale(element) : createCoords(1); + const width = element.clientWidth * scale.x; + const height = element.clientHeight * scale.y; + const x = left * scale.x; + const y = top * scale.y; + return { + width, + height, + x, + y + }; +} +function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) { + let rect; + if (clippingAncestor === 'viewport') { + rect = getViewportRect(element, strategy); + } else if (clippingAncestor === 'document') { + rect = getDocumentRect(getDocumentElement(element)); + } else if (isElement(clippingAncestor)) { + rect = getInnerBoundingClientRect(clippingAncestor, strategy); + } else { + const visualOffsets = getVisualOffsets(element); + rect = { + x: clippingAncestor.x - visualOffsets.x, + y: clippingAncestor.y - visualOffsets.y, + width: clippingAncestor.width, + height: clippingAncestor.height + }; + } + return rectToClientRect(rect); +} +function hasFixedPositionAncestor(element, stopNode) { + const parentNode = getParentNode(element); + if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) { + return false; + } + return getComputedStyle$1(parentNode).position === 'fixed' || hasFixedPositionAncestor(parentNode, stopNode); +} + +// A "clipping ancestor" is an `overflow` element with the characteristic of +// clipping (or hiding) child elements. This returns all clipping ancestors +// of the given element up the tree. +function getClippingElementAncestors(element, cache) { + const cachedResult = cache.get(element); + if (cachedResult) { + return cachedResult; + } + let result = getOverflowAncestors(element, [], false).filter(el => isElement(el) && getNodeName(el) !== 'body'); + let currentContainingBlockComputedStyle = null; + const elementIsFixed = getComputedStyle$1(element).position === 'fixed'; + let currentNode = elementIsFixed ? getParentNode(element) : element; + + // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block + while (isElement(currentNode) && !isLastTraversableNode(currentNode)) { + const computedStyle = getComputedStyle$1(currentNode); + const currentNodeIsContaining = isContainingBlock(currentNode); + if (!currentNodeIsContaining && computedStyle.position === 'fixed') { + currentContainingBlockComputedStyle = null; + } + const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === 'static' && !!currentContainingBlockComputedStyle && (currentContainingBlockComputedStyle.position === 'absolute' || currentContainingBlockComputedStyle.position === 'fixed') || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode); + if (shouldDropCurrentNode) { + // Drop non-containing blocks. + result = result.filter(ancestor => ancestor !== currentNode); + } else { + // Record last containing block for next iteration. + currentContainingBlockComputedStyle = computedStyle; + } + currentNode = getParentNode(currentNode); + } + cache.set(element, result); + return result; +} + +// Gets the maximum area that the element is visible in due to any number of +// clipping ancestors. +function getClippingRect(_ref) { + let { + element, + boundary, + rootBoundary, + strategy + } = _ref; + const elementClippingAncestors = boundary === 'clippingAncestors' ? isTopLayer(element) ? [] : getClippingElementAncestors(element, this._c) : [].concat(boundary); + const clippingAncestors = [...elementClippingAncestors, rootBoundary]; + const firstRect = getClientRectFromClippingAncestor(element, clippingAncestors[0], strategy); + let top = firstRect.top; + let right = firstRect.right; + let bottom = firstRect.bottom; + let left = firstRect.left; + for (let i = 1; i < clippingAncestors.length; i++) { + const rect = getClientRectFromClippingAncestor(element, clippingAncestors[i], strategy); + top = max(rect.top, top); + right = min(rect.right, right); + bottom = min(rect.bottom, bottom); + left = max(rect.left, left); + } + return { + width: right - left, + height: bottom - top, + x: left, + y: top + }; +} + +function getDimensions(element) { + const { + width, + height + } = getCssDimensions(element); + return { + width, + height + }; +} + +function getRectRelativeToOffsetParent(element, offsetParent, strategy) { + const isOffsetParentAnElement = isHTMLElement(offsetParent); + const documentElement = getDocumentElement(offsetParent); + const isFixed = strategy === 'fixed'; + const rect = getBoundingClientRect(element, true, isFixed, offsetParent); + let scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + const offsets = createCoords(0); + + // If the scrollbar appears on the left (e.g. RTL systems). Use + // Firefox with layout.scrollbar.side = 3 in about:config to test this. + function setLeftRTLScrollbarOffset() { + offsets.x = getWindowScrollBarX(documentElement); + } + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== 'body' || isOverflowElement(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isOffsetParentAnElement) { + const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent); + offsets.x = offsetRect.x + offsetParent.clientLeft; + offsets.y = offsetRect.y + offsetParent.clientTop; + } else if (documentElement) { + setLeftRTLScrollbarOffset(); + } + } + if (isFixed && !isOffsetParentAnElement && documentElement) { + setLeftRTLScrollbarOffset(); + } + const htmlOffset = documentElement && !isOffsetParentAnElement && !isFixed ? getHTMLOffset(documentElement, scroll) : createCoords(0); + const x = rect.left + scroll.scrollLeft - offsets.x - htmlOffset.x; + const y = rect.top + scroll.scrollTop - offsets.y - htmlOffset.y; + return { + x, + y, + width: rect.width, + height: rect.height + }; +} + +function isStaticPositioned(element) { + return getComputedStyle$1(element).position === 'static'; +} + +function getTrueOffsetParent(element, polyfill) { + if (!isHTMLElement(element) || getComputedStyle$1(element).position === 'fixed') { + return null; + } + if (polyfill) { + return polyfill(element); + } + let rawOffsetParent = element.offsetParent; + + // Firefox returns the element as the offsetParent if it's non-static, + // while Chrome and Safari return the element. The element must + // be used to perform the correct calculations even if the element is + // non-static. + if (getDocumentElement(element) === rawOffsetParent) { + rawOffsetParent = rawOffsetParent.ownerDocument.body; + } + return rawOffsetParent; +} + +// Gets the closest ancestor positioned element. Handles some edge cases, +// such as table ancestors and cross browser bugs. +function getOffsetParent(element, polyfill) { + const win = getWindow(element); + if (isTopLayer(element)) { + return win; + } + if (!isHTMLElement(element)) { + let svgOffsetParent = getParentNode(element); + while (svgOffsetParent && !isLastTraversableNode(svgOffsetParent)) { + if (isElement(svgOffsetParent) && !isStaticPositioned(svgOffsetParent)) { + return svgOffsetParent; + } + svgOffsetParent = getParentNode(svgOffsetParent); + } + return win; + } + let offsetParent = getTrueOffsetParent(element, polyfill); + while (offsetParent && isTableElement(offsetParent) && isStaticPositioned(offsetParent)) { + offsetParent = getTrueOffsetParent(offsetParent, polyfill); + } + if (offsetParent && isLastTraversableNode(offsetParent) && isStaticPositioned(offsetParent) && !isContainingBlock(offsetParent)) { + return win; + } + return offsetParent || getContainingBlock(element) || win; +} + +const getElementRects = async function (data) { + const getOffsetParentFn = this.getOffsetParent || getOffsetParent; + const getDimensionsFn = this.getDimensions; + const floatingDimensions = await getDimensionsFn(data.floating); + return { + reference: getRectRelativeToOffsetParent(data.reference, await getOffsetParentFn(data.floating), data.strategy), + floating: { + x: 0, + y: 0, + width: floatingDimensions.width, + height: floatingDimensions.height + } + }; +}; + +function isRTL(element) { + return getComputedStyle$1(element).direction === 'rtl'; +} + +const platform = { + convertOffsetParentRelativeRectToViewportRelativeRect, + getDocumentElement, + getClippingRect, + getOffsetParent, + getElementRects, + getClientRects, + getDimensions, + getScale, + isElement, + isRTL +}; + +function rectsAreEqual(a, b) { + return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; +} + +// https://samthor.au/2021/observing-dom/ +function observeMove(element, onMove) { + let io = null; + let timeoutId; + const root = getDocumentElement(element); + function cleanup() { + var _io; + clearTimeout(timeoutId); + (_io = io) == null || _io.disconnect(); + io = null; + } + function refresh(skip, threshold) { + if (skip === void 0) { + skip = false; + } + if (threshold === void 0) { + threshold = 1; + } + cleanup(); + const elementRectForRootMargin = element.getBoundingClientRect(); + const { + left, + top, + width, + height + } = elementRectForRootMargin; + if (!skip) { + onMove(); + } + if (!width || !height) { + return; + } + const insetTop = floor(top); + const insetRight = floor(root.clientWidth - (left + width)); + const insetBottom = floor(root.clientHeight - (top + height)); + const insetLeft = floor(left); + const rootMargin = -insetTop + "px " + -insetRight + "px " + -insetBottom + "px " + -insetLeft + "px"; + const options = { + rootMargin, + threshold: max(0, min(1, threshold)) || 1 + }; + let isFirstUpdate = true; + function handleObserve(entries) { + const ratio = entries[0].intersectionRatio; + if (ratio !== threshold) { + if (!isFirstUpdate) { + return refresh(); + } + if (!ratio) { + // If the reference is clipped, the ratio is 0. Throttle the refresh + // to prevent an infinite loop of updates. + timeoutId = setTimeout(() => { + refresh(false, 1e-7); + }, 1000); + } else { + refresh(false, ratio); + } + } + if (ratio === 1 && !rectsAreEqual(elementRectForRootMargin, element.getBoundingClientRect())) { + // It's possible that even though the ratio is reported as 1, the + // element is not actually fully within the IntersectionObserver's root + // area anymore. This can happen under performance constraints. This may + // be a bug in the browser's IntersectionObserver implementation. To + // work around this, we compare the element's bounding rect now with + // what it was at the time we created the IntersectionObserver. If they + // are not equal then the element moved, so we refresh. + refresh(); + } + isFirstUpdate = false; + } + + // Older browsers don't support a `document` as the root and will throw an + // error. + try { + io = new IntersectionObserver(handleObserve, { + ...options, + // Handle s + root: root.ownerDocument + }); + } catch (_e) { + io = new IntersectionObserver(handleObserve, options); + } + io.observe(element); + } + refresh(true); + return cleanup; +} + +/** + * Automatically updates the position of the floating element when necessary. + * Should only be called when the floating element is mounted on the DOM or + * visible on the screen. + * @returns cleanup function that should be invoked when the floating element is + * removed from the DOM or hidden from the screen. + * @see https://floating-ui.com/docs/autoUpdate + */ +function autoUpdate(reference, floating, update, options) { + if (options === void 0) { + options = {}; + } + const { + ancestorScroll = true, + ancestorResize = true, + elementResize = typeof ResizeObserver === 'function', + layoutShift = typeof IntersectionObserver === 'function', + animationFrame = false + } = options; + const referenceEl = unwrapElement(reference); + const ancestors = ancestorScroll || ancestorResize ? [...(referenceEl ? getOverflowAncestors(referenceEl) : []), ...(floating ? getOverflowAncestors(floating) : [])] : []; + ancestors.forEach(ancestor => { + ancestorScroll && ancestor.addEventListener('scroll', update, { + passive: true + }); + ancestorResize && ancestor.addEventListener('resize', update); + }); + const cleanupIo = referenceEl && layoutShift ? observeMove(referenceEl, update) : null; + let reobserveFrame = -1; + let resizeObserver = null; + if (elementResize) { + resizeObserver = new ResizeObserver(_ref => { + let [firstEntry] = _ref; + if (firstEntry && firstEntry.target === referenceEl && resizeObserver && floating) { + // Prevent update loops when using the `size` middleware. + // https://github.com/floating-ui/floating-ui/issues/1740 + resizeObserver.unobserve(floating); + cancelAnimationFrame(reobserveFrame); + reobserveFrame = requestAnimationFrame(() => { + var _resizeObserver; + (_resizeObserver = resizeObserver) == null || _resizeObserver.observe(floating); + }); + } + update(); + }); + if (referenceEl && !animationFrame) { + resizeObserver.observe(referenceEl); + } + if (floating) { + resizeObserver.observe(floating); + } + } + let frameId; + let prevRefRect = animationFrame ? getBoundingClientRect(reference) : null; + if (animationFrame) { + frameLoop(); + } + function frameLoop() { + const nextRefRect = getBoundingClientRect(reference); + if (prevRefRect && !rectsAreEqual(prevRefRect, nextRefRect)) { + update(); + } + prevRefRect = nextRefRect; + frameId = requestAnimationFrame(frameLoop); + } + update(); + return () => { + var _resizeObserver2; + ancestors.forEach(ancestor => { + ancestorScroll && ancestor.removeEventListener('scroll', update); + ancestorResize && ancestor.removeEventListener('resize', update); + }); + cleanupIo == null || cleanupIo(); + (_resizeObserver2 = resizeObserver) == null || _resizeObserver2.disconnect(); + resizeObserver = null; + if (animationFrame) { + cancelAnimationFrame(frameId); + } + }; +} + +/** + * Modifies the placement by translating the floating element along the + * specified axes. + * A number (shorthand for `mainAxis` or distance), or an axes configuration + * object may be passed. + * @see https://floating-ui.com/docs/offset + */ +const offset = offset$1; + +/** + * Optimizes the visibility of the floating element by shifting it in order to + * keep it in view when it will overflow the clipping boundary. + * @see https://floating-ui.com/docs/shift + */ +const shift = shift$1; + +/** + * Optimizes the visibility of the floating element by flipping the `placement` + * in order to keep it in view when the preferred placement(s) will overflow the + * clipping boundary. Alternative to `autoPlacement`. + * @see https://floating-ui.com/docs/flip + */ +const flip = flip$1; + +/** + * Provides data to position an inner element of the floating element so that it + * appears centered to the reference element. + * @see https://floating-ui.com/docs/arrow + */ +const arrow = arrow$1; + +/** + * Computes the `x` and `y` coordinates that will place the floating element + * next to a given reference element. + */ +const computePosition = (reference, floating, options) => { + // This caches the expensive `getClippingElementAncestors` function so that + // multiple lifecycle resets re-use the same result. It only lives for a + // single call. If other functions become expensive, we can add them as well. + const cache = new Map(); + const mergedOptions = { + platform, + ...options + }; + const platformWithCache = { + ...mergedOptions.platform, + _c: cache + }; + return computePosition$1(reference, floating, { + ...mergedOptions, + platform: platformWithCache + }); +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/floating-ui.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Breakpoints for responsive placement (matches SCSS $breakpoints) + */ +const BREAKPOINTS = { + sm: 576, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536 +}; + +/** + * Parse a placement string that may contain responsive prefixes + * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' } + * + * @param {string} placementString - The placement string to parse + * @param {string} defaultPlacement - The default placement to use for xs/base + * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive + */ +const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => { + // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end") + if (!placementString || !placementString.includes(':')) { + return null; + } + + // Parse the placement string into breakpoint-keyed object + const parts = placementString.split(/\s+/); + const placements = { + xs: defaultPlacement + }; // Default fallback + + for (const part of parts) { + if (part.includes(':')) { + // Responsive placement like "md:top-end" + const [breakpoint, placement] = part.split(':'); + if (BREAKPOINTS[breakpoint] !== undefined) { + placements[breakpoint] = placement; + } + } else { + // Base placement (no prefix = xs/default) + placements.xs = part; + } + } + return placements; +}; + +/** + * Get the active placement for the current viewport width + * + * @param {object} responsivePlacements - Object with breakpoint keys and placement values + * @param {string} defaultPlacement - Fallback placement + * @returns {string} - The active placement for current viewport + */ +const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => { + if (!responsivePlacements) { + return defaultPlacement; + } + + // Get current viewport width + const viewportWidth = window.innerWidth; + + // Find the largest breakpoint that matches + let activePlacement = responsivePlacements.xs || defaultPlacement; + + // Check breakpoints in order (sm, md, lg, xl, 2xl) + const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']; + for (const breakpoint of breakpointOrder) { + const minWidth = BREAKPOINTS[breakpoint]; + if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) { + activePlacement = responsivePlacements[breakpoint]; + } + } + return activePlacement; +}; + +/** + * Create media query listeners for responsive placement changes + * + * @param {Function} callback - Callback to run when breakpoint changes + * @returns {Array} - Array of { mql, handler } objects for cleanup + */ +const createBreakpointListeners = callback => { + const listeners = []; + for (const breakpoint of Object.keys(BREAKPOINTS)) { + const minWidth = BREAKPOINTS[breakpoint]; + const mql = window.matchMedia(`(min-width: ${minWidth}px)`); + mql.addEventListener('change', callback); + listeners.push({ + mql, + handler: callback + }); + } + return listeners; +}; + +/** + * Clean up media query listeners + * + * @param {Array} listeners - Array of { mql, handler } objects + */ +const disposeBreakpointListeners = listeners => { + for (const { + mql, + handler + } of listeners) { + mql.removeEventListener('change', handler); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap menu.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$h = 'menu'; +const DATA_KEY$d = 'bs.menu'; +const EVENT_KEY$e = `.${DATA_KEY$d}`; +const DATA_API_KEY$9 = '.data-api'; +const ESCAPE_KEY$2 = 'Escape'; +const TAB_KEY$1 = 'Tab'; +const ARROW_UP_KEY$2 = 'ArrowUp'; +const ARROW_DOWN_KEY$2 = 'ArrowDown'; +const ARROW_LEFT_KEY$1 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$1 = 'ArrowRight'; +const HOME_KEY$2 = 'Home'; +const END_KEY$2 = 'End'; +const ENTER_KEY$1 = 'Enter'; +const SPACE_KEY$1 = ' '; +const RIGHT_MOUSE_BUTTON = 2; +const SUBMENU_CLOSE_DELAY = 100; +const EVENT_HIDE$5 = `hide${EVENT_KEY$e}`; +const EVENT_HIDDEN$7 = `hidden${EVENT_KEY$e}`; +const EVENT_SHOW$6 = `show${EVENT_KEY$e}`; +const EVENT_SHOWN$5 = `shown${EVENT_KEY$e}`; +const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$e}${DATA_API_KEY$9}`; +const CLASS_NAME_SHOW$4 = 'show'; +const SELECTOR_DATA_TOGGLE$8 = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'; +const SELECTOR_MENU$2 = '.menu'; +const SELECTOR_SUBMENU = '.submenu'; +const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'; +const SELECTOR_NAVBAR_NAV = '.navbar-nav'; +const SELECTOR_VISIBLE_ITEMS$1 = '.menu-item:not(.disabled):not(:disabled)'; +const DEFAULT_PLACEMENT = 'bottom-start'; +const SUBMENU_PLACEMENT = 'end-start'; +const resolveLogicalPlacement = placement => { + if (isRTL$1()) { + return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left'); + } + return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right'); +}; +const triangleSign = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +const Default$g = { + autoClose: true, + boundary: 'clippingParents', + container: false, + display: 'dynamic', + offset: [0, 2], + floatingConfig: null, + menu: null, + placement: DEFAULT_PLACEMENT, + reference: 'toggle', + strategy: 'absolute', + submenuTrigger: 'both', + submenuDelay: SUBMENU_CLOSE_DELAY +}; +const DefaultType$g = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + container: '(string|element|boolean)', + display: 'string', + offset: '(array|string|function)', + floatingConfig: '(null|object|function)', + menu: '(null|element)', + placement: 'string', + reference: '(string|element|object)', + strategy: 'string', + submenuTrigger: 'string', + submenuDelay: 'number' +}; + +/** + * Class definition + */ + +class Menu extends BaseComponent { + static _openInstances = new Set(); + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)'); + } + super(element, config); + this._floatingCleanup = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + this._parent = this._element.parentNode; // menu wrapper + this._openSubmenus = new Map(); + this._submenuCloseTimeouts = new Map(); + this._hoverIntentData = null; + this._menu = this._config.menu || this._findMenu(); + + // When the menu was discovered from the DOM, refine the wrapper to the closest + // ancestor that actually contains it, so the toggle doesn't have to be a direct + // sibling of `.menu` (e.g. when wrapped by web components). The wrapper still + // receives `.show` and acts as the `reference: 'parent'` positioning anchor. + if (!this._config.menu && this._menu) { + this._parent = this._findWrapper(this._menu); + } + this._isSubmenu = this._parent.classList?.contains('submenu'); + this._menuOriginalParent = this._menu?.parentNode; + this._parseResponsivePlacements(); + this._setupSubmenuListeners(); + } + + // Getters + static get Default() { + return Default$g; + } + static get DefaultType() { + return DefaultType$g; + } + static get NAME() { + return NAME$h; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$6, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._moveMenuToContainer(); + this._createFloating(); + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus({ + focusVisible: false + }); + this._element.setAttribute('aria-expanded', 'true'); + this._menu.classList.add(CLASS_NAME_SHOW$4); + this._element.classList.add(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_SHOW$4); + } + Menu._openInstances.add(this); + EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._disposeMediaQueryListeners(); + this._closeAllSubmenus(); + this._clearAllSubmenuTimeouts(); + Menu._openInstances.delete(this); + super.dispose(); + } + update() { + if (this._floatingCleanup) { + this._updateFloatingPosition(); + } + } + + // Private + _findMenu() { + // Fall back to the closest ancestor that contains a menu so the toggle can be + // nested deeper than a direct sibling of `.menu`. + const wrapper = SelectorEngine.closest(this._element, `:has(${SELECTOR_MENU$2})`); + return SelectorEngine.next(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.findOne(SELECTOR_MENU$2, wrapper || this._parent); + } + _findWrapper(menu) { + let wrapper = this._element.parentNode; + while (wrapper instanceof Element && !wrapper.contains(menu)) { + wrapper = wrapper.parentNode; + } + return wrapper instanceof Element ? wrapper : this._element.parentNode; + } + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + this._closeAllSubmenus(); + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._menu.classList.remove(CLASS_NAME_SHOW$4); + this._element.classList.remove(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_SHOW$4); + } + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'placement'); + Manipulator.removeDataAttribute(this._menu, 'display'); + Menu._openInstances.delete(this); + EventHandler.trigger(this._element, EVENT_HIDDEN$7, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement$1(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + throw new TypeError(`${NAME$h.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createFloating() { + if (this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'display', 'static'); + return; + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement$1(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + this._updateFloatingPosition(referenceElement); + this._floatingCleanup = autoUpdate(referenceElement, this._menu, () => this._updateFloatingPosition(referenceElement)); + } + async _updateFloatingPosition(referenceElement = null) { + if (!this._menu) { + return; + } + if (!referenceElement) { + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement$1(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } else { + referenceElement = this._element; + } + } + const placement = this._getPlacement(); + const middleware = this._getFloatingMiddleware(); + const floatingConfig = this._getFloatingConfig(placement, middleware); + await this._applyFloatingPosition(referenceElement, this._menu, floatingConfig.placement, floatingConfig.middleware, floatingConfig.strategy); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$4); + } + _getPlacement() { + const placement = this._responsivePlacements ? getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) : this._config.placement; + return resolveLogicalPlacement(placement); + } + _parseResponsivePlacements() { + this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + _getOffset() { + const { + offset: offsetConfig + } = this._config; + if (typeof offsetConfig === 'string') { + return offsetConfig.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offsetConfig === 'function') { + return ({ + placement, + rects + }) => { + const result = offsetConfig({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offsetConfig; + } + _getFloatingMiddleware() { + const offsetValue = this._getOffset(); + const middleware = [offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), flip({ + fallbackPlacements: this._getFallbackPlacements() + }), shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + return middleware; + } + _getFallbackPlacements() { + const placement = this._getPlacement(); + const fallbackMap = { + bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'], + 'bottom-start': ['top-start', 'bottom-end', 'top-end'], + 'bottom-end': ['top-end', 'bottom-start', 'top-start'], + top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'], + 'top-start': ['bottom-start', 'top-end', 'bottom-end'], + 'top-end': ['bottom-end', 'top-start', 'bottom-start'], + right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'], + 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'], + 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'], + left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'], + 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'], + 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end'] + }; + return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware, + strategy: this._config.strategy + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + } + _getContainer() { + const { + container + } = this._config; + if (container === false) { + return null; + } + return container === true ? document.body : getElement(container); + } + _moveMenuToContainer() { + const container = this._getContainer(); + if (!container || !this._menu) { + return; + } + if (this._menu.parentNode !== container) { + container.append(this._menu); + } + } + _restoreMenuToOriginalParent() { + if (!this._menuOriginalParent || !this._menu) { + return; + } + if (this._menu.parentNode !== this._menuOriginalParent) { + this._menuOriginalParent.append(this._menu); + } + } + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { + if (!floating.isConnected) { + return null; + } + const { + x, + y, + placement: finalPlacement + } = await computePosition(reference, floating, { + placement, + middleware, + strategy + }); + if (!floating.isConnected) { + return null; + } + Object.assign(floating.style, { + position: strategy, + left: `${x}px`, + top: `${y}px`, + margin: '0' + }); + Manipulator.setDataAttribute(floating, 'placement', finalPlacement); + return finalPlacement; + } + + // ------------------------------------------------------------------------- + // Submenu handling + // ------------------------------------------------------------------------- + + _setupSubmenuListeners() { + if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerEnter(event); + }); + EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => { + this._onSubmenuLeave(event); + }); + EventHandler.on(this._menu, 'mousemove', event => { + this._trackMousePosition(event); + }); + } + if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerClick(event); + }); + } + } + _onSubmenuTriggerEnter(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + this._cancelSubmenuCloseTimeout(submenu); + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + _onSubmenuLeave(event) { + const submenuWrapper = event.target.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu || !this._openSubmenus.has(submenu)) { + return; + } + if (this._isMovingTowardSubmenu(event, submenu)) { + return; + } + this._scheduleSubmenuClose(submenu, submenuWrapper); + } + _onSubmenuTriggerClick(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + event.preventDefault(); + event.stopPropagation(); + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + if (this._openSubmenus.has(submenu)) { + this._closeSubmenu(submenu, submenuWrapper); + } else { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + } + _openSubmenu(trigger, submenu, submenuWrapper) { + if (this._openSubmenus.has(submenu)) { + return; + } + trigger.setAttribute('aria-expanded', 'true'); + trigger.setAttribute('aria-haspopup', 'true'); + + // Keep the submenu transparent until Floating UI applies the first position, so + // it doesn't flash at its CSS fallback position (top: 0, over the parent menu) + // before being moved into place. `opacity` (unlike `visibility`/`display`) keeps + // the submenu measurable for flip/shift and focusable for keyboard navigation. + submenu.style.opacity = '0'; + submenu.classList.add(CLASS_NAME_SHOW$4); + submenuWrapper.classList.add(CLASS_NAME_SHOW$4); + const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper); + this._openSubmenus.set(submenu, cleanup); + EventHandler.on(submenu, 'mouseenter', () => { + this._cancelSubmenuCloseTimeout(submenu); + }); + } + _closeSubmenu(submenu, submenuWrapper) { + if (!this._openSubmenus.has(submenu)) { + return; + } + const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, submenu); + for (const nested of nestedSubmenus) { + const nestedWrapper = nested.closest(SELECTOR_SUBMENU); + this._closeSubmenu(nested, nestedWrapper); + } + const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper); + const cleanup = this._openSubmenus.get(submenu); + if (cleanup) { + cleanup(); + } + this._openSubmenus.delete(submenu); + EventHandler.off(submenu, 'mouseenter'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + submenu.classList.remove(CLASS_NAME_SHOW$4); + submenuWrapper.classList.remove(CLASS_NAME_SHOW$4); + + // Keep the Floating UI position styles in place while the submenu fades out. + // Clearing them here would let the submenu snap back to its CSS fallback + // (`top: 0`, over the parent menu) for the duration of the close transition, + // causing it to flash over the parent. They get recomputed on the next open + // (and the opacity gate in `_openSubmenu` hides any stale position until then). + submenu.style.opacity = ''; + } + _closeAllSubmenus() { + for (const [submenu] of this._openSubmenus) { + const submenuWrapper = submenu.closest(SELECTOR_SUBMENU); + this._closeSubmenu(submenu, submenuWrapper); + } + } + _closeSiblingSubmenus(currentSubmenuWrapper) { + const parent = currentSubmenuWrapper.parentNode; + const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, parent); + for (const siblingMenu of siblingSubmenus) { + const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU); + if (siblingWrapper !== currentSubmenuWrapper) { + this._closeSubmenu(siblingMenu, siblingWrapper); + } + } + } + _createSubmenuFloating(trigger, submenu, submenuWrapper) { + const referenceElement = submenuWrapper; + const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT); + const middleware = [offset({ + mainAxis: 0, + crossAxis: -4 + }), flip({ + fallbackPlacements: [resolveLogicalPlacement('start-start'), resolveLogicalPlacement('end-end'), resolveLogicalPlacement('start-end')] + }), shift({ + padding: 8 + })]; + const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware).then(finalPlacement => { + // Reveal the submenu now that it has been positioned (see `_openSubmenu`); + // clearing the inline opacity lets the CSS fade-in transition take over. + submenu.style.opacity = ''; + return finalPlacement; + }); + updatePosition(); + return autoUpdate(referenceElement, submenu, updatePosition); + } + _scheduleSubmenuClose(submenu, submenuWrapper) { + this._cancelSubmenuCloseTimeout(submenu); + const timeoutId = setTimeout(() => { + this._closeSubmenu(submenu, submenuWrapper); + this._submenuCloseTimeouts.delete(submenu); + }, this._config.submenuDelay); + this._submenuCloseTimeouts.set(submenu, timeoutId); + } + _cancelSubmenuCloseTimeout(submenu) { + const timeoutId = this._submenuCloseTimeouts.get(submenu); + if (timeoutId) { + clearTimeout(timeoutId); + this._submenuCloseTimeouts.delete(submenu); + } + } + _clearAllSubmenuTimeouts() { + for (const timeoutId of this._submenuCloseTimeouts.values()) { + clearTimeout(timeoutId); + } + this._submenuCloseTimeouts.clear(); + } + + // ------------------------------------------------------------------------- + // Hover intent / Safe triangle + // ------------------------------------------------------------------------- + + _trackMousePosition(event) { + this._hoverIntentData = { + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + }; + } + _isMovingTowardSubmenu(event, submenu) { + if (!this._hoverIntentData) { + return false; + } + const submenuRect = submenu.getBoundingClientRect(); + const currentPos = { + x: event.clientX, + y: event.clientY + }; + const lastPos = { + x: this._hoverIntentData.x, + y: this._hoverIntentData.y + }; + const isRtl = isRTL$1(); + const targetX = isRtl ? submenuRect.right : submenuRect.left; + const topCorner = { + x: targetX, + y: submenuRect.top + }; + const bottomCorner = { + x: targetX, + y: submenuRect.bottom + }; + return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner); + } + _pointInTriangle(point, v1, v2, v3) { + const d1 = triangleSign(point, v1, v2); + const d2 = triangleSign(point, v2, v3); + const d3 = triangleSign(point, v3, v1); + const hasNeg = d1 < 0 || d2 < 0 || d3 < 0; + const hasPos = d1 > 0 || d2 > 0 || d3 > 0; + return !(hasNeg && hasPos); + } + + // ------------------------------------------------------------------------- + // Keyboard navigation + // ------------------------------------------------------------------------- + + _selectMenuItem({ + key, + target + }) { + const currentMenu = target.closest(SELECTOR_MENU$2) || this._menu; + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$2, !items.includes(target)).focus(); + } + _handleSubmenuKeydown(event) { + const { + key, + target + } = event; + const isRtl = isRTL$1(); + const enterKey = isRtl ? ARROW_LEFT_KEY$1 : ARROW_RIGHT_KEY$1; + const exitKey = isRtl ? ARROW_RIGHT_KEY$1 : ARROW_LEFT_KEY$1; + const submenuWrapper = target.closest(SELECTOR_SUBMENU); + const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE); + if ((key === ENTER_KEY$1 || key === SPACE_KEY$1) && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === enterKey && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === exitKey) { + const currentMenu = target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper) { + event.preventDefault(); + event.stopPropagation(); + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + this._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return true; + } + } + if (key === HOME_KEY$2 || key === END_KEY$2) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = target.closest(SELECTOR_MENU$2); + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (items.length) { + const targetItem = key === HOME_KEY$2 ? items[0] : items.at(-1); + targetItem.focus(); + } + return true; + } + return false; + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { + return; + } + for (const instance of Menu._openInstances) { + if (instance._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(instance._menu); + if (composedPath.includes(instance._element) || instance._config.autoClose === 'inside' && !isMenuTarget || instance._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Don't auto-close when interacting with a form inside the menu — clicks + // on a form's labels, buttons, etc. (not just inputs) should keep it open. + const formAncestor = event.target.closest?.('form'); + const isInsideMenuForm = Boolean(formAncestor) && instance._menu.contains(formAncestor); + if (instance._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName) || isInsideMenuForm)) { + continue; + } + const relatedTarget = { + relatedTarget: instance._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + instance._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the + // menu doesn't hijack their arrow keys. + const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable; + const isEscapeEvent = event.key === ESCAPE_KEY$2; + const isUpOrDownEvent = [ARROW_UP_KEY$2, ARROW_DOWN_KEY$2].includes(event.key); + const isLeftOrRightEvent = [ARROW_LEFT_KEY$1, ARROW_RIGHT_KEY$1].includes(event.key); + const isHomeOrEndEvent = [HOME_KEY$2, END_KEY$2].includes(event.key); + const isEnterOrSpaceEvent = [ENTER_KEY$1, SPACE_KEY$1].includes(event.key); + const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE); + if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent && !(isEnterOrSpaceEvent && isSubmenuTrigger)) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$8) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8, event.delegateTarget.parentNode); + if (!getToggleButton) { + return; + } + const instance = Menu.getOrCreateInstance(getToggleButton); + if ((isLeftOrRightEvent || isHomeOrEndEvent || isEnterOrSpaceEvent && isSubmenuTrigger) && instance._handleSubmenuKeydown(event)) { + return; + } + if (isUpOrDownEvent) { + event.preventDefault(); + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (isEscapeEvent && instance._isShown()) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = event.target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper && instance._openSubmenus.size > 0) { + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + instance._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return; + } + instance.hide(); + getToggleButton.focus(); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$8, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU$2, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, Menu.clearMenus); +EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_TOGGLE$8, function (event) { + event.preventDefault(); + Menu.getOrCreateInstance(this).toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap combobox.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$g = 'combobox'; +const DATA_KEY$c = 'bs.combobox'; +const EVENT_KEY$d = `.${DATA_KEY$c}`; +const DATA_API_KEY$8 = '.data-api'; +const ESCAPE_KEY$1 = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY$1 = 'ArrowUp'; +const ARROW_DOWN_KEY$1 = 'ArrowDown'; +const HOME_KEY$1 = 'Home'; +const END_KEY$1 = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const EVENT_CHANGE$3 = `change${EVENT_KEY$d}`; +const EVENT_SHOW$5 = `show${EVENT_KEY$d}`; +const EVENT_SHOWN$4 = `shown${EVENT_KEY$d}`; +const EVENT_HIDE$4 = `hide${EVENT_KEY$d}`; +const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$d}`; +const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$d}${DATA_API_KEY$8}`; +const CLASS_NAME_SHOW$3 = 'show'; +const CLASS_NAME_SELECTED = 'selected'; +const CLASS_NAME_PLACEHOLDER = 'combobox-placeholder'; +const SELECTOR_DATA_TOGGLE$7 = '[data-bs-toggle="combobox"]'; +const SELECTOR_MENU$1 = '.menu'; +const SELECTOR_MENU_ITEM = '.menu-item[data-bs-value]'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item[data-bs-value]:not(.disabled):not(:disabled)'; +const SELECTOR_VALUE = '.combobox-value'; +const SELECTOR_SEARCH_INPUT = '.combobox-search-input'; +const SELECTOR_NO_RESULTS = '.combobox-no-results'; +const Default$f = { + boundary: 'clippingParents', + multiple: false, + name: null, + offset: [0, 2], + placeholder: '', + placement: 'bottom-start', + search: false, + searchNormalize: false +}; +const DefaultType$f = { + boundary: '(string|element)', + multiple: 'boolean', + name: '(string|null)', + offset: '(array|string|function)', + placeholder: 'string', + placement: 'string', + search: 'boolean', + searchNormalize: 'boolean' +}; + +/** + * Class definition + */ + +class Combobox extends BaseComponent { + constructor(element, config) { + super(element, config); + this._toggle = this._element; + this._menu = SelectorEngine.next(this._toggle, SELECTOR_MENU$1)[0]; + this._valueDisplay = SelectorEngine.findOne(SELECTOR_VALUE, this._toggle); + this._searchInput = SelectorEngine.findOne(SELECTOR_SEARCH_INPUT, this._menu); + this._noResults = SelectorEngine.findOne(SELECTOR_NO_RESULTS, this._menu); + this._hiddenInput = null; + this._menuInstance = null; + this._createHiddenInput(); + this._createMenuInstance(); + this._syncInitialSelection(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$f; + } + static get DefaultType() { + return DefaultType$f; + } + static get NAME() { + return NAME$g; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._toggle) || this._isShown()) { + return; + } + const showEvent = EventHandler.trigger(this._toggle, EVENT_SHOW$5); + if (showEvent.defaultPrevented) { + return; + } + this._menuInstance.show(); + if (this._searchInput) { + this._searchInput.value = ''; + this._filterItems(''); + requestAnimationFrame(() => this._searchInput.focus()); + } + EventHandler.trigger(this._toggle, EVENT_SHOWN$4); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._toggle, EVENT_HIDE$4); + if (hideEvent.defaultPrevented) { + return; + } + this._menuInstance.hide(); + EventHandler.trigger(this._toggle, EVENT_HIDDEN$6); + } + dispose() { + if (this._menuInstance) { + this._menuInstance.dispose(); + this._menuInstance = null; + } + if (this._hiddenInput) { + this._hiddenInput.remove(); + this._hiddenInput = null; + } + EventHandler.off(this._menu, EVENT_KEY$d); + EventHandler.off(this._toggle, EVENT_KEY$d); + super.dispose(); + } + + // Private + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$3); + } + _createHiddenInput() { + const { + name + } = this._config; + if (!name) { + return; + } + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = name; + this._hiddenInput.value = ''; + this._toggle.parentNode.insertBefore(this._hiddenInput, this._toggle); + } + _createMenuInstance() { + this._menuInstance = new Menu(this._toggle, { + menu: this._menu, + autoClose: this._config.multiple ? 'outside' : true, + boundary: this._config.boundary, + offset: this._config.offset, + placement: this._config.placement + }); + } + _syncInitialSelection() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length > 0) { + this._updateToggleText(); + this._updateHiddenInput(); + } else { + this._showPlaceholder(); + } + } + _addEventListeners() { + EventHandler.on(this._menu, 'click', SELECTOR_MENU_ITEM, event => { + const item = event.target.closest(SELECTOR_MENU_ITEM); + if (!item || isDisabled(item)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this._selectItem(item); + }); + EventHandler.on(this._toggle, 'keydown', event => { + this._handleToggleKeydown(event); + }); + EventHandler.on(this._menu, 'keydown', event => { + this._handleMenuKeydown(event); + }); + if (this._searchInput) { + EventHandler.on(this._searchInput, 'input', () => { + this._filterItems(this._searchInput.value); + }); + EventHandler.on(this._searchInput, 'keydown', event => { + if (event.key === ARROW_DOWN_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + items[0].focus(); + } + } + if (event.key === ESCAPE_KEY$1) { + this.hide(); + this._toggle.focus(); + } + }); + } + } + _selectItem(item) { + if (this._config.multiple) { + item.classList.toggle(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', item.classList.contains(CLASS_NAME_SELECTED)); + } else { + const previouslySelected = SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + for (const prev of previouslySelected) { + prev.classList.remove(CLASS_NAME_SELECTED); + prev.setAttribute('aria-selected', 'false'); + } + item.classList.add(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', 'true'); + } + this._updateToggleText(); + this._updateHiddenInput(); + const value = this._config.multiple ? this._getSelectedItems().map(el => el.dataset.bsValue) : item.dataset.bsValue; + EventHandler.trigger(this._toggle, EVENT_CHANGE$3, { + value, + item + }); + if (!this._config.multiple) { + this.hide(); + this._toggle.focus(); + } + } + _updateToggleText() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length === 0) { + this._showPlaceholder(); + return; + } + this._valueDisplay.classList.remove(CLASS_NAME_PLACEHOLDER); + if (this._config.multiple && selectedItems.length > 1) { + this._valueDisplay.textContent = `${selectedItems.length} selected`; + } else { + const item = selectedItems[0]; + const label = SelectorEngine.findOne('.menu-item-content > span:first-child', item); + this._valueDisplay.textContent = label ? label.textContent : item.textContent.trim(); + } + } + _showPlaceholder() { + const { + placeholder + } = this._config; + if (placeholder) { + this._valueDisplay.textContent = placeholder; + this._valueDisplay.classList.add(CLASS_NAME_PLACEHOLDER); + } + } + _updateHiddenInput() { + if (!this._hiddenInput) { + return; + } + const selectedItems = this._getSelectedItems(); + const values = selectedItems.map(el => el.dataset.bsValue); + this._hiddenInput.value = this._config.multiple ? values.join(',') : values[0] || ''; + } + _getSelectedItems() { + return SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + } + _getVisibleItems() { + return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(item => isVisible(item)); + } + _filterItems(query) { + const normalizedQuery = this._normalizeText(query.toLowerCase().trim()); + const items = SelectorEngine.find(SELECTOR_MENU_ITEM, this._menu); + let visibleCount = 0; + for (const item of items) { + const text = this._normalizeText(item.textContent.toLowerCase().trim()); + const matches = !normalizedQuery || text.includes(normalizedQuery); + item.style.display = matches ? '' : 'none'; + if (matches) { + visibleCount++; + } + } + if (this._noResults) { + this._noResults.classList.toggle('d-none', visibleCount > 0); + } + } + _normalizeText(text) { + if (this._config.searchNormalize) { + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + } + return text; + } + _handleToggleKeydown(event) { + const { + key + } = event; + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + if (!this._isShown()) { + this.show(); + } + const items = this._getVisibleItems(); + if (items.length > 0) { + const target = key === ARROW_DOWN_KEY$1 ? items[0] : items.at(-1); + target.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !this._isShown()) { + event.preventDefault(); + this.show(); + } + } + _handleMenuKeydown(event) { + const { + key, + target + } = event; + if (key === ESCAPE_KEY$1) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + this._toggle.focus(); + return; + } + if (key === TAB_KEY) { + this.hide(); + return; + } + const isInput = target.matches('input'); + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); + } + return; + } + if (key === HOME_KEY$1 || key === END_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const targetItem = key === HOME_KEY$1 ? items[0] : items.at(-1); + targetItem.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !isInput) { + event.preventDefault(); + const item = target.closest(SELECTOR_MENU_ITEM); + if (item && !isDisabled(item)) { + this._selectItem(item); + } + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Combobox.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$7, function (event) { + event.preventDefault(); + Combobox.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const toggle of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7)) { + Combobox.getOrCreateInstance(toggle); + } +}); + +/*! name: vanilla-calendar-pro v3.1.0 | url: https://github.com/uvarov-frontend/vanilla-calendar-pro */ +var __defProp=Object.defineProperty,__defProps=Object.defineProperties,__getOwnPropDescs=Object.getOwnPropertyDescriptors,__getOwnPropSymbols=Object.getOwnPropertySymbols,__hasOwnProp=Object.prototype.hasOwnProperty,__propIsEnum=Object.prototype.propertyIsEnumerable,__defNormalProp=(e,t,n)=>t in e?__defProp(e,t,{enumerable:true,configurable:true,writable:true,value:n}):e[t]=n,__spreadValues=(e,t)=>{for(var n in t||(t={}))__hasOwnProp.call(t,n)&&__defNormalProp(e,n,t[n]);if(__getOwnPropSymbols)for(var n of __getOwnPropSymbols(t))__propIsEnum.call(t,n)&&__defNormalProp(e,n,t[n]);return e},__spreadProps=(e,t)=>__defProps(e,__getOwnPropDescs(t)),__publicField=(e,t,n)=>(__defNormalProp(e,"symbol"!=typeof t?t+"":t,n),n);const errorMessages={notFoundSelector:e=>`${e} is not found, check the first argument passed to new Calendar.`,notInit:'The calendar has not been initialized, please initialize it using the "init()" method first.',notLocale:"You specified an incorrect language label or did not specify the required number of values for «locale.weekdays» or «locale.months».",incorrectTime:"The value of the time property can be: false, 12 or 24.",incorrectMonthsCount:"For the «multiple» calendar type, the «displayMonthsCount» parameter can have a value from 2 to 12, and for all others it cannot be greater than 1."},setContext=(e,t,n)=>{e.context[t]=n;},destroy=e=>{var t,n,a,o,l;if(!e.context.isInit)throw new Error(errorMessages.notInit);e.inputMode?(null==(t=e.context.mainElement.parentElement)||t.removeChild(e.context.mainElement),null==(a=null==(n=e.context.inputElement)?void 0:n.replaceWith)||a.call(n,e.context.originalElement),setContext(e,"inputElement",void 0)):null==(l=(o=e.context.mainElement).replaceWith)||l.call(o,e.context.originalElement),setContext(e,"mainElement",e.context.originalElement),e.onDestroy&&e.onDestroy(e);},skipOpenOnFocus=new WeakSet,shouldSkipOpenOnFocus=e=>skipOpenOnFocus.has(e),setSkipOpenOnFocus=e=>{skipOpenOnFocus.add(e);},clearSkipOpenOnFocus=e=>{skipOpenOnFocus.delete(e);},PREV_TABINDEX_ATTR="data-vc-prev-tabindex",isFocusable=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),storePrevTabIndex=e=>{if(e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute("tabindex");e.setAttribute(PREV_TABINDEX_ATTR,null!=t?t:"");},restorePrevTabIndex=e=>{if(!e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute(PREV_TABINDEX_ATTR);""===t||null===t?e.removeAttribute("tabindex"):e.setAttribute("tabindex",t),e.removeAttribute(PREV_TABINDEX_ATTR);},disableTabbing=e=>{isFocusable(e)&&(storePrevTabIndex(e),e.tabIndex=-1);const t=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>isFocusable(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP});for(;t.nextNode();){const e=t.currentNode;storePrevTabIndex(e),e.tabIndex=-1;}},restoreTabbing=e=>{restorePrevTabIndex(e),e.querySelectorAll(`[${PREV_TABINDEX_ATTR}]`).forEach(restorePrevTabIndex);},hide=e=>{if(e.context.isShowInInputMode&&e.context.currentType){if(e.context.mainElement.dataset.vcCalendarHidden="",setContext(e,"isShowInInputMode",false),e.inputMode&&disableTabbing(e.context.mainElement),e.context.cleanupHandlers[0]&&(e.context.cleanupHandlers.forEach((e=>e())),setContext(e,"cleanupHandlers",[])),e.inputMode&&e.context.inputElement&&e.context.mainElement.contains(document.activeElement)){("function"==typeof e.openOnFocus||true===e.openOnFocus)&&setSkipOpenOnFocus(e),e.context.inputElement.focus();}e.onHide&&e.onHide(e);}};function getOffset(e){if(!e||!e.getBoundingClientRect)return {top:0,bottom:0,left:0,right:0};const t=e.getBoundingClientRect(),n=document.documentElement;return {bottom:t.bottom,right:t.right,top:t.top+window.scrollY-n.clientTop,left:t.left+window.scrollX-n.clientLeft}}function getViewportDimensions(){return {vw:Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),vh:Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)}}function getWindowScrollPosition(){return {left:window.scrollX||document.documentElement.scrollLeft||0,top:window.scrollY||document.documentElement.scrollTop||0}}function calculateAvailableSpace(e){const{top:t,left:n}=getWindowScrollPosition(),{top:a,left:o}=getOffset(e),{vh:l,vw:s}=getViewportDimensions(),i=a-t,r=o-n;return {top:i,bottom:l-(i+e.clientHeight),left:r,right:s-(r+e.clientWidth)}}function getAvailablePosition(e,t,n=5){const a={top:true,bottom:true,left:true,right:true},o=[];if(!t||!e)return {canShow:a,parentPositions:o};const{bottom:l,top:s}=calculateAvailableSpace(e),{top:i,left:r}=getOffset(e),{height:c,width:d}=t.getBoundingClientRect(),{vh:u,vw:m}=getViewportDimensions(),p=m/2,h=u/2;return [{condition:ih,position:"bottom"},{condition:rp,position:"right"}].forEach((({condition:e,position:t})=>{e&&o.push(t);})),Object.assign(a,{top:c<=s-n,bottom:c<=l-n,left:d<=r,right:d<=m-r}),{canShow:a,parentPositions:o}}const handleDay=(e,t,n,a)=>{var o;const l=a.querySelector(`[data-vc-date="${t}"]`),s=null==l?void 0:l.querySelector("[data-vc-date-btn]");if(!l||!s)return;if((null==n?void 0:n.modifier)&&s.classList.add(...n.modifier.trim().split(" ")),!(null==n?void 0:n.html))return;const i=document.createElement("div");i.className=e.styles.datePopup,i.dataset.vcDatePopup="",i.innerHTML=e.sanitizerHTML(n.html),s.ariaExpanded="true",s.ariaLabel=`${s.ariaLabel}, ${null==(o=null==i?void 0:i.textContent)?void 0:o.replace(/^\s+|\s+(?=\s)|\s+$/g,"").replace(/ /g," ")}`,l.appendChild(i),requestAnimationFrame((()=>{if(!i)return;const{canShow:e}=getAvailablePosition(l,i),t=e.bottom?l.offsetHeight:-i.offsetHeight,n=e.left&&!e.right?l.offsetWidth-i.offsetWidth/2:!e.left&&e.right?i.offsetWidth/2:0;Object.assign(i.style,{left:`${n}px`,top:`${t}px`});}));},createDatePopup=(e,t)=>{var n;e.popups&&(null==(n=Object.entries(e.popups))||n.forEach((([n,a])=>handleDay(e,n,a,t))));},getDate=e=>new Date(`${e}T00:00:00`),getDateString=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`,parseDates=e=>e.reduce(((e,t)=>{if(t instanceof Date||"number"==typeof t){const n=t instanceof Date?t:new Date(t);e.push(n.toISOString().substring(0,10));}else t.match(/^(\d{4}-\d{2}-\d{2})$/g)?e.push(t):t.replace(/(\d{4}-\d{2}-\d{2}).*?(\d{4}-\d{2}-\d{2})/g,((t,n,a)=>{const o=getDate(n),l=getDate(a),s=new Date(o.getTime());for(;s<=l;s.setDate(s.getDate()+1))e.push(getDateString(s));return t}));return e}),[]),updateAttribute=(e,t,n,a="")=>{t?e.setAttribute(n,a):e.getAttribute(n)===a&&e.removeAttribute(n);},setDateModifier=(e,t,n,a,o,l,s)=>{var i,r,c,d;const u=getDate(e.context.displayDateMin)>getDate(l)||getDate(e.context.displayDateMax)1&&"multiple-ranged"===e.selectionDatesMode&&(e.context.selectedDates[0]===l&&e.context.selectedDates[e.context.selectedDates.length-1]===l?n.setAttribute("data-vc-date-selected","first-and-last"):e.context.selectedDates[0]===l?n.setAttribute("data-vc-date-selected","first"):e.context.selectedDates[e.context.selectedDates.length-1]===l&&n.setAttribute("data-vc-date-selected","last"),e.context.selectedDates[0]!==l&&e.context.selectedDates[e.context.selectedDates.length-1]!==l&&n.setAttribute("data-vc-date-selected","middle"))):n.hasAttribute("data-vc-date-selected")&&(n.removeAttribute("data-vc-date-selected"),a&&a.removeAttribute("aria-selected")),!e.context.disableDates.includes(l)&&e.enableEdgeDatesOnly&&e.context.selectedDates.length>1&&"multiple-ranged"===e.selectionDatesMode){const t=getDate(e.context.selectedDates[0]),a=getDate(e.context.selectedDates[e.context.selectedDates.length-1]),o=getDate(l);updateAttribute(n,o>t&&onew Date(`${e}T00:00:00.000Z`).toLocaleString(t,n),getWeekNumber=(e,t)=>{const n=getDate(e),a=(n.getDay()-t+7)%7;n.setDate(n.getDate()+4-a);const o=new Date(n.getFullYear(),0,1),l=Math.ceil(((+n-+o)/864e5+1)/7);return {year:n.getFullYear(),week:l}},addWeekNumberForDate=(e,t,n)=>{const a=getWeekNumber(n,e.firstWeekday);a&&(t.dataset.vcDateWeekNumber=String(a.week));},setDaysAsDisabled=(e,t,n)=>{var a,o,l,s,i;const r=null==(a=e.disableWeekdays)?void 0:a.includes(n),c=e.disableAllDates&&!!(null==(o=e.context.enableDates)?void 0:o[0]);!r&&!c||(null==(l=e.context.enableDates)?void 0:l.includes(t))||(null==(s=e.context.disableDates)?void 0:s.includes(t))||(e.context.disableDates.push(t),null==(i=e.context.disableDates)||i.sort(((e,t)=>+new Date(e)-+new Date(t))));},createDate=(e,t,n,a,o,l)=>{const s=getDate(o).getDay(),i="string"==typeof e.locale&&e.locale.length?e.locale:"en",r=document.createElement("div");let c;r.className=e.styles.date,r.dataset.vcDate=o,r.dataset.vcDateMonth=l,r.dataset.vcDateWeekDay=String(s),r.role="gridcell",("current"===l||e.displayDatesOutside)&&(c=document.createElement("button"),c.className=e.styles.dateBtn,c.type="button",c.ariaLabel=getLocaleString(o,i,{dateStyle:"long",timeZone:"UTC"}),c.dataset.vcDateBtn="",c.innerText=String(a),r.appendChild(c)),e.enableWeekNumbers&&addWeekNumberForDate(e,r,o),setDaysAsDisabled(e,o,s),setDateModifier(e,t,r,c,s,o,l),n.addDate(r),e.onCreateDateEls&&e.onCreateDateEls(e,r);},createDatesFromCurrentMonth=(e,t,n,a,o)=>{for(let l=1;l<=n;l++){const n=new Date(a,o,l);createDate(e,a,t,l,getDateString(n),"current");}},createDatesFromNextMonth=(e,t,n,a,o)=>{const l=o+1===12?a+1:a,s=o+1===12?"01":o+2<10?`0${o+2}`:o+2;for(let o=1;o<=n;o++){const n=o<10?`0${o}`:String(o);createDate(e,a,t,o,`${l}-${s}-${n}`,"next");}},createDatesFromPrevMonth=(e,t,n,a,o)=>{let l=new Date(n,a,0).getDate()-(o-1);const s=0===a?n-1:n,i=0===a?12:a<10?`0${a}`:a;for(let a=o;a>0;a--,l++){createDate(e,n,t,l,`${s}-${i}-${l}`,"prev");}},createWeekNumbers=(e,t,n,a,o)=>{if(!e.enableWeekNumbers)return;a.textContent="";const l=document.createElement("b");l.className=e.styles.weekNumbersTitle,l.innerText="#",l.dataset.vcWeekNumbers="title",a.appendChild(l);const s=document.createElement("div");s.className=e.styles.weekNumbersContent,s.dataset.vcWeekNumbers="content",a.appendChild(s);const i=document.createElement("button");i.type="button",i.className=e.styles.weekNumber;const r=o.querySelectorAll("[data-vc-date]"),c=Math.ceil((t+n)/7);for(let t=0;t{const t=new Date(e.context.selectedYear,e.context.selectedMonth,1),n=e.context.mainElement.querySelectorAll('[data-vc="dates"]'),a=e.context.mainElement.querySelectorAll('[data-vc-week="numbers"]');n.forEach(((n,o)=>{e.selectionDatesMode||(n.dataset.vcDatesDisabled=""),n.textContent="";const l=new Date(t);l.setMonth(l.getMonth()+o);const s=l.getMonth(),i=l.getFullYear(),r=(new Date(i,s,1).getDay()-e.firstWeekday+7)%7,c=new Date(i,s+1,0).getDate(),d=r+c,u=Math.ceil(d/7),m=7*u-d,p=[];for(let t=0;t{p[h].appendChild(e),v++,v>=7&&(h++,v=0);}};createDatesFromPrevMonth(e,g,i,s,r),createDatesFromCurrentMonth(e,g,c,i,s),createDatesFromNextMonth(e,g,m,i,s);for(const e of p)n.appendChild(e);createDatePopup(e,n),createWeekNumbers(e,r,c,a[o],n);}));},layoutDefault=e=>`\n \n <#ArrowPrev [month] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [month] />\n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n <#DateRangeTooltip />\n \n \n <#ControlTime />\n`,layoutMonths=e=>`\n \n \n <#Month />\n <#Year />\n \n \n \n \n <#Months />\n \n \n`,layoutMultiple=e=>`\n \n <#ArrowPrev [month] />\n <#ArrowNext [month] />\n \n \n <#Multiple>\n \n \n \n <#Month />\n <#Year />\n \n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n \n \n \n <#/Multiple>\n <#DateRangeTooltip />\n \n <#ControlTime />\n`,layoutYears=e=>`\n \n <#ArrowPrev [year] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [year] />\n \n \n \n <#Years />\n \n \n`,ArrowNext=(e,t)=>``,ArrowPrev=(e,t)=>``,ControlTime=e=>e.selectionTimeMode?``:"",DateRangeTooltip=e=>e.onCreateDateRangeTooltip?``:"",Dates=e=>``,Month=e=>``,Months=e=>``,Week=e=>``,WeekNumbers=e=>e.enableWeekNumbers?``:"",Year=e=>``,Years=e=>``,components={ArrowNext:ArrowNext,ArrowPrev:ArrowPrev,ControlTime:ControlTime,Dates:Dates,DateRangeTooltip:DateRangeTooltip,Month:Month,Months:Months,Week:Week,WeekNumbers:WeekNumbers,Year:Year,Years:Years},getComponent=e=>components[e],parseLayout=(e,t)=>t.replace(/[\n\t]/g,"").replace(/<#(?!\/?Multiple)(.*?)>/g,((t,n)=>{const a=(n.match(/\[(.*?)\]/)||[])[1],o=n.replace(/[/\s\n\t]|\[(.*?)\]/g,""),l=getComponent(o),s=l?l(e,null!=a?a:null):"";return e.sanitizerHTML(s)})).replace(/[\n\t]/g,""),parseMultipleLayout=(e,t)=>t.replace(new RegExp("<#Multiple>(.*?)<#\\/Multiple>","gs"),((t,n)=>{const a=Array(e.context.displayMonthsCount).fill(n).join("");return e.sanitizerHTML(a)})).replace(/[\n\t]/g,""),createLayouts=(e,t)=>{const n={default:layoutDefault,month:layoutMonths,year:layoutYears,multiple:layoutMultiple};if(Object.keys(n).forEach((t=>{const a=t;e.layouts[a].length||(e.layouts[a]=n[a](e));})),e.context.mainElement.className=e.styles.calendar,e.context.mainElement.dataset.vc="calendar",e.context.mainElement.dataset.vcType=e.context.currentType,e.context.mainElement.role="application",e.context.mainElement.tabIndex=0,e.context.mainElement.ariaLabel=e.labels.application,"multiple"!==e.context.currentType){if("multiple"===e.type&&t){const n=e.context.mainElement.querySelector('[data-vc="controls"]'),a=e.context.mainElement.querySelector('[data-vc="grid"]'),o=t.closest('[data-vc="column"]');return n&&n.remove(),a&&(a.dataset.vcGrid="hidden"),o&&(o.dataset.vcColumn=e.context.currentType),void(o&&(o.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))))}e.context.mainElement.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]));}else e.context.mainElement.innerHTML=e.sanitizerHTML(parseMultipleLayout(e,parseLayout(e,e.layouts[e.context.currentType])));},setVisibilityArrows=(e,t,n,a)=>{e.style.visibility=n?"hidden":"",t.style.visibility=a?"hidden":"";},handleDefaultType=(e,t,n)=>{const a=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1))),o=new Date(a.getTime()),l=new Date(a.getTime());o.setMonth(o.getMonth()-e.monthsToSwitch),l.setMonth(l.getMonth()+e.monthsToSwitch);const s=getDate(e.context.dateMin),i=getDate(e.context.dateMax);e.selectionYearsMode||(s.setFullYear(a.getFullYear()),i.setFullYear(a.getFullYear()));const r=!e.selectionMonthsMode||o.getFullYear()i.getFullYear()||l.getFullYear()===i.getFullYear()&&l.getMonth()>i.getMonth()-(e.context.displayMonthsCount-1);setVisibilityArrows(t,n,r,c);},handleYearType=(e,t,n)=>{const a=getDate(e.context.dateMin),o=getDate(e.context.dateMax),l=!!(a.getFullYear()&&e.context.displayYear-7<=a.getFullYear()),s=!!(o.getFullYear()&&e.context.displayYear+7>=o.getFullYear());setVisibilityArrows(t,n,l,s);},visibilityArrows=e=>{if("month"===e.context.currentType)return;const t=e.context.mainElement.querySelector('[data-vc-arrow="prev"]'),n=e.context.mainElement.querySelector('[data-vc-arrow="next"]');if(!t||!n)return;({default:()=>handleDefaultType(e,t,n),year:()=>handleYearType(e,t,n)})["multiple"===e.context.currentType?"default":e.context.currentType]();},visibilityHandler=(e,t,n,a,o)=>{const l=new Date(a.setFullYear(e.context.selectedYear,e.context.selectedMonth+n)).getFullYear(),s=new Date(a.setMonth(e.context.selectedMonth+n)).getMonth(),i=e.context.locale.months.long[s],r=t.closest('[data-vc="column"]');r&&(r.ariaLabel=`${i} ${l}`);const c={month:{id:s,label:i},year:{id:l,label:l}};t.innerText=String(c[o].label),t.dataset[`vc${o.charAt(0).toUpperCase()+o.slice(1)}`]=String(c[o].id),t.ariaLabel=`${e.labels[o]} ${c[o].label}`;const d={month:e.selectionMonthsMode,year:e.selectionYearsMode},u=false===d[o]||"only-arrows"===d[o];u&&(t.tabIndex=-1),t.disabled=u;},visibilityTitle=e=>{const t=e.context.mainElement.querySelectorAll('[data-vc="month"]'),n=e.context.mainElement.querySelectorAll('[data-vc="year"]'),a=new Date(e.context.selectedYear,e.context.selectedMonth,1);[t,n].forEach((t=>null==t?void 0:t.forEach(((t,n)=>visibilityHandler(e,t,n,a,t.dataset.vc)))));},setYearModifier=(e,t,n,a,o)=>{var l;const s={month:"[data-vc-months-month]",year:"[data-vc-years-year]"},i={month:{selected:"data-vc-months-month-selected",aria:"aria-selected",value:"vcMonthsMonth",selectedProperty:"selectedMonth"},year:{selected:"data-vc-years-year-selected",aria:"aria-selected",value:"vcYearsYear",selectedProperty:"selectedYear"}};o&&(null==(l=e.context.mainElement.querySelectorAll(s[n]))||l.forEach((e=>{e.removeAttribute(i[n].selected),e.removeAttribute(i[n].aria);})),setContext(e,i[n].selectedProperty,Number(t.dataset[i[n].value])),visibilityTitle(e),"year"===n&&visibilityArrows(e)),a&&(t.setAttribute(i[n].selected,""),t.setAttribute(i[n].aria,"true"));},getColumnID=(e,t)=>{var n;if("multiple"!==e.type)return {currentValue:null,columnID:0};const a=e.context.mainElement.querySelectorAll('[data-vc="column"]'),o=Array.from(a).findIndex((e=>e.closest(`[data-vc-column="${t}"]`)));return {currentValue:o>=0?Number(null==(n=a[o].querySelector(`[data-vc="${t}"]`))?void 0:n.getAttribute(`data-vc-${t}`)):null,columnID:Math.max(o,0)}},createMonthEl=(e,t,n,a,o,l,s)=>{const i=t.cloneNode(false);return i.className=e.styles.monthsMonth,i.innerText=a,i.ariaLabel=o,i.role="gridcell",i.dataset.vcMonthsMonth=`${s}`,l&&(i.ariaDisabled="true"),l&&(i.tabIndex=-1),i.disabled=l,setYearModifier(e,i,"month",n===s,false),i},createMonths=(e,t)=>{var n,a;const o=null==(n=null==t?void 0:t.closest('[data-vc="header"]'))?void 0:n.querySelector('[data-vc="year"]'),l=o?Number(o.dataset.vcYear):e.context.selectedYear,s=(null==t?void 0:t.dataset.vcMonth)?Number(t.dataset.vcMonth):e.context.selectedMonth;setContext(e,"currentType","month"),createLayouts(e,t),visibilityTitle(e);const i=e.context.mainElement.querySelector('[data-vc="months"]');if(!e.selectionMonthsMode||!i)return;const r=e.monthsToSwitch>1?e.context.locale.months.long.map(((t,n)=>s-e.monthsToSwitch*n)).concat(e.context.locale.months.long.map(((t,n)=>s+e.monthsToSwitch*n))).filter((e=>e>=0&&e<=12)):Array.from(Array(12).keys()),c=document.createElement("button");c.type="button";for(let t=0;t<12;t++){const n=getDate(e.context.dateMin),a=getDate(e.context.dateMax),o=e.context.displayMonthsCount-1,{columnID:d}=getColumnID(e,"month"),u=l<=n.getFullYear()&&t=a.getFullYear()&&t>a.getMonth()-o+d||l>a.getFullYear()||t!==s&&!r.includes(t),m=createMonthEl(e,c,s,e.context.locale.months.short[t],e.context.locale.months.long[t],u,t);i.appendChild(m),e.onCreateMonthEls&&e.onCreateMonthEls(e,m);}null==(a=e.context.mainElement.querySelector("[data-vc-months-month]:not([disabled])"))||a.focus();},TimeInput=(e,t,n,a,o)=>`\n \n \n \n`,TimeRange=(e,t,n,a,o,l,s)=>`\n \n \n \n`,handleActions=(e,t,n,a)=>{(({hour:()=>setContext(e,"selectedHours",n),minute:()=>setContext(e,"selectedMinutes",n)}))[a](),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${e.context.selectedKeeping?` ${e.context.selectedKeeping}`:""}`),e.onChangeTime&&e.onChangeTime(e,t,false),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);},transformTime24=(e,t)=>{var n;return (null==(n={0:{AM:"00",PM:"12"},1:{AM:"01",PM:"13"},2:{AM:"02",PM:"14"},3:{AM:"03",PM:"15"},4:{AM:"04",PM:"16"},5:{AM:"05",PM:"17"},6:{AM:"06",PM:"18"},7:{AM:"07",PM:"19"},8:{AM:"08",PM:"20"},9:{AM:"09",PM:"21"},10:{AM:"10",PM:"22"},11:{AM:"11",PM:"23"},12:{AM:"00",PM:"12"}}[Number(e)])?void 0:n[t])||String(e)},handleClickKeepingTime=(e,t,n,a,o)=>{const l=l=>{const s="AM"===e.context.selectedKeeping?"PM":"AM",i=transformTime24(e.context.selectedHours,s);Number(i)<=a&&Number(i)>=o?(setContext(e,"selectedKeeping",s),n.value=i,handleActions(e,l,e.context.selectedHours,"hour"),t.ariaLabel=`${e.labels.btnKeeping} ${e.context.selectedKeeping}`,t.innerText=e.context.selectedKeeping):e.onChangeTime&&e.onChangeTime(e,l,true);};return t.addEventListener("click",l),()=>{t.removeEventListener("click",l);}},transformTime12=e=>({0:"12",13:"01",14:"02",15:"03",16:"04",17:"05",18:"06",19:"07",20:"08",21:"09",22:"10",23:"11"}[Number(e)]||String(e)),updateInputAndRange=(e,t,n,a)=>{e.value=n,t.value=a;},updateKeepingTime$1=(e,t,n)=>{t&&n&&(setContext(e,"selectedKeeping",n),t.innerText=n);},handleInput$1=(e,t,n,a,o,l,s)=>{const i={hour:(i,r,c)=>{if(!e.selectionTimeMode)return;({12:()=>{if(!e.context.selectedKeeping)return;const d=Number(transformTime24(r,e.context.selectedKeeping));if(!(d<=l&&d>=s))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,true));updateInputAndRange(n,t,transformTime12(r),transformTime24(r,e.context.selectedKeeping)),i>12&&updateKeepingTime$1(e,a,"PM"),handleActions(e,c,transformTime12(r),o);},24:()=>{if(!(i<=l&&i>=s))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,true));updateInputAndRange(n,t,r,r),handleActions(e,c,r,o);}})[e.selectionTimeMode]();},minute:(a,i,r)=>{if(!(a<=l&&a>=s))return n.value=e.context.selectedMinutes,void(e.onChangeTime&&e.onChangeTime(e,r,true));n.value=i,t.value=i,handleActions(e,r,i,o);}},r=e=>{const t=Number(n.value),a=n.value.padStart(2,"0");i[o]&&i[o](t,a,e);};return n.addEventListener("change",r),()=>{n.removeEventListener("change",r);}},updateInputAndTime=(e,t,n,a,o)=>{t.value=o,handleActions(e,n,o,a);},updateKeepingTime=(e,t,n)=>{t&&(setContext(e,"selectedKeeping",n),t.innerText=n);},handleRange=(e,t,n,a,o)=>{const l=l=>{const s=Number(t.value),i=t.value.padStart(2,"0"),r="hour"===o,c=24===e.selectionTimeMode,d=s>0&&s<12;r&&!c&&updateKeepingTime(e,a,0===s||d?"AM":"PM"),updateInputAndTime(e,n,l,o,!r||c||d?i:transformTime12(t.value));};return t.addEventListener("input",l),()=>{t.removeEventListener("input",l);}},handleMouseOver=e=>e.setAttribute("data-vc-input-focus",""),handleMouseOut=e=>e.removeAttribute("data-vc-input-focus"),handleTime=(e,t)=>{const n=t.querySelector('[data-vc-time-range="hour"] input[name="hour"]'),a=t.querySelector('[data-vc-time-range="minute"] input[name="minute"]'),o=t.querySelector('[data-vc-time-input="hour"] input[name="hour"]'),l=t.querySelector('[data-vc-time-input="minute"] input[name="minute"]'),s=t.querySelector('[data-vc-time="keeping"]');if(!(n&&a&&o&&l))return;const i=e=>{e.target===n&&handleMouseOver(o),e.target===a&&handleMouseOver(l);},r=e=>{e.target===n&&handleMouseOut(o),e.target===a&&handleMouseOut(l);};return t.addEventListener("mouseover",i),t.addEventListener("mouseout",r),handleInput$1(e,n,o,s,"hour",e.timeMaxHour,e.timeMinHour),handleInput$1(e,a,l,s,"minute",e.timeMaxMinute,e.timeMinMinute),handleRange(e,n,o,s,"hour"),handleRange(e,a,l,s,"minute"),s&&handleClickKeepingTime(e,s,n,e.timeMaxHour,e.timeMinHour),()=>{t.removeEventListener("mouseover",i),t.removeEventListener("mouseout",r);}},createTime=e=>{const t=e.context.mainElement.querySelector('[data-vc="time"]');if(!e.selectionTimeMode||!t)return;const[n,a]=[e.timeMinHour,e.timeMaxHour],[o,l]=[e.timeMinMinute,e.timeMaxMinute],s=e.context.selectedKeeping?transformTime24(e.context.selectedHours,e.context.selectedKeeping):e.context.selectedHours,i="range"===e.timeControls;var r;t.innerHTML=e.sanitizerHTML(`\n \n ${TimeInput("hour",e.styles.timeHour,e.labels,e.context.selectedHours,i)}\n ${TimeInput("minute",e.styles.timeMinute,e.labels,e.context.selectedMinutes,i)}\n ${12===e.selectionTimeMode?(r=e.context.selectedKeeping,`${r}`):""}\n \n \n ${TimeRange("hour",e.styles.timeRange,e.labels,n,a,e.timeStepHour,s)}\n ${TimeRange("minute",e.styles.timeRange,e.labels,o,l,e.timeStepMinute,e.context.selectedMinutes)}\n \n `),handleTime(e,t);},createWeek=e=>{const t=e.selectedWeekends?[...e.selectedWeekends]:[],n=[...e.context.locale.weekdays.long].reduce(((n,a,o)=>[...n,{id:o,titleShort:e.context.locale.weekdays.short[o],titleLong:a,isWeekend:t.includes(o)}]),[]),a=[...n.slice(e.firstWeekday),...n.slice(0,e.firstWeekday)];e.context.mainElement.querySelectorAll('[data-vc="week"]').forEach((t=>{const n=e.onClickWeekDay?document.createElement("button"):document.createElement("b");e.onClickWeekDay&&(n.type="button"),a.forEach((a=>{const o=n.cloneNode(true);o.innerText=a.titleShort,o.className=e.styles.weekDay,o.role="columnheader",o.ariaLabel=a.titleLong,o.dataset.vcWeekDay=String(a.id),a.isWeekend&&(o.dataset.vcWeekDayOff=""),t.appendChild(o);}));}));},createYearEl=(e,t,n,a,o)=>{const l=t.cloneNode(false);return l.className=e.styles.yearsYear,l.innerText=String(o),l.ariaLabel=String(o),l.role="gridcell",l.dataset.vcYearsYear=`${o}`,a&&(l.ariaDisabled="true"),a&&(l.tabIndex=-1),l.disabled=a,setYearModifier(e,l,"year",n===o,false),l},createYears=(e,t)=>{var n;const a=(null==t?void 0:t.dataset.vcYear)?Number(t.dataset.vcYear):e.context.selectedYear;setContext(e,"currentType","year"),createLayouts(e,t),visibilityTitle(e),visibilityArrows(e);const o=e.context.mainElement.querySelector('[data-vc="years"]');if(!e.selectionYearsMode||!o)return;const l="multiple"!==e.type||e.context.selectedYear===a?0:1,s=document.createElement("button");s.type="button";for(let t=e.context.displayYear-7;tgetDate(e.context.dateMax).getFullYear(),i=createYearEl(e,s,a,n,t);o.appendChild(i),e.onCreateYearEls&&e.onCreateYearEls(e,i);}null==(n=e.context.mainElement.querySelector("[data-vc-years-year]:not([disabled])"))||n.focus();},trackChangesHTMLElement=(e,t,n)=>{new MutationObserver((e=>{for(let a=0;ahaveListener.value=true,check:()=>haveListener.value},setTheme=(e,t)=>e.dataset.vcTheme=t,trackChangesThemeInSystemSettings=(e,t)=>{if(setTheme(e.context.mainElement,t.matches?"dark":"light"),"system"!==e.selectedTheme||haveListener.check())return;const n=e=>{const t=document.querySelectorAll('[data-vc="calendar"]');null==t||t.forEach((t=>setTheme(t,e.matches?"dark":"light")));};t.addEventListener?t.addEventListener("change",n):t.addListener(n),haveListener.set();},detectTheme=(e,t)=>{const n=e.themeAttrDetect.length?document.querySelector(e.themeAttrDetect):null,a=e.themeAttrDetect.replace(/^.*\[(.+)\]/g,((e,t)=>t));if(!n||"system"===n.getAttribute(a))return void trackChangesThemeInSystemSettings(e,t);const o=n.getAttribute(a);o?(setTheme(e.context.mainElement,o),trackChangesHTMLElement(n,a,(()=>{const t=n.getAttribute(a);t&&setTheme(e.context.mainElement,t);}))):trackChangesThemeInSystemSettings(e,t);},handleTheme=e=>{"not all"!==window.matchMedia("(prefers-color-scheme)").media?"system"===e.selectedTheme?detectTheme(e,window.matchMedia("(prefers-color-scheme: dark)")):setTheme(e.context.mainElement,e.selectedTheme):setTheme(e.context.mainElement,"light");},capitalizeFirstLetter=e=>e.charAt(0).toUpperCase()+e.slice(1).replace(/\./,""),getLocaleWeekday=(e,t,n)=>{const a=new Date(`1978-01-0${t+1}T00:00:00.000Z`),o=a.toLocaleString(n,{weekday:"short",timeZone:"UTC"}),l=a.toLocaleString(n,{weekday:"long",timeZone:"UTC"});e.context.locale.weekdays.short.push(capitalizeFirstLetter(o)),e.context.locale.weekdays.long.push(capitalizeFirstLetter(l));},getLocaleMonth=(e,t,n)=>{const a=new Date(`1978-${String(t+1).padStart(2,"0")}-01T00:00:00.000Z`),o=a.toLocaleString(n,{month:"short",timeZone:"UTC"}),l=a.toLocaleString(n,{month:"long",timeZone:"UTC"});e.context.locale.months.short.push(capitalizeFirstLetter(o)),e.context.locale.months.long.push(capitalizeFirstLetter(l));},getLocale=e=>{var t,n,a,o,l,s,i,r;if(!(e.context.locale.weekdays.short[6]&&e.context.locale.weekdays.long[6]&&e.context.locale.months.short[11]&&e.context.locale.months.long[11]))if("string"==typeof e.locale){if("string"==typeof e.locale&&!e.locale.length)throw new Error(errorMessages.notLocale);Array.from({length:7},((t,n)=>getLocaleWeekday(e,n,e.locale))),Array.from({length:12},((t,n)=>getLocaleMonth(e,n,e.locale)));}else {if(!((null==(n=null==(t=e.locale)?void 0:t.weekdays)?void 0:n.short[6])&&(null==(o=null==(a=e.locale)?void 0:a.weekdays)?void 0:o.long[6])&&(null==(s=null==(l=e.locale)?void 0:l.months)?void 0:s.short[11])&&(null==(r=null==(i=e.locale)?void 0:i.months)?void 0:r.long[11])))throw new Error(errorMessages.notLocale);setContext(e,"locale",__spreadValues({},e.locale));}},create=e=>{const t={default:()=>{createWeek(e),createDates(e);},multiple:()=>{createWeek(e),createDates(e);},month:()=>createMonths(e),year:()=>createYears(e)};handleTheme(e),getLocale(e),createLayouts(e),visibilityTitle(e),visibilityArrows(e),createTime(e),t[e.context.currentType]();},handleArrowKeys=e=>{const t=t=>{var n;const a=t.target;if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key)||"button"!==a.localName)return;const o=Array.from(e.context.mainElement.querySelectorAll('[data-vc="calendar"] button')),l=o.indexOf(a);if(-1===l)return;const s=(i=o[l]).hasAttribute("data-vc-date-btn")?7:i.hasAttribute("data-vc-months-month")?4:i.hasAttribute("data-vc-years-year")?5:1;var i;const r=(0, {ArrowUp:()=>Math.max(0,l-s),ArrowDown:()=>Math.min(o.length-1,l+s),ArrowLeft:()=>Math.max(0,l-1),ArrowRight:()=>Math.min(o.length-1,l+1)}[t.key])();null==(n=o[r])||n.focus();};return e.context.mainElement.addEventListener("keydown",t),()=>e.context.mainElement.removeEventListener("keydown",t)},handleMonth=(e,t)=>{const n=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1)));(({prev:()=>n.setMonth(n.getMonth()-e.monthsToSwitch),next:()=>n.setMonth(n.getMonth()+e.monthsToSwitch)}))[t](),setContext(e,"selectedMonth",n.getMonth()),setContext(e,"selectedYear",n.getFullYear()),visibilityTitle(e),visibilityArrows(e),createDates(e);},handleClickArrow=(e,t)=>{const n=t.target.closest("[data-vc-arrow]");if(n){if(["default","multiple"].includes(e.context.currentType))handleMonth(e,n.dataset.vcArrow);else if("year"===e.context.currentType&&void 0!==e.context.displayYear){const a={prev:-15,next:15}[n.dataset.vcArrow];setContext(e,"displayYear",e.context.displayYear+a),createYears(e,t.target);}e.onClickArrow&&e.onClickArrow(e,t);}},resolveToggle=(e,t)=>void 0===t||("function"==typeof t?t(e):t),canToggleSelection=e=>resolveToggle(e,e.enableDateToggle),handleSelectDate=(e,t,n)=>{const a=t.dataset.vcDate,o=t.closest("[data-vc-date][data-vc-date-selected]"),l=canToggleSelection(e);if(o&&!l)return;const s=o?e.context.selectedDates.filter((e=>e!==a)):n?[...e.context.selectedDates,a]:[a];setContext(e,"selectedDates",s);},createDateRangeTooltip=(e,t,n)=>{if(!t)return;if(!n)return t.dataset.vcDateRangeTooltip="hidden",void(t.textContent="");const a=e.context.mainElement.getBoundingClientRect(),o=n.getBoundingClientRect();t.style.left=o.left-a.left+o.width/2+"px",t.style.top=o.bottom-a.top-o.height+"px",t.dataset.vcDateRangeTooltip="visible",t.innerHTML=e.sanitizerHTML(e.onCreateDateRangeTooltip(e,n,t,o,a));},state={self:null,lastDateEl:null,isHovering:false,rangeMin:void 0,rangeMax:void 0,tooltipEl:null,timeoutId:null},addHoverEffect=(e,t,n)=>{var a,o,l;if(!(null==(o=null==(a=state.self)?void 0:a.context)?void 0:o.selectedDates[0]))return;const s=getDateString(e);(null==(l=state.self.context.disableDates)?void 0:l.includes(s))||(state.self.context.mainElement.querySelectorAll(`[data-vc-date="${s}"]`).forEach((e=>e.dataset.vcDateHover="")),t.forEach((e=>e.dataset.vcDateHover="first")),n.forEach((e=>{"first"===e.dataset.vcDateHover?e.dataset.vcDateHover="first-and-last":e.dataset.vcDateHover="last";})));},removeHoverEffect=()=>{var e,t;if(!(null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.mainElement))return;state.self.context.mainElement.querySelectorAll("[data-vc-date-hover]").forEach((e=>e.removeAttribute("data-vc-date-hover")));},handleHoverDatesEvent=e=>{var t,n;if(!e||!(null==(n=null==(t=state.self)?void 0:t.context)?void 0:n.selectedDates[0]))return;if(!e.closest('[data-vc="dates"]'))return state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),void removeHoverEffect();const a=e.closest("[data-vc-date]");if(!a||state.lastDateEl===a)return;state.lastDateEl=a,createDateRangeTooltip(state.self,state.tooltipEl,a),removeHoverEffect();const o=a.dataset.vcDate,l=getDate(state.self.context.selectedDates[0]),s=getDate(o),i=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${state.self.context.selectedDates[0]}"]`),r=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${o}"]`),[c,d]=l{const t=null==e?void 0:e.closest("[data-vc-date-selected]");if(!t&&state.lastDateEl)return state.lastDateEl=null,void createDateRangeTooltip(state.self,state.tooltipEl,null);t&&state.lastDateEl!==t&&(state.lastDateEl=t,createDateRangeTooltip(state.self,state.tooltipEl,t));},optimizedHoverHandler=e=>t=>{const n=t.target;state.isHovering||(state.isHovering=true,requestAnimationFrame((()=>{e(n),state.isHovering=false;})));},optimizedHandleHoverDatesEvent=optimizedHoverHandler(handleHoverDatesEvent),optimizedHandleHoverSelectedDatesRangeEvent=optimizedHoverHandler(handleHoverSelectedDatesRangeEvent),handleCancelSelectionDates=e=>{state.self&&"Escape"===e.key&&(state.lastDateEl=null,setContext(state.self,"selectedDates",[]),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect());},handleMouseLeave=()=>{null!==state.timeoutId&&clearTimeout(state.timeoutId),state.timeoutId=setTimeout((()=>{state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect();}),50);},updateDisabledDates=()=>{var e,t,n,a;if(!(null==(n=null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.selectedDates)?void 0:n[0])||!(null==(a=state.self.context.disableDates)?void 0:a[0]))return;const o=getDate(state.self.context.selectedDates[0]),[l,s]=state.self.context.disableDates.map((e=>getDate(e))).reduce((([e,t],n)=>[o>=n?n:e,o{state.self=e,state.lastDateEl=t,removeHoverEffect(),e.disableDatesGaps&&(state.rangeMin=state.rangeMin?state.rangeMin:e.context.displayDateMin,state.rangeMax=state.rangeMax?state.rangeMax:e.context.displayDateMax),e.onCreateDateRangeTooltip&&(state.tooltipEl=e.context.mainElement.querySelector("[data-vc-date-range-tooltip]"));const n=null==t?void 0:t.dataset.vcDate;if(n){const t=1===e.context.selectedDates.length&&e.context.selectedDates[0].includes(n),a=t&&!canToggleSelection(e)?[n,n]:t&&canToggleSelection(e)?[]:e.context.selectedDates.length>1?[n]:[...e.context.selectedDates,n];setContext(e,"selectedDates",a),e.context.selectedDates.length>1&&e.context.selectedDates.sort(((e,t)=>+new Date(e)-+new Date(t)));}({set:()=>(e.disableDatesGaps&&updateDisabledDates(),createDateRangeTooltip(state.self,state.tooltipEl,t),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.addEventListener("keydown",handleCancelSelectionDates),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates);}),reset:()=>{const[n,a]=[e.context.selectedDates[0],e.context.selectedDates[e.context.selectedDates.length-1]],o=e.context.selectedDates[0]!==e.context.selectedDates[e.context.selectedDates.length-1],l=parseDates([`${n}:${a}`]).filter((t=>!e.context.disableDates.includes(t))),s=o?e.enableEdgeDatesOnly?[n,a]:l:[e.context.selectedDates[0],e.context.selectedDates[0]];if(setContext(e,"selectedDates",s),e.disableDatesGaps&&(setContext(e,"displayDateMin",state.rangeMin),setContext(e,"displayDateMax",state.rangeMax)),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),e.onCreateDateRangeTooltip)return e.context.selectedDates[0]||(state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,null)),e.context.selectedDates[0]&&(state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,t)),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave);}}})[1===e.context.selectedDates.length?"set":"reset"]();},updateDateModifier=e=>{e.context.mainElement.querySelectorAll("[data-vc-date]").forEach((t=>{const n=t.querySelector("[data-vc-date-btn]"),a=t.dataset.vcDate,o=getDate(a).getDay();setDateModifier(e,e.context.selectedYear,t,n,o,a,"current");}));},handleClickDate=(e,t)=>{var n;const a=t.target,o=a.closest("[data-vc-date-btn]");if(!e.selectionDatesMode||!["single","multiple","multiple-ranged"].includes(e.selectionDatesMode)||!o)return;const l=o.closest("[data-vc-date]");(({single:()=>handleSelectDate(e,l,false),multiple:()=>handleSelectDate(e,l,true),"multiple-ranged":()=>handleSelectDateRange(e,l)}))[e.selectionDatesMode](),null==(n=e.context.selectedDates)||n.sort(((e,t)=>+new Date(e)-+new Date(t))),e.onClickDate&&e.onClickDate(e,t),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);const s=a.closest('[data-vc-date-month="prev"]'),i=a.closest('[data-vc-date-month="next"]');({prev:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"prev"):updateDateModifier(e),next:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"next"):updateDateModifier(e),current:()=>updateDateModifier(e)})[s?"prev":i?"next":"current"]();},typeClick=["month","year"],getValue=(e,t,n)=>{const{currentValue:a,columnID:o}=getColumnID(e,t);return "month"===e.context.currentType&&o>=0?n-o:"year"===e.context.currentType&&e.context.selectedYear!==a?n-1:n},handleMultipleYearSelection=(e,t)=>{const n=getValue(e,"year",Number(t.dataset.vcYearsYear)),a=getDate(e.context.dateMin),o=getDate(e.context.dateMax),l=e.context.displayMonthsCount-1,{columnID:s}=getColumnID(e,"year"),i=e.context.selectedMontho.getMonth()-l+s&&n>=o.getFullYear(),c=no.getFullYear(),u=i||c?a.getFullYear():r||d?o.getFullYear():n,m=i||c?a.getMonth():r||d?o.getMonth()-l+s:e.context.selectedMonth;setContext(e,"selectedYear",u),setContext(e,"selectedMonth",m);},handleMultipleMonthSelection=(e,t)=>{const n=t.closest('[data-vc-column="month"]').querySelector('[data-vc="year"]'),a=getValue(e,"month",Number(t.dataset.vcMonthsMonth)),o=Number(n.dataset.vcYear),l=getDate(e.context.dateMin),s=getDate(e.context.dateMax),i=as.getMonth()&&o>=s.getFullYear();setContext(e,"selectedYear",o),setContext(e,"selectedMonth",i?l.getMonth():r?s.getMonth():a);},handleItemClick=(e,t,n,a)=>{var o;({year:()=>{if("multiple"===e.type)return handleMultipleYearSelection(e,a);setContext(e,"selectedYear",Number(a.dataset.vcYearsYear));},month:()=>{if("multiple"===e.type)return handleMultipleMonthSelection(e,a);setContext(e,"selectedMonth",Number(a.dataset.vcMonthsMonth));}})[n]();(({year:()=>{var n;return null==(n=e.onClickYear)?void 0:n.call(e,e,t)},month:()=>{var n;return null==(n=e.onClickMonth)?void 0:n.call(e,e,t)}}))[n](),e.context.currentType!==e.type?(setContext(e,"currentType",e.type),create(e),null==(o=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||o.focus()):setYearModifier(e,a,n,true,true);},handleClickType=(e,t,n)=>{var a;const o=t.target,l=o.closest(`[data-vc="${n}"]`),s={year:()=>createYears(e,o),month:()=>createMonths(e,o)};if(l&&e.onClickTitle&&e.onClickTitle(e,t),l&&e.context.currentType!==n)return s[n]();const i=o.closest(`[data-vc-${n}s-${n}]`);if(i)return handleItemClick(e,t,n,i);const r=o.closest('[data-vc="grid"]'),c=o.closest('[data-vc="column"]');(e.context.currentType===n&&l||"multiple"===e.type&&e.context.currentType===n&&r&&!c)&&(setContext(e,"currentType",e.type),create(e),null==(a=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||a.focus());},handleClickMonthOrYear=(e,t)=>{const n={month:e.selectionMonthsMode,year:e.selectionYearsMode};typeClick.forEach((a=>{n[a]&&t.target&&handleClickType(e,t,a);}));},handleClickWeekNumber=(e,t)=>{if(!e.enableWeekNumbers||!e.onClickWeekNumber)return;const n=t.target.closest("[data-vc-week-number]"),a=e.context.mainElement.querySelectorAll("[data-vc-date-week-number]");if(!n||!a[0])return;const o=Number(n.innerText),l=Number(n.dataset.vcWeekYear),s=Array.from(a).filter((e=>Number(e.dataset.vcDateWeekNumber)===o));e.onClickWeekNumber(e,o,l,s,t);},handleClickWeekDay=(e,t)=>{if(!e.onClickWeekDay)return;const n=t.target.closest("[data-vc-week-day]"),a=t.target.closest('[data-vc="column"]'),o=a?a.querySelectorAll("[data-vc-date-week-day]"):e.context.mainElement.querySelectorAll("[data-vc-date-week-day]");if(!n||!o[0])return;const l=Number(n.dataset.vcWeekDay),s=Array.from(o).filter((e=>Number(e.dataset.vcDateWeekDay)===l));e.onClickWeekDay(e,l,s,t);},handleClick=e=>{const t=t=>{handleClickArrow(e,t),handleClickWeekDay(e,t),handleClickWeekNumber(e,t),handleClickDate(e,t),handleClickMonthOrYear(e,t);};return e.context.mainElement.addEventListener("click",t),()=>e.context.mainElement.removeEventListener("click",t)},initMonthsCount=e=>{if("multiple"===e.type&&(e.displayMonthsCount<=1||e.displayMonthsCount>12))throw new Error(errorMessages.incorrectMonthsCount);if("multiple"!==e.type&&e.displayMonthsCount>1)throw new Error(errorMessages.incorrectMonthsCount);setContext(e,"displayMonthsCount",e.displayMonthsCount?e.displayMonthsCount:"multiple"===e.type?2:1);},getLocalDate=()=>{const e=new Date;return new Date(e.getTime()-6e4*e.getTimezoneOffset()).toISOString().substring(0,10)},resolveDate=(e,t)=>"today"===e?getLocalDate():e instanceof Date||"number"==typeof e||"string"==typeof e?parseDates([e])[0]:t,initRange=e=>{var t,n,a;const o=resolveDate(e.dateMin,e.dateMin),l=resolveDate(e.dateMax,e.dateMax),s=resolveDate(e.displayDateMin,o),i=resolveDate(e.displayDateMax,l);setContext(e,"dateToday",resolveDate(e.dateToday,e.dateToday)),setContext(e,"displayDateMin",s?getDate(o)>=getDate(s)?o:s:o),setContext(e,"displayDateMax",i?getDate(l)<=getDate(i)?l:i:l);const r=e.disableDatesPast&&!e.disableAllDates&&getDate(s)1&&e.context.disableDates.sort(((e,t)=>+new Date(e)-+new Date(t))),setContext(e,"enableDates",e.enableDates[0]?parseDates(e.enableDates):[]),(null==(t=e.context.enableDates)?void 0:t[0])&&(null==(n=e.context.disableDates)?void 0:n[0])&&setContext(e,"disableDates",e.context.disableDates.filter((t=>!e.context.enableDates.includes(t)))),e.context.enableDates.length>1&&e.context.enableDates.sort(((e,t)=>+new Date(e)-+new Date(t))),(null==(a=e.context.enableDates)?void 0:a[0])&&e.disableAllDates&&(setContext(e,"displayDateMin",e.context.enableDates[0]),setContext(e,"displayDateMax",e.context.enableDates[e.context.enableDates.length-1])),setContext(e,"dateMin",e.displayDisabledDates?o:e.context.displayDateMin),setContext(e,"dateMax",e.displayDisabledDates?l:e.context.displayDateMax);},initSelectedDates=e=>{var t;setContext(e,"selectedDates",(null==(t=e.selectedDates)?void 0:t[0])?parseDates(e.selectedDates):[]);},displayClosestValidDate=e=>{const t=t=>{const n=new Date(t);setInitialContext(e,n.getMonth(),n.getFullYear());};if(e.displayDateMin&&"today"!==e.displayDateMin&&(n=e.displayDateMin,a=new Date,new Date(n).getTime()>a.getTime())){const n=e.selectedDates.length&&e.selectedDates[0]?parseDates(e.selectedDates)[0]:e.displayDateMin;return t(getDate(resolveDate(n,e.displayDateMin))),true}var n,a;if(e.displayDateMax&&"today"!==e.displayDateMax&&((e,t)=>new Date(e).getTime(){setContext(e,"selectedMonth",t),setContext(e,"selectedYear",n),setContext(e,"displayYear",n);},initSelectedMonthYear=e=>{var t;if(e.enableJumpToSelectedDate&&(null==(t=e.selectedDates)?void 0:t[0])&&void 0===e.selectedMonth&&void 0===e.selectedYear){const t=getDate(parseDates(e.selectedDates)[0]);return void setInitialContext(e,t.getMonth(),t.getFullYear())}if(displayClosestValidDate(e))return;const n=void 0!==e.selectedMonth&&Number(e.selectedMonth)>=0&&Number(e.selectedMonth)<12,a=void 0!==e.selectedYear&&Number(e.selectedYear)>=0&&Number(e.selectedYear)<=9999;setInitialContext(e,n?Number(e.selectedMonth):getDate(e.context.dateToday).getMonth(),a?Number(e.selectedYear):getDate(e.context.dateToday).getFullYear());},initTime=e=>{var t,n,a;if(!e.selectionTimeMode)return;if(![12,24].includes(e.selectionTimeMode))throw new Error(errorMessages.incorrectTime);const o=12===e.selectionTimeMode,l=o?/^(0[1-9]|1[0-2]):([0-5][0-9]) ?(AM|PM)?$/i:/^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;let[s,i,r]=null!=(a=null==(n=null==(t=e.selectedTime)?void 0:t.match(l))?void 0:n.slice(1))?a:[];s?o&&!r&&(r="AM"):(s=o?transformTime12(String(e.timeMinHour)):String(e.timeMinHour),i=String(e.timeMinMinute),r=o?Number(transformTime12(String(e.timeMinHour)))>=12?"PM":"AM":null),setContext(e,"selectedHours",s.padStart(2,"0")),setContext(e,"selectedMinutes",i.padStart(2,"0")),setContext(e,"selectedKeeping",r),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${r?` ${r}`:""}`);},initAllVariables=e=>{setContext(e,"currentType",e.type),initMonthsCount(e),initRange(e),initSelectedMonthYear(e),initSelectedDates(e),initTime(e);},reset=(e,{year:t,month:n,dates:a,time:o,locale:l},s=true)=>{var i;const r={year:e.selectedYear,month:e.selectedMonth,dates:e.selectedDates,time:e.selectedTime};if(e.selectedYear=t?r.year:e.context.selectedYear,e.selectedMonth=n?r.month:e.context.selectedMonth,e.selectedTime=o?r.time:e.context.selectedTime,e.selectedDates="only-first"===a&&(null==(i=e.context.selectedDates)?void 0:i[0])?[e.context.selectedDates[0]]:true===a?r.dates:e.context.selectedDates,l){setContext(e,"locale",{months:{short:[],long:[]},weekdays:{short:[],long:[]}});}initAllVariables(e),s&&create(e),e.selectedYear=r.year,e.selectedMonth=r.month,e.selectedDates=r.dates,e.selectedTime=r.time,"multiple-ranged"===e.selectionDatesMode&&a&&handleSelectDateRange(e,null);},createToInput=e=>{const t=document.createElement("div");return t.className=e.styles.calendar,t.dataset.vc="calendar",t.dataset.vcInput="",t.dataset.vcCalendarHidden="",setContext(e,"inputModeInit",true),setContext(e,"isShowInInputMode",false),setContext(e,"mainElement",t),document.body.appendChild(e.context.mainElement),reset(e,{year:true,month:true,dates:true,time:true,locale:true}),setTimeout((()=>show(e))),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e)},canOpenOnFocus=e=>resolveToggle(e,e.openOnFocus),handleInput=e=>{setContext(e,"inputElement",e.context.mainElement);const t=()=>{e.context.inputModeInit?setTimeout((()=>show(e))):createToInput(e);};e.context.inputElement.addEventListener("click",t);const n="function"==typeof e.openOnFocus||true===e.openOnFocus,a=()=>{shouldSkipOpenOnFocus(e)?clearSkipOpenOnFocus(e):canOpenOnFocus(e)&&t();};n&&e.context.inputElement.addEventListener("focus",a);const o=t=>{const n="Tab"===t.key&&!t.shiftKey,a=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key);(n||a)&&(t=>{var n;if(!e.context.isShowInInputMode)return false;if(document.activeElement!==e.context.inputElement)return false;const a=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),o=null!=(n=document.createTreeWalker(e.context.mainElement,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>a(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}).nextNode())?n:a(e.context.mainElement)?e.context.mainElement:null;!o||o.tabIndex<0||(t.preventDefault(),o.focus());})(t);};return e.context.inputElement.addEventListener("keydown",o),()=>{e.context.inputElement.removeEventListener("click",t),n&&e.context.inputElement.removeEventListener("focus",a),e.context.inputElement.removeEventListener("keydown",o);}},init=e=>(setContext(e,"originalElement",e.context.mainElement.cloneNode(true)),setContext(e,"isInit",true),e.inputMode?handleInput(e):(initAllVariables(e),create(e),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e))),update=(e,t)=>{if(!e.context.isInit)throw new Error(errorMessages.notInit);reset(e,__spreadValues(__spreadValues({},{year:true,month:true,dates:true,time:true,locale:true}),t),!(e.inputMode&&!e.context.inputModeInit)),e.onUpdate&&e.onUpdate(e);},replaceProperties=(e,t)=>{const n=Object.keys(t);for(let a=0;a{replaceProperties(e,t),e.context.isInit&&update(e,n);};function findBestPickerPosition(e,t){const n="left";if(!t||!e)return n;const{canShow:a,parentPositions:o}=getAvailablePosition(e,t),l=a.left&&a.right;return (l&&a.bottom?"center":l&&a.top?["top","center"]:Array.isArray(o)?["bottom"===o[0]?"top":"bottom",...o.slice(1)]:o)||n}const setPosition=(e,t,n)=>{if(!e)return;const a="auto"===n?findBestPickerPosition(e,t):n,o={top:-t.offsetHeight,bottom:e.offsetHeight,left:0,center:e.offsetWidth/2-t.offsetWidth/2,right:e.offsetWidth-t.offsetWidth},l=Array.isArray(a)?a[0]:"bottom",s=Array.isArray(a)?a[1]:a;t.dataset.vcPosition=l;const{top:i,left:r}=getOffset(e),c=i+o[l];let d=r+o[s];const{vw:u}=getViewportDimensions();if(d+t.clientWidth>u){const e=window.innerWidth-document.body.clientWidth;d=u-t.clientWidth-e;}else d<0&&(d=0);Object.assign(t.style,{left:`${d}px`,top:`${c}px`});},show=e=>{if(e.context.isShowInInputMode)return;if(!e.context.currentType)return void e.context.mainElement.click();setContext(e,"cleanupHandlers",[]),setContext(e,"isShowInInputMode",true),e.inputMode&&restoreTabbing(e.context.mainElement),setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput),e.context.mainElement.removeAttribute("data-vc-calendar-hidden");const t=()=>{setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput);};window.addEventListener("resize",t),e.context.cleanupHandlers.push((()=>window.removeEventListener("resize",t)));const n=t=>{"Escape"===t.key&&hide(e);};document.addEventListener("keydown",n),e.context.cleanupHandlers.push((()=>document.removeEventListener("keydown",n)));const a=t=>{t.target===e.context.inputElement||e.context.mainElement.contains(t.target)||hide(e);};document.addEventListener("click",a,{capture:true}),e.context.cleanupHandlers.push((()=>document.removeEventListener("click",a,{capture:true}))),e.onShow&&e.onShow(e);},labels={application:"Calendar",navigation:"Calendar Navigation",arrowNext:{month:"Next month",year:"Next list of years"},arrowPrev:{month:"Previous month",year:"Previous list of years"},month:"Select month, current selected month:",months:"List of months",year:"Select year, current selected year:",years:"List of years",week:"Days of the week",weekNumber:"Numbers of weeks in a year",dates:"Dates in the current month",selectingTime:"Selecting a time ",inputHour:"Hours",inputMinute:"Minutes",rangeHour:"Slider for selecting hours",rangeMinute:"Slider for selecting minutes",btnKeeping:"Switch AM/PM, current position:"},styles={calendar:"vc",controls:"vc-controls",grid:"vc-grid",column:"vc-column",header:"vc-header",headerContent:"vc-header__content",month:"vc-month",year:"vc-year",arrowPrev:"vc-arrow vc-arrow_prev",arrowNext:"vc-arrow vc-arrow_next",wrapper:"vc-wrapper",content:"vc-content",months:"vc-months",monthsMonth:"vc-months__month",years:"vc-years",yearsYear:"vc-years__year",week:"vc-week",weekDay:"vc-week__day",weekNumbers:"vc-week-numbers",weekNumbersTitle:"vc-week-numbers__title",weekNumbersContent:"vc-week-numbers__content",weekNumber:"vc-week-number",dates:"vc-dates",datesRow:"vc-dates__row",date:"vc-date",dateBtn:"vc-date__btn",datePopup:"vc-date__popup",dateRangeTooltip:"vc-date-range-tooltip",time:"vc-time",timeContent:"vc-time__content",timeHour:"vc-time__hour",timeMinute:"vc-time__minute",timeKeeping:"vc-time__keeping",timeRanges:"vc-time__ranges",timeRange:"vc-time__range"};class OptionsCalendar{constructor(){__publicField(this,"type","default"),__publicField(this,"inputMode",false),__publicField(this,"openOnFocus",true),__publicField(this,"positionToInput","left"),__publicField(this,"firstWeekday",1),__publicField(this,"monthsToSwitch",1),__publicField(this,"themeAttrDetect","html[data-theme]"),__publicField(this,"locale","en"),__publicField(this,"dateToday","today"),__publicField(this,"dateMin","1970-01-01"),__publicField(this,"dateMax","2470-12-31"),__publicField(this,"displayDateMin"),__publicField(this,"displayDateMax"),__publicField(this,"displayDatesOutside",true),__publicField(this,"displayDisabledDates",false),__publicField(this,"displayMonthsCount"),__publicField(this,"disableDates",[]),__publicField(this,"disableAllDates",false),__publicField(this,"disableDatesPast",false),__publicField(this,"disableDatesGaps",false),__publicField(this,"disableWeekdays",[]),__publicField(this,"disableToday",false),__publicField(this,"enableDates",[]),__publicField(this,"enableEdgeDatesOnly",true),__publicField(this,"enableDateToggle",true),__publicField(this,"enableWeekNumbers",false),__publicField(this,"enableMonthChangeOnDayClick",true),__publicField(this,"enableJumpToSelectedDate",false),__publicField(this,"selectionDatesMode","single"),__publicField(this,"selectionMonthsMode",true),__publicField(this,"selectionYearsMode",true),__publicField(this,"selectionTimeMode",false),__publicField(this,"selectedDates",[]),__publicField(this,"selectedMonth"),__publicField(this,"selectedYear"),__publicField(this,"selectedHolidays",[]),__publicField(this,"selectedWeekends",[0,6]),__publicField(this,"selectedTime"),__publicField(this,"selectedTheme","system"),__publicField(this,"timeMinHour",0),__publicField(this,"timeMaxHour",23),__publicField(this,"timeMinMinute",0),__publicField(this,"timeMaxMinute",59),__publicField(this,"timeControls","all"),__publicField(this,"timeStepHour",1),__publicField(this,"timeStepMinute",1),__publicField(this,"sanitizerHTML",(e=>e)),__publicField(this,"onClickDate"),__publicField(this,"onClickWeekDay"),__publicField(this,"onClickWeekNumber"),__publicField(this,"onClickTitle"),__publicField(this,"onClickMonth"),__publicField(this,"onClickYear"),__publicField(this,"onClickArrow"),__publicField(this,"onChangeTime"),__publicField(this,"onChangeToInput"),__publicField(this,"onCreateDateRangeTooltip"),__publicField(this,"onCreateDateEls"),__publicField(this,"onCreateMonthEls"),__publicField(this,"onCreateYearEls"),__publicField(this,"onInit"),__publicField(this,"onUpdate"),__publicField(this,"onDestroy"),__publicField(this,"onShow"),__publicField(this,"onHide"),__publicField(this,"popups",{}),__publicField(this,"labels",__spreadValues({},labels)),__publicField(this,"layouts",{default:"",multiple:"",month:"",year:""}),__publicField(this,"styles",__spreadValues({},styles));}}const _Calendar=class e extends OptionsCalendar{constructor(t,n){var a;super(),__publicField(this,"init",(()=>init(this))),__publicField(this,"update",(e=>update(this,e))),__publicField(this,"destroy",(()=>destroy(this))),__publicField(this,"show",(()=>show(this))),__publicField(this,"hide",(()=>hide(this))),__publicField(this,"set",((e,t)=>set(this,e,t))),__publicField(this,"context"),this.context=__spreadProps(__spreadValues({},this.context),{locale:{months:{short:[],long:[]},weekdays:{short:[],long:[]}}}),setContext(this,"mainElement","string"==typeof t?null!=(a=e.memoizedElements.get(t))?a:this.queryAndMemoize(t):t),n&&replaceProperties(this,n);}queryAndMemoize(t){const n=document.querySelector(t);if(!n)throw new Error(errorMessages.notFoundSelector(t));return e.memoizedElements.set(t,n),n}};__publicField(_Calendar,"memoizedElements",new Map);let Calendar=_Calendar; + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$f = 'datepicker'; +const DATA_KEY$b = 'bs.datepicker'; +const EVENT_KEY$c = `.${DATA_KEY$b}`; +const DATA_API_KEY$7 = '.data-api'; +const EVENT_CHANGE$2 = `change${EVENT_KEY$c}`; +const EVENT_SHOW$4 = `show${EVENT_KEY$c}`; +const EVENT_SHOWN$3 = `shown${EVENT_KEY$c}`; +const EVENT_HIDE$3 = `hide${EVENT_KEY$c}`; +const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$c}`; +const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$c}${DATA_API_KEY$7}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY$c}${DATA_API_KEY$7}`; +const SELECTOR_DATA_TOGGLE$6 = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default$e = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType$e = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default$e; + } + static get DefaultType() { + return DefaultType$e; + } + static get NAME() { + return NAME$f; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN$3); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN$5); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; + } + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; + } + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); + } + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + EventHandler.trigger(this._element, EVENT_CHANGE$2, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; + + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); + } + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); + } + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; + } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$6, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE$6, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$c}${DATA_API_KEY$7}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { + return; + } + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; + } + this.hide(); + }); + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$e = 'dialog'; +const DATA_KEY$a = 'bs.dialog'; +const EVENT_KEY$b = `.${DATA_KEY$a}`; +const DATA_API_KEY$6 = '.data-api'; +const EVENT_SHOW$3 = `show${EVENT_KEY$b}`; +const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$b}`; +const EVENT_CANCEL = `cancel${EVENT_KEY$b}`; +const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$b}${DATA_API_KEY$6}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="dialog"]'; +const Default$d = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType$d = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default$d; + } + static get DefaultType() { + return DefaultType$d; + } + static get NAME() { + return NAME$e; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); + } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$5, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW$3, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$4, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY$b}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN$4, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); + }); + currentInstance.hide(); + } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$d = 'navoverflow'; +const DATA_KEY$9 = 'bs.navoverflow'; +const EVENT_KEY$a = `.${DATA_KEY$9}`; +const EVENT_UPDATE = `update${EVENT_KEY$a}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY$a}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default$c = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType$c = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); + } + + // Getters + static get Default() { + return Default$c; + } + static get DefaultType() { + return DefaultType$c; + } + static get NAME() { + return NAME$d; + } + + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); + } + super.dispose(); + } + + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); + + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); + + // Setup resize observer + this._setupResizeObserver(); + + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; + } + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; + } + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; + } + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + return; + } + const overflowWidth = overflowItem?.offsetWidth || 0; + + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer + + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; + } + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); + } + } + + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); + } + + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); + + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; + } + + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; + } + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); + } + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); + } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); + } + } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$c = 'swipe'; +const EVENT_KEY$9 = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default$b = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType$b = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; + +/** + * Class definition + */ + +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; + } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } + + // Getters + static get Default() { + return Default$b; + } + static get DefaultType() { + return DefaultType$b; + } + static get NAME() { + return NAME$c; + } + + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY$9); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; + } + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; + } + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; + } + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; + } + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); + + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; + } + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe + const direction = absDeltaX / this._deltaX; + this._deltaX = 0; + this._deltaY = 0; + if (!direction) { + return; + } + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; + } + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); + } + } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } + + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$b = 'drawer'; +const DATA_KEY$8 = 'bs.drawer'; +const EVENT_KEY$8 = `.${DATA_KEY$8}`; +const DATA_API_KEY$5 = '.data-api'; +const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; +const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$8}`; +const EVENT_RESIZE$1 = `resize${EVENT_KEY$8}`; +const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; +const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="drawer"]'; +const Default$a = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType$a = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + + // Getters + static get Default() { + return Default$a; + } + static get DefaultType() { + return DefaultType$a; + } + static get NAME() { + return NAME$b; + } + + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } + + // Protected — hook overrides + + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } + + // Private + + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; + } + + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL$1()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); + } + } else if (isRTL$1()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); + } + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$4, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$3, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE$1, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); + } + } +}); +enableDismissTrigger(Drawer); + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$a = 'strength'; +const DATA_KEY$7 = 'bs.strength'; +const EVENT_KEY$7 = `.${DATA_KEY$7}`; +const DATA_API_KEY$4 = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY$7}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default$9 = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType$9 = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); + } + } + + // Getters + static get Default() { + return Default$9; + } + static get DefaultType() { + return DefaultType$9; + } + static get NAME() { + return NAME$a; + } + + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } + + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } + + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } + + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; + + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; + } + + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; + } + if (/[A-Z]/.test(password)) { + score += weights.uppercase; + } + if (/\d/.test(password)) { + score += weights.numbers; + } + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; + } + + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; + } + if (password.length >= 16) { + score += weights.longPassword; + } + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; + } + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); + } + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; + } + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$7}${DATA_API_KEY$4}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$9 = 'otpInput'; +const DATA_KEY$6 = 'bs.otpInput'; +const EVENT_KEY$6 = `.${DATA_KEY$6}`; +const DATA_API_KEY$3 = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY$6}`; +const EVENT_INPUT$1 = `input${EVENT_KEY$6}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$6}${DATA_API_KEY$3}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT$1 = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default$8 = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType$8 = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT$1, this._element); + if (!this._input) { + return; + } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default$8; + } + static get DefaultType() { + return DefaultType$8; + } + static get NAME() { + return NAME$9; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } + + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; + } + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; + } + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); + } + + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); + } + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } + } + } + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); + } + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT$1, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); + } + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$8 = 'chips'; +const DATA_KEY$5 = 'bs.chips'; +const EVENT_KEY$5 = `.${DATA_KEY$5}`; +const DATA_API_KEY$2 = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY$5}`; +const EVENT_REMOVE = `remove${EVENT_KEY$5}`; +const EVENT_CHANGE$1 = `change${EVENT_KEY$5}`; +const EVENT_SELECT = `select${EVENT_KEY$5}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE$2 = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default$7 = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType$7 = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$7; + } + static get DefaultType() { + return DefaultType$7; + } + static get NAME() { + return NAME$8; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; + } + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; + } + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; + } + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; + } + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); + } + if (!chip || !value) { + return false; + } + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; + } + + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; + } + + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); + } + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE$2); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + focus() { + this._input?.focus(); + } + + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } + } + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); + + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); + } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); + + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; + } + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); + } + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); + } + + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { + return; + } + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey + }); + chip.focus(); + } + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); + } + }); + + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; + } + + // No default + } + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; + } + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; + } + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; + } + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; + } + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; + } + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default + } + } + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; + } + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { + return; + } + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; + } + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE$2); + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); + } + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; + } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); + } + } + } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$5}${DATA_API_KEY$2}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list + +const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); + +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); + } + return true; + } + + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; + } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); + } + } + } + return createdDocument.body.innerHTML; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$7 = 'TemplateFactory'; +const Default$6 = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType$6 = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; + +/** + * Class definition + */ + +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } + + // Getters + static get Default() { + return Default$6; + } + static get DefaultType() { + return DefaultType$6; + } + static get NAME() { + return NAME$7; + } + + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); + } + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); + } + return template; + } + + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); + } + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; + } + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; + } + if (isElement$1(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; + } + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; + } + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; + } + templateElement.textContent = element.textContent; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$6 = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE$2 = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW$2 = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE$2 = 'hide'; +const EVENT_HIDDEN$2 = 'hidden'; +const EVENT_SHOW$2 = 'show'; +const EVENT_SHOWN$2 = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK$3 = 'click'; +const EVENT_FOCUSIN$2 = 'focusin'; +const EVENT_FOCUSOUT$1 = 'focusout'; +const EVENT_MOUSEENTER$1 = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN$1 = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL$1() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL$1() ? 'right' : 'left' +}; +const Default$5 = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType$5 = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); + } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); + } + } + + // Getters + static get Default() { + return Default$5; + } + static get DefaultType() { + return DefaultType$5; + } + static get NAME() { + return NAME$6; + } + + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; + } + if (this._isShown()) { + this._leave(); + return; + } + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (!(this._isWithContent() && this._isEnabled)) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; + } + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW$2); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); + if (this._isHovered === false) { + this._leave(); + } + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); + if (hideEvent.defaultPrevented) { + return; + } + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW$2); + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering + + const complete = () => { + if (this._isWithActiveTrigger()) { + return; + } + if (!this._isHovered) { + this._disposeFloating(); + } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); + } + } + + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); + } + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE$2); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); + } + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); + } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } + + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; + } + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; + } + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; + } + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; + } + + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { + const { + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); + } + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); + } + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$3), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER$1) : this.constructor.eventName(EVENT_FOCUSIN$2); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); + } + } + this._hideModalHandler = () => { + if (this._element) { + this.hide(); + } + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; + } + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { + return; + } + + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; + } + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; + } + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); + } + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { + this._isHovered = true; + return; + } + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { + this.show(); + } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; + } + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); + } + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; + } + } + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); + } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; + } + } + config.selector = false; + config.trigger = 'manual'; + + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + if (this.tip) { + this.tip.remove(); + this.tip = null; + } + } +} + +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$3); + if (!target) { + return; + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; + +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN$2, SELECTOR_DATA_TOGGLE$3, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER$1, SELECTOR_DATA_TOGGLE$3, initTooltip); + +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$5 = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="popover"]'; +const EVENT_CLICK$2 = 'click'; +const EVENT_FOCUSIN$1 = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default$4 = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType$4 = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; + +/** + * Class definition + */ + +class Popover extends Tooltip { + // Getters + static get Default() { + return Default$4; + } + static get DefaultType() { + return DefaultType$4; + } + static get NAME() { + return NAME$5; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } + + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} + +/** + * Data API implementation - auto-initialize popovers + */ + +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$2); + if (!target) { + return; + } + + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; + +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK$2, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_FOCUSIN$1, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE$2, initPopover); + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$4 = 'range'; +const DATA_KEY$4 = 'bs.range'; +const EVENT_KEY$4 = `.${DATA_KEY$4}`; +const DATA_API_KEY$1 = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY$4}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$4}${DATA_API_KEY$1}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default$3 = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType$3 = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; + } + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); + } + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default$3; + } + static get DefaultType() { + return DefaultType$3; + } + static get NAME() { + return NAME$4; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; + } + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; + } + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); + } + } + if (points.length === 0) { + return; + } + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$3 = 'scrollspy'; +const DATA_KEY$3 = 'bs.scrollspy'; +const EVENT_KEY$3 = `.${DATA_KEY$3}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY$3}`; +const EVENT_CLICK$1 = `click${EVENT_KEY$3}`; +const EVENT_SCROLL = `scroll${EVENT_KEY$3}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY$3}`; +const EVENT_RESIZE = `resize${EVENT_KEY$3}`; +const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$3}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE$1 = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE$1 = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default$2 = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType$2 = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default$2; + } + static get DefaultType() { + return DefaultType$2; + } + static get NAME() { + return NAME$3; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK$1); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); + } + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- + + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); + } else { + this._intersecting.delete(entry.target); + } + } + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; + } + } + + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); + } + if (!active) { + return; + } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); + } + } + + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } + + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; + + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; + } + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } + + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } + + // --- Bottom sentinel ----------------------------------------------------- + + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; + } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } + + // --- Resize (px activation lines only) ----------------------------------- + + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; + } + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; + } + } + _rebuildObserver() { + if (!this._observer) { + return; + } + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + } + + // --- Smooth-scroll settle (hash + focus) --------------------------------- + + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; + } + + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK$1); + EventHandler.on(this._config.target, EVENT_CLICK$1, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { + return; + } + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); + return; + } + + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + } else { + root.scrollTop = height; + } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; + } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; + } + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; + } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; + + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); + } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); + } + section.focus({ + preventScroll: true + }); + } + + // --- Targets / observables ---------------------------------------------- + + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } + + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section + + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); + } + } + + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; + } + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE$1); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE$1)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE$1); + } + return; + } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE$1); + } + } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE$1); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE$1); + } + } +} + +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$2 = 'tab'; +const DATA_KEY$2 = 'bs.tab'; +const EVENT_KEY$2 = `.${DATA_KEY$2}`; +const EVENT_HIDE$1 = `hide${EVENT_KEY$2}`; +const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$2}`; +const EVENT_SHOW$1 = `show${EVENT_KEY$2}`; +const EVENT_SHOWN$1 = `shown${EVENT_KEY$2}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY$2}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY$2}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY$2}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE$1 = 'fade'; +const CLASS_NAME_SHOW$1 = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; + +/** + * Class definition + */ + +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) + } + + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } + + // Getters + static get NAME() { + return NAME$2; + } + + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; + } + + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; + } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } + + // Private + _activate(element, relatedElem) { + if (!element) { + return; + } + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW$1); + return; + } + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; + } + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW$1); + return; + } + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; + } + + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; + } + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); + } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); + } + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); + } + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); + } + if (!isActive) { + child.setAttribute('tabindex', '-1'); + } + this._setAttributeIfNotExists(child, 'role', 'tab'); + + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; + } + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); + } + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; + } + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW$1, open); + } + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); + } + } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } + + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } + + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE$1, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); + +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$1 = 'toast'; +const DATA_KEY$1 = 'bs.toast'; +const EVENT_KEY$1 = `.${DATA_KEY$1}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY$1}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY$1}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY$1}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY$1}`; +const EVENT_HIDE = `hide${EVENT_KEY$1}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY$1}`; +const EVENT_SHOW = `show${EVENT_KEY$1}`; +const EVENT_SHOWN = `shown${EVENT_KEY$1}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType$1 = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default$1 = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); + } + + // Getters + static get Default() { + return Default$1; + } + static get DefaultType() { + return DefaultType$1; + } + static get NAME() { + return NAME$1; + } + + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); + } + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); + } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } + + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; + } + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; + } + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } + } + if (isInteracting) { + this._clearTimeout(); + return; + } + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; + } + this._maybeScheduleHide(); + } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Toast); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; + } + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; + } + + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; + } + this._element.setAttribute(attribute, value); + } +} + +/** + * Data API implementation + */ + +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); + +export { Alert, Button, Carousel, Chips, Collapse, Combobox, Datepicker, Dialog, Drawer, Menu, NavOverflow, OtpInput, Popover, Range, ScrollSpy, Strength, Tab, Toast, Toggler, Tooltip }; diff --git a/assets/javascripts/bootstrap.bundle.min.js b/assets/javascripts/bootstrap.bundle.min.js new file mode 100644 index 00000000..e8dcf8c4 --- /dev/null +++ b/assets/javascripts/bootstrap.bundle.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +const elementMap=new Map,Data={set(e,t,n){elementMap.has(e)||elementMap.set(e,new Map);const s=elementMap.get(e);s.has(t)||0===s.size?s.set(t,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...s.keys()][0]}.`)},get:(e,t)=>elementMap.has(e)&&elementMap.get(e).get(t)||null,getAny:e=>elementMap.has(e)&&elementMap.get(e).values().next().value||null,remove(e,t){if(!elementMap.has(e))return;const n=elementMap.get(e);n.delete(t),0===n.size&&elementMap.delete(e)}},namespaceRegex=/[^.]*(?=\..*)\.|.*/,stripNameRegex=/\..*/,stripUidRegex=/::\d+$/,eventRegistry={};let uidEvent=1;const customEvents={mouseenter:"mouseover",mouseleave:"mouseout"},nativeEvents=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll","scrollend"]);function makeEventUid(e,t){return t&&`${t}::${uidEvent++}`||e.uidEvent||uidEvent++}function getElementEvents(e){const t=makeEventUid(e);return e.uidEvent=t,eventRegistry[t]=eventRegistry[t]||{},eventRegistry[t]}function bootstrapHandler(e,t){return function n(s){return hydrateObj(s,{delegateTarget:e}),n.oneOff&&EventHandler.off(e,s.type,t),t.apply(e,[s])}}function bootstrapDelegationHandler(e,t,n){return function s(i){const o=e.querySelectorAll(t);for(let{target:a}=i;a&&a!==this;a=a.parentNode)for(const l of o)if(l===a)return hydrateObj(i,{delegateTarget:a}),s.oneOff&&EventHandler.off(e,i.type,t,n),n.apply(a,[i])}}function findHandler(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function normalizeParameters(e,t,n){const s="string"==typeof t,i=s?n:t||n;let o=getTypeEvent(e);return nativeEvents.has(o)||(o=e),[s,i,o]}function addHandler(e,t,n,s,i){if("string"!=typeof t||!e)return;let[o,a,l]=normalizeParameters(t,n,s);if(t in customEvents){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};a=e(a)}const r=getElementEvents(e),c=r[l]||(r[l]={}),d=findHandler(c,a,o?n:null);if(d)return void(d.oneOff=d.oneOff&&i);const u=makeEventUid(a,t.replace(namespaceRegex,"")),h=o?bootstrapDelegationHandler(e,n,a):bootstrapHandler(e,a);h.delegationSelector=o?n:null,h.callable=a,h.oneOff=i,h.uidEvent=u,c[u]=h,e.addEventListener(l,h,o)}function removeHandler(e,t,n,s,i){const o=findHandler(t[n],s,i);o&&(e.removeEventListener(n,o,Boolean(i)),delete t[n][o.uidEvent])}function removeNamespacedHandlers(e,t,n,s){const i=t[n]||{};for(const[o,a]of Object.entries(i))o.includes(s)&&removeHandler(e,t,n,a.callable,a.delegationSelector)}function getTypeEvent(e){return e=e.replace(stripNameRegex,""),customEvents[e]||e}const EventHandler={on(e,t,n,s){addHandler(e,t,n,s,!1)},one(e,t,n,s){addHandler(e,t,n,s,!0)},off(e,t,n,s){if("string"!=typeof t||!e)return;const[i,o,a]=normalizeParameters(t,n,s),l=a!==t,r=getElementEvents(e),c=r[a]||{},d=t.startsWith(".");if(void 0===o){if(d)for(const n of Object.keys(r))removeNamespacedHandlers(e,r,n,t.slice(1));for(const[n,s]of Object.entries(c)){const i=n.replace(stripUidRegex,"");l&&!t.includes(i)||removeHandler(e,r,a,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;removeHandler(e,r,a,o,i?n:null)}},trigger(e,t,n){if("string"!=typeof t||!e)return null;const s=hydrateObj(new Event(t,{bubbles:!0,cancelable:!0}),n);return e.dispatchEvent(s),s}};function hydrateObj(e,t={}){for(const[n,s]of Object.entries(t))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get:()=>s})}return e}function normalizeData(e){if("true"===e)return!0;if("false"===e)return!1;if(e===Number(e).toString())return Number(e);if(""===e||"null"===e)return null;if("string"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function normalizeDataKey(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const Manipulator={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${normalizeDataKey(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${normalizeDataKey(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const s of n){let n=s.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=normalizeData(e.dataset[s])}return t},getDataAttribute:(e,t)=>normalizeData(e.getAttribute(`data-bs-${normalizeDataKey(t)}`))},MAX_UID=1e6,MILLISECONDS_MULTIPLIER=1e3,TRANSITION_END="transitionend",parseSelector=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),e),toType=e=>null==e?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),getUID=e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e},getTransitionDurationFromElement=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),i=Number.parseFloat(n);return s||i?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0},triggerTransitionEnd=e=>{e.dispatchEvent(new Event(TRANSITION_END))},isElement$1=e=>!(!e||"object"!=typeof e)&&void 0!==e.nodeType,getElement=e=>isElement$1(e)?e:"string"==typeof e&&e.length>0?document.querySelector(parseSelector(e)):null,isVisible=e=>{if(!isElement$1(e)||0===e.getClientRects().length)return!1;const t="visible"===getComputedStyle(e).getPropertyValue("visibility"),n=e.closest("details:not([open])");if(!n)return t;if(n!==e){const t=e.closest("summary");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},isDisabled=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(void 0!==e.disabled?e.disabled:e.hasAttribute("disabled")&&"false"!==e.getAttribute("disabled")),findShadowRoot=e=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?findShadowRoot(e.parentNode):null},noop=()=>{},reflow=e=>{e.offsetHeight},isRTL$1=()=>"rtl"===document.documentElement.dir,execute=(e,t=[],n=e)=>"function"==typeof e?e.call(...t):n,executeAfterTransition=(e,t,n=!0)=>{if(!n)return void execute(e);const s=getTransitionDurationFromElement(t)+5;let i=!1;const o=({target:n})=>{n===t&&(i=!0,t.removeEventListener(TRANSITION_END,o),execute(e))};t.addEventListener(TRANSITION_END,o),setTimeout(()=>{i||triggerTransitionEnd(t)},s)},getNextActiveElement=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return-1===o?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])};class Config{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=isElement$1(t)?Manipulator.getDataAttribute(t,"config"):{};return{...this.constructor.Default,..."object"==typeof n?n:{},...isElement$1(t)?Manipulator.getDataAttributes(t):{},..."object"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[n,s]of Object.entries(t)){const t=e[n],i=isElement$1(t)?"element":toType(t);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const VERSION="6.0.0-alpha1";class BaseComponent extends Config{constructor(e,t){if(super(),!(e=getElement(e)))return;this._element=e,this._config=this._getConfig(t);const n=Data.get(this._element,this.constructor.DATA_KEY);n&&n.dispose(),Data.set(this._element,this.constructor.DATA_KEY,this)}dispose(){Data.remove(this._element,this.constructor.DATA_KEY),EventHandler.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){executeAfterTransition(()=>{this._element&&e()},t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return Data.get(getElement(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return VERSION}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const getSelector=e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&"#"!==n?n.trim():null}return t?t.split(",").map(e=>parseSelector(e)).join(","):null},SelectorEngine={find:(e,t=document.documentElement)=>[...Element.prototype.querySelectorAll.call(t,e)],findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[...e.children].filter(e=>e.matches(t)),parents(e,t){const n=[];let s=e.parentNode.closest(t);for(;s;)n.push(s),s=s.parentNode.closest(t);return n},closest:(e,t)=>Element.prototype.closest.call(e,t),prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!isDisabled(e)&&isVisible(e))},getSelectorFromElement(e){const t=getSelector(e);return t&&SelectorEngine.findOne(t)?t:null},getElementFromSelector(e){const t=getSelector(e);return t?SelectorEngine.findOne(t):null},getMultipleElementsFromSelector(e){const t=getSelector(e);return t?SelectorEngine.find(t):[]}},enableDismissTrigger=(e,t="hide")=>{const n=`click.dismiss${e.EVENT_KEY}`,s=e.NAME;EventHandler.on(document,n,`[data-bs-dismiss="${s}"]`,function(n){if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),isDisabled(this))return;const i=SelectorEngine.getElementFromSelector(this)||this.closest(`.${s}`);e.getOrCreateInstance(i)[t]()})},eventActionOnPlugin=(e,t,n,s,i=null)=>{eventAction(`${t}.${e.NAME}`,n,t=>{const n=t.targets.filter(Boolean).map(t=>e.getOrCreateInstance(t));"function"==typeof i&&i({...t,instances:n});for(const e of n)e[s]()})},eventAction=(e,t,n)=>{const s=`${t}:not(.disabled):not(:disabled)`;EventHandler.on(document,e,s,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault();const t=SelectorEngine.getSelectorFromElement(this),s=t?SelectorEngine.find(t):[this];n({targets:s,event:e})})},NAME$l="alert",DATA_KEY$h="bs.alert",EVENT_KEY$i=".bs.alert",EVENT_CLOSE="close.bs.alert",EVENT_CLOSED="closed.bs.alert",CLASS_NAME_FADE$4="fade",CLASS_NAME_SHOW$6="show";class Alert extends BaseComponent{static get NAME(){return NAME$l}close(){if(EventHandler.trigger(this._element,EVENT_CLOSE).defaultPrevented)return;this._element.classList.remove("show");const e=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,e)}_destroyElement(){this._element.remove(),EventHandler.trigger(this._element,EVENT_CLOSED),this.dispose()}}enableDismissTrigger(Alert,"close");const NAME$k="button",DATA_KEY$g="bs.button",EVENT_KEY$h=`.${DATA_KEY$g}`,DATA_API_KEY$c=".data-api",CLASS_NAME_ACTIVE$4="active",SELECTOR_DATA_TOGGLE$a='[data-bs-toggle="button"]',EVENT_CLICK_DATA_API$8=`click${EVENT_KEY$h}.data-api`;class Button extends BaseComponent{static get NAME(){return NAME$k}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}}EventHandler.on(document,EVENT_CLICK_DATA_API$8,SELECTOR_DATA_TOGGLE$a,e=>{e.preventDefault();const t=e.target.closest(SELECTOR_DATA_TOGGLE$a);Button.getOrCreateInstance(t).toggle()});const NAME$j="carousel",DATA_KEY$f="bs.carousel",EVENT_KEY$g=`.${DATA_KEY$f}`,DATA_API_KEY$b=".data-api",ARROW_LEFT_KEY$2="ArrowLeft",ARROW_RIGHT_KEY$2="ArrowRight",DIRECTION_LEFT="left",DIRECTION_RIGHT="right",EVENT_SLIDE=`slide${EVENT_KEY$g}`,EVENT_SLID=`slid${EVENT_KEY$g}`,EVENT_KEYDOWN$2=`keydown${EVENT_KEY$g}`,EVENT_MOUSEENTER$2=`mouseenter${EVENT_KEY$g}`,EVENT_MOUSELEAVE$1=`mouseleave${EVENT_KEY$g}`,EVENT_POINTERDOWN$1=`pointerdown${EVENT_KEY$g}`,EVENT_LOAD_DATA_API$3=`load${EVENT_KEY$g}.data-api`,EVENT_CLICK_DATA_API$7=`click${EVENT_KEY$g}.data-api`,CLASS_NAME_CAROUSEL="carousel",CLASS_NAME_ACTIVE$3="active",CLASS_NAME_FADE$3="carousel-fade",CLASS_NAME_CENTER="carousel-center",CLASS_NAME_AUTO="carousel-auto",CLASS_NAME_CLONE="carousel-item-clone",CLASS_NAME_PAUSED="paused",CLASS_NAME_PLAYING="carousel-playing",PROPERTY_INTERVAL="--bs-carousel-interval",SCROLL_DURATION=300,ACTIVE_RATIO_TOLERANCE=.05,SELECTOR_ACTIVE=".active",SELECTOR_ITEM=`.carousel-item:not(.${CLASS_NAME_CLONE})`,SELECTOR_ACTIVE_ITEM=".active"+SELECTOR_ITEM,SELECTOR_INNER$1=".carousel-inner",SELECTOR_INDICATORS=".carousel-indicators",SELECTOR_PLAY_PAUSE=".carousel-control-play-pause",SELECTOR_DATA_SLIDE="[data-bs-slide], [data-bs-slide-to]",SELECTOR_DATA_SLIDE_PREV='[data-bs-slide="prev"]',SELECTOR_DATA_SLIDE_NEXT='[data-bs-slide="next"]',SELECTOR_DATA_AUTOPLAY='[data-bs-autoplay="true"]',KEY_TO_DIRECTION={[ARROW_LEFT_KEY$2]:"right",[ARROW_RIGHT_KEY$2]:"left"},ENDS_STOP="stop",ENDS_WRAP="wrap",ENDS_LOOP="loop",Default$i={autoplay:!1,ends:ENDS_LOOP,interval:5e3,keyboard:!0,pause:"hover"},DefaultType$i={autoplay:"boolean",ends:"string",interval:"number",keyboard:"boolean",pause:"(string|boolean)"},easeInOutCubic=e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2;class Carousel extends BaseComponent{constructor(e,t){super(e,t),this._viewport=SelectorEngine.findOne(SELECTOR_INNER$1,this._element)||this._element,this._indicatorsElement=SelectorEngine.findOne(SELECTOR_INDICATORS,this._element),this._playPauseElement=SelectorEngine.findOne(SELECTOR_PLAY_PAUSE,this._element),this._prevControls=SelectorEngine.find('[data-bs-slide="prev"]',this._element),this._nextControls=SelectorEngine.find('[data-bs-slide="next"]',this._element),this._interval=null,this._observer=null,this._scrollFrame=null,this._looping=!1,this._visibility=new Map,this._playing=this._config.autoplay,this._activeIndex=this._initialActiveIndex(),this._addEventListeners(),this._observeItems(),this._refreshActiveState(),this._playing&&this.cycle(),this._updatePlayPauseControl()}static get Default(){return Default$i}static get DefaultType(){return DefaultType$i}static get NAME(){return NAME$j}next(){this.to(this._navIndex()+1)}nextWhenVisible(){"visible"===document.visibilityState&&isVisible(this._element)&&this.next()}prev(){this.to(this._navIndex()-1)}pause(){this._clearInterval(),this._element.classList.remove("carousel-playing")}cycle(){this._clearInterval(),this._scheduleAutoplay(),this._element.classList.add("carousel-playing")}to(e){if(this._looping)return;const t=this._getItems(),n=Number.parseInt(e,10);if(this._config.ends===ENDS_LOOP&&!this._prefersReducedMotion()&&this._canLoop()){if(n>t.length-1)return void this._loopTransition(!0);if(n<0)return void this._loopTransition(!1)}const s=this._normalizeIndex(n,t.length),i=this._navIndex();null!==s&&s!==i&&(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[s],direction:this._direction(i,s),from:i,to:s}).defaultPrevented||(this._isFade()?this._fadeTo(s):this._scrollToIndex(s)))}dispose(){this._clearInterval(),this._observer&&this._observer.disconnect(),null!==this._scrollFrame&&cancelAnimationFrame(this._scrollFrame);for(const e of SelectorEngine.find(`.${CLASS_NAME_CLONE}`,this._viewport))e.remove();this._viewport.style.scrollSnapType="",EventHandler.off(this._viewport,EVENT_KEY$g),super.dispose()}_configAfterMerge(e){return[ENDS_STOP,ENDS_WRAP,ENDS_LOOP].includes(e.ends)||(e.ends=Default$i.ends),e}_initialActiveIndex(){const e=SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM,this._element),t=e?this._getItems().indexOf(e):0;return Math.max(t,0)}_addEventListeners(){this._config.keyboard&&EventHandler.on(this._element,EVENT_KEYDOWN$2,e=>this._keydown(e)),"hover"===this._config.pause&&(EventHandler.on(this._element,EVENT_MOUSEENTER$2,()=>this.pause()),EventHandler.on(this._element,EVENT_MOUSELEAVE$1,()=>this._maybeEnableCycle())),EventHandler.on(this._viewport,EVENT_POINTERDOWN$1,()=>this._pauseFromInteraction())}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=KEY_TO_DIRECTION[e.key];t&&(e.preventDefault(),this._pauseFromInteraction(),"right"===t?this.prev():this.next())}_observeItems(){if(!this._isFade()&&"undefined"!=typeof IntersectionObserver){this._observer=new IntersectionObserver(e=>this._handleIntersection(e),{root:this._viewport,threshold:[0,.25,.5,.75,1]});for(const e of this._getItems())this._observer.observe(e)}}_handleIntersection(e){if(this._looping)return;for(const t of e)this._visibility.set(t.target,t.isIntersecting?t.intersectionRatio:0);const t=this._getItems().map(e=>this._visibility.get(e)??0),n=Math.max(...t);let s=this._activeIndex;n>0&&(s=t.findIndex(e=>e>=n-.05)),this._setActive(s),this._updateEndControls()}_navIndex(){if(this._isFade()||this._viewport.scrollWidth-this._viewport.clientWidth<=0)return this._activeIndex;let e=this._activeIndex,t=Number.POSITIVE_INFINITY;for(const[n,s]of this._getItems().entries()){const i=Math.abs(this._scrollDelta(s));i{this._viewport.style.scrollSnapType="",this._observer||this._setActive(e),this._updateEndControls()})}_animateScroll(e,t){null!==this._scrollFrame&&(cancelAnimationFrame(this._scrollFrame),this._scrollFrame=null);const n=this._viewport.scrollLeft,s=e-n;if(this._prefersReducedMotion()||"undefined"==typeof requestAnimationFrame)return this._viewport.scrollTo({left:e,behavior:"instant"}),void t();let i=null;const o=a=>{null===i&&(i=a);const l=Math.min((a-i)/300,1);this._viewport.scrollTo({left:n+s*easeInOutCubic(l),behavior:"instant"}),l<1?this._scrollFrame=requestAnimationFrame(o):(this._viewport.scrollTo({left:e,behavior:"instant"}),this._scrollFrame=null,t())};this._scrollFrame=requestAnimationFrame(o)}_scrollDelta(e){const t=this._viewport.getBoundingClientRect(),n=e.getBoundingClientRect();if(this._element.classList.contains("carousel-center"))return n.left+n.width/2-(t.left+t.width/2);const s=Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart)||0;return isRTL$1()?n.right-(t.right-s):n.left-(t.left+s)}_loopTransition(e){const t=this._getItems(),n=t.length-1,s=this._activeIndex,i=e?0:n,o=this._loopDirection(e);if(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[i],direction:o,from:s,to:i}).defaultPrevented)return;this._looping=!0;const a=(e?t[0]:t[n]).cloneNode(!0);a.classList.add(CLASS_NAME_CLONE),a.classList.remove("active"),a.removeAttribute("id");for(const e of SelectorEngine.find("[id]",a))e.removeAttribute("id");a.setAttribute("aria-hidden","true"),a.inert=!0,this._viewport.style.scrollSnapType="none",e?this._viewport.append(a):(this._viewport.prepend(a),this._jumpScroll(this._scrollDelta(t[s]))),this._animateScroll(this._viewport.scrollLeft+this._scrollDelta(a),()=>{a.remove(),this._jumpScroll(this._scrollDelta(t[i])),this._activeIndex=i,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[i],direction:o,from:s,to:i}),this._viewport.style.scrollSnapType="",this._looping=!1})}_loopDirection(e){return isRTL$1()?e?"right":"left":e?"left":"right"}_jumpScroll(e){this._viewport.style.scrollSnapType="none",this._viewport.scrollBy({left:e,top:0,behavior:"instant"})}_fadeTo(e){this._setActive(e)}_setActive(e){const t=this._getItems();if(e===this._activeIndex||!t[e])return;const n=this._activeIndex;this._activeIndex=e,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[e],direction:this._direction(n,e),from:n,to:e})}_refreshActiveState(){const e=this._getItems();for(const[t,n]of e.entries())n.classList.toggle("active",t===this._activeIndex);this._setActiveIndicatorElement(this._activeIndex),this._updateEndControls()}_updateEndControls(){if(this._config.ends!==ENDS_STOP)return;const e=this._viewport,t=e.scrollWidth-e.clientWidth;let n,s;if(t>0){const i=Math.abs(e.scrollLeft);n=i<=1,s=i>=t-1}else{const e=this._getItems().length-1;n=this._activeIndex<=0,s=this._activeIndex>=e}this._setControlsDisabled(this._prevControls,n),this._setControlsDisabled(this._nextControls,s)}_setControlsDisabled(e,t){for(const n of e)t&&n===document.activeElement&&((e===this._prevControls?this._nextControls:this._prevControls)[0]??this._viewport).focus({preventScroll:!0}),n.disabled=t}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=SelectorEngine.findOne(".active",this._indicatorsElement);t&&(t.classList.remove("active"),t.removeAttribute("aria-current"));const n=SelectorEngine.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add("active"),n.setAttribute("aria-current","true"))}_normalizeIndex(e,t){return Number.isNaN(e)||0===t?null:e<0?this._wrapsAround()?t-1:null:e>t-1?this._wrapsAround()?0:null:e}_wrapsAround(){return this._config.ends===ENDS_WRAP||this._config.ends===ENDS_LOOP}_canLoop(){if(this._isFade()||this._getItems().length<2)return!1;const e=getComputedStyle(this._element),t=t=>Number.parseFloat(e.getPropertyValue(t))||0;return 1===(t("--bs-carousel-items")||1)&&0===t("--bs-carousel-items-peek")&&!this._element.classList.contains("carousel-center")&&!this._element.classList.contains("carousel-auto")}_direction(e,t){const n=t>e;return isRTL$1()?n?"right":"left":n?"left":"right"}_scheduleAutoplay(e=this._activeIndex){const t=this._itemInterval(e);this._element.style.setProperty(PROPERTY_INTERVAL,`${t}ms`),this._interval=setTimeout(()=>{const e=this._upcomingIndex();this.nextWhenVisible(),null!==e?this._scheduleAutoplay(e):this.pause()},t)}_upcomingIndex(){return this._normalizeIndex(this._navIndex()+1,this._getItems().length)}_itemInterval(e=this._activeIndex){const t=this._getItems()[e],n=t?Number.parseInt(t.getAttribute("data-bs-interval"),10):Number.NaN;return Number.isNaN(n)?this._config.interval:n}_maybeEnableCycle(){this._playing&&this.cycle()}_pauseFromInteraction(){this._playing=!1,this.pause(),this._updatePlayPauseControl()}_togglePlayPause(){this._playing?this._pauseFromInteraction():(this._playing=!0,this.cycle(),this._updatePlayPauseControl())}_updatePlayPauseControl(){if(!this._playPauseElement)return;this._playPauseElement.classList.toggle("paused",!this._playing);const e=this._playPauseElement.getAttribute(this._playing?"data-bs-pause-label":"data-bs-play-label");e&&this._playPauseElement.setAttribute("aria-label",e)}_isFade(){return this._element.classList.contains("carousel-fade")}_prefersReducedMotion(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}_getItems(){return SelectorEngine.find(SELECTOR_ITEM,this._element)}_clearInterval(){this._interval&&(clearTimeout(this._interval),this._interval=null)}}EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_DATA_SLIDE,function(e){const t=SelectorEngine.getElementFromSelector(this);if(!t||!t.classList.contains("carousel"))return;e.preventDefault();const n=Carousel.getOrCreateInstance(t);n._pauseFromInteraction();const s=this.getAttribute("data-bs-slide-to");s?n.to(s):"next"!==Manipulator.getDataAttribute(this,"slide")?n.prev():n.next()}),EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_PLAY_PAUSE,function(e){const t=SelectorEngine.getElementFromSelector(this);t&&t.classList.contains("carousel")&&(e.preventDefault(),Carousel.getOrCreateInstance(t)._togglePlayPause())}),EventHandler.on(window,EVENT_LOAD_DATA_API$3,()=>{const e=SelectorEngine.find(SELECTOR_DATA_AUTOPLAY);for(const t of e)Carousel.getOrCreateInstance(t)});const NAME$i="collapse",DATA_KEY$e="bs.collapse",EVENT_KEY$f=`.${DATA_KEY$e}`,DATA_API_KEY$a=".data-api",EVENT_SHOW$7=`show${EVENT_KEY$f}`,EVENT_SHOWN$6=`shown${EVENT_KEY$f}`,EVENT_HIDE$6=`hide${EVENT_KEY$f}`,EVENT_HIDDEN$8=`hidden${EVENT_KEY$f}`,EVENT_CLICK_DATA_API$6=`click${EVENT_KEY$f}.data-api`,CLASS_NAME_SHOW$5="show",CLASS_NAME_COLLAPSE="collapse",CLASS_NAME_COLLAPSING="collapsing",CLASS_NAME_COLLAPSED="collapsed",CLASS_NAME_DEEPER_CHILDREN=":scope .collapse .collapse",CLASS_NAME_HORIZONTAL="collapse-horizontal",WIDTH="width",HEIGHT="height",SELECTOR_ACTIVES=".collapse.show, .collapse.collapsing",SELECTOR_DATA_TOGGLE$9='[data-bs-toggle="collapse"]',Default$h={parent:null,toggle:!0},DefaultType$h={parent:"(null|element)",toggle:"boolean"};class Collapse extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=SelectorEngine.find(SELECTOR_DATA_TOGGLE$9);for(const e of n){const t=SelectorEngine.getSelectorFromElement(e),n=SelectorEngine.find(t).filter(e=>e===this._element);null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Default$h}static get DefaultType(){return DefaultType$h}static get NAME(){return NAME$i}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(e=>e!==this._element).map(e=>Collapse.getOrCreateInstance(e,{toggle:!1}))),e.length&&e[0]._isTransitioning)return;if(EventHandler.trigger(this._element,EVENT_SHOW$7).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[t]="",EventHandler.trigger(this._element,EVENT_SHOWN$6)},this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(EventHandler.trigger(this._element,EVENT_HIDE$6).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,reflow(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");for(const e of this._triggerArray){const t=SelectorEngine.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),EventHandler.trigger(this._element,EVENT_HIDDEN$8)},this._element,!0)}_isShown(e=this._element){return e.classList.contains("show")}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=getElement(e.parent),e}_getDimension(){return this._element.classList.contains("collapse-horizontal")?WIDTH:HEIGHT}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9);for(const t of e){const e=SelectorEngine.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN,this._config.parent);return SelectorEngine.find(e,this._config.parent).filter(e=>!t.includes(e))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle("collapsed",!t),n.setAttribute("aria-expanded",t)}}EventHandler.on(document,EVENT_CLICK_DATA_API$6,SELECTOR_DATA_TOGGLE$9,function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of SelectorEngine.getMultipleElementsFromSelector(this))Collapse.getOrCreateInstance(e,{toggle:!1}).toggle()});const min=Math.min,max=Math.max,round=Math.round,floor=Math.floor,createCoords=e=>({x:e,y:e}),oppositeSideMap={left:"right",right:"left",bottom:"top",top:"bottom"};function clamp(e,t,n){return max(e,min(t,n))}function evaluate(e,t){return"function"==typeof e?e(t):e}function getSide(e){return e.split("-")[0]}function getAlignment(e){return e.split("-")[1]}function getOppositeAxis(e){return"x"===e?"y":"x"}function getAxisLength(e){return"y"===e?"height":"width"}function getSideAxis(e){const t=e[0];return"t"===t||"b"===t?"y":"x"}function getAlignmentAxis(e){return getOppositeAxis(getSideAxis(e))}function getAlignmentSides(e,t,n){void 0===n&&(n=!1);const s=getAlignment(e),i=getAlignmentAxis(e),o=getAxisLength(i);let a="x"===i?s===(n?"end":"start")?"right":"left":"start"===s?"bottom":"top";return t.reference[o]>t.floating[o]&&(a=getOppositePlacement(a)),[a,getOppositePlacement(a)]}function getExpandedPlacements(e){const t=getOppositePlacement(e);return[getOppositeAlignmentPlacement(e),t,getOppositeAlignmentPlacement(t)]}function getOppositeAlignmentPlacement(e){return e.includes("start")?e.replace("start","end"):e.replace("end","start")}const lrPlacement=["left","right"],rlPlacement=["right","left"],tbPlacement=["top","bottom"],btPlacement=["bottom","top"];function getSideList(e,t,n){switch(e){case"top":case"bottom":return n?t?rlPlacement:lrPlacement:t?lrPlacement:rlPlacement;case"left":case"right":return t?tbPlacement:btPlacement;default:return[]}}function getOppositeAxisPlacements(e,t,n,s){const i=getAlignment(e);let o=getSideList(getSide(e),"start"===n,s);return i&&(o=o.map(e=>e+"-"+i),t&&(o=o.concat(o.map(getOppositeAlignmentPlacement)))),o}function getOppositePlacement(e){const t=getSide(e);return oppositeSideMap[t]+e.slice(t.length)}function expandPaddingObject(e){return{top:0,right:0,bottom:0,left:0,...e}}function getPaddingObject(e){return"number"!=typeof e?expandPaddingObject(e):{top:e,right:e,bottom:e,left:e}}function rectToClientRect(e){const{x:t,y:n,width:s,height:i}=e;return{width:s,height:i,top:n,left:t,right:t+s,bottom:n+i,x:t,y:n}}function computeCoordsFromPlacement(e,t,n){let{reference:s,floating:i}=e;const o=getSideAxis(t),a=getAlignmentAxis(t),l=getAxisLength(a),r=getSide(t),c="y"===o,d=s.x+s.width/2-i.width/2,u=s.y+s.height/2-i.height/2,h=s[l]/2-i[l]/2;let _;switch(r){case"top":_={x:d,y:s.y-i.height};break;case"bottom":_={x:d,y:s.y+s.height};break;case"right":_={x:s.x+s.width,y:u};break;case"left":_={x:s.x-i.width,y:u};break;default:_={x:s.x,y:s.y}}switch(getAlignment(t)){case"start":_[a]-=h*(n&&c?-1:1);break;case"end":_[a]+=h*(n&&c?-1:1)}return _}async function detectOverflow(e,t){var n;void 0===t&&(t={});const{x:s,y:i,platform:o,rects:a,elements:l,strategy:r}=e,{boundary:c="clippingAncestors",rootBoundary:d="viewport",elementContext:u="floating",altBoundary:h=!1,padding:_=0}=evaluate(t,e),m=getPaddingObject(_),g=l[h?"floating"===u?"reference":"floating":u],p=rectToClientRect(await o.getClippingRect({element:null==(n=await(null==o.isElement?void 0:o.isElement(g)))||n?g:g.contextElement||await(null==o.getDocumentElement?void 0:o.getDocumentElement(l.floating)),boundary:c,rootBoundary:d,strategy:r})),E="floating"===u?{x:s,y:i,width:a.floating.width,height:a.floating.height}:a.reference,f=await(null==o.getOffsetParent?void 0:o.getOffsetParent(l.floating)),v=await(null==o.isElement?void 0:o.isElement(f))&&await(null==o.getScale?void 0:o.getScale(f))||{x:1,y:1},b=rectToClientRect(o.convertOffsetParentRelativeRectToViewportRelativeRect?await o.convertOffsetParentRelativeRectToViewportRelativeRect({elements:l,rect:E,offsetParent:f,strategy:r}):E);return{top:(p.top-b.top+m.top)/v.y,bottom:(b.bottom-p.bottom+m.bottom)/v.y,left:(p.left-b.left+m.left)/v.x,right:(b.right-p.right+m.right)/v.x}}const MAX_RESET_COUNT=50,computePosition$1=async(e,t,n)=>{const{placement:s="bottom",strategy:i="absolute",middleware:o=[],platform:a}=n,l=a.detectOverflow?a:{...a,detectOverflow:detectOverflow},r=await(null==a.isRTL?void 0:a.isRTL(t));let c=await a.getElementRects({reference:e,floating:t,strategy:i}),{x:d,y:u}=computeCoordsFromPlacement(c,s,r),h=s,_=0;const m={};for(let n=0;n({name:"arrow",options:e,async fn(t){const{x:n,y:s,placement:i,rects:o,platform:a,elements:l,middlewareData:r}=t,{element:c,padding:d=0}=evaluate(e,t)||{};if(null==c)return{};const u=getPaddingObject(d),h={x:n,y:s},_=getAlignmentAxis(i),m=getAxisLength(_),g=await a.getDimensions(c),p="y"===_,E=p?"top":"left",f=p?"bottom":"right",v=p?"clientHeight":"clientWidth",b=o.reference[m]+o.reference[_]-h[_]-o.floating[m],T=h[_]-o.reference[_],A=await(null==a.getOffsetParent?void 0:a.getOffsetParent(c));let y=A?A[v]:0;y&&await(null==a.isElement?void 0:a.isElement(A))||(y=l.floating[v]||o.floating[m]);const S=b/2-T/2,D=y/2-g[m]/2-1,C=min(u[E],D),N=min(u[f],D),w=C,x=y-g[m]-N,L=y/2-g[m]/2+S,M=clamp(w,L,x),O=!r.arrow&&null!=getAlignment(i)&&L!==M&&o.reference[m]/2-(Le<=0)){var N,w;const e=((null==(N=o.flip)?void 0:N.index)||0)+1,t=y[e];if(t&&("alignment"!==u||f===getSideAxis(t)||C.every(e=>getSideAxis(e.placement)!==f||e.overflows[0]>0)))return{data:{index:e,overflows:C},reset:{placement:t}};let n=null==(w=C.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0])?void 0:w.placement;if(!n)switch(_){case"bestFit":{var x;const e=null==(x=C.filter(e=>{if(A){const t=getSideAxis(e.placement);return t===f||"y"===t}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0])?void 0:x[0];e&&(n=e);break}case"initialPlacement":n=l}if(i!==n)return{reset:{placement:n}}}return{}}}},originSides=new Set(["left","top"]);async function convertValueToCoords(e,t){const{placement:n,platform:s,elements:i}=e,o=await(null==s.isRTL?void 0:s.isRTL(i.floating)),a=getSide(n),l=getAlignment(n),r="y"===getSideAxis(n),c=originSides.has(a)?-1:1,d=o&&r?-1:1,u=evaluate(t,e);let{mainAxis:h,crossAxis:_,alignmentAxis:m}="number"==typeof u?{mainAxis:u,crossAxis:0,alignmentAxis:null}:{mainAxis:u.mainAxis||0,crossAxis:u.crossAxis||0,alignmentAxis:u.alignmentAxis};return l&&"number"==typeof m&&(_="end"===l?-1*m:m),r?{x:_*d,y:h*c}:{x:h*c,y:_*d}}const offset$1=function(e){return void 0===e&&(e=0),{name:"offset",options:e,async fn(t){var n,s;const{x:i,y:o,placement:a,middlewareData:l}=t,r=await convertValueToCoords(t,e);return a===(null==(n=l.offset)?void 0:n.placement)&&null!=(s=l.arrow)&&s.alignmentOffset?{}:{x:i+r.x,y:o+r.y,data:{...r,placement:a}}}}},shift$1=function(e){return void 0===e&&(e={}),{name:"shift",options:e,async fn(t){const{x:n,y:s,placement:i,platform:o}=t,{mainAxis:a=!0,crossAxis:l=!1,limiter:r={fn:e=>{let{x:t,y:n}=e;return{x:t,y:n}}},...c}=evaluate(e,t),d={x:n,y:s},u=await o.detectOverflow(t,c),h=getSideAxis(getSide(i)),_=getOppositeAxis(h);let m=d[_],g=d[h];if(a){const e="y"===_?"bottom":"right";m=clamp(m+u["y"===_?"top":"left"],m,m-u[e])}if(l){const e="y"===h?"bottom":"right";g=clamp(g+u["y"===h?"top":"left"],g,g-u[e])}const p=r.fn({...t,[_]:m,[h]:g});return{...p,data:{x:p.x-n,y:p.y-s,enabled:{[_]:a,[h]:l}}}}}};function hasWindow(){return"undefined"!=typeof window}function getNodeName(e){return isNode(e)?(e.nodeName||"").toLowerCase():"#document"}function getWindow(e){var t;return(null==e||null==(t=e.ownerDocument)?void 0:t.defaultView)||window}function getDocumentElement(e){var t;return null==(t=(isNode(e)?e.ownerDocument:e.document)||window.document)?void 0:t.documentElement}function isNode(e){return!!hasWindow()&&(e instanceof Node||e instanceof getWindow(e).Node)}function isElement(e){return!!hasWindow()&&(e instanceof Element||e instanceof getWindow(e).Element)}function isHTMLElement(e){return!!hasWindow()&&(e instanceof HTMLElement||e instanceof getWindow(e).HTMLElement)}function isShadowRoot(e){return!(!hasWindow()||"undefined"==typeof ShadowRoot)&&(e instanceof ShadowRoot||e instanceof getWindow(e).ShadowRoot)}function isOverflowElement(e){const{overflow:t,overflowX:n,overflowY:s,display:i}=getComputedStyle$1(e);return/auto|scroll|overlay|hidden|clip/.test(t+s+n)&&"inline"!==i&&"contents"!==i}function isTableElement(e){return/^(table|td|th)$/.test(getNodeName(e))}function isTopLayer(e){try{if(e.matches(":popover-open"))return!0}catch(e){}try{return e.matches(":modal")}catch(e){return!1}}const willChangeRe=/transform|translate|scale|rotate|perspective|filter/,containRe=/paint|layout|strict|content/,isNotNone=e=>!!e&&"none"!==e;let isWebKitValue;function isContainingBlock(e){const t=isElement(e)?getComputedStyle$1(e):e;return isNotNone(t.transform)||isNotNone(t.translate)||isNotNone(t.scale)||isNotNone(t.rotate)||isNotNone(t.perspective)||!isWebKit()&&(isNotNone(t.backdropFilter)||isNotNone(t.filter))||willChangeRe.test(t.willChange||"")||containRe.test(t.contain||"")}function getContainingBlock(e){let t=getParentNode(e);for(;isHTMLElement(t)&&!isLastTraversableNode(t);){if(isContainingBlock(t))return t;if(isTopLayer(t))return null;t=getParentNode(t)}return null}function isWebKit(){return null==isWebKitValue&&(isWebKitValue="undefined"!=typeof CSS&&CSS.supports&&CSS.supports("-webkit-backdrop-filter","none")),isWebKitValue}function isLastTraversableNode(e){return/^(html|body|#document)$/.test(getNodeName(e))}function getComputedStyle$1(e){return getWindow(e).getComputedStyle(e)}function getNodeScroll(e){return isElement(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function getParentNode(e){if("html"===getNodeName(e))return e;const t=e.assignedSlot||e.parentNode||isShadowRoot(e)&&e.host||getDocumentElement(e);return isShadowRoot(t)?t.host:t}function getNearestOverflowAncestor(e){const t=getParentNode(e);return isLastTraversableNode(t)?e.ownerDocument?e.ownerDocument.body:e.body:isHTMLElement(t)&&isOverflowElement(t)?t:getNearestOverflowAncestor(t)}function getOverflowAncestors(e,t,n){var s;void 0===t&&(t=[]),void 0===n&&(n=!0);const i=getNearestOverflowAncestor(e),o=i===(null==(s=e.ownerDocument)?void 0:s.body),a=getWindow(i);if(o){const e=getFrameElement(a);return t.concat(a,a.visualViewport||[],isOverflowElement(i)?i:[],e&&n?getOverflowAncestors(e):[])}return t.concat(i,getOverflowAncestors(i,[],n))}function getFrameElement(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function getCssDimensions(e){const t=getComputedStyle$1(e);let n=parseFloat(t.width)||0,s=parseFloat(t.height)||0;const i=isHTMLElement(e),o=i?e.offsetWidth:n,a=i?e.offsetHeight:s,l=round(n)!==o||round(s)!==a;return l&&(n=o,s=a),{width:n,height:s,$:l}}function unwrapElement(e){return isElement(e)?e:e.contextElement}function getScale(e){const t=unwrapElement(e);if(!isHTMLElement(t))return createCoords(1);const n=t.getBoundingClientRect(),{width:s,height:i,$:o}=getCssDimensions(t);let a=(o?round(n.width):n.width)/s,l=(o?round(n.height):n.height)/i;return a&&Number.isFinite(a)||(a=1),l&&Number.isFinite(l)||(l=1),{x:a,y:l}}const noOffsets=createCoords(0);function getVisualOffsets(e){const t=getWindow(e);return isWebKit()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:noOffsets}function shouldAddVisualOffsets(e,t,n){return void 0===t&&(t=!1),!(!n||t&&n!==getWindow(e))&&t}function getBoundingClientRect(e,t,n,s){void 0===t&&(t=!1),void 0===n&&(n=!1);const i=e.getBoundingClientRect(),o=unwrapElement(e);let a=createCoords(1);t&&(s?isElement(s)&&(a=getScale(s)):a=getScale(e));const l=shouldAddVisualOffsets(o,n,s)?getVisualOffsets(o):createCoords(0);let r=(i.left+l.x)/a.x,c=(i.top+l.y)/a.y,d=i.width/a.x,u=i.height/a.y;if(o){const e=getWindow(o),t=s&&isElement(s)?getWindow(s):s;let n=e,i=getFrameElement(n);for(;i&&s&&t!==n;){const e=getScale(i),t=i.getBoundingClientRect(),s=getComputedStyle$1(i),o=t.left+(i.clientLeft+parseFloat(s.paddingLeft))*e.x,a=t.top+(i.clientTop+parseFloat(s.paddingTop))*e.y;r*=e.x,c*=e.y,d*=e.x,u*=e.y,r+=o,c+=a,n=getWindow(i),i=getFrameElement(n)}}return rectToClientRect({width:d,height:u,x:r,y:c})}function getWindowScrollBarX(e,t){const n=getNodeScroll(e).scrollLeft;return t?t.left+n:getBoundingClientRect(getDocumentElement(e)).left+n}function getHTMLOffset(e,t){const n=e.getBoundingClientRect();return{x:n.left+t.scrollLeft-getWindowScrollBarX(e,n),y:n.top+t.scrollTop}}function convertOffsetParentRelativeRectToViewportRelativeRect(e){let{elements:t,rect:n,offsetParent:s,strategy:i}=e;const o="fixed"===i,a=getDocumentElement(s),l=!!t&&isTopLayer(t.floating);if(s===a||l&&o)return n;let r={scrollLeft:0,scrollTop:0},c=createCoords(1);const d=createCoords(0),u=isHTMLElement(s);if((u||!u&&!o)&&(("body"!==getNodeName(s)||isOverflowElement(a))&&(r=getNodeScroll(s)),u)){const e=getBoundingClientRect(s);c=getScale(s),d.x=e.x+s.clientLeft,d.y=e.y+s.clientTop}const h=!a||u||o?createCoords(0):getHTMLOffset(a,r);return{width:n.width*c.x,height:n.height*c.y,x:n.x*c.x-r.scrollLeft*c.x+d.x+h.x,y:n.y*c.y-r.scrollTop*c.y+d.y+h.y}}function getClientRects(e){return Array.from(e.getClientRects())}function getDocumentRect(e){const t=getDocumentElement(e),n=getNodeScroll(e),s=e.ownerDocument.body,i=max(t.scrollWidth,t.clientWidth,s.scrollWidth,s.clientWidth),o=max(t.scrollHeight,t.clientHeight,s.scrollHeight,s.clientHeight);let a=-n.scrollLeft+getWindowScrollBarX(e);const l=-n.scrollTop;return"rtl"===getComputedStyle$1(s).direction&&(a+=max(t.clientWidth,s.clientWidth)-i),{width:i,height:o,x:a,y:l}}const SCROLLBAR_MAX=25;function getViewportRect(e,t){const n=getWindow(e),s=getDocumentElement(e),i=n.visualViewport;let o=s.clientWidth,a=s.clientHeight,l=0,r=0;if(i){o=i.width,a=i.height;const e=isWebKit();(!e||e&&"fixed"===t)&&(l=i.offsetLeft,r=i.offsetTop)}const c=getWindowScrollBarX(s);if(c<=0){const e=s.ownerDocument,t=e.body,n=getComputedStyle(t),i="CSS1Compat"===e.compatMode&&parseFloat(n.marginLeft)+parseFloat(n.marginRight)||0,a=Math.abs(s.clientWidth-t.clientWidth-i);a<=25&&(o-=a)}else c<=25&&(o+=c);return{width:o,height:a,x:l,y:r}}function getInnerBoundingClientRect(e,t){const n=getBoundingClientRect(e,!0,"fixed"===t),s=n.top+e.clientTop,i=n.left+e.clientLeft,o=isHTMLElement(e)?getScale(e):createCoords(1);return{width:e.clientWidth*o.x,height:e.clientHeight*o.y,x:i*o.x,y:s*o.y}}function getClientRectFromClippingAncestor(e,t,n){let s;if("viewport"===t)s=getViewportRect(e,n);else if("document"===t)s=getDocumentRect(getDocumentElement(e));else if(isElement(t))s=getInnerBoundingClientRect(t,n);else{const n=getVisualOffsets(e);s={x:t.x-n.x,y:t.y-n.y,width:t.width,height:t.height}}return rectToClientRect(s)}function hasFixedPositionAncestor(e,t){const n=getParentNode(e);return!(n===t||!isElement(n)||isLastTraversableNode(n))&&("fixed"===getComputedStyle$1(n).position||hasFixedPositionAncestor(n,t))}function getClippingElementAncestors(e,t){const n=t.get(e);if(n)return n;let s=getOverflowAncestors(e,[],!1).filter(e=>isElement(e)&&"body"!==getNodeName(e)),i=null;const o="fixed"===getComputedStyle$1(e).position;let a=o?getParentNode(e):e;for(;isElement(a)&&!isLastTraversableNode(a);){const t=getComputedStyle$1(a),n=isContainingBlock(a);n||"fixed"!==t.position||(i=null),(o?!n&&!i:!n&&"static"===t.position&&i&&("absolute"===i.position||"fixed"===i.position)||isOverflowElement(a)&&!n&&hasFixedPositionAncestor(e,a))?s=s.filter(e=>e!==a):i=t,a=getParentNode(a)}return t.set(e,s),s}function getClippingRect(e){let{element:t,boundary:n,rootBoundary:s,strategy:i}=e;const o=[..."clippingAncestors"===n?isTopLayer(t)?[]:getClippingElementAncestors(t,this._c):[].concat(n),s],a=getClientRectFromClippingAncestor(t,o[0],i);let l=a.top,r=a.right,c=a.bottom,d=a.left;for(let e=1;e{a(!1,1e-7)},1e3)}1!==s||rectsAreEqual(c,e.getBoundingClientRect())||a(),g=!1}try{s=new IntersectionObserver(p,{...m,root:i.ownerDocument})}catch(e){s=new IntersectionObserver(p,m)}s.observe(e)}(!0),o}function autoUpdate(e,t,n,s){void 0===s&&(s={});const{ancestorScroll:i=!0,ancestorResize:o=!0,elementResize:a="function"==typeof ResizeObserver,layoutShift:l="function"==typeof IntersectionObserver,animationFrame:r=!1}=s,c=unwrapElement(e),d=i||o?[...c?getOverflowAncestors(c):[],...t?getOverflowAncestors(t):[]]:[];d.forEach(e=>{i&&e.addEventListener("scroll",n,{passive:!0}),o&&e.addEventListener("resize",n)});const u=c&&l?observeMove(c,n):null;let h,_=-1,m=null;a&&(m=new ResizeObserver(e=>{let[s]=e;s&&s.target===c&&m&&t&&(m.unobserve(t),cancelAnimationFrame(_),_=requestAnimationFrame(()=>{var e;null==(e=m)||e.observe(t)})),n()}),c&&!r&&m.observe(c),t&&m.observe(t));let g=r?getBoundingClientRect(e):null;return r&&function t(){const s=getBoundingClientRect(e);g&&!rectsAreEqual(g,s)&&n(),g=s,h=requestAnimationFrame(t)}(),n(),()=>{var e;d.forEach(e=>{i&&e.removeEventListener("scroll",n),o&&e.removeEventListener("resize",n)}),null==u||u(),null==(e=m)||e.disconnect(),m=null,r&&cancelAnimationFrame(h)}}const offset=offset$1,shift=shift$1,flip=flip$1,arrow=arrow$1,computePosition=(e,t,n)=>{const s=new Map,i={platform:platform,...n},o={...i.platform,_c:s};return computePosition$1(e,t,{...i,platform:o})},BREAKPOINTS={sm:576,md:768,lg:1024,xl:1280,"2xl":1536},parseResponsivePlacement=(e,t="bottom")=>{if(!e||!e.includes(":"))return null;const n=e.split(/\s+/),s={xs:t};for(const e of n)if(e.includes(":")){const[t,n]=e.split(":");void 0!==BREAKPOINTS[t]&&(s[t]=n)}else s.xs=e;return s},getResponsivePlacement=(e,t="bottom")=>{if(!e)return t;const n=window.innerWidth;let s=e.xs||t;const i=["sm","md","lg","xl","2xl"];for(const t of i)n>=BREAKPOINTS[t]&&e[t]&&(s=e[t]);return s},createBreakpointListeners=e=>{const t=[];for(const n of Object.keys(BREAKPOINTS)){const s=BREAKPOINTS[n],i=window.matchMedia(`(min-width: ${s}px)`);i.addEventListener("change",e),t.push({mql:i,handler:e})}return t},disposeBreakpointListeners=e=>{for(const{mql:t,handler:n}of e)t.removeEventListener("change",n)},NAME$h="menu",DATA_KEY$d="bs.menu",EVENT_KEY$e=".bs.menu",DATA_API_KEY$9=".data-api",ESCAPE_KEY$2="Escape",TAB_KEY$1="Tab",ARROW_UP_KEY$2="ArrowUp",ARROW_DOWN_KEY$2="ArrowDown",ARROW_LEFT_KEY$1="ArrowLeft",ARROW_RIGHT_KEY$1="ArrowRight",HOME_KEY$2="Home",END_KEY$2="End",ENTER_KEY$1="Enter",SPACE_KEY$1=" ",RIGHT_MOUSE_BUTTON=2,SUBMENU_CLOSE_DELAY=100,EVENT_HIDE$5="hide.bs.menu",EVENT_HIDDEN$7="hidden.bs.menu",EVENT_SHOW$6="show.bs.menu",EVENT_SHOWN$5="shown.bs.menu",EVENT_CLICK_DATA_API$5="click.bs.menu.data-api",EVENT_KEYDOWN_DATA_API="keydown.bs.menu.data-api",EVENT_KEYUP_DATA_API="keyup.bs.menu.data-api",CLASS_NAME_SHOW$4="show",SELECTOR_DATA_TOGGLE$8='[data-bs-toggle="menu"]:not(.disabled):not(:disabled)',SELECTOR_MENU$2=".menu",SELECTOR_SUBMENU=".submenu",SELECTOR_SUBMENU_TOGGLE=".submenu > .menu-item",SELECTOR_NAVBAR_NAV=".navbar-nav",SELECTOR_VISIBLE_ITEMS$1=".menu-item:not(.disabled):not(:disabled)",DEFAULT_PLACEMENT="bottom-start",SUBMENU_PLACEMENT="end-start",resolveLogicalPlacement=e=>isRTL$1()?e.replace(/^start(?=-|$)/,"right").replace(/^end(?=-|$)/,"left"):e.replace(/^start(?=-|$)/,"left").replace(/^end(?=-|$)/,"right"),triangleSign=(e,t,n)=>(e.x-n.x)*(t.y-n.y)-(t.x-n.x)*(e.y-n.y),Default$g={autoClose:!0,boundary:"clippingParents",container:!1,display:"dynamic",offset:[0,2],floatingConfig:null,menu:null,placement:"bottom-start",reference:"toggle",strategy:"absolute",submenuTrigger:"both",submenuDelay:100},DefaultType$g={autoClose:"(boolean|string)",boundary:"(string|element)",container:"(string|element|boolean)",display:"string",offset:"(array|string|function)",floatingConfig:"(null|object|function)",menu:"(null|element)",placement:"string",reference:"(string|element|object)",strategy:"string",submenuTrigger:"string",submenuDelay:"number"};class Menu extends BaseComponent{static _openInstances=new Set;constructor(e,t){super(e,t),this._floatingCleanup=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this._parent=this._element.parentNode,this._openSubmenus=new Map,this._submenuCloseTimeouts=new Map,this._hoverIntentData=null,this._menu=this._config.menu||this._findMenu(),!this._config.menu&&this._menu&&(this._parent=this._findWrapper(this._menu)),this._isSubmenu=this._parent.classList?.contains("submenu"),this._menuOriginalParent=this._menu?.parentNode,this._parseResponsivePlacements(),this._setupSubmenuListeners()}static get Default(){return Default$g}static get DefaultType(){return DefaultType$g}static get NAME(){return"menu"}toggle(){return this._isShown()?this.hide():this.show()}show(){if(isDisabled(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!EventHandler.trigger(this._element,EVENT_SHOW$6,e).defaultPrevented){if(this._moveMenuToContainer(),this._createFloating(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._element.focus({focusVisible:!1}),this._element.setAttribute("aria-expanded","true"),this._menu.classList.add("show"),this._element.classList.add("show"),this._parent&&this._parent.classList.add("show"),Menu._openInstances.add(this),EventHandler.trigger(this._element,EVENT_SHOWN$5,e)}}hide(){if(isDisabled(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._disposeFloating(),this._restoreMenuToOriginalParent(),this._disposeMediaQueryListeners(),this._closeAllSubmenus(),this._clearAllSubmenuTimeouts(),Menu._openInstances.delete(this),super.dispose()}update(){this._floatingCleanup&&this._updateFloatingPosition()}_findMenu(){const e=SelectorEngine.closest(this._element,":has(.menu)");return SelectorEngine.next(this._element,".menu")[0]||SelectorEngine.prev(this._element,".menu")[0]||SelectorEngine.findOne(".menu",e||this._parent)}_findWrapper(e){let t=this._element.parentNode;for(;t instanceof Element&&!t.contains(e);)t=t.parentNode;return t instanceof Element?t:this._element.parentNode}_completeHide(e){if(!EventHandler.trigger(this._element,EVENT_HIDE$5,e).defaultPrevented){if(this._closeAllSubmenus(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._disposeFloating(),this._restoreMenuToOriginalParent(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._parent&&this._parent.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),Manipulator.removeDataAttribute(this._menu,"placement"),Manipulator.removeDataAttribute(this._menu,"display"),Menu._openInstances.delete(this),EventHandler.trigger(this._element,EVENT_HIDDEN$7,e)}}_getConfig(e){if("object"==typeof(e=super._getConfig(e)).reference&&!isElement$1(e.reference)&&"function"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${"menu".toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createFloating(){if("static"===this._config.display)return void Manipulator.setDataAttribute(this._menu,"display","static");let e=this._element;"parent"===this._config.reference?e=this._parent:isElement$1(this._config.reference)?e=getElement(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference),this._updateFloatingPosition(e),this._floatingCleanup=autoUpdate(e,this._menu,()=>this._updateFloatingPosition(e))}async _updateFloatingPosition(e=null){if(!this._menu)return;e||(e="parent"===this._config.reference?this._parent:isElement$1(this._config.reference)?getElement(this._config.reference):"object"==typeof this._config.reference?this._config.reference:this._element);const t=this._getPlacement(),n=this._getFloatingMiddleware(),s=this._getFloatingConfig(t,n);await this._applyFloatingPosition(e,this._menu,s.placement,s.middleware,s.strategy)}_isShown(){return this._menu.classList.contains("show")}_getPlacement(){const e=this._responsivePlacements?getResponsivePlacement(this._responsivePlacements,"bottom-start"):this._config.placement;return resolveLogicalPlacement(e)}_parseResponsivePlacements(){this._responsivePlacements=parseResponsivePlacement(this._config.placement,"bottom-start"),this._responsivePlacements&&this._setupMediaQueryListeners()}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_getFloatingMiddleware(){const e=this._getOffset();return[offset("function"==typeof e?e:{mainAxis:e[1]||0,crossAxis:e[0]||0}),flip({fallbackPlacements:this._getFallbackPlacements()}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})]}_getFallbackPlacements(){return{bottom:["top","bottom-start","bottom-end","top-start","top-end"],"bottom-start":["top-start","bottom-end","top-end"],"bottom-end":["top-end","bottom-start","top-start"],top:["bottom","top-start","top-end","bottom-start","bottom-end"],"top-start":["bottom-start","top-end","bottom-end"],"top-end":["bottom-end","top-start","bottom-start"],right:["left","right-start","right-end","left-start","left-end"],"right-start":["left-start","right-end","left-end","top-start","bottom-start"],"right-end":["left-end","right-start","left-start","top-end","bottom-end"],left:["right","left-start","left-end","right-start","right-end"],"left-start":["right-start","left-end","right-end","top-start","bottom-start"],"left-end":["right-end","left-start","right-start","top-end","bottom-end"]}[this._getPlacement()]||["top","bottom","right","left"]}_getFloatingConfig(e,t){const n={placement:e,middleware:t,strategy:this._config.strategy};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null)}_getContainer(){const{container:e}=this._config;return!1===e?null:!0===e?document.body:getElement(e)}_moveMenuToContainer(){const e=this._getContainer();e&&this._menu&&this._menu.parentNode!==e&&e.append(this._menu)}_restoreMenuToOriginalParent(){this._menuOriginalParent&&this._menu&&this._menu.parentNode!==this._menuOriginalParent&&this._menuOriginalParent.append(this._menu)}async _applyFloatingPosition(e,t,n,s,i="absolute"){if(!t.isConnected)return null;const{x:o,y:a,placement:l}=await computePosition(e,t,{placement:n,middleware:s,strategy:i});return t.isConnected?(Object.assign(t.style,{position:i,left:`${o}px`,top:`${a}px`,margin:"0"}),Manipulator.setDataAttribute(t,"placement",l),l):null}_setupSubmenuListeners(){"hover"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||(EventHandler.on(this._menu,"mouseenter",".submenu > .menu-item",e=>{this._onSubmenuTriggerEnter(e)}),EventHandler.on(this._menu,"mouseleave",".submenu",e=>{this._onSubmenuLeave(e)}),EventHandler.on(this._menu,"mousemove",e=>{this._trackMousePosition(e)})),"click"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||EventHandler.on(this._menu,"click",".submenu > .menu-item",e=>{this._onSubmenuTriggerClick(e)})}_onSubmenuTriggerEnter(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._cancelSubmenuCloseTimeout(s),this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n))}_onSubmenuLeave(e){const t=e.target.closest(".submenu"),n=SelectorEngine.findOne(".menu",t);n&&this._openSubmenus.has(n)&&(this._isMovingTowardSubmenu(e,n)||this._scheduleSubmenuClose(n,t))}_onSubmenuTriggerClick(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;e.preventDefault(),e.stopPropagation();const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._openSubmenus.has(s)?this._closeSubmenu(s,n):(this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n)))}_openSubmenu(e,t,n){if(this._openSubmenus.has(t))return;e.setAttribute("aria-expanded","true"),e.setAttribute("aria-haspopup","true"),t.style.opacity="0",t.classList.add("show"),n.classList.add("show");const s=this._createSubmenuFloating(e,t,n);this._openSubmenus.set(t,s),EventHandler.on(t,"mouseenter",()=>{this._cancelSubmenuCloseTimeout(t)})}_closeSubmenu(e,t){if(!this._openSubmenus.has(e))return;const n=SelectorEngine.find(".submenu .menu.show",e);for(const e of n){const t=e.closest(".submenu");this._closeSubmenu(e,t)}const s=SelectorEngine.findOne(".submenu > .menu-item",t),i=this._openSubmenus.get(e);i&&i(),this._openSubmenus.delete(e),EventHandler.off(e,"mouseenter"),s&&s.setAttribute("aria-expanded","false"),e.classList.remove("show"),t.classList.remove("show"),e.style.opacity=""}_closeAllSubmenus(){for(const[e]of this._openSubmenus){const t=e.closest(".submenu");this._closeSubmenu(e,t)}}_closeSiblingSubmenus(e){const t=e.parentNode,n=SelectorEngine.find(".submenu > .menu.show",t);for(const t of n){const n=t.closest(".submenu");n!==e&&this._closeSubmenu(t,n)}}_createSubmenuFloating(e,t,n){const s=n,i=resolveLogicalPlacement("end-start"),o=[offset({mainAxis:0,crossAxis:-4}),flip({fallbackPlacements:[resolveLogicalPlacement("start-start"),resolveLogicalPlacement("end-end"),resolveLogicalPlacement("start-end")]}),shift({padding:8})],a=()=>this._applyFloatingPosition(s,t,i,o).then(e=>(t.style.opacity="",e));return a(),autoUpdate(s,t,a)}_scheduleSubmenuClose(e,t){this._cancelSubmenuCloseTimeout(e);const n=setTimeout(()=>{this._closeSubmenu(e,t),this._submenuCloseTimeouts.delete(e)},this._config.submenuDelay);this._submenuCloseTimeouts.set(e,n)}_cancelSubmenuCloseTimeout(e){const t=this._submenuCloseTimeouts.get(e);t&&(clearTimeout(t),this._submenuCloseTimeouts.delete(e))}_clearAllSubmenuTimeouts(){for(const e of this._submenuCloseTimeouts.values())clearTimeout(e);this._submenuCloseTimeouts.clear()}_trackMousePosition(e){this._hoverIntentData={x:e.clientX,y:e.clientY,timestamp:Date.now()}}_isMovingTowardSubmenu(e,t){if(!this._hoverIntentData)return!1;const n=t.getBoundingClientRect(),s={x:e.clientX,y:e.clientY},i={x:this._hoverIntentData.x,y:this._hoverIntentData.y},o=isRTL$1()?n.right:n.left,a={x:o,y:n.top},l={x:o,y:n.bottom};return this._pointInTriangle(s,i,a,l)}_pointInTriangle(e,t,n,s){const i=triangleSign(e,t,n),o=triangleSign(e,n,s),a=triangleSign(e,s,t);return!((i<0||o<0||a<0)&&(i>0||o>0||a>0))}_selectMenuItem({key:e,target:t}){const n=t.closest(".menu")||this._menu,s=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,n).filter(e=>isVisible(e));s.length&&getNextActiveElement(s,t,e===ARROW_DOWN_KEY$2,!s.includes(t)).focus()}_handleSubmenuKeydown(e){const{key:t,target:n}=e,s=isRTL$1(),i=s?ARROW_LEFT_KEY$1:ARROW_RIGHT_KEY$1,o=s?ARROW_RIGHT_KEY$1:ARROW_LEFT_KEY$1,a=n.closest(".submenu"),l=a&&n.matches(".submenu > .menu-item");if((t===ENTER_KEY$1||t===SPACE_KEY$1)&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",a);return t&&(this._closeSiblingSubmenus(a),this._openSubmenu(n,t,a),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===i&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",a);return t&&(this._closeSiblingSubmenus(a),this._openSubmenu(n,t,a),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===o){const t=n.closest(".menu"),s=t?.closest(".submenu");if(s){e.preventDefault(),e.stopPropagation();const n=SelectorEngine.findOne(".submenu > .menu-item",s);return this._closeSubmenu(t,s),n&&n.focus(),!0}}if(t===HOME_KEY$2||t===END_KEY$2){e.preventDefault(),e.stopPropagation();const s=n.closest(".menu"),i=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,s).filter(e=>isVisible(e));return i.length&&(t===HOME_KEY$2?i[0]:i.at(-1)).focus(),!0}return!1}static clearMenus(e){if(2!==e.button&&("keyup"!==e.type||"Tab"===e.key))for(const t of Menu._openInstances){if(!1===t._config.autoClose)continue;const n=e.composedPath(),s=n.includes(t._menu);if(n.includes(t._element)||"inside"===t._config.autoClose&&!s||"outside"===t._config.autoClose&&s)continue;const i=e.target.closest?.("form"),o=Boolean(i)&&t._menu.contains(i);if(t._menu.contains(e.target)&&("keyup"===e.type&&"Tab"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)||o))continue;const a={relatedTarget:t._element};"click"===e.type&&(a.clickEvent=e),t._completeHide(a)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName)||e.target.isContentEditable,n="Escape"===e.key,s=[ARROW_UP_KEY$2,ARROW_DOWN_KEY$2].includes(e.key),i=[ARROW_LEFT_KEY$1,ARROW_RIGHT_KEY$1].includes(e.key),o=[HOME_KEY$2,END_KEY$2].includes(e.key),a=[ENTER_KEY$1,SPACE_KEY$1].includes(e.key),l=e.target.matches(".submenu > .menu-item");if(!(s||n||i||o||a&&l))return;if(t&&!n)return;const r=this.matches(SELECTOR_DATA_TOGGLE$8)?this:SelectorEngine.prev(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.next(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8,e.delegateTarget.parentNode);if(!r)return;const c=Menu.getOrCreateInstance(r);if(!(i||o||a&&l)||!c._handleSubmenuKeydown(e)){if(s)return e.preventDefault(),e.stopPropagation(),c.show(),void c._selectMenuItem(e);if(n&&c._isShown()){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".menu"),n=t?.closest(".submenu");if(n&&c._openSubmenus.size>0){const e=SelectorEngine.findOne(".submenu > .menu-item",n);return c._closeSubmenu(t,n),void(e&&e.focus())}c.hide(),r.focus()}}}}EventHandler.on(document,EVENT_KEYDOWN_DATA_API,SELECTOR_DATA_TOGGLE$8,Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_KEYDOWN_DATA_API,".menu",Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_CLICK_DATA_API$5,Menu.clearMenus),EventHandler.on(document,EVENT_KEYUP_DATA_API,Menu.clearMenus),EventHandler.on(document,EVENT_CLICK_DATA_API$5,SELECTOR_DATA_TOGGLE$8,function(e){e.preventDefault(),Menu.getOrCreateInstance(this).toggle()});const NAME$g="combobox",DATA_KEY$c="bs.combobox",EVENT_KEY$d=`.${DATA_KEY$c}`,DATA_API_KEY$8=".data-api",ESCAPE_KEY$1="Escape",TAB_KEY="Tab",ARROW_UP_KEY$1="ArrowUp",ARROW_DOWN_KEY$1="ArrowDown",HOME_KEY$1="Home",END_KEY$1="End",ENTER_KEY="Enter",SPACE_KEY=" ",EVENT_CHANGE$3=`change${EVENT_KEY$d}`,EVENT_SHOW$5=`show${EVENT_KEY$d}`,EVENT_SHOWN$4=`shown${EVENT_KEY$d}`,EVENT_HIDE$4=`hide${EVENT_KEY$d}`,EVENT_HIDDEN$6=`hidden${EVENT_KEY$d}`,EVENT_CLICK_DATA_API$4=`click${EVENT_KEY$d}.data-api`,CLASS_NAME_SHOW$3="show",CLASS_NAME_SELECTED="selected",CLASS_NAME_PLACEHOLDER="combobox-placeholder",SELECTOR_DATA_TOGGLE$7='[data-bs-toggle="combobox"]',SELECTOR_MENU$1=".menu",SELECTOR_MENU_ITEM=".menu-item[data-bs-value]",SELECTOR_VISIBLE_ITEMS=".menu-item[data-bs-value]:not(.disabled):not(:disabled)",SELECTOR_VALUE=".combobox-value",SELECTOR_SEARCH_INPUT=".combobox-search-input",SELECTOR_NO_RESULTS=".combobox-no-results",Default$f={boundary:"clippingParents",multiple:!1,name:null,offset:[0,2],placeholder:"",placement:"bottom-start",search:!1,searchNormalize:!1},DefaultType$f={boundary:"(string|element)",multiple:"boolean",name:"(string|null)",offset:"(array|string|function)",placeholder:"string",placement:"string",search:"boolean",searchNormalize:"boolean"};class Combobox extends BaseComponent{constructor(e,t){super(e,t),this._toggle=this._element,this._menu=SelectorEngine.next(this._toggle,".menu")[0],this._valueDisplay=SelectorEngine.findOne(SELECTOR_VALUE,this._toggle),this._searchInput=SelectorEngine.findOne(SELECTOR_SEARCH_INPUT,this._menu),this._noResults=SelectorEngine.findOne(SELECTOR_NO_RESULTS,this._menu),this._hiddenInput=null,this._menuInstance=null,this._createHiddenInput(),this._createMenuInstance(),this._syncInitialSelection(),this._addEventListeners()}static get Default(){return Default$f}static get DefaultType(){return DefaultType$f}static get NAME(){return NAME$g}toggle(){return this._isShown()?this.hide():this.show()}show(){isDisabled(this._toggle)||this._isShown()||EventHandler.trigger(this._toggle,EVENT_SHOW$5).defaultPrevented||(this._menuInstance.show(),this._searchInput&&(this._searchInput.value="",this._filterItems(""),requestAnimationFrame(()=>this._searchInput.focus())),EventHandler.trigger(this._toggle,EVENT_SHOWN$4))}hide(){this._isShown()&&(EventHandler.trigger(this._toggle,EVENT_HIDE$4).defaultPrevented||(this._menuInstance.hide(),EventHandler.trigger(this._toggle,EVENT_HIDDEN$6)))}dispose(){this._menuInstance&&(this._menuInstance.dispose(),this._menuInstance=null),this._hiddenInput&&(this._hiddenInput.remove(),this._hiddenInput=null),EventHandler.off(this._menu,EVENT_KEY$d),EventHandler.off(this._toggle,EVENT_KEY$d),super.dispose()}_isShown(){return this._menu.classList.contains("show")}_createHiddenInput(){const{name:e}=this._config;e&&(this._hiddenInput=document.createElement("input"),this._hiddenInput.type="hidden",this._hiddenInput.name=e,this._hiddenInput.value="",this._toggle.parentNode.insertBefore(this._hiddenInput,this._toggle))}_createMenuInstance(){this._menuInstance=new Menu(this._toggle,{menu:this._menu,autoClose:!this._config.multiple||"outside",boundary:this._config.boundary,offset:this._config.offset,placement:this._config.placement})}_syncInitialSelection(){this._getSelectedItems().length>0?(this._updateToggleText(),this._updateHiddenInput()):this._showPlaceholder()}_addEventListeners(){EventHandler.on(this._menu,"click",SELECTOR_MENU_ITEM,e=>{const t=e.target.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&(e.preventDefault(),e.stopPropagation(),this._selectItem(t))}),EventHandler.on(this._toggle,"keydown",e=>{this._handleToggleKeydown(e)}),EventHandler.on(this._menu,"keydown",e=>{this._handleMenuKeydown(e)}),this._searchInput&&(EventHandler.on(this._searchInput,"input",()=>{this._filterItems(this._searchInput.value)}),EventHandler.on(this._searchInput,"keydown",e=>{if("ArrowDown"===e.key){e.preventDefault();const t=this._getVisibleItems();t.length>0&&t[0].focus()}"Escape"===e.key&&(this.hide(),this._toggle.focus())}))}_selectItem(e){if(this._config.multiple)e.classList.toggle("selected"),e.setAttribute("aria-selected",e.classList.contains("selected"));else{const t=SelectorEngine.find(".selected",this._menu);for(const e of t)e.classList.remove("selected"),e.setAttribute("aria-selected","false");e.classList.add("selected"),e.setAttribute("aria-selected","true")}this._updateToggleText(),this._updateHiddenInput();const t=this._config.multiple?this._getSelectedItems().map(e=>e.dataset.bsValue):e.dataset.bsValue;EventHandler.trigger(this._toggle,EVENT_CHANGE$3,{value:t,item:e}),this._config.multiple||(this.hide(),this._toggle.focus())}_updateToggleText(){const e=this._getSelectedItems();if(0!==e.length)if(this._valueDisplay.classList.remove("combobox-placeholder"),this._config.multiple&&e.length>1)this._valueDisplay.textContent=`${e.length} selected`;else{const t=e[0],n=SelectorEngine.findOne(".menu-item-content > span:first-child",t);this._valueDisplay.textContent=n?n.textContent:t.textContent.trim()}else this._showPlaceholder()}_showPlaceholder(){const{placeholder:e}=this._config;e&&(this._valueDisplay.textContent=e,this._valueDisplay.classList.add("combobox-placeholder"))}_updateHiddenInput(){if(!this._hiddenInput)return;const e=this._getSelectedItems().map(e=>e.dataset.bsValue);this._hiddenInput.value=this._config.multiple?e.join(","):e[0]||""}_getSelectedItems(){return SelectorEngine.find(".selected",this._menu)}_getVisibleItems(){return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS,this._menu).filter(e=>isVisible(e))}_filterItems(e){const t=this._normalizeText(e.toLowerCase().trim()),n=SelectorEngine.find(SELECTOR_MENU_ITEM,this._menu);let s=0;for(const e of n){const n=this._normalizeText(e.textContent.toLowerCase().trim()),i=!t||n.includes(t);e.style.display=i?"":"none",i&&s++}this._noResults&&this._noResults.classList.toggle("d-none",s>0)}_normalizeText(e){return this._config.searchNormalize?e.normalize("NFD").replace(/[\u0300-\u036F]/g,""):e}_handleToggleKeydown(e){const{key:t}=e;if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault(),this._isShown()||this.show();const n=this._getVisibleItems();return void(n.length>0&&("ArrowDown"===t?n[0]:n.at(-1)).focus())}"Enter"!==t&&" "!==t||this._isShown()||(e.preventDefault(),this.show())}_handleMenuKeydown(e){const{key:t,target:n}=e;if("Escape"===t)return e.preventDefault(),e.stopPropagation(),this.hide(),void this._toggle.focus();if("Tab"===t)return void this.hide();const s=n.matches("input");if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault();const s=this._getVisibleItems();return void(s.length>0&&getNextActiveElement(s,n,"ArrowDown"===t,!s.includes(n)).focus())}if("Home"===t||"End"===t){e.preventDefault();const n=this._getVisibleItems();return void(n.length>0&&("Home"===t?n[0]:n.at(-1)).focus())}if(("Enter"===t||" "===t)&&!s){e.preventDefault();const t=n.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&this._selectItem(t)}}static jQueryInterface(e){return this.each(function(){const t=Combobox.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}})}}EventHandler.on(document,EVENT_CLICK_DATA_API$4,SELECTOR_DATA_TOGGLE$7,function(e){e.preventDefault(),Combobox.getOrCreateInstance(this).toggle()}),EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7))Combobox.getOrCreateInstance(e)}); +/*! name: vanilla-calendar-pro v3.1.0 | url: https://github.com/uvarov-frontend/vanilla-calendar-pro */ +var __defProp=Object.defineProperty,__defProps=Object.defineProperties,__getOwnPropDescs=Object.getOwnPropertyDescriptors,__getOwnPropSymbols=Object.getOwnPropertySymbols,__hasOwnProp=Object.prototype.hasOwnProperty,__propIsEnum=Object.prototype.propertyIsEnumerable,__defNormalProp=(e,t,n)=>t in e?__defProp(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,__spreadValues=(e,t)=>{for(var n in t||(t={}))__hasOwnProp.call(t,n)&&__defNormalProp(e,n,t[n]);if(__getOwnPropSymbols)for(var n of __getOwnPropSymbols(t))__propIsEnum.call(t,n)&&__defNormalProp(e,n,t[n]);return e},__spreadProps=(e,t)=>__defProps(e,__getOwnPropDescs(t)),__publicField=(e,t,n)=>(__defNormalProp(e,"symbol"!=typeof t?t+"":t,n),n);const errorMessages={notFoundSelector:e=>`${e} is not found, check the first argument passed to new Calendar.`,notInit:'The calendar has not been initialized, please initialize it using the "init()" method first.',notLocale:"You specified an incorrect language label or did not specify the required number of values for «locale.weekdays» or «locale.months».",incorrectTime:"The value of the time property can be: false, 12 or 24.",incorrectMonthsCount:"For the «multiple» calendar type, the «displayMonthsCount» parameter can have a value from 2 to 12, and for all others it cannot be greater than 1."},setContext=(e,t,n)=>{e.context[t]=n},destroy=e=>{var t,n,s,i,o;if(!e.context.isInit)throw new Error(errorMessages.notInit);e.inputMode?(null==(t=e.context.mainElement.parentElement)||t.removeChild(e.context.mainElement),null==(s=null==(n=e.context.inputElement)?void 0:n.replaceWith)||s.call(n,e.context.originalElement),setContext(e,"inputElement",void 0)):null==(o=(i=e.context.mainElement).replaceWith)||o.call(i,e.context.originalElement),setContext(e,"mainElement",e.context.originalElement),e.onDestroy&&e.onDestroy(e)},skipOpenOnFocus=new WeakSet,shouldSkipOpenOnFocus=e=>skipOpenOnFocus.has(e),setSkipOpenOnFocus=e=>{skipOpenOnFocus.add(e)},clearSkipOpenOnFocus=e=>{skipOpenOnFocus.delete(e)},PREV_TABINDEX_ATTR="data-vc-prev-tabindex",isFocusable=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),storePrevTabIndex=e=>{if(e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute("tabindex");e.setAttribute(PREV_TABINDEX_ATTR,null!=t?t:"")},restorePrevTabIndex=e=>{if(!e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute(PREV_TABINDEX_ATTR);""===t||null===t?e.removeAttribute("tabindex"):e.setAttribute("tabindex",t),e.removeAttribute(PREV_TABINDEX_ATTR)},disableTabbing=e=>{isFocusable(e)&&(storePrevTabIndex(e),e.tabIndex=-1);const t=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>isFocusable(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP});for(;t.nextNode();){const e=t.currentNode;storePrevTabIndex(e),e.tabIndex=-1}},restoreTabbing=e=>{restorePrevTabIndex(e),e.querySelectorAll(`[${PREV_TABINDEX_ATTR}]`).forEach(restorePrevTabIndex)},hide=e=>{e.context.isShowInInputMode&&e.context.currentType&&(e.context.mainElement.dataset.vcCalendarHidden="",setContext(e,"isShowInInputMode",!1),e.inputMode&&disableTabbing(e.context.mainElement),e.context.cleanupHandlers[0]&&(e.context.cleanupHandlers.forEach(e=>e()),setContext(e,"cleanupHandlers",[])),e.inputMode&&e.context.inputElement&&e.context.mainElement.contains(document.activeElement)&&(("function"==typeof e.openOnFocus||!0===e.openOnFocus)&&setSkipOpenOnFocus(e),e.context.inputElement.focus()),e.onHide&&e.onHide(e))};function getOffset(e){if(!e||!e.getBoundingClientRect)return{top:0,bottom:0,left:0,right:0};const t=e.getBoundingClientRect(),n=document.documentElement;return{bottom:t.bottom,right:t.right,top:t.top+window.scrollY-n.clientTop,left:t.left+window.scrollX-n.clientLeft}}function getViewportDimensions(){return{vw:Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),vh:Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)}}function getWindowScrollPosition(){return{left:window.scrollX||document.documentElement.scrollLeft||0,top:window.scrollY||document.documentElement.scrollTop||0}}function calculateAvailableSpace(e){const{top:t,left:n}=getWindowScrollPosition(),{top:s,left:i}=getOffset(e),{vh:o,vw:a}=getViewportDimensions(),l=s-t,r=i-n;return{top:l,bottom:o-(l+e.clientHeight),left:r,right:a-(r+e.clientWidth)}}function getAvailablePosition(e,t,n=5){const s={top:!0,bottom:!0,left:!0,right:!0},i=[];if(!t||!e)return{canShow:s,parentPositions:i};const{bottom:o,top:a}=calculateAvailableSpace(e),{top:l,left:r}=getOffset(e),{height:c,width:d}=t.getBoundingClientRect(),{vh:u,vw:h}=getViewportDimensions(),_=h/2,m=u/2;return[{condition:lm,position:"bottom"},{condition:r<_,position:"left"},{condition:r>_,position:"right"}].forEach(({condition:e,position:t})=>{e&&i.push(t)}),Object.assign(s,{top:c<=a-n,bottom:c<=o-n,left:d<=r,right:d<=h-r}),{canShow:s,parentPositions:i}}const handleDay=(e,t,n,s)=>{var i;const o=s.querySelector(`[data-vc-date="${t}"]`),a=null==o?void 0:o.querySelector("[data-vc-date-btn]");if(!o||!a)return;if((null==n?void 0:n.modifier)&&a.classList.add(...n.modifier.trim().split(" ")),!(null==n?void 0:n.html))return;const l=document.createElement("div");l.className=e.styles.datePopup,l.dataset.vcDatePopup="",l.innerHTML=e.sanitizerHTML(n.html),a.ariaExpanded="true",a.ariaLabel=`${a.ariaLabel}, ${null==(i=null==l?void 0:l.textContent)?void 0:i.replace(/^\s+|\s+(?=\s)|\s+$/g,"").replace(/ /g," ")}`,o.appendChild(l),requestAnimationFrame(()=>{if(!l)return;const{canShow:e}=getAvailablePosition(o,l),t=e.bottom?o.offsetHeight:-l.offsetHeight,n=e.left&&!e.right?o.offsetWidth-l.offsetWidth/2:!e.left&&e.right?l.offsetWidth/2:0;Object.assign(l.style,{left:`${n}px`,top:`${t}px`})})},createDatePopup=(e,t)=>{var n;e.popups&&(null==(n=Object.entries(e.popups))||n.forEach(([n,s])=>handleDay(e,n,s,t)))},getDate=e=>new Date(`${e}T00:00:00`),getDateString=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`,parseDates=e=>e.reduce((e,t)=>{if(t instanceof Date||"number"==typeof t){const n=t instanceof Date?t:new Date(t);e.push(n.toISOString().substring(0,10))}else t.match(/^(\d{4}-\d{2}-\d{2})$/g)?e.push(t):t.replace(/(\d{4}-\d{2}-\d{2}).*?(\d{4}-\d{2}-\d{2})/g,(t,n,s)=>{const i=getDate(n),o=getDate(s),a=new Date(i.getTime());for(;a<=o;a.setDate(a.getDate()+1))e.push(getDateString(a));return t});return e},[]),updateAttribute=(e,t,n,s="")=>{t?e.setAttribute(n,s):e.getAttribute(n)===s&&e.removeAttribute(n)},setDateModifier=(e,t,n,s,i,o,a)=>{var l,r,c,d;const u=getDate(e.context.displayDateMin)>getDate(o)||getDate(e.context.displayDateMax)1&&"multiple-ranged"===e.selectionDatesMode&&(e.context.selectedDates[0]===o&&e.context.selectedDates[e.context.selectedDates.length-1]===o?n.setAttribute("data-vc-date-selected","first-and-last"):e.context.selectedDates[0]===o?n.setAttribute("data-vc-date-selected","first"):e.context.selectedDates[e.context.selectedDates.length-1]===o&&n.setAttribute("data-vc-date-selected","last"),e.context.selectedDates[0]!==o&&e.context.selectedDates[e.context.selectedDates.length-1]!==o&&n.setAttribute("data-vc-date-selected","middle"))):n.hasAttribute("data-vc-date-selected")&&(n.removeAttribute("data-vc-date-selected"),s&&s.removeAttribute("aria-selected")),!e.context.disableDates.includes(o)&&e.enableEdgeDatesOnly&&e.context.selectedDates.length>1&&"multiple-ranged"===e.selectionDatesMode){const t=getDate(e.context.selectedDates[0]),s=getDate(e.context.selectedDates[e.context.selectedDates.length-1]),i=getDate(o);updateAttribute(n,i>t&&inew Date(`${e}T00:00:00.000Z`).toLocaleString(t,n),getWeekNumber=(e,t)=>{const n=getDate(e),s=(n.getDay()-t+7)%7;n.setDate(n.getDate()+4-s);const i=new Date(n.getFullYear(),0,1),o=Math.ceil(((+n-+i)/864e5+1)/7);return{year:n.getFullYear(),week:o}},addWeekNumberForDate=(e,t,n)=>{const s=getWeekNumber(n,e.firstWeekday);s&&(t.dataset.vcDateWeekNumber=String(s.week))},setDaysAsDisabled=(e,t,n)=>{var s,i,o,a,l;const r=null==(s=e.disableWeekdays)?void 0:s.includes(n),c=e.disableAllDates&&!!(null==(i=e.context.enableDates)?void 0:i[0]);!r&&!c||(null==(o=e.context.enableDates)?void 0:o.includes(t))||(null==(a=e.context.disableDates)?void 0:a.includes(t))||(e.context.disableDates.push(t),null==(l=e.context.disableDates)||l.sort((e,t)=>+new Date(e)-+new Date(t)))},createDate=(e,t,n,s,i,o)=>{const a=getDate(i).getDay(),l="string"==typeof e.locale&&e.locale.length?e.locale:"en",r=document.createElement("div");let c;r.className=e.styles.date,r.dataset.vcDate=i,r.dataset.vcDateMonth=o,r.dataset.vcDateWeekDay=String(a),r.role="gridcell",("current"===o||e.displayDatesOutside)&&(c=document.createElement("button"),c.className=e.styles.dateBtn,c.type="button",c.ariaLabel=getLocaleString(i,l,{dateStyle:"long",timeZone:"UTC"}),c.dataset.vcDateBtn="",c.innerText=String(s),r.appendChild(c)),e.enableWeekNumbers&&addWeekNumberForDate(e,r,i),setDaysAsDisabled(e,i,a),setDateModifier(e,t,r,c,a,i,o),n.addDate(r),e.onCreateDateEls&&e.onCreateDateEls(e,r)},createDatesFromCurrentMonth=(e,t,n,s,i)=>{for(let o=1;o<=n;o++){const n=new Date(s,i,o);createDate(e,s,t,o,getDateString(n),"current")}},createDatesFromNextMonth=(e,t,n,s,i)=>{const o=i+1===12?s+1:s,a=i+1===12?"01":i+2<10?`0${i+2}`:i+2;for(let i=1;i<=n;i++){const n=i<10?`0${i}`:String(i);createDate(e,s,t,i,`${o}-${a}-${n}`,"next")}},createDatesFromPrevMonth=(e,t,n,s,i)=>{let o=new Date(n,s,0).getDate()-(i-1);const a=0===s?n-1:n,l=0===s?12:s<10?`0${s}`:s;for(let s=i;s>0;s--,o++)createDate(e,n,t,o,`${a}-${l}-${o}`,"prev")},createWeekNumbers=(e,t,n,s,i)=>{if(!e.enableWeekNumbers)return;s.textContent="";const o=document.createElement("b");o.className=e.styles.weekNumbersTitle,o.innerText="#",o.dataset.vcWeekNumbers="title",s.appendChild(o);const a=document.createElement("div");a.className=e.styles.weekNumbersContent,a.dataset.vcWeekNumbers="content",s.appendChild(a);const l=document.createElement("button");l.type="button",l.className=e.styles.weekNumber;const r=i.querySelectorAll("[data-vc-date]"),c=Math.ceil((t+n)/7);for(let t=0;t{const t=new Date(e.context.selectedYear,e.context.selectedMonth,1),n=e.context.mainElement.querySelectorAll('[data-vc="dates"]'),s=e.context.mainElement.querySelectorAll('[data-vc-week="numbers"]');n.forEach((n,i)=>{e.selectionDatesMode||(n.dataset.vcDatesDisabled=""),n.textContent="";const o=new Date(t);o.setMonth(o.getMonth()+i);const a=o.getMonth(),l=o.getFullYear(),r=(new Date(l,a,1).getDay()-e.firstWeekday+7)%7,c=new Date(l,a+1,0).getDate(),d=r+c,u=Math.ceil(d/7),h=7*u-d,_=[];for(let t=0;t{_[m].appendChild(e),g++,g>=7&&(m++,g=0)}};createDatesFromPrevMonth(e,p,l,a,r),createDatesFromCurrentMonth(e,p,c,l,a),createDatesFromNextMonth(e,p,h,l,a);for(const e of _)n.appendChild(e);createDatePopup(e,n),createWeekNumbers(e,r,c,s[i],n)})},layoutDefault=e=>`\n \n <#ArrowPrev [month] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [month] />\n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n <#DateRangeTooltip />\n \n \n <#ControlTime />\n`,layoutMonths=e=>`\n \n \n <#Month />\n <#Year />\n \n \n \n \n <#Months />\n \n \n`,layoutMultiple=e=>`\n \n <#ArrowPrev [month] />\n <#ArrowNext [month] />\n \n \n <#Multiple>\n \n \n \n <#Month />\n <#Year />\n \n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n \n \n \n <#/Multiple>\n <#DateRangeTooltip />\n \n <#ControlTime />\n`,layoutYears=e=>`\n \n <#ArrowPrev [year] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [year] />\n \n \n \n <#Years />\n \n \n`,ArrowNext=(e,t)=>``,ArrowPrev=(e,t)=>``,ControlTime=e=>e.selectionTimeMode?``:"",DateRangeTooltip=e=>e.onCreateDateRangeTooltip?``:"",Dates=e=>``,Month=e=>``,Months=e=>``,Week=e=>``,WeekNumbers=e=>e.enableWeekNumbers?``:"",Year=e=>``,Years=e=>``,components={ArrowNext:ArrowNext,ArrowPrev:ArrowPrev,ControlTime:ControlTime,Dates:Dates,DateRangeTooltip:DateRangeTooltip,Month:Month,Months:Months,Week:Week,WeekNumbers:WeekNumbers,Year:Year,Years:Years},getComponent=e=>components[e],parseLayout=(e,t)=>t.replace(/[\n\t]/g,"").replace(/<#(?!\/?Multiple)(.*?)>/g,(t,n)=>{const s=(n.match(/\[(.*?)\]/)||[])[1],i=n.replace(/[/\s\n\t]|\[(.*?)\]/g,""),o=getComponent(i),a=o?o(e,null!=s?s:null):"";return e.sanitizerHTML(a)}).replace(/[\n\t]/g,""),parseMultipleLayout=(e,t)=>t.replace(new RegExp("<#Multiple>(.*?)<#\\/Multiple>","gs"),(t,n)=>{const s=Array(e.context.displayMonthsCount).fill(n).join("");return e.sanitizerHTML(s)}).replace(/[\n\t]/g,""),createLayouts=(e,t)=>{const n={default:layoutDefault,month:layoutMonths,year:layoutYears,multiple:layoutMultiple};if(Object.keys(n).forEach(t=>{const s=t;e.layouts[s].length||(e.layouts[s]=n[s](e))}),e.context.mainElement.className=e.styles.calendar,e.context.mainElement.dataset.vc="calendar",e.context.mainElement.dataset.vcType=e.context.currentType,e.context.mainElement.role="application",e.context.mainElement.tabIndex=0,e.context.mainElement.ariaLabel=e.labels.application,"multiple"!==e.context.currentType){if("multiple"===e.type&&t){const n=e.context.mainElement.querySelector('[data-vc="controls"]'),s=e.context.mainElement.querySelector('[data-vc="grid"]'),i=t.closest('[data-vc="column"]');return n&&n.remove(),s&&(s.dataset.vcGrid="hidden"),i&&(i.dataset.vcColumn=e.context.currentType),void(i&&(i.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))))}e.context.mainElement.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))}else e.context.mainElement.innerHTML=e.sanitizerHTML(parseMultipleLayout(e,parseLayout(e,e.layouts[e.context.currentType])))},setVisibilityArrows=(e,t,n,s)=>{e.style.visibility=n?"hidden":"",t.style.visibility=s?"hidden":""},handleDefaultType=(e,t,n)=>{const s=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1))),i=new Date(s.getTime()),o=new Date(s.getTime());i.setMonth(i.getMonth()-e.monthsToSwitch),o.setMonth(o.getMonth()+e.monthsToSwitch);const a=getDate(e.context.dateMin),l=getDate(e.context.dateMax);e.selectionYearsMode||(a.setFullYear(s.getFullYear()),l.setFullYear(s.getFullYear()));const r=!e.selectionMonthsMode||i.getFullYear()l.getFullYear()||o.getFullYear()===l.getFullYear()&&o.getMonth()>l.getMonth()-(e.context.displayMonthsCount-1);setVisibilityArrows(t,n,r,c)},handleYearType=(e,t,n)=>{const s=getDate(e.context.dateMin),i=getDate(e.context.dateMax),o=!!(s.getFullYear()&&e.context.displayYear-7<=s.getFullYear()),a=!!(i.getFullYear()&&e.context.displayYear+7>=i.getFullYear());setVisibilityArrows(t,n,o,a)},visibilityArrows=e=>{if("month"===e.context.currentType)return;const t=e.context.mainElement.querySelector('[data-vc-arrow="prev"]'),n=e.context.mainElement.querySelector('[data-vc-arrow="next"]');t&&n&&{default:()=>handleDefaultType(e,t,n),year:()=>handleYearType(e,t,n)}["multiple"===e.context.currentType?"default":e.context.currentType]()},visibilityHandler=(e,t,n,s,i)=>{const o=new Date(s.setFullYear(e.context.selectedYear,e.context.selectedMonth+n)).getFullYear(),a=new Date(s.setMonth(e.context.selectedMonth+n)).getMonth(),l=e.context.locale.months.long[a],r=t.closest('[data-vc="column"]');r&&(r.ariaLabel=`${l} ${o}`);const c={month:{id:a,label:l},year:{id:o,label:o}};t.innerText=String(c[i].label),t.dataset[`vc${i.charAt(0).toUpperCase()+i.slice(1)}`]=String(c[i].id),t.ariaLabel=`${e.labels[i]} ${c[i].label}`;const d={month:e.selectionMonthsMode,year:e.selectionYearsMode},u=!1===d[i]||"only-arrows"===d[i];u&&(t.tabIndex=-1),t.disabled=u},visibilityTitle=e=>{const t=e.context.mainElement.querySelectorAll('[data-vc="month"]'),n=e.context.mainElement.querySelectorAll('[data-vc="year"]'),s=new Date(e.context.selectedYear,e.context.selectedMonth,1);[t,n].forEach(t=>null==t?void 0:t.forEach((t,n)=>visibilityHandler(e,t,n,s,t.dataset.vc)))},setYearModifier=(e,t,n,s,i)=>{var o;const a={month:{selected:"data-vc-months-month-selected",aria:"aria-selected",value:"vcMonthsMonth",selectedProperty:"selectedMonth"},year:{selected:"data-vc-years-year-selected",aria:"aria-selected",value:"vcYearsYear",selectedProperty:"selectedYear"}};i&&(null==(o=e.context.mainElement.querySelectorAll({month:"[data-vc-months-month]",year:"[data-vc-years-year]"}[n]))||o.forEach(e=>{e.removeAttribute(a[n].selected),e.removeAttribute(a[n].aria)}),setContext(e,a[n].selectedProperty,Number(t.dataset[a[n].value])),visibilityTitle(e),"year"===n&&visibilityArrows(e)),s&&(t.setAttribute(a[n].selected,""),t.setAttribute(a[n].aria,"true"))},getColumnID=(e,t)=>{var n;if("multiple"!==e.type)return{currentValue:null,columnID:0};const s=e.context.mainElement.querySelectorAll('[data-vc="column"]'),i=Array.from(s).findIndex(e=>e.closest(`[data-vc-column="${t}"]`));return{currentValue:i>=0?Number(null==(n=s[i].querySelector(`[data-vc="${t}"]`))?void 0:n.getAttribute(`data-vc-${t}`)):null,columnID:Math.max(i,0)}},createMonthEl=(e,t,n,s,i,o,a)=>{const l=t.cloneNode(!1);return l.className=e.styles.monthsMonth,l.innerText=s,l.ariaLabel=i,l.role="gridcell",l.dataset.vcMonthsMonth=`${a}`,o&&(l.ariaDisabled="true"),o&&(l.tabIndex=-1),l.disabled=o,setYearModifier(e,l,"month",n===a,!1),l},createMonths=(e,t)=>{var n,s;const i=null==(n=null==t?void 0:t.closest('[data-vc="header"]'))?void 0:n.querySelector('[data-vc="year"]'),o=i?Number(i.dataset.vcYear):e.context.selectedYear,a=(null==t?void 0:t.dataset.vcMonth)?Number(t.dataset.vcMonth):e.context.selectedMonth;setContext(e,"currentType","month"),createLayouts(e,t),visibilityTitle(e);const l=e.context.mainElement.querySelector('[data-vc="months"]');if(!e.selectionMonthsMode||!l)return;const r=e.monthsToSwitch>1?e.context.locale.months.long.map((t,n)=>a-e.monthsToSwitch*n).concat(e.context.locale.months.long.map((t,n)=>a+e.monthsToSwitch*n)).filter(e=>e>=0&&e<=12):Array.from(Array(12).keys()),c=document.createElement("button");c.type="button";for(let t=0;t<12;t++){const n=getDate(e.context.dateMin),s=getDate(e.context.dateMax),i=e.context.displayMonthsCount-1,{columnID:d}=getColumnID(e,"month"),u=o<=n.getFullYear()&&t=s.getFullYear()&&t>s.getMonth()-i+d||o>s.getFullYear()||t!==a&&!r.includes(t),h=createMonthEl(e,c,a,e.context.locale.months.short[t],e.context.locale.months.long[t],u,t);l.appendChild(h),e.onCreateMonthEls&&e.onCreateMonthEls(e,h)}null==(s=e.context.mainElement.querySelector("[data-vc-months-month]:not([disabled])"))||s.focus()},TimeInput=(e,t,n,s,i)=>`\n \n \n \n`,TimeRange=(e,t,n,s,i,o,a)=>`\n \n \n \n`,handleActions=(e,t,n,s)=>{({hour:()=>setContext(e,"selectedHours",n),minute:()=>setContext(e,"selectedMinutes",n)})[s](),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${e.context.selectedKeeping?` ${e.context.selectedKeeping}`:""}`),e.onChangeTime&&e.onChangeTime(e,t,!1),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t)},transformTime24=(e,t)=>{var n;return(null==(n={0:{AM:"00",PM:"12"},1:{AM:"01",PM:"13"},2:{AM:"02",PM:"14"},3:{AM:"03",PM:"15"},4:{AM:"04",PM:"16"},5:{AM:"05",PM:"17"},6:{AM:"06",PM:"18"},7:{AM:"07",PM:"19"},8:{AM:"08",PM:"20"},9:{AM:"09",PM:"21"},10:{AM:"10",PM:"22"},11:{AM:"11",PM:"23"},12:{AM:"00",PM:"12"}}[Number(e)])?void 0:n[t])||String(e)},handleClickKeepingTime=(e,t,n,s,i)=>{const o=o=>{const a="AM"===e.context.selectedKeeping?"PM":"AM",l=transformTime24(e.context.selectedHours,a);Number(l)<=s&&Number(l)>=i?(setContext(e,"selectedKeeping",a),n.value=l,handleActions(e,o,e.context.selectedHours,"hour"),t.ariaLabel=`${e.labels.btnKeeping} ${e.context.selectedKeeping}`,t.innerText=e.context.selectedKeeping):e.onChangeTime&&e.onChangeTime(e,o,!0)};return t.addEventListener("click",o),()=>{t.removeEventListener("click",o)}},transformTime12=e=>({0:"12",13:"01",14:"02",15:"03",16:"04",17:"05",18:"06",19:"07",20:"08",21:"09",22:"10",23:"11"}[Number(e)]||String(e)),updateInputAndRange=(e,t,n,s)=>{e.value=n,t.value=s},updateKeepingTime$1=(e,t,n)=>{t&&n&&(setContext(e,"selectedKeeping",n),t.innerText=n)},handleInput$1=(e,t,n,s,i,o,a)=>{const l={hour:(l,r,c)=>{e.selectionTimeMode&&{12:()=>{if(!e.context.selectedKeeping)return;const d=Number(transformTime24(r,e.context.selectedKeeping));if(!(d<=o&&d>=a))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,!0));updateInputAndRange(n,t,transformTime12(r),transformTime24(r,e.context.selectedKeeping)),l>12&&updateKeepingTime$1(e,s,"PM"),handleActions(e,c,transformTime12(r),i)},24:()=>{if(!(l<=o&&l>=a))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,!0));updateInputAndRange(n,t,r,r),handleActions(e,c,r,i)}}[e.selectionTimeMode]()},minute:(s,l,r)=>{if(!(s<=o&&s>=a))return n.value=e.context.selectedMinutes,void(e.onChangeTime&&e.onChangeTime(e,r,!0));n.value=l,t.value=l,handleActions(e,r,l,i)}},r=e=>{const t=Number(n.value),s=n.value.padStart(2,"0");l[i]&&l[i](t,s,e)};return n.addEventListener("change",r),()=>{n.removeEventListener("change",r)}},updateInputAndTime=(e,t,n,s,i)=>{t.value=i,handleActions(e,n,i,s)},updateKeepingTime=(e,t,n)=>{t&&(setContext(e,"selectedKeeping",n),t.innerText=n)},handleRange=(e,t,n,s,i)=>{const o=o=>{const a=Number(t.value),l=t.value.padStart(2,"0"),r="hour"===i,c=24===e.selectionTimeMode,d=a>0&&a<12;r&&!c&&updateKeepingTime(e,s,0===a||d?"AM":"PM"),updateInputAndTime(e,n,o,i,!r||c||d?l:transformTime12(t.value))};return t.addEventListener("input",o),()=>{t.removeEventListener("input",o)}},handleMouseOver=e=>e.setAttribute("data-vc-input-focus",""),handleMouseOut=e=>e.removeAttribute("data-vc-input-focus"),handleTime=(e,t)=>{const n=t.querySelector('[data-vc-time-range="hour"] input[name="hour"]'),s=t.querySelector('[data-vc-time-range="minute"] input[name="minute"]'),i=t.querySelector('[data-vc-time-input="hour"] input[name="hour"]'),o=t.querySelector('[data-vc-time-input="minute"] input[name="minute"]'),a=t.querySelector('[data-vc-time="keeping"]');if(!(n&&s&&i&&o))return;const l=e=>{e.target===n&&handleMouseOver(i),e.target===s&&handleMouseOver(o)},r=e=>{e.target===n&&handleMouseOut(i),e.target===s&&handleMouseOut(o)};return t.addEventListener("mouseover",l),t.addEventListener("mouseout",r),handleInput$1(e,n,i,a,"hour",e.timeMaxHour,e.timeMinHour),handleInput$1(e,s,o,a,"minute",e.timeMaxMinute,e.timeMinMinute),handleRange(e,n,i,a,"hour"),handleRange(e,s,o,a,"minute"),a&&handleClickKeepingTime(e,a,n,e.timeMaxHour,e.timeMinHour),()=>{t.removeEventListener("mouseover",l),t.removeEventListener("mouseout",r)}},createTime=e=>{const t=e.context.mainElement.querySelector('[data-vc="time"]');if(!e.selectionTimeMode||!t)return;const[n,s]=[e.timeMinHour,e.timeMaxHour],[i,o]=[e.timeMinMinute,e.timeMaxMinute],a=e.context.selectedKeeping?transformTime24(e.context.selectedHours,e.context.selectedKeeping):e.context.selectedHours,l="range"===e.timeControls;var r;t.innerHTML=e.sanitizerHTML(`\n \n ${TimeInput("hour",e.styles.timeHour,e.labels,e.context.selectedHours,l)}\n ${TimeInput("minute",e.styles.timeMinute,e.labels,e.context.selectedMinutes,l)}\n ${12===e.selectionTimeMode?(r=e.context.selectedKeeping,`${r}`):""}\n \n \n ${TimeRange("hour",e.styles.timeRange,e.labels,n,s,e.timeStepHour,a)}\n ${TimeRange("minute",e.styles.timeRange,e.labels,i,o,e.timeStepMinute,e.context.selectedMinutes)}\n \n `),handleTime(e,t)},createWeek=e=>{const t=e.selectedWeekends?[...e.selectedWeekends]:[],n=[...e.context.locale.weekdays.long].reduce((n,s,i)=>[...n,{id:i,titleShort:e.context.locale.weekdays.short[i],titleLong:s,isWeekend:t.includes(i)}],[]),s=[...n.slice(e.firstWeekday),...n.slice(0,e.firstWeekday)];e.context.mainElement.querySelectorAll('[data-vc="week"]').forEach(t=>{const n=e.onClickWeekDay?document.createElement("button"):document.createElement("b");e.onClickWeekDay&&(n.type="button"),s.forEach(s=>{const i=n.cloneNode(!0);i.innerText=s.titleShort,i.className=e.styles.weekDay,i.role="columnheader",i.ariaLabel=s.titleLong,i.dataset.vcWeekDay=String(s.id),s.isWeekend&&(i.dataset.vcWeekDayOff=""),t.appendChild(i)})})},createYearEl=(e,t,n,s,i)=>{const o=t.cloneNode(!1);return o.className=e.styles.yearsYear,o.innerText=String(i),o.ariaLabel=String(i),o.role="gridcell",o.dataset.vcYearsYear=`${i}`,s&&(o.ariaDisabled="true"),s&&(o.tabIndex=-1),o.disabled=s,setYearModifier(e,o,"year",n===i,!1),o},createYears=(e,t)=>{var n;const s=(null==t?void 0:t.dataset.vcYear)?Number(t.dataset.vcYear):e.context.selectedYear;setContext(e,"currentType","year"),createLayouts(e,t),visibilityTitle(e),visibilityArrows(e);const i=e.context.mainElement.querySelector('[data-vc="years"]');if(!e.selectionYearsMode||!i)return;const o="multiple"!==e.type||e.context.selectedYear===s?0:1,a=document.createElement("button");a.type="button";for(let t=e.context.displayYear-7;tgetDate(e.context.dateMax).getFullYear(),l=createYearEl(e,a,s,n,t);i.appendChild(l),e.onCreateYearEls&&e.onCreateYearEls(e,l)}null==(n=e.context.mainElement.querySelector("[data-vc-years-year]:not([disabled])"))||n.focus()},trackChangesHTMLElement=(e,t,n)=>{new MutationObserver(e=>{for(let s=0;shaveListener.value=!0,check:()=>haveListener.value},setTheme=(e,t)=>e.dataset.vcTheme=t,trackChangesThemeInSystemSettings=(e,t)=>{if(setTheme(e.context.mainElement,t.matches?"dark":"light"),"system"!==e.selectedTheme||haveListener.check())return;const n=e=>{const t=document.querySelectorAll('[data-vc="calendar"]');null==t||t.forEach(t=>setTheme(t,e.matches?"dark":"light"))};t.addEventListener?t.addEventListener("change",n):t.addListener(n),haveListener.set()},detectTheme=(e,t)=>{const n=e.themeAttrDetect.length?document.querySelector(e.themeAttrDetect):null,s=e.themeAttrDetect.replace(/^.*\[(.+)\]/g,(e,t)=>t);if(!n||"system"===n.getAttribute(s))return void trackChangesThemeInSystemSettings(e,t);const i=n.getAttribute(s);i?(setTheme(e.context.mainElement,i),trackChangesHTMLElement(n,s,()=>{const t=n.getAttribute(s);t&&setTheme(e.context.mainElement,t)})):trackChangesThemeInSystemSettings(e,t)},handleTheme=e=>{"not all"!==window.matchMedia("(prefers-color-scheme)").media?"system"===e.selectedTheme?detectTheme(e,window.matchMedia("(prefers-color-scheme: dark)")):setTheme(e.context.mainElement,e.selectedTheme):setTheme(e.context.mainElement,"light")},capitalizeFirstLetter=e=>e.charAt(0).toUpperCase()+e.slice(1).replace(/\./,""),getLocaleWeekday=(e,t,n)=>{const s=new Date(`1978-01-0${t+1}T00:00:00.000Z`),i=s.toLocaleString(n,{weekday:"short",timeZone:"UTC"}),o=s.toLocaleString(n,{weekday:"long",timeZone:"UTC"});e.context.locale.weekdays.short.push(capitalizeFirstLetter(i)),e.context.locale.weekdays.long.push(capitalizeFirstLetter(o))},getLocaleMonth=(e,t,n)=>{const s=new Date(`1978-${String(t+1).padStart(2,"0")}-01T00:00:00.000Z`),i=s.toLocaleString(n,{month:"short",timeZone:"UTC"}),o=s.toLocaleString(n,{month:"long",timeZone:"UTC"});e.context.locale.months.short.push(capitalizeFirstLetter(i)),e.context.locale.months.long.push(capitalizeFirstLetter(o))},getLocale=e=>{var t,n,s,i,o,a,l,r;if(!(e.context.locale.weekdays.short[6]&&e.context.locale.weekdays.long[6]&&e.context.locale.months.short[11]&&e.context.locale.months.long[11]))if("string"==typeof e.locale){if("string"==typeof e.locale&&!e.locale.length)throw new Error(errorMessages.notLocale);Array.from({length:7},(t,n)=>getLocaleWeekday(e,n,e.locale)),Array.from({length:12},(t,n)=>getLocaleMonth(e,n,e.locale))}else{if(!((null==(n=null==(t=e.locale)?void 0:t.weekdays)?void 0:n.short[6])&&(null==(i=null==(s=e.locale)?void 0:s.weekdays)?void 0:i.long[6])&&(null==(a=null==(o=e.locale)?void 0:o.months)?void 0:a.short[11])&&(null==(r=null==(l=e.locale)?void 0:l.months)?void 0:r.long[11])))throw new Error(errorMessages.notLocale);setContext(e,"locale",__spreadValues({},e.locale))}},create=e=>{const t={default:()=>{createWeek(e),createDates(e)},multiple:()=>{createWeek(e),createDates(e)},month:()=>createMonths(e),year:()=>createYears(e)};handleTheme(e),getLocale(e),createLayouts(e),visibilityTitle(e),visibilityArrows(e),createTime(e),t[e.context.currentType]()},handleArrowKeys=e=>{const t=t=>{var n;const s=t.target;if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key)||"button"!==s.localName)return;const i=Array.from(e.context.mainElement.querySelectorAll('[data-vc="calendar"] button')),o=i.indexOf(s);if(-1===o)return;const a=(l=i[o]).hasAttribute("data-vc-date-btn")?7:l.hasAttribute("data-vc-months-month")?4:l.hasAttribute("data-vc-years-year")?5:1;var l;const r=(0,{ArrowUp:()=>Math.max(0,o-a),ArrowDown:()=>Math.min(i.length-1,o+a),ArrowLeft:()=>Math.max(0,o-1),ArrowRight:()=>Math.min(i.length-1,o+1)}[t.key])();null==(n=i[r])||n.focus()};return e.context.mainElement.addEventListener("keydown",t),()=>e.context.mainElement.removeEventListener("keydown",t)},handleMonth=(e,t)=>{const n=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1)));({prev:()=>n.setMonth(n.getMonth()-e.monthsToSwitch),next:()=>n.setMonth(n.getMonth()+e.monthsToSwitch)})[t](),setContext(e,"selectedMonth",n.getMonth()),setContext(e,"selectedYear",n.getFullYear()),visibilityTitle(e),visibilityArrows(e),createDates(e)},handleClickArrow=(e,t)=>{const n=t.target.closest("[data-vc-arrow]");if(n){if(["default","multiple"].includes(e.context.currentType))handleMonth(e,n.dataset.vcArrow);else if("year"===e.context.currentType&&void 0!==e.context.displayYear){const s={prev:-15,next:15}[n.dataset.vcArrow];setContext(e,"displayYear",e.context.displayYear+s),createYears(e,t.target)}e.onClickArrow&&e.onClickArrow(e,t)}},resolveToggle=(e,t)=>void 0===t||("function"==typeof t?t(e):t),canToggleSelection=e=>resolveToggle(e,e.enableDateToggle),handleSelectDate=(e,t,n)=>{const s=t.dataset.vcDate,i=t.closest("[data-vc-date][data-vc-date-selected]"),o=canToggleSelection(e);if(i&&!o)return;const a=i?e.context.selectedDates.filter(e=>e!==s):n?[...e.context.selectedDates,s]:[s];setContext(e,"selectedDates",a)},createDateRangeTooltip=(e,t,n)=>{if(!t)return;if(!n)return t.dataset.vcDateRangeTooltip="hidden",void(t.textContent="");const s=e.context.mainElement.getBoundingClientRect(),i=n.getBoundingClientRect();t.style.left=i.left-s.left+i.width/2+"px",t.style.top=i.bottom-s.top-i.height+"px",t.dataset.vcDateRangeTooltip="visible",t.innerHTML=e.sanitizerHTML(e.onCreateDateRangeTooltip(e,n,t,i,s))},state={self:null,lastDateEl:null,isHovering:!1,rangeMin:void 0,rangeMax:void 0,tooltipEl:null,timeoutId:null},addHoverEffect=(e,t,n)=>{var s,i,o;if(!(null==(i=null==(s=state.self)?void 0:s.context)?void 0:i.selectedDates[0]))return;const a=getDateString(e);(null==(o=state.self.context.disableDates)?void 0:o.includes(a))||(state.self.context.mainElement.querySelectorAll(`[data-vc-date="${a}"]`).forEach(e=>e.dataset.vcDateHover=""),t.forEach(e=>e.dataset.vcDateHover="first"),n.forEach(e=>{"first"===e.dataset.vcDateHover?e.dataset.vcDateHover="first-and-last":e.dataset.vcDateHover="last"}))},removeHoverEffect=()=>{var e,t;(null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.mainElement)&&state.self.context.mainElement.querySelectorAll("[data-vc-date-hover]").forEach(e=>e.removeAttribute("data-vc-date-hover"))},handleHoverDatesEvent=e=>{var t,n;if(!e||!(null==(n=null==(t=state.self)?void 0:t.context)?void 0:n.selectedDates[0]))return;if(!e.closest('[data-vc="dates"]'))return state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),void removeHoverEffect();const s=e.closest("[data-vc-date]");if(!s||state.lastDateEl===s)return;state.lastDateEl=s,createDateRangeTooltip(state.self,state.tooltipEl,s),removeHoverEffect();const i=s.dataset.vcDate,o=getDate(state.self.context.selectedDates[0]),a=getDate(i),l=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${state.self.context.selectedDates[0]}"]`),r=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${i}"]`),[c,d]=o{const t=null==e?void 0:e.closest("[data-vc-date-selected]");if(!t&&state.lastDateEl)return state.lastDateEl=null,void createDateRangeTooltip(state.self,state.tooltipEl,null);t&&state.lastDateEl!==t&&(state.lastDateEl=t,createDateRangeTooltip(state.self,state.tooltipEl,t))},optimizedHoverHandler=e=>t=>{const n=t.target;state.isHovering||(state.isHovering=!0,requestAnimationFrame(()=>{e(n),state.isHovering=!1}))},optimizedHandleHoverDatesEvent=optimizedHoverHandler(handleHoverDatesEvent),optimizedHandleHoverSelectedDatesRangeEvent=optimizedHoverHandler(handleHoverSelectedDatesRangeEvent),handleCancelSelectionDates=e=>{state.self&&"Escape"===e.key&&(state.lastDateEl=null,setContext(state.self,"selectedDates",[]),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect())},handleMouseLeave=()=>{null!==state.timeoutId&&clearTimeout(state.timeoutId),state.timeoutId=setTimeout(()=>{state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect()},50)},updateDisabledDates=()=>{var e,t,n,s;if(!(null==(n=null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.selectedDates)?void 0:n[0])||!(null==(s=state.self.context.disableDates)?void 0:s[0]))return;const i=getDate(state.self.context.selectedDates[0]),[o,a]=state.self.context.disableDates.map(e=>getDate(e)).reduce(([e,t],n)=>[i>=n?n:e,i{state.self=e,state.lastDateEl=t,removeHoverEffect(),e.disableDatesGaps&&(state.rangeMin=state.rangeMin?state.rangeMin:e.context.displayDateMin,state.rangeMax=state.rangeMax?state.rangeMax:e.context.displayDateMax),e.onCreateDateRangeTooltip&&(state.tooltipEl=e.context.mainElement.querySelector("[data-vc-date-range-tooltip]"));const n=null==t?void 0:t.dataset.vcDate;if(n){const t=1===e.context.selectedDates.length&&e.context.selectedDates[0].includes(n),s=t&&!canToggleSelection(e)?[n,n]:t&&canToggleSelection(e)?[]:e.context.selectedDates.length>1?[n]:[...e.context.selectedDates,n];setContext(e,"selectedDates",s),e.context.selectedDates.length>1&&e.context.selectedDates.sort((e,t)=>+new Date(e)-+new Date(t))}({set:()=>(e.disableDatesGaps&&updateDisabledDates(),createDateRangeTooltip(state.self,state.tooltipEl,t),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.addEventListener("keydown",handleCancelSelectionDates),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates)}),reset:()=>{const[n,s]=[e.context.selectedDates[0],e.context.selectedDates[e.context.selectedDates.length-1]],i=e.context.selectedDates[0]!==e.context.selectedDates[e.context.selectedDates.length-1],o=parseDates([`${n}:${s}`]).filter(t=>!e.context.disableDates.includes(t)),a=i?e.enableEdgeDatesOnly?[n,s]:o:[e.context.selectedDates[0],e.context.selectedDates[0]];if(setContext(e,"selectedDates",a),e.disableDatesGaps&&(setContext(e,"displayDateMin",state.rangeMin),setContext(e,"displayDateMax",state.rangeMax)),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),e.onCreateDateRangeTooltip)return e.context.selectedDates[0]||(state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,null)),e.context.selectedDates[0]&&(state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,t)),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave)}}})[1===e.context.selectedDates.length?"set":"reset"]()},updateDateModifier=e=>{e.context.mainElement.querySelectorAll("[data-vc-date]").forEach(t=>{const n=t.querySelector("[data-vc-date-btn]"),s=t.dataset.vcDate,i=getDate(s).getDay();setDateModifier(e,e.context.selectedYear,t,n,i,s,"current")})},handleClickDate=(e,t)=>{var n;const s=t.target,i=s.closest("[data-vc-date-btn]");if(!e.selectionDatesMode||!["single","multiple","multiple-ranged"].includes(e.selectionDatesMode)||!i)return;const o=i.closest("[data-vc-date]");({single:()=>handleSelectDate(e,o,!1),multiple:()=>handleSelectDate(e,o,!0),"multiple-ranged":()=>handleSelectDateRange(e,o)})[e.selectionDatesMode](),null==(n=e.context.selectedDates)||n.sort((e,t)=>+new Date(e)-+new Date(t)),e.onClickDate&&e.onClickDate(e,t),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);const a=s.closest('[data-vc-date-month="prev"]'),l=s.closest('[data-vc-date-month="next"]');({prev:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"prev"):updateDateModifier(e),next:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"next"):updateDateModifier(e),current:()=>updateDateModifier(e)})[a?"prev":l?"next":"current"]()},typeClick=["month","year"],getValue=(e,t,n)=>{const{currentValue:s,columnID:i}=getColumnID(e,t);return"month"===e.context.currentType&&i>=0?n-i:"year"===e.context.currentType&&e.context.selectedYear!==s?n-1:n},handleMultipleYearSelection=(e,t)=>{const n=getValue(e,"year",Number(t.dataset.vcYearsYear)),s=getDate(e.context.dateMin),i=getDate(e.context.dateMax),o=e.context.displayMonthsCount-1,{columnID:a}=getColumnID(e,"year"),l=e.context.selectedMonthi.getMonth()-o+a&&n>=i.getFullYear(),c=ni.getFullYear(),u=l||c?s.getFullYear():r||d?i.getFullYear():n,h=l||c?s.getMonth():r||d?i.getMonth()-o+a:e.context.selectedMonth;setContext(e,"selectedYear",u),setContext(e,"selectedMonth",h)},handleMultipleMonthSelection=(e,t)=>{const n=t.closest('[data-vc-column="month"]').querySelector('[data-vc="year"]'),s=getValue(e,"month",Number(t.dataset.vcMonthsMonth)),i=Number(n.dataset.vcYear),o=getDate(e.context.dateMin),a=getDate(e.context.dateMax),l=sa.getMonth()&&i>=a.getFullYear();setContext(e,"selectedYear",i),setContext(e,"selectedMonth",l?o.getMonth():r?a.getMonth():s)},handleItemClick=(e,t,n,s)=>{var i;({year:()=>{if("multiple"===e.type)return handleMultipleYearSelection(e,s);setContext(e,"selectedYear",Number(s.dataset.vcYearsYear))},month:()=>{if("multiple"===e.type)return handleMultipleMonthSelection(e,s);setContext(e,"selectedMonth",Number(s.dataset.vcMonthsMonth))}})[n](),{year:()=>{var n;return null==(n=e.onClickYear)?void 0:n.call(e,e,t)},month:()=>{var n;return null==(n=e.onClickMonth)?void 0:n.call(e,e,t)}}[n](),e.context.currentType!==e.type?(setContext(e,"currentType",e.type),create(e),null==(i=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||i.focus()):setYearModifier(e,s,n,!0,!0)},handleClickType=(e,t,n)=>{var s;const i=t.target,o=i.closest(`[data-vc="${n}"]`),a={year:()=>createYears(e,i),month:()=>createMonths(e,i)};if(o&&e.onClickTitle&&e.onClickTitle(e,t),o&&e.context.currentType!==n)return a[n]();const l=i.closest(`[data-vc-${n}s-${n}]`);if(l)return handleItemClick(e,t,n,l);const r=i.closest('[data-vc="grid"]'),c=i.closest('[data-vc="column"]');(e.context.currentType===n&&o||"multiple"===e.type&&e.context.currentType===n&&r&&!c)&&(setContext(e,"currentType",e.type),create(e),null==(s=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||s.focus())},handleClickMonthOrYear=(e,t)=>{const n={month:e.selectionMonthsMode,year:e.selectionYearsMode};typeClick.forEach(s=>{n[s]&&t.target&&handleClickType(e,t,s)})},handleClickWeekNumber=(e,t)=>{if(!e.enableWeekNumbers||!e.onClickWeekNumber)return;const n=t.target.closest("[data-vc-week-number]"),s=e.context.mainElement.querySelectorAll("[data-vc-date-week-number]");if(!n||!s[0])return;const i=Number(n.innerText),o=Number(n.dataset.vcWeekYear),a=Array.from(s).filter(e=>Number(e.dataset.vcDateWeekNumber)===i);e.onClickWeekNumber(e,i,o,a,t)},handleClickWeekDay=(e,t)=>{if(!e.onClickWeekDay)return;const n=t.target.closest("[data-vc-week-day]"),s=t.target.closest('[data-vc="column"]'),i=s?s.querySelectorAll("[data-vc-date-week-day]"):e.context.mainElement.querySelectorAll("[data-vc-date-week-day]");if(!n||!i[0])return;const o=Number(n.dataset.vcWeekDay),a=Array.from(i).filter(e=>Number(e.dataset.vcDateWeekDay)===o);e.onClickWeekDay(e,o,a,t)},handleClick=e=>{const t=t=>{handleClickArrow(e,t),handleClickWeekDay(e,t),handleClickWeekNumber(e,t),handleClickDate(e,t),handleClickMonthOrYear(e,t)};return e.context.mainElement.addEventListener("click",t),()=>e.context.mainElement.removeEventListener("click",t)},initMonthsCount=e=>{if("multiple"===e.type&&(e.displayMonthsCount<=1||e.displayMonthsCount>12))throw new Error(errorMessages.incorrectMonthsCount);if("multiple"!==e.type&&e.displayMonthsCount>1)throw new Error(errorMessages.incorrectMonthsCount);setContext(e,"displayMonthsCount",e.displayMonthsCount?e.displayMonthsCount:"multiple"===e.type?2:1)},getLocalDate=()=>{const e=new Date;return new Date(e.getTime()-6e4*e.getTimezoneOffset()).toISOString().substring(0,10)},resolveDate=(e,t)=>"today"===e?getLocalDate():e instanceof Date||"number"==typeof e||"string"==typeof e?parseDates([e])[0]:t,initRange=e=>{var t,n,s;const i=resolveDate(e.dateMin,e.dateMin),o=resolveDate(e.dateMax,e.dateMax),a=resolveDate(e.displayDateMin,i),l=resolveDate(e.displayDateMax,o);setContext(e,"dateToday",resolveDate(e.dateToday,e.dateToday)),setContext(e,"displayDateMin",a?getDate(i)>=getDate(a)?i:a:i),setContext(e,"displayDateMax",l?getDate(o)<=getDate(l)?o:l:o);const r=e.disableDatesPast&&!e.disableAllDates&&getDate(a)1&&e.context.disableDates.sort((e,t)=>+new Date(e)-+new Date(t)),setContext(e,"enableDates",e.enableDates[0]?parseDates(e.enableDates):[]),(null==(t=e.context.enableDates)?void 0:t[0])&&(null==(n=e.context.disableDates)?void 0:n[0])&&setContext(e,"disableDates",e.context.disableDates.filter(t=>!e.context.enableDates.includes(t))),e.context.enableDates.length>1&&e.context.enableDates.sort((e,t)=>+new Date(e)-+new Date(t)),(null==(s=e.context.enableDates)?void 0:s[0])&&e.disableAllDates&&(setContext(e,"displayDateMin",e.context.enableDates[0]),setContext(e,"displayDateMax",e.context.enableDates[e.context.enableDates.length-1])),setContext(e,"dateMin",e.displayDisabledDates?i:e.context.displayDateMin),setContext(e,"dateMax",e.displayDisabledDates?o:e.context.displayDateMax)},initSelectedDates=e=>{var t;setContext(e,"selectedDates",(null==(t=e.selectedDates)?void 0:t[0])?parseDates(e.selectedDates):[])},displayClosestValidDate=e=>{const t=t=>{const n=new Date(t);setInitialContext(e,n.getMonth(),n.getFullYear())};if(e.displayDateMin&&"today"!==e.displayDateMin&&(n=e.displayDateMin,s=new Date,new Date(n).getTime()>s.getTime())){const n=e.selectedDates.length&&e.selectedDates[0]?parseDates(e.selectedDates)[0]:e.displayDateMin;return t(getDate(resolveDate(n,e.displayDateMin))),!0}var n,s;if(e.displayDateMax&&"today"!==e.displayDateMax&&((e,t)=>new Date(e).getTime(){setContext(e,"selectedMonth",t),setContext(e,"selectedYear",n),setContext(e,"displayYear",n)},initSelectedMonthYear=e=>{var t;if(e.enableJumpToSelectedDate&&(null==(t=e.selectedDates)?void 0:t[0])&&void 0===e.selectedMonth&&void 0===e.selectedYear){const t=getDate(parseDates(e.selectedDates)[0]);return void setInitialContext(e,t.getMonth(),t.getFullYear())}if(displayClosestValidDate(e))return;const n=void 0!==e.selectedMonth&&Number(e.selectedMonth)>=0&&Number(e.selectedMonth)<12,s=void 0!==e.selectedYear&&Number(e.selectedYear)>=0&&Number(e.selectedYear)<=9999;setInitialContext(e,n?Number(e.selectedMonth):getDate(e.context.dateToday).getMonth(),s?Number(e.selectedYear):getDate(e.context.dateToday).getFullYear())},initTime=e=>{var t,n,s;if(!e.selectionTimeMode)return;if(![12,24].includes(e.selectionTimeMode))throw new Error(errorMessages.incorrectTime);const i=12===e.selectionTimeMode,o=i?/^(0[1-9]|1[0-2]):([0-5][0-9]) ?(AM|PM)?$/i:/^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;let[a,l,r]=null!=(s=null==(n=null==(t=e.selectedTime)?void 0:t.match(o))?void 0:n.slice(1))?s:[];a?i&&!r&&(r="AM"):(a=i?transformTime12(String(e.timeMinHour)):String(e.timeMinHour),l=String(e.timeMinMinute),r=i?Number(transformTime12(String(e.timeMinHour)))>=12?"PM":"AM":null),setContext(e,"selectedHours",a.padStart(2,"0")),setContext(e,"selectedMinutes",l.padStart(2,"0")),setContext(e,"selectedKeeping",r),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${r?` ${r}`:""}`)},initAllVariables=e=>{setContext(e,"currentType",e.type),initMonthsCount(e),initRange(e),initSelectedMonthYear(e),initSelectedDates(e),initTime(e)},reset=(e,{year:t,month:n,dates:s,time:i,locale:o},a=!0)=>{var l;const r={year:e.selectedYear,month:e.selectedMonth,dates:e.selectedDates,time:e.selectedTime};e.selectedYear=t?r.year:e.context.selectedYear,e.selectedMonth=n?r.month:e.context.selectedMonth,e.selectedTime=i?r.time:e.context.selectedTime,e.selectedDates="only-first"===s&&(null==(l=e.context.selectedDates)?void 0:l[0])?[e.context.selectedDates[0]]:!0===s?r.dates:e.context.selectedDates,o&&setContext(e,"locale",{months:{short:[],long:[]},weekdays:{short:[],long:[]}}),initAllVariables(e),a&&create(e),e.selectedYear=r.year,e.selectedMonth=r.month,e.selectedDates=r.dates,e.selectedTime=r.time,"multiple-ranged"===e.selectionDatesMode&&s&&handleSelectDateRange(e,null)},createToInput=e=>{const t=document.createElement("div");return t.className=e.styles.calendar,t.dataset.vc="calendar",t.dataset.vcInput="",t.dataset.vcCalendarHidden="",setContext(e,"inputModeInit",!0),setContext(e,"isShowInInputMode",!1),setContext(e,"mainElement",t),document.body.appendChild(e.context.mainElement),reset(e,{year:!0,month:!0,dates:!0,time:!0,locale:!0}),setTimeout(()=>show(e)),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e)},canOpenOnFocus=e=>resolveToggle(e,e.openOnFocus),handleInput=e=>{setContext(e,"inputElement",e.context.mainElement);const t=()=>{e.context.inputModeInit?setTimeout(()=>show(e)):createToInput(e)};e.context.inputElement.addEventListener("click",t);const n="function"==typeof e.openOnFocus||!0===e.openOnFocus,s=()=>{shouldSkipOpenOnFocus(e)?clearSkipOpenOnFocus(e):canOpenOnFocus(e)&&t()};n&&e.context.inputElement.addEventListener("focus",s);const i=t=>{const n="Tab"===t.key&&!t.shiftKey,s=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key);(n||s)&&(t=>{var n;if(!e.context.isShowInInputMode)return!1;if(document.activeElement!==e.context.inputElement)return!1;const s=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),i=null!=(n=document.createTreeWalker(e.context.mainElement,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>s(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}).nextNode())?n:s(e.context.mainElement)?e.context.mainElement:null;!i||i.tabIndex<0||(t.preventDefault(),i.focus())})(t)};return e.context.inputElement.addEventListener("keydown",i),()=>{e.context.inputElement.removeEventListener("click",t),n&&e.context.inputElement.removeEventListener("focus",s),e.context.inputElement.removeEventListener("keydown",i)}},init=e=>(setContext(e,"originalElement",e.context.mainElement.cloneNode(!0)),setContext(e,"isInit",!0),e.inputMode?handleInput(e):(initAllVariables(e),create(e),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e))),update=(e,t)=>{if(!e.context.isInit)throw new Error(errorMessages.notInit);reset(e,__spreadValues(__spreadValues({},{year:!0,month:!0,dates:!0,time:!0,locale:!0}),t),!(e.inputMode&&!e.context.inputModeInit)),e.onUpdate&&e.onUpdate(e)},replaceProperties=(e,t)=>{const n=Object.keys(t);for(let s=0;s{replaceProperties(e,t),e.context.isInit&&update(e,n)};function findBestPickerPosition(e,t){const n="left";if(!t||!e)return n;const{canShow:s,parentPositions:i}=getAvailablePosition(e,t),o=s.left&&s.right;return(o&&s.bottom?"center":o&&s.top?["top","center"]:Array.isArray(i)?["bottom"===i[0]?"top":"bottom",...i.slice(1)]:i)||n}const setPosition=(e,t,n)=>{if(!e)return;const s="auto"===n?findBestPickerPosition(e,t):n,i={top:-t.offsetHeight,bottom:e.offsetHeight,left:0,center:e.offsetWidth/2-t.offsetWidth/2,right:e.offsetWidth-t.offsetWidth},o=Array.isArray(s)?s[0]:"bottom",a=Array.isArray(s)?s[1]:s;t.dataset.vcPosition=o;const{top:l,left:r}=getOffset(e),c=l+i[o];let d=r+i[a];const{vw:u}=getViewportDimensions();if(d+t.clientWidth>u){const e=window.innerWidth-document.body.clientWidth;d=u-t.clientWidth-e}else d<0&&(d=0);Object.assign(t.style,{left:`${d}px`,top:`${c}px`})},show=e=>{if(e.context.isShowInInputMode)return;if(!e.context.currentType)return void e.context.mainElement.click();setContext(e,"cleanupHandlers",[]),setContext(e,"isShowInInputMode",!0),e.inputMode&&restoreTabbing(e.context.mainElement),setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput),e.context.mainElement.removeAttribute("data-vc-calendar-hidden");const t=()=>{setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput)};window.addEventListener("resize",t),e.context.cleanupHandlers.push(()=>window.removeEventListener("resize",t));const n=t=>{"Escape"===t.key&&hide(e)};document.addEventListener("keydown",n),e.context.cleanupHandlers.push(()=>document.removeEventListener("keydown",n));const s=t=>{t.target===e.context.inputElement||e.context.mainElement.contains(t.target)||hide(e)};document.addEventListener("click",s,{capture:!0}),e.context.cleanupHandlers.push(()=>document.removeEventListener("click",s,{capture:!0})),e.onShow&&e.onShow(e)},labels={application:"Calendar",navigation:"Calendar Navigation",arrowNext:{month:"Next month",year:"Next list of years"},arrowPrev:{month:"Previous month",year:"Previous list of years"},month:"Select month, current selected month:",months:"List of months",year:"Select year, current selected year:",years:"List of years",week:"Days of the week",weekNumber:"Numbers of weeks in a year",dates:"Dates in the current month",selectingTime:"Selecting a time ",inputHour:"Hours",inputMinute:"Minutes",rangeHour:"Slider for selecting hours",rangeMinute:"Slider for selecting minutes",btnKeeping:"Switch AM/PM, current position:"},styles={calendar:"vc",controls:"vc-controls",grid:"vc-grid",column:"vc-column",header:"vc-header",headerContent:"vc-header__content",month:"vc-month",year:"vc-year",arrowPrev:"vc-arrow vc-arrow_prev",arrowNext:"vc-arrow vc-arrow_next",wrapper:"vc-wrapper",content:"vc-content",months:"vc-months",monthsMonth:"vc-months__month",years:"vc-years",yearsYear:"vc-years__year",week:"vc-week",weekDay:"vc-week__day",weekNumbers:"vc-week-numbers",weekNumbersTitle:"vc-week-numbers__title",weekNumbersContent:"vc-week-numbers__content",weekNumber:"vc-week-number",dates:"vc-dates",datesRow:"vc-dates__row",date:"vc-date",dateBtn:"vc-date__btn",datePopup:"vc-date__popup",dateRangeTooltip:"vc-date-range-tooltip",time:"vc-time",timeContent:"vc-time__content",timeHour:"vc-time__hour",timeMinute:"vc-time__minute",timeKeeping:"vc-time__keeping",timeRanges:"vc-time__ranges",timeRange:"vc-time__range"};class OptionsCalendar{constructor(){__publicField(this,"type","default"),__publicField(this,"inputMode",!1),__publicField(this,"openOnFocus",!0),__publicField(this,"positionToInput","left"),__publicField(this,"firstWeekday",1),__publicField(this,"monthsToSwitch",1),__publicField(this,"themeAttrDetect","html[data-theme]"),__publicField(this,"locale","en"),__publicField(this,"dateToday","today"),__publicField(this,"dateMin","1970-01-01"),__publicField(this,"dateMax","2470-12-31"),__publicField(this,"displayDateMin"),__publicField(this,"displayDateMax"),__publicField(this,"displayDatesOutside",!0),__publicField(this,"displayDisabledDates",!1),__publicField(this,"displayMonthsCount"),__publicField(this,"disableDates",[]),__publicField(this,"disableAllDates",!1),__publicField(this,"disableDatesPast",!1),__publicField(this,"disableDatesGaps",!1),__publicField(this,"disableWeekdays",[]),__publicField(this,"disableToday",!1),__publicField(this,"enableDates",[]),__publicField(this,"enableEdgeDatesOnly",!0),__publicField(this,"enableDateToggle",!0),__publicField(this,"enableWeekNumbers",!1),__publicField(this,"enableMonthChangeOnDayClick",!0),__publicField(this,"enableJumpToSelectedDate",!1),__publicField(this,"selectionDatesMode","single"),__publicField(this,"selectionMonthsMode",!0),__publicField(this,"selectionYearsMode",!0),__publicField(this,"selectionTimeMode",!1),__publicField(this,"selectedDates",[]),__publicField(this,"selectedMonth"),__publicField(this,"selectedYear"),__publicField(this,"selectedHolidays",[]),__publicField(this,"selectedWeekends",[0,6]),__publicField(this,"selectedTime"),__publicField(this,"selectedTheme","system"),__publicField(this,"timeMinHour",0),__publicField(this,"timeMaxHour",23),__publicField(this,"timeMinMinute",0),__publicField(this,"timeMaxMinute",59),__publicField(this,"timeControls","all"),__publicField(this,"timeStepHour",1),__publicField(this,"timeStepMinute",1),__publicField(this,"sanitizerHTML",e=>e),__publicField(this,"onClickDate"),__publicField(this,"onClickWeekDay"),__publicField(this,"onClickWeekNumber"),__publicField(this,"onClickTitle"),__publicField(this,"onClickMonth"),__publicField(this,"onClickYear"),__publicField(this,"onClickArrow"),__publicField(this,"onChangeTime"),__publicField(this,"onChangeToInput"),__publicField(this,"onCreateDateRangeTooltip"),__publicField(this,"onCreateDateEls"),__publicField(this,"onCreateMonthEls"),__publicField(this,"onCreateYearEls"),__publicField(this,"onInit"),__publicField(this,"onUpdate"),__publicField(this,"onDestroy"),__publicField(this,"onShow"),__publicField(this,"onHide"),__publicField(this,"popups",{}),__publicField(this,"labels",__spreadValues({},labels)),__publicField(this,"layouts",{default:"",multiple:"",month:"",year:""}),__publicField(this,"styles",__spreadValues({},styles))}}const _Calendar=class e extends OptionsCalendar{constructor(t,n){var s;super(),__publicField(this,"init",()=>init(this)),__publicField(this,"update",e=>update(this,e)),__publicField(this,"destroy",()=>destroy(this)),__publicField(this,"show",()=>show(this)),__publicField(this,"hide",()=>hide(this)),__publicField(this,"set",(e,t)=>set(this,e,t)),__publicField(this,"context"),this.context=__spreadProps(__spreadValues({},this.context),{locale:{months:{short:[],long:[]},weekdays:{short:[],long:[]}}}),setContext(this,"mainElement","string"==typeof t?null!=(s=e.memoizedElements.get(t))?s:this.queryAndMemoize(t):t),n&&replaceProperties(this,n)}queryAndMemoize(t){const n=document.querySelector(t);if(!n)throw new Error(errorMessages.notFoundSelector(t));return e.memoizedElements.set(t,n),n}};__publicField(_Calendar,"memoizedElements",new Map);let Calendar=_Calendar;const NAME$f="datepicker",DATA_KEY$b="bs.datepicker",EVENT_KEY$c=`.${DATA_KEY$b}`,DATA_API_KEY$7=".data-api",EVENT_CHANGE$2=`change${EVENT_KEY$c}`,EVENT_SHOW$4=`show${EVENT_KEY$c}`,EVENT_SHOWN$3=`shown${EVENT_KEY$c}`,EVENT_HIDE$3=`hide${EVENT_KEY$c}`,EVENT_HIDDEN$5=`hidden${EVENT_KEY$c}`,EVENT_CLICK_DATA_API$3=`click${EVENT_KEY$c}.data-api`,EVENT_FOCUSIN_DATA_API=`focusin${EVENT_KEY$c}.data-api`,SELECTOR_DATA_TOGGLE$6='[data-bs-toggle="datepicker"]',HIDE_DELAY=100,Default$e={datepickerTheme:null,dateMin:null,dateMax:null,dateFormat:null,displayElement:null,displayMonthsCount:1,firstWeekday:1,inline:!1,locale:"default",positionElement:null,selectedDates:[],selectionMode:"single",placement:"left",vcpOptions:{}},DefaultType$e={datepickerTheme:"(null|string)",dateMin:"(null|string|number|object)",dateMax:"(null|string|number|object)",dateFormat:"(null|object|function)",displayElement:"(null|string|element|boolean)",displayMonthsCount:"number",firstWeekday:"number",inline:"boolean",locale:"string",positionElement:"(null|string|element)",selectedDates:"array",selectionMode:"string",placement:"string",vcpOptions:"object"};class Datepicker extends BaseComponent{constructor(e,t){super(e,t),this._calendar=null,this._isShown=!1,this._initCalendar()}static get Default(){return Default$e}static get DefaultType(){return DefaultType$e}static get NAME(){return NAME$f}toggle(){if(!this._config.inline)return this._isShown?this.hide():this.show()}show(){this._config.inline||!this._calendar||isDisabled(this._element)||this._isShown||EventHandler.trigger(this._element,EVENT_SHOW$4).defaultPrevented||(this._calendar.show(),this._isShown=!0,EventHandler.trigger(this._element,EVENT_SHOWN$3))}hide(){this._config.inline||this._calendar&&this._isShown&&(EventHandler.trigger(this._element,EVENT_HIDE$3).defaultPrevented||(this._calendar.hide(),this._isShown=!1,EventHandler.trigger(this._element,EVENT_HIDDEN$5)))}dispose(){this._themeObserver&&(this._themeObserver.disconnect(),this._themeObserver=null),this._calendar&&this._calendar.destroy(),this._calendar=null,super.dispose()}getSelectedDates(){const e=this._calendar?.context?.selectedDates;return e?[...e]:[]}setSelectedDates(e){this._calendar&&this._calendar.set({selectedDates:e})}_initCalendar(){this._isInput="INPUT"===this._element.tagName,this._isInline=this._config.inline,this._isInline&&!this._isInput&&(this._boundInput=this._element.querySelector('input[type="hidden"], input[name]')),this._positionElement=this._resolvePositionElement(),this._displayElement=this._resolveDisplayElement();const e=this._buildCalendarOptions();this._calendar=new Calendar(this._positionElement,e),this._calendar.init(),this._setupThemeObserver(),this._isInput&&this._element.value&&this._parseInputValue(),this._updateDisplayWithSelectedDates()}_updateDisplayWithSelectedDates(){const{selectedDates:e}=this._config;if(!e||0===e.length)return;const t=this._formatDateForInput(e);this._isInput&&(this._element.value=t),this._boundInput&&(this._boundInput.value=e.join(",")),this._displayElement&&(this._displayElement.textContent=t)}_resolvePositionElement(){let{positionElement:e}=this._config;if("string"==typeof e&&(e=document.querySelector(e)),!e&&this._isInput&&!this._isInline){const t=this._element.closest(".form-adorn");t&&(e=t)}return e||this._element}_resolveDisplayElement(){const{displayElement:e}=this._config;return"string"==typeof e?document.querySelector(e):!0===e||null===e&&!this._isInput&&!this._isInline?this._element.querySelector("[data-bs-datepicker-display]")||this._element:e}_getThemeAncestor(){return this._element.closest("[data-bs-theme]")}_getEffectiveTheme(){const{datepickerTheme:e}=this._config;if(e)return e;const t=this._getThemeAncestor();return t?.getAttribute("data-bs-theme")||null}_syncThemeAttribute(e){if(!e)return;const t=this._getEffectiveTheme();t?e.setAttribute("data-bs-theme",t):e.removeAttribute("data-bs-theme")}_setupThemeObserver(){const e=this._getThemeAncestor();e&&!this._config.datepickerTheme&&(this._themeObserver=new MutationObserver(()=>{this._syncThemeAttribute(this._calendar?.context?.mainElement)}),this._themeObserver.observe(e,{attributes:!0,attributeFilter:["data-bs-theme"]}))}_buildCalendarOptions(){const e=this._getEffectiveTheme(),t=e&&"auto"!==e?e:"system",n={...this._config.vcpOptions,inputMode:!this._isInline,positionToInput:this._config.placement,firstWeekday:this._config.firstWeekday,locale:this._config.locale,selectionDatesMode:this._config.selectionMode,selectedDates:this._config.selectedDates,displayMonthsCount:this._config.displayMonthsCount,type:this._config.displayMonthsCount>1?"multiple":"default",selectedTheme:t,themeAttrDetect:"[data-bs-theme]",onClickDate:(e,t)=>this._handleDateClick(e,t),onInit:e=>{this._syncThemeAttribute(e.context.mainElement)},onShow:()=>{this._isShown=!0,this._syncThemeAttribute(this._calendar.context.mainElement)},onHide:()=>{this._isShown=!1}};if(this._config.selectedDates.length>0){const e=this._parseDate(this._config.selectedDates[0]);n.selectedMonth=e.getMonth(),n.selectedYear=e.getFullYear()}return this._config.dateMin&&(n.dateMin=this._config.dateMin),this._config.dateMax&&(n.dateMax=this._config.dateMax),n}_handleDateClick(e,t){const n=[...e.context.selectedDates];if(n.length>0){const e=this._formatDateForInput(n);this._isInput&&(this._element.value=e),this._boundInput&&(this._boundInput.value=n.join(",")),this._displayElement&&(this._displayElement.textContent=e)}EventHandler.trigger(this._element,EVENT_CHANGE$2,{dates:n,event:t}),this._maybeHideAfterSelection(n)}_maybeHideAfterSelection(e){this._isInline||("single"===this._config.selectionMode&&e.length>0||"multiple-ranged"===this._config.selectionMode&&e.length>=2)&&setTimeout(()=>this.hide(),100)}_parseDate(e){const[t,n,s]=e.split("-");return new Date(t,n-1,s)}_formatDate(e){const t=this._parseDate(e),n="default"===this._config.locale?void 0:this._config.locale,{dateFormat:s}=this._config;return"function"==typeof s?s(t,n):s&&"object"==typeof s?new Intl.DateTimeFormat(n,s).format(t):t.toLocaleDateString(n)}_formatDateForInput(e){if(0===e.length)return"";if(1===e.length)return this._formatDate(e[0]);const t="multiple-ranged"===this._config.selectionMode?" – ":", ";return e.map(e=>this._formatDate(e)).join(t)}_parseInputValue(){const e=this._element.value.trim();if(!e)return;const t=new Date(e);if(!Number.isNaN(t.getTime())){const e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;this._calendar.set({selectedDates:[e]})}}}EventHandler.on(document,EVENT_CLICK_DATA_API$3,SELECTOR_DATA_TOGGLE$6,function(e){"INPUT"!==this.tagName&&"true"!==this.dataset.bsInline&&(e.preventDefault(),Datepicker.getOrCreateInstance(this).toggle())}),EventHandler.on(document,EVENT_FOCUSIN_DATA_API,SELECTOR_DATA_TOGGLE$6,function(){"INPUT"===this.tagName&&Datepicker.getOrCreateInstance(this).show()}),EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$c}.data-api`,()=>{for(const e of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`))Datepicker.getOrCreateInstance(e)});const CLASS_NAME_OPEN="dialog-open";class DialogBase extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._openedAsModal=!1,this._addDialogListeners()}static get NAME(){return"dialogbase"}toggle(e){return this._element.open?this.hide():this.show(e)}show(e){if(this._element.open||this._isTransitioning)return;if(EventHandler.trigger(this._element,this.constructor.eventName("show"),{relatedTarget:e}).defaultPrevented)return;this._isTransitioning=!0,this._onBeforeShow();const{modal:t,preventBodyScroll:n}=this._getShowOptions();this._showElement({modal:t,preventBodyScroll:n}),this._queueCallback(()=>{this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("shown"),{relatedTarget:e})},this._element,this._isAnimated())}hide(){this._element.open&&!this._isTransitioning&&(EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented||(this._isTransitioning=!0,this._hideElement(),this._queueCallback(()=>{this._element.open&&this._closeAndCleanup(),this._element.classList.remove("hiding"),this._onAfterHide(),this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("hidden"))},this._element,this._isAnimated())))}dispose(){this._element.open&&this._closeAndCleanup(),super.dispose()}_getShowOptions(){return{modal:!0,preventBodyScroll:!0}}_onBeforeShow(){}_onAfterHide(){}_isAnimated(){return!this._element.classList.contains(this._getInstantClassName())}_getInstantClassName(){return"dialog-instant"}_getStaticClassName(){return"dialog-static"}_onCancel(){}_showElement({modal:e=!0,preventBodyScroll:t=!0}={}){this._openedAsModal=e,e?this._element.showModal():this._element.show(),t&&document.documentElement.classList.add("dialog-open")}_hideElement(){this._hideChildComponents(),this._element.classList.add("hiding"),this._shouldDeferClose()||this._closeAndCleanup()}_closeAndCleanup(){this._element.close(),this._openedAsModal=!1,document.querySelector("dialog[open]:modal")||document.documentElement.classList.remove("dialog-open")}_shouldDeferClose(){return!1}_triggerBackdropTransition(){if(EventHandler.trigger(this._element,this.constructor.eventName("hidePrevented")).defaultPrevented)return;const e=this._getStaticClassName();this._element.classList.add(e),this._queueCallback(()=>{this._element.classList.remove(e)},this._element)}_hideChildComponents(){for(const e of SelectorEngine.find('[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]',this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}for(const e of SelectorEngine.find(".toast.show",this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}}_addDialogListeners(){const e=this.constructor.EVENT_KEY;EventHandler.on(this._element,"cancel",e=>{e.preventDefault(),this._config.keyboard?(this._onCancel(),this.hide()):this._triggerBackdropTransition()}),EventHandler.on(this._element,`keydown${e}`,e=>{"Escape"!==e.key||this._openedAsModal||(e.preventDefault(),this._config.keyboard&&(this._onCancel(),this.hide()))}),EventHandler.on(this._element,`click${e}`,e=>{e.target===this._element&&this._openedAsModal&&("static"!==this._config.backdrop?this.hide():this._triggerBackdropTransition())})}}const NAME$e="dialog",DATA_KEY$a="bs.dialog",EVENT_KEY$b=`.${DATA_KEY$a}`,DATA_API_KEY$6=".data-api",EVENT_SHOW$3=`show${EVENT_KEY$b}`,EVENT_HIDDEN$4=`hidden${EVENT_KEY$b}`,EVENT_CANCEL=`cancel${EVENT_KEY$b}`,EVENT_CLICK_DATA_API$2=`click${EVENT_KEY$b}.data-api`,CLASS_NAME_NONMODAL="dialog-nonmodal",CLASS_NAME_INSTANT="dialog-instant",CLASS_NAME_SWAP_IN="dialog-swap-in",SELECTOR_DATA_TOGGLE$5='[data-bs-toggle="dialog"]',Default$d={backdrop:!0,keyboard:!0,modal:!0},DefaultType$d={backdrop:"(boolean|string)",keyboard:"boolean",modal:"boolean"};class Dialog extends DialogBase{static get Default(){return Default$d}static get DefaultType(){return DefaultType$d}static get NAME(){return NAME$e}handleUpdate(){}_getShowOptions(){return{modal:this._config.modal,preventBodyScroll:this._config.modal}}_onBeforeShow(){this._config.modal||this._element.classList.add("dialog-nonmodal")}_onAfterHide(){this._element.classList.remove("dialog-nonmodal")}_shouldDeferClose(){return this._isAnimated()}_onCancel(){EventHandler.trigger(this._element,EVENT_CANCEL)}}EventHandler.on(document,EVENT_CLICK_DATA_API$2,SELECTOR_DATA_TOGGLE$5,function(e){const t=SelectorEngine.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&e.preventDefault(),EventHandler.one(t,EVENT_SHOW$3,e=>{e.defaultPrevented||EventHandler.one(t,EVENT_HIDDEN$4,()=>{isVisible(this)&&this.focus({preventScroll:!0})})});const n=Manipulator.getDataAttributes(this),s=this.closest("dialog[open]");if(s&&s!==t){const e=Dialog.getOrCreateInstance(t,n);t.classList.add("dialog-swap-in"),e.show(this),EventHandler.one(t,`shown${EVENT_KEY$b}`,()=>{t.classList.remove("dialog-swap-in")});const i=Dialog.getInstance(s);return void(i&&(s.classList.add("dialog-instant"),EventHandler.one(s,EVENT_HIDDEN$4,()=>{s.classList.remove("dialog-instant")}),i.hide()))}Dialog.getOrCreateInstance(t,n).toggle(this)}),enableDismissTrigger(Dialog);const NAME$d="navoverflow",DATA_KEY$9="bs.navoverflow",EVENT_KEY$a=`.${DATA_KEY$9}`,EVENT_UPDATE=`update${EVENT_KEY$a}`,EVENT_OVERFLOW=`overflow${EVENT_KEY$a}`,CLASS_NAME_OVERFLOW="nav-overflow",CLASS_NAME_OVERFLOW_MENU="nav-overflow-menu",CLASS_NAME_HIDDEN="d-none",SELECTOR_NAV_ITEM=".nav-item",SELECTOR_NAV_LINK=".nav-link",SELECTOR_OVERFLOW_TOGGLE=".nav-overflow-toggle",SELECTOR_OVERFLOW_MENU=".nav-overflow-menu",SELECTOR_CUSTOM_ICON="[data-bs-overflow-icon]",CLASS_NAME_KEEP="nav-overflow-keep",Default$c={collapseBelow:0,iconPlacement:"start",menuPlacement:"bottom-end",moreText:"More",moreIcon:'',threshold:0},DefaultType$c={collapseBelow:"(number|string)",iconPlacement:"string",menuPlacement:"string",moreText:"string",moreIcon:"string",threshold:"number"};class NavOverflow extends BaseComponent{constructor(e,t){super(e,t),this._items=[],this._overflowItems=[],this._overflowMenu=null,this._overflowToggle=null,this._resizeObserver=null,this._collapseBelow=0,this._isInitialized=!1,this._init()}static get Default(){return Default$c}static get DefaultType(){return DefaultType$c}static get NAME(){return NAME$d}update(){this._calculateOverflow(),EventHandler.trigger(this._element,EVENT_UPDATE)}dispose(){this._resizeObserver&&this._resizeObserver.disconnect(),this._restoreItems(),this._overflowToggle&&this._overflowToggle.parentElement&&this._overflowToggle.parentElement.remove(),super.dispose()}_init(){this._element.classList.add("nav-overflow"),this._items=[...SelectorEngine.find(".nav-item",this._element)];for(const[e,t]of this._items.entries())t.dataset.bsNavOrder=e;this._collapseBelow=this._resolveCollapseBelow(),this._createOverflowMenu(),this._setupResizeObserver(),this._calculateOverflow(),this._isInitialized=!0}_createOverflowMenu(){if(this._overflowToggle=SelectorEngine.findOne(".nav-overflow-toggle",this._element),this._overflowToggle)return void(this._overflowMenu=SelectorEngine.findOne(".nav-overflow-menu",this._element));const e=`${this._resolveIcon()}`,t=`${this._config.moreText}`,n="end"===this._config.iconPlacement?`${t}${e}`:`${e}${t}`,s=document.createElement("li");s.className="nav-item nav-overflow-item",s.innerHTML=`\n \n ${n}\n \n \n `,this._element.append(s),this._overflowToggle=s.querySelector(".nav-overflow-toggle"),this._overflowMenu=s.querySelector(".nav-overflow-menu")}_resolveIcon(){const e=SelectorEngine.findOne(SELECTOR_CUSTOM_ICON,this._element);if(!e)return this._config.moreIcon;const t=e.cloneNode(!0);t.removeAttribute("data-bs-overflow-icon");const n=t.outerHTML;return e.remove(),n}_resolveCollapseBelow(){const e=this._config.collapseBelow;if("number"==typeof e)return e;if("string"==typeof e&&""!==e){const t=getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${e}`);return Number.parseFloat(t)||0}return 0}_setupResizeObserver(){"undefined"!=typeof ResizeObserver?(this._resizeObserver=new ResizeObserver(()=>{this._calculateOverflow()}),this._resizeObserver.observe(this._element)):EventHandler.on(window,"resize",()=>this._calculateOverflow())}_calculateOverflow(){this._restoreItems();const e=this._element.offsetWidth,t=this._overflowToggle?.closest(".nav-item");if(this._collapseBelow>0&&e!e.classList.contains(CLASS_NAME_KEEP));return this._moveToOverflow(e),t&&(e.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),void(e.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:e.length,visibleCount:this._items.length-e.length}))}let n=0;const s=[],i=e-(t?.offsetWidth||0)-this._items.filter(e=>e.classList.contains(CLASS_NAME_KEEP)).reduce((e,t)=>e+t.offsetWidth,0)-10;for(const e of this._items)e.classList.contains(CLASS_NAME_KEEP)||(n+=e.offsetWidth,n>i&&s.push(e));if(this._items.length-s.lengththis._config.threshold){const e=this._items.slice(this._config.threshold).filter(e=>!e.classList.contains(CLASS_NAME_KEEP));s.length=0,s.push(...e)}this._moveToOverflow(s),t&&(s.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),s.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:s.length,visibleCount:this._items.length-s.length})}_moveToOverflow(e){if(this._overflowMenu){this._overflowMenu.innerHTML="",this._overflowItems=[];for(const t of e){const e=SelectorEngine.findOne(".nav-link",t);if(!e)continue;const n=e.cloneNode(!0);n.className="menu-item",e.classList.contains("active")&&n.classList.add("active"),(e.classList.contains("disabled")||e.hasAttribute("disabled"))&&n.classList.add("disabled"),this._overflowMenu.append(n),t.classList.add("d-none"),t.dataset.bsNavOverflow="true",this._overflowItems.push(t)}}}_restoreItems(){for(const e of this._items)e.classList.remove("d-none"),delete e.dataset.bsNavOverflow;this._overflowMenu&&(this._overflowMenu.innerHTML=""),this._overflowItems=[]}}EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find('[data-bs-toggle="nav-overflow"]'))NavOverflow.getOrCreateInstance(e)});const NAME$c="swipe",EVENT_KEY$9=".bs.swipe",EVENT_TOUCHSTART="touchstart.bs.swipe",EVENT_TOUCHMOVE="touchmove.bs.swipe",EVENT_TOUCHEND="touchend.bs.swipe",EVENT_POINTERDOWN="pointerdown.bs.swipe",EVENT_POINTERUP="pointerup.bs.swipe",POINTER_TYPE_TOUCH="touch",POINTER_TYPE_PEN="pen",CLASS_NAME_POINTER_EVENT="pointer-event",SWIPE_THRESHOLD=40,Default$b={endCallback:null,leftCallback:null,rightCallback:null,upCallback:null,downCallback:null},DefaultType$b={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)",upCallback:"(function|null)",downCallback:"(function|null)"};class Swipe extends Config{constructor(e,t){super(),this._element=e,e&&Swipe.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._deltaY=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Default$b}static get DefaultType(){return DefaultType$b}static get NAME(){return NAME$c}dispose(){EventHandler.off(this._element,".bs.swipe")}_start(e){if(!this._supportPointerEvents)return this._deltaX=e.touches[0].clientX,void(this._deltaY=e.touches[0].clientY);this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX,this._deltaY=e.clientY)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX,this._deltaY=e.clientY-this._deltaY),this._handleSwipe(),execute(this._config.endCallback)}_move(e){if(e.touches&&e.touches.length>1)return this._deltaX=0,void(this._deltaY=0);this._deltaX=e.touches[0].clientX-this._deltaX,this._deltaY=e.touches[0].clientY-this._deltaY}_handleSwipe(){const e=Math.abs(this._deltaX),t=Math.abs(this._deltaY);if(t>e&&t>40){const e=this._deltaY>0?"down":"up";return this._deltaX=0,this._deltaY=0,void execute("down"===e?this._config.downCallback:this._config.upCallback)}if(e>40){const t=e/this._deltaX;if(this._deltaX=0,this._deltaY=0,!t)return;return void execute(t>0?this._config.rightCallback:this._config.leftCallback)}this._deltaX=0,this._deltaY=0}_initEvents(){this._supportPointerEvents?(EventHandler.on(this._element,EVENT_POINTERDOWN,e=>this._start(e)),EventHandler.on(this._element,EVENT_POINTERUP,e=>this._end(e)),this._element.classList.add("pointer-event")):(EventHandler.on(this._element,EVENT_TOUCHSTART,e=>this._start(e)),EventHandler.on(this._element,EVENT_TOUCHMOVE,e=>this._move(e)),EventHandler.on(this._element,EVENT_TOUCHEND,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&("pen"===e.pointerType||"touch"===e.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const NAME$b="drawer",DATA_KEY$8="bs.drawer",EVENT_KEY$8=`.${DATA_KEY$8}`,DATA_API_KEY$5=".data-api",EVENT_LOAD_DATA_API$2=`load${EVENT_KEY$8}.data-api`,EVENT_HIDDEN$3=`hidden${EVENT_KEY$8}`,EVENT_RESIZE$1=`resize${EVENT_KEY$8}`,EVENT_CLICK_DATA_API$1=`click${EVENT_KEY$8}.data-api`,SELECTOR_DATA_TOGGLE$4='[data-bs-toggle="drawer"]',Default$a={backdrop:!0,keyboard:!0,scroll:!1},DefaultType$a={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Drawer extends DialogBase{constructor(e,t){super(e,t),this._swipeHelper=null}static get Default(){return Default$a}static get DefaultType(){return DefaultType$a}static get NAME(){return NAME$b}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_getShowOptions(){return{modal:Boolean(this._config.backdrop)||!this._config.scroll,preventBodyScroll:!this._config.scroll}}_onBeforeShow(){this._initSwipe()}_getInstantClassName(){return"drawer-instant"}_getStaticClassName(){return"drawer-static"}_initSwipe(){if(this._swipeHelper||!Swipe.isSupported())return;const e={},t=this._element;t.classList.contains("drawer-bottom")?e.downCallback=()=>this.hide():t.classList.contains("drawer-top")?e.upCallback=()=>this.hide():t.classList.contains("drawer-end")?isRTL$1()?e.leftCallback=()=>this.hide():e.rightCallback=()=>this.hide():isRTL$1()?e.rightCallback=()=>this.hide():e.leftCallback=()=>this.hide(),this._swipeHelper=new Swipe(t,e)}}EventHandler.on(document,EVENT_CLICK_DATA_API$1,SELECTOR_DATA_TOGGLE$4,function(e){const t=SelectorEngine.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this))return;EventHandler.one(t,EVENT_HIDDEN$3,()=>{isVisible(this)&&this.focus({preventScroll:!0})});const n=SelectorEngine.findOne("dialog.drawer[open]");n&&n!==t&&Drawer.getInstance(n).hide(),Drawer.getOrCreateInstance(t).toggle(this)}),EventHandler.on(window,EVENT_LOAD_DATA_API$2,()=>{for(const e of SelectorEngine.find("dialog.drawer[open]"))Drawer.getOrCreateInstance(e).show()}),EventHandler.on(window,EVENT_RESIZE$1,()=>{for(const e of SelectorEngine.find('dialog[open][class*="\\:drawer"]'))"fixed"!==getComputedStyle(e).position&&Drawer.getOrCreateInstance(e).hide()}),enableDismissTrigger(Drawer);const NAME$a="strength",DATA_KEY$7="bs.strength",EVENT_KEY$7=`.${DATA_KEY$7}`,DATA_API_KEY$4=".data-api",EVENT_STRENGTH_CHANGE=`strengthChange${EVENT_KEY$7}`,SELECTOR_DATA_STRENGTH="[data-bs-strength]",STRENGTH_LEVELS=["weak","fair","good","strong"],Default$9={input:null,minLength:8,messages:{weak:"Weak",fair:"Fair",good:"Good",strong:"Strong"},weights:{minLength:1,extraLength:1,lowercase:1,uppercase:1,numbers:1,special:1,multipleSpecial:1,longPassword:1},thresholds:[2,4,6],scorer:null},DefaultType$9={input:"(string|element|null)",minLength:"number",messages:"object",weights:"object",thresholds:"array",scorer:"(function|null)"};class Strength extends BaseComponent{constructor(e,t){super(e,t),this._input=this._getInput(),this._segments=SelectorEngine.find(".strength-segment",this._element),this._textElement=SelectorEngine.findOne(".strength-text",this._element.parentElement),this._currentStrength=null,this._input&&(this._addEventListeners(),this._evaluate())}static get Default(){return Default$9}static get DefaultType(){return DefaultType$9}static get NAME(){return NAME$a}getStrength(){return this._currentStrength}evaluate(){this._evaluate()}_getInput(){if(this._config.input)return"string"==typeof this._config.input?SelectorEngine.findOne(this._config.input):this._config.input;const e=this._element.parentElement;return SelectorEngine.findOne('input[type="password"]',e)}_addEventListeners(){EventHandler.on(this._input,"input",()=>this._evaluate()),EventHandler.on(this._input,"change",()=>this._evaluate())}_evaluate(){const e=this._input.value,t=this._calculateScore(e),n=this._scoreToStrength(t);n!==this._currentStrength&&(this._currentStrength=n,this._updateUI(n,t),EventHandler.trigger(this._element,EVENT_STRENGTH_CHANGE,{strength:n,score:t,password:e.length>0?"***":""}))}_calculateScore(e){if(!e)return 0;if("function"==typeof this._config.scorer)return this._config.scorer(e);const{weights:t}=this._config;let n=0;return e.length>=this._config.minLength&&(n+=t.minLength),e.length>=this._config.minLength+4&&(n+=t.extraLength),/[a-z]/.test(e)&&(n+=t.lowercase),/[A-Z]/.test(e)&&(n+=t.uppercase),/\d/.test(e)&&(n+=t.numbers),/[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.special),/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.multipleSpecial),e.length>=16&&(n+=t.longPassword),n}_scoreToStrength(e){if(0===e)return null;const[t,n,s]=this._config.thresholds;return e<=t?"weak":e<=n?"fair":e<=s?"good":"strong"}_updateUI(e){e?this._element.dataset.bsStrength=e:delete this._element.dataset.bsStrength;const t=e?STRENGTH_LEVELS.indexOf(e):-1;for(const[e,n]of this._segments.entries())e<=t?n.classList.add("active"):n.classList.remove("active");if(this._textElement)if(e&&this._config.messages[e]){this._textElement.textContent=this._config.messages[e],this._textElement.dataset.bsStrength=e;const t={weak:"danger",fair:"warning",good:"info",strong:"success"};this._textElement.style.setProperty("--strength-color",`var(--${t[e]}-text)`)}else this._textElement.textContent="",delete this._textElement.dataset.bsStrength}}EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$7}.data-api`,()=>{for(const e of SelectorEngine.find("[data-bs-strength]"))Strength.getOrCreateInstance(e)});const NAME$9="otpInput",DATA_KEY$6="bs.otpInput",EVENT_KEY$6=`.${DATA_KEY$6}`,DATA_API_KEY$3=".data-api",EVENT_COMPLETE=`complete${EVENT_KEY$6}`,EVENT_INPUT$1=`input${EVENT_KEY$6}`,EVENT_DOMCONTENT_LOADED=`DOMContentLoaded${EVENT_KEY$6}.data-api`,SELECTOR_DATA_OTP="[data-bs-otp]",SELECTOR_INPUT$1="input",SYNC_EVENTS=["blur","keyup","click","select"],CLASS_NAME_INPUT="otp-input",CLASS_NAME_RENDERED="otp-rendered",CLASS_NAME_SLOTS="otp-slots",CLASS_NAME_SLOT="otp-slot",CLASS_NAME_SLOT_FILLED="otp-slot-filled",CLASS_NAME_SLOT_ACTIVE="otp-slot-active",CLASS_NAME_SEPARATOR="otp-separator",MASK_CHARACTER="•",TYPES={numeric:{inputmode:"numeric",pattern:"[0-9]*",filter:/[^0-9]/g},alphanumeric:{inputmode:"text",pattern:"[A-Za-z0-9]*",filter:/[^A-Za-z0-9]/g},alpha:{inputmode:"text",pattern:"[A-Za-z]*",filter:/[^A-Za-z]/g}},Default$8={groups:null,length:null,mask:!1,separator:"·",type:"numeric"},DefaultType$8={groups:"(array|null)",length:"(number|null)",mask:"boolean",separator:"string",type:"string"};class OtpInput extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne("input",this._element),this._input&&(this._type=TYPES[this._config.type]||TYPES.numeric,this._length=this._resolveLength(),this._slots=[],this._setupInput(),this._renderSlots(),this._addEventListeners(),this._render())}static get Default(){return Default$8}static get DefaultType(){return DefaultType$8}static get NAME(){return NAME$9}getValue(){return this._input.value}setValue(e){this._input.value=this._sanitize(String(e)),this._render(),this._checkComplete()}clear(){this._input.value="",this._render(),this._input.focus()}focus(){this._input.focus();const e=this._input.value.length;this._input.setSelectionRange(e,e),this._render()}dispose(){EventHandler.off(this._input,"input",this._onInput),EventHandler.off(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.off(this._input,e,this._onSync);this._slotsContainer?.remove(),this._element.classList.remove("otp-rendered"),super.dispose()}_resolveLength(){if(this._config.length)return this._config.length;const e=Number.parseInt(this._input.getAttribute("maxlength"),10);return Number.isNaN(e)||e<1?6:e}_setupInput(){const e=this._input;"number"!==e.type&&"password"!==e.type||(e.type="text"),e.classList.add("otp-input"),e.setAttribute("maxlength",String(this._length)),e.setAttribute("inputmode",this._type.inputmode),e.setAttribute("pattern",this._type.pattern),e.getAttribute("autocomplete")||e.setAttribute("autocomplete","one-time-code"),e.value&&(e.value=this._sanitize(e.value))}_renderSlots(){const e=document.createElement("div");e.className="otp-slots",e.setAttribute("aria-hidden","true");const{groups:t}=this._config;let n=0,s=0;for(let i=0;i0&&(s++,s===t[n]&&ithis._handleInput(),this._onFocus=()=>this.focus(),this._onSync=()=>this._render(),EventHandler.on(this._input,"input",this._onInput),EventHandler.on(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.on(this._input,e,this._onSync)}_handleInput(){const e=this._sanitize(this._input.value);e!==this._input.value&&(this._input.value=e),this._render(),EventHandler.trigger(this._element,EVENT_INPUT$1,{value:this._input.value}),this._checkComplete()}_sanitize(e){return e.replace(this._type.filter,"").slice(0,this._length)}_render(){const{value:e}=this._input,t=document.activeElement===this._input,n=Math.min(this._input.selectionStart??e.length,this._length-1);for(const[s,i]of this._slots.entries()){const o=e[s]??"";i.textContent=o&&this._config.mask?"•":o,i.classList.toggle("otp-slot-filled",Boolean(o)),i.classList.toggle("otp-slot-active",t&&s===n)}}_checkComplete(){const{value:e}=this._input;e.length===this._length&&EventHandler.trigger(this._element,EVENT_COMPLETE,{value:e})}}EventHandler.on(document,EVENT_DOMCONTENT_LOADED,()=>{for(const e of SelectorEngine.find("[data-bs-otp]"))OtpInput.getOrCreateInstance(e)});const NAME$8="chips",DATA_KEY$5="bs.chips",EVENT_KEY$5=".bs.chips",DATA_API_KEY$2=".data-api",EVENT_ADD="add.bs.chips",EVENT_REMOVE="remove.bs.chips",EVENT_CHANGE$1="change.bs.chips",EVENT_SELECT="select.bs.chips",SELECTOR_DATA_CHIPS="[data-bs-chips]",SELECTOR_GHOST_INPUT=".form-ghost",SELECTOR_CHIP=".chip",SELECTOR_CHIP_DISMISS=".chip-dismiss",CLASS_NAME_CHIP="chip",CLASS_NAME_CHIP_DISMISS="chip-dismiss",CLASS_NAME_ACTIVE$2="active",DEFAULT_DISMISS_ICON='',Default$7={separator:",",allowDuplicates:!1,maxChips:null,placeholder:"",dismissible:!0,dismissIcon:DEFAULT_DISMISS_ICON,createOnBlur:!0},DefaultType$7={separator:"(string|null)",allowDuplicates:"boolean",maxChips:"(number|null)",placeholder:"string",dismissible:"boolean",dismissIcon:"string",createOnBlur:"boolean"};class Chips extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne(".form-ghost",this._element),this._chips=[],this._selectedChips=new Set,this._anchorChip=null,this._input||this._createInput(),this._initializeExistingChips(),this._addEventListeners()}static get Default(){return Default$7}static get DefaultType(){return DefaultType$7}static get NAME(){return NAME$8}add(e){const t=String(e).trim();if(!t)return null;if(!this._config.allowDuplicates&&this._chips.includes(t))return null;if(null!==this._config.maxChips&&this._chips.length>=this._config.maxChips)return null;if(EventHandler.trigger(this._element,EVENT_ADD,{value:t,relatedTarget:this._input}).defaultPrevented)return null;const n=this._createChip(t);return this._element.insertBefore(n,this._input),this._chips.push(t),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),n}remove(e){let t,n;return"string"==typeof e?(n=e,t=this._findChipByValue(n)):(t=e,n=this._getChipValue(t)),!(!t||!n)&&(!EventHandler.trigger(this._element,EVENT_REMOVE,{value:n,chip:t,relatedTarget:this._input}).defaultPrevented&&(this._selectedChips.delete(t),this._anchorChip===t&&(this._anchorChip=null),t.remove(),this._chips=this._chips.filter(e=>e!==n),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),!0))}removeSelected(){const e=[...this._selectedChips];for(const t of e)this.remove(t);this._input?.focus()}getValues(){return[...this._chips]}getSelectedValues(){return[...this._selectedChips].map(e=>this._getChipValue(e))}clear(){const e=SelectorEngine.find(".chip",this._element);for(const t of e)t.remove();this._chips=[],this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:[]})}clearSelection(){for(const e of this._selectedChips)e.classList.remove("active");this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_SELECT,{selected:[]})}selectChip(e,t={}){const{addToSelection:n=!1,rangeSelect:s=!1}=t,i=this._getChipElements();if(i.includes(e)){if(s&&this._anchorChip){const t=i.indexOf(this._anchorChip),s=i.indexOf(e),o=Math.min(t,s),a=Math.max(t,s);n||this.clearSelection();for(let e=o;e<=a;e++)this._selectedChips.add(i[e]),i[e].classList.add("active")}else n?this._selectedChips.has(e)?(this._selectedChips.delete(e),e.classList.remove("active")):(this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e):(this.clearSelection(),this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e);EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}focus(){this._input?.focus()}_getChipElements(){return SelectorEngine.find(".chip",this._element)}_createInput(){const e=document.createElement("input");e.type="text",e.className="form-ghost",this._config.placeholder&&(e.placeholder=this._config.placeholder),this._element.append(e),this._input=e}_initializeExistingChips(){const e=SelectorEngine.find(".chip",this._element);for(const t of e){const e=this._getChipValue(t);e&&(this._chips.push(e),this._setupChip(t))}}_setupChip(e){e.setAttribute("tabindex","0"),this._config.dismissible&&!SelectorEngine.findOne(".chip-dismiss",e)&&e.append(this._createDismissButton())}_createChip(e){const t=document.createElement("span");return t.className="chip",t.dataset.bsChipValue=e,t.append(document.createTextNode(e)),this._setupChip(t),t}_createDismissButton(){const e=document.createElement("button");return e.type="button",e.className="chip-dismiss",e.setAttribute("aria-label","Remove"),e.setAttribute("tabindex","-1"),e.innerHTML=this._config.dismissIcon,e}_findChipByValue(e){return this._getChipElements().find(t=>this._getChipValue(t)===e)}_getChipValue(e){if(e.dataset.bsChipValue)return e.dataset.bsChipValue;const t=e.cloneNode(!0),n=SelectorEngine.findOne(".chip-dismiss",t);return n&&n.remove(),t.textContent?.trim()||""}_addEventListeners(){EventHandler.on(this._input,"keydown",e=>this._handleInputKeydown(e)),EventHandler.on(this._input,"input",e=>this._handleInput(e)),EventHandler.on(this._input,"paste",e=>this._handlePaste(e)),EventHandler.on(this._input,"focus",()=>this.clearSelection()),this._config.createOnBlur&&EventHandler.on(this._input,"blur",e=>{e.relatedTarget?.closest(".chip")||this._createChipFromInput()}),EventHandler.on(this._element,"click",".chip",e=>{if(e.target.closest(".chip-dismiss"))return;const t=e.target.closest(".chip");t&&(e.preventDefault(),this.selectChip(t,{addToSelection:e.metaKey||e.ctrlKey,rangeSelect:e.shiftKey}),t.focus())}),EventHandler.on(this._element,"click",".chip-dismiss",e=>{e.stopPropagation();const t=e.target.closest(".chip");t&&(this.remove(t),this._input?.focus())}),EventHandler.on(this._element,"keydown",".chip",e=>{this._handleChipKeydown(e)}),EventHandler.on(this._element,"click",e=>{e.target===this._element&&(this.clearSelection(),this._input?.focus())})}_handleInputKeydown(e){const{key:t}=e;switch(t){case"Enter":e.preventDefault(),this._createChipFromInput();break;case"Backspace":case"Delete":if(""===this._input.value){e.preventDefault();const t=this._getChipElements();if(t.length>0){const e=t.at(-1);this.selectChip(e),e.focus()}}break;case"ArrowLeft":if(0===this._input.selectionStart&&0===this._input.selectionEnd){e.preventDefault();const t=this._getChipElements();if(t.length>0){const n=t.at(-1);e.shiftKey?this.selectChip(n,{addToSelection:!0}):this.selectChip(n),n.focus()}}break;case"Escape":this._input.value="",this.clearSelection(),this._input.blur()}}_handleChipKeydown(e){const{key:t}=e,n=e.target.closest(".chip");if(!n)return;const s=this._getChipElements(),i=s.indexOf(n);switch(t){case"Backspace":case"Delete":e.preventDefault(),this._handleChipDelete(i,s);break;case"ArrowLeft":e.preventDefault(),this._navigateChip(s,i,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._navigateChip(s,i,1,e.shiftKey);break;case"Home":e.preventDefault(),this._navigateToEdge(s,0,e.shiftKey);break;case"End":case"Escape":e.preventDefault(),this.clearSelection(),this._input?.focus();break;case"a":this._handleSelectAll(e,s)}}_handleChipDelete(e,t){if(0===this._selectedChips.size)return;const n=Math.min(e,t.length-this._selectedChips.size-1);this.removeSelected();const s=this._getChipElements();if(s.length>0){const e=Math.max(0,Math.min(n,s.length-1));s[e].focus(),this.selectChip(s[e])}else this._input?.focus()}_navigateChip(e,t,n,s){const i=t+n;if(n<0&&i>=0){const t=e[i];this.selectChip(t,s?{addToSelection:!0,rangeSelect:!0}:{}),t.focus()}else if(n>0&&i0&&(this.clearSelection(),this._input?.focus())}_navigateToEdge(e,t,n){if(0===e.length)return;const s=e[t];this.selectChip(s,n?{rangeSelect:!0}:{}),s.focus()}_handleSelectAll(e,t){if(e.metaKey||e.ctrlKey){e.preventDefault();for(const e of t)this._selectedChips.add(e),e.classList.add("active");EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}_handleInput(e){const{value:t}=e.target,{separator:n}=this._config;if(n&&t.includes(n)){const e=t.split(n);for(const t of e.slice(0,-1))this.add(t.trim());this._input.value=e.at(-1)}}_handlePaste(e){const{separator:t}=this._config;if(!t)return;const n=(e.clipboardData||window.clipboardData).getData("text");if(n.includes(t)){e.preventDefault();const s=n.split(t);for(const e of s)this.add(e.trim())}}_createChipFromInput(){const e=this._input.value.trim();e&&(this.add(e),this._input.value="")}}EventHandler.on(document,"DOMContentLoaded.bs.chips.data-api",()=>{for(const e of SelectorEngine.find("[data-bs-chips]"))Chips.getOrCreateInstance(e)});const ARIA_ATTRIBUTE_PATTERN=/^aria-[\w-]*$/i,DefaultAllowlist={"*":["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:[]},uriAttributes=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),SAFE_URL_PATTERN=/^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,DATA_URL_PATTERN=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i,allowedAttribute=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!uriAttributes.has(n)||Boolean(SAFE_URL_PATTERN.test(e.nodeValue)||DATA_URL_PATTERN.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))};function sanitizeHtml(e,t,n){if(!e.length)return e;if(n&&"function"==typeof n)return n(e);const s=(new window.DOMParser).parseFromString(e,"text/html"),i=[...s.body.querySelectorAll("*")];for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[...e.attributes],i=[...t["*"]||[],...t[n]||[]];for(const t of s)allowedAttribute(t,i)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const NAME$7="TemplateFactory",Default$6={allowList:DefaultAllowlist,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},DefaultType$6={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},DefaultContentType={entry:"(string|element|function|null)",selector:"(string|element)"};class TemplateFactory extends Config{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Default$6}static get DefaultType(){return DefaultType$6}static get NAME(){return NAME$7}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},DefaultContentType)}_setContent(e,t,n){const s=SelectorEngine.findOne(n,e);s&&((t=this._resolvePossibleFunction(t))?isElement$1(t)?this._putElementInTemplate(getElement(t),s):this._config.html?s.innerHTML=this._maybeSanitize(t):s.textContent=t:s.remove())}_maybeSanitize(e){return this._config.sanitize?sanitizeHtml(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return execute(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML="",void t.append(e);t.textContent=e.textContent}}const NAME$6="tooltip",DISALLOWED_ATTRIBUTES=new Set(["sanitize","allowList","sanitizeFn"]),ESCAPE_KEY="Escape",CLASS_NAME_FADE$2="fade",CLASS_NAME_MODAL="modal",CLASS_NAME_SHOW$2="show",SELECTOR_TOOLTIP_INNER=".tooltip-inner",SELECTOR_MODAL=".modal",SELECTOR_DATA_TOGGLE$3='[data-bs-toggle="tooltip"]',EVENT_MODAL_HIDE="hide.bs.modal",TRIGGER_HOVER="hover",TRIGGER_FOCUS="focus",TRIGGER_CLICK="click",TRIGGER_MANUAL="manual",EVENT_HIDE$2="hide",EVENT_HIDDEN$2="hidden",EVENT_SHOW$2="show",EVENT_SHOWN$2="shown",EVENT_INSERTED="inserted",EVENT_CLICK$3="click",EVENT_FOCUSIN$2="focusin",EVENT_FOCUSOUT$1="focusout",EVENT_MOUSEENTER$1="mouseenter",EVENT_MOUSELEAVE="mouseleave",EVENT_KEYDOWN$1="keydown",AttachmentMap={AUTO:"auto",TOP:"top",RIGHT:isRTL$1()?"left":"right",BOTTOM:"bottom",LEFT:isRTL$1()?"right":"left"},Default$5={allowList:DefaultAllowlist,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",floatingConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},DefaultType$5={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",floatingConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Tooltip extends BaseComponent{constructor(e,t){super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._floatingCleanup=null,this._keydownHandler=null,this._templateFactory=null,this._newContent=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this.tip=null,this._parseResponsivePlacements(),this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Default$5}static get DefaultType(){return DefaultType$5}static get NAME(){return NAME$6}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),this._removeEscapeListener(),EventHandler.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposeFloating(),this._disposeMediaQueryListeners(),super.dispose()}async show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const e=EventHandler.trigger(this._element,this.constructor.eventName("show")),t=(findShadowRoot(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return void(this._isHovered=!1);this._disposeFloating();const n=this._getTipElement();this._element.setAttribute("aria-describedby",n.getAttribute("id"));let{container:s}=this._config;const i=this._element.closest("dialog[open]");if(i&&s===document.body&&(s=i),this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(n),EventHandler.trigger(this._element,this.constructor.eventName("inserted"))),await this._createFloating(n),n.classList.add("show"),this._setEscapeListener(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._queueCallback(()=>{EventHandler.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._removeEscapeListener(),this._getTipElement().classList.remove("show"),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposeFloating(),this._element.removeAttribute("aria-describedby"),EventHandler.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._floatingCleanup&&this.tip&&this._updateFloatingPosition()}_isWithContent(){return Boolean(this._getTitle())||this._hasNewContent()}_hasNewContent(){return Boolean(this._newContent)&&Object.values(this._newContent).some(Boolean)}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();t.classList.remove("fade","show"),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=getUID(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add("fade"),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposeFloating(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new TemplateFactory({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[SELECTOR_TOOLTIP_INNER]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains("fade")}_isShown(){return this.tip&&this.tip.classList.contains("show")}_getPlacement(e){if(this._responsivePlacements){const e=getResponsivePlacement(this._responsivePlacements,"top");return AttachmentMap[e.toUpperCase()]||e}const t=execute(this._config.placement,[this,e,this._element]);return AttachmentMap[t.toUpperCase()]||t}_parseResponsivePlacements(){"string"==typeof this._config.placement?(this._responsivePlacements=parseResponsivePlacement(this._config.placement,"top"),this._responsivePlacements&&this._setupMediaQueryListeners()):this._responsivePlacements=null}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}async _createFloating(e){const t=this._getPlacement(e),n=e.querySelector(`.${this.constructor.NAME}-arrow`);await this._updateFloatingPosition(e,t,n),this._floatingCleanup=autoUpdate(this._element,e,()=>this._updateFloatingPosition(e,null,n))}async _updateFloatingPosition(e=this.tip,t=null,n=null){if(!e)return;t||(t=this._getPlacement(e)),n||(n=e.querySelector(`.${this.constructor.NAME}-arrow`));const s=this._getFloatingMiddleware(n),i=this._getFloatingConfig(t,s),{x:o,y:a,placement:l,middlewareData:r}=await computePosition(this._element,e,i);if(Object.assign(e.style,{position:"absolute",left:`${o}px`,top:`${a}px`}),n&&(n.style.position="absolute"),Manipulator.setDataAttribute(e,"placement",l),n&&r.arrow){const{x:e,y:t}=r.arrow,s=l.startsWith("top")||l.startsWith("bottom");Object.assign(n.style,{left:s&&null!==e?`${e}px`:"",top:s||null===t?"":`${t}px`,right:"",bottom:""})}}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_resolvePossibleFunction(e){return execute(e,[this._element,this._element])}_getFloatingMiddleware(e){const t=this._getOffset(),n=[offset("function"==typeof t?t:{mainAxis:t[1]||0,crossAxis:t[0]||0}),flip({fallbackPlacements:this._config.fallbackPlacements}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})];return e&&n.push(arrow({element:e})),n}_getFloatingConfig(e,t){const n={placement:e,middleware:t};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_setListeners(){const e=this._config.trigger.split(" ");for(const t of e)if("click"===t)EventHandler.on(this._element,this.constructor.eventName("click"),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger.click=!(t._isShown()&&t._activeTrigger.click),t.toggle()});else if("manual"!==t){const e="hover"===t?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),n="hover"===t?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");EventHandler.on(this._element,e,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusin"===e.type?"focus":"hover"]=!0,t._enter()}),EventHandler.on(this._element,n,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusout"===e.type?"focus":"hover"]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},EventHandler.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler)}_setEscapeListener(){this._keydownHandler||(this._keydownHandler=e=>{"Escape"===e.key&&this._isShown()&&this.tip.isConnected&&(e.preventDefault(),e.stopPropagation(),this.hide())},this._element.ownerDocument.addEventListener("keydown",this._keydownHandler,!0))}_removeEscapeListener(){this._keydownHandler&&(this._element.ownerDocument.removeEventListener("keydown",this._keydownHandler,!0),this._keydownHandler=null)}_fixTitle(){const e=this._element.getAttribute("title");e&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=Manipulator.getDataAttributes(this._element);for(const e of Object.keys(t))DISALLOWED_ATTRIBUTES.has(e)&&delete t[e];return e={...t,..."object"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:getElement(e.container),"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),"number"!=typeof e.title&&"boolean"!=typeof e.title||(e.title=e.title.toString()),"number"!=typeof e.content&&"boolean"!=typeof e.content||(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger="manual",e}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null),this.tip&&(this.tip.remove(),this.tip=null)}}const initTooltip=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$3);t&&Tooltip.getOrCreateInstance(t)};EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$3,initTooltip),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$3,initTooltip);const NAME$5="popover",SELECTOR_TITLE=".popover-header",SELECTOR_CONTENT=".popover-body",SELECTOR_DATA_TOGGLE$2='[data-bs-toggle="popover"]',EVENT_CLICK$2="click",EVENT_FOCUSIN$1="focusin",EVENT_MOUSEENTER="mouseenter",Default$4={...Tooltip.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},DefaultType$4={...Tooltip.DefaultType,content:"(null|string|element|function)"};class Popover extends Tooltip{static get Default(){return Default$4}static get DefaultType(){return DefaultType$4}static get NAME(){return NAME$5}_isWithContent(){return Boolean(this._getTitle()||this._getContent())||this._hasNewContent()}_getContentForTemplate(){return{[SELECTOR_TITLE]:this._getTitle(),[SELECTOR_CONTENT]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}}const initPopover=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$2);t&&("click"===e.type&&e.preventDefault(),Popover.getOrCreateInstance(t))};EventHandler.on(document,"click",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$2,initPopover);const NAME$4="range",DATA_KEY$4="bs.range",EVENT_KEY$4=".bs.range",DATA_API_KEY$1=".data-api",EVENT_CHANGED="changed.bs.range",EVENT_DOM_CONTENT_LOADED="DOMContentLoaded.bs.range.data-api",EVENT_INPUT="input",EVENT_CHANGE="change",SELECTOR_RANGE=".form-range",SELECTOR_INPUT=".form-range-input",CLASS_NAME_BUBBLE="form-range-bubble",CLASS_NAME_TICKS="form-range-ticks",CLASS_NAME_TICK="form-range-tick",CLASS_NAME_TICK_LABEL="form-range-tick-label",PROPERTY_FILL="--bs-range-fill",Default$3={bubble:!1,formatter:null},DefaultType$3={bubble:"(boolean|null)",formatter:"(function|null)"};class Range extends BaseComponent{constructor(e,t){super(e,t),this._element&&(this._input=SelectorEngine.findOne(SELECTOR_INPUT,this._element),this._input&&(this._bubble=null,this._bubbleText=null,this._ticks=null,this._updateHandler=()=>this._update(),this._config.bubble&&this._createBubble(),this._createTicks(),this._addEventListeners(),this._update()))}static get Default(){return Default$3}static get DefaultType(){return DefaultType$3}static get NAME(){return NAME$4}update(){this._update()}dispose(){EventHandler.off(this._input,"input",this._updateHandler),EventHandler.off(this._input,"change",this._updateHandler),this._bubble?.remove(),this._ticks?.remove(),super.dispose()}_configAfterMerge(e){return null===e.bubble&&(e.bubble=!0),e}_addEventListeners(){EventHandler.on(this._input,"input",this._updateHandler),EventHandler.on(this._input,"change",this._updateHandler)}_min(){return""===this._input.min?0:Number.parseFloat(this._input.min)}_max(){return""===this._input.max?100:Number.parseFloat(this._input.max)}_value(){return Number.parseFloat(this._input.value)}_ratio(){const e=this._max()-this._min();return e>0?(this._value()-this._min())/e:0}_update(){this._element.style.setProperty(PROPERTY_FILL,`${this._ratio()}`),this._bubbleText&&(this._bubbleText.textContent=this._format(this._value())),EventHandler.trigger(this._input,EVENT_CHANGED,{value:this._value()})}_format(e){return"function"==typeof this._config.formatter?this._config.formatter(e):String(e)}_createBubble(){this._bubble=document.createElement("output"),this._bubble.className=`${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`,this._bubble.setAttribute("aria-hidden","true");const e=document.createElement("div");e.className="tooltip-arrow",this._bubbleText=document.createElement("div"),this._bubbleText.className="tooltip-inner",this._bubble.append(e,this._bubbleText),this._input.insertAdjacentElement("afterend",this._bubble)}_createTicks(){const e=this._input.getAttribute("list"),t=e?document.getElementById(e):null;if(!t)return;const n=this._min(),s=this._max()-n||1,i=[];for(const e of SelectorEngine.find("option",t)){const t=Number.parseFloat(e.value);if(!Number.isNaN(t)){const o=Math.min(Math.max((t-n)/s,0),1);i.push({ratio:o,label:e.label})}}if(0===i.length)return;i.sort((e,t)=>e.ratio-t.ratio),this._ticks=document.createElement("div"),this._ticks.className=CLASS_NAME_TICKS,this._ticks.setAttribute("aria-hidden","true");const o=[0,...i.map(e=>e.ratio),1];this._ticks.style.gridTemplateColumns=o.slice(1).map((e,t)=>e-o[t]+"fr").join(" ");for(const[e,t]of i.entries()){const n=document.createElement("span");if(n.className=CLASS_NAME_TICK,n.style.gridColumnStart=`${e+2}`,t.label){const e=document.createElement("span");e.className=CLASS_NAME_TICK_LABEL,e.textContent=t.label,n.append(e)}this._ticks.append(n)}this._element.append(this._ticks)}}EventHandler.on(document,EVENT_DOM_CONTENT_LOADED,()=>{for(const e of SelectorEngine.find(".form-range"))Range.getOrCreateInstance(e)});const NAME$3="scrollspy",DATA_KEY$3="bs.scrollspy",EVENT_KEY$3=`.${DATA_KEY$3}`,DATA_API_KEY=".data-api",EVENT_ACTIVATE=`activate${EVENT_KEY$3}`,EVENT_CLICK$1=`click${EVENT_KEY$3}`,EVENT_SCROLL=`scroll${EVENT_KEY$3}`,EVENT_SCROLLEND=`scrollend${EVENT_KEY$3}`,EVENT_RESIZE=`resize${EVENT_KEY$3}`,EVENT_LOAD_DATA_API$1=`load${EVENT_KEY$3}.data-api`,CLASS_NAME_MENU_ITEM="menu-item",CLASS_NAME_ACTIVE$1="active",SELECTOR_DATA_SPY='[data-bs-spy="scroll"]',SELECTOR_TARGET_LINKS="[href]",SELECTOR_NAV_LIST_GROUP=".nav, .list-group",SELECTOR_NAV_LINKS=".nav-link",SELECTOR_NAV_ITEMS=".nav-item",SELECTOR_LIST_ITEMS=".list-group-item",SELECTOR_LINK_ITEMS=".nav-link, .nav-item > .nav-link, .list-group-item",SELECTOR_MENU_TOGGLE$1='[data-bs-toggle="menu"]',SCROLL_IDLE_TIMEOUT=100,RESIZE_DEBOUNCE=100,Default$2={rootMargin:null,smoothScroll:!1,target:null,threshold:[0],topMargin:"12%"},DefaultType$2={rootMargin:"(string|null)",smoothScroll:"boolean",target:"element",threshold:"array",topMargin:"string"};class ScrollSpy extends BaseComponent{constructor(e,t){super(e,t),this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map,this._intersecting=new Set,this._activeTarget=null,this._lastActive=null,this._atBottom=!1,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._observer=null,this._sentinel=null,this._sentinelObserver=null,this._pendingNavigation=null,this._settleTimeout=null,this._settleHandler=null,this._scrollIdleHandler=null,this._resizeHandler=null,this._resizeTimeout=null,this.refresh()}static get Default(){return Default$2}static get DefaultType(){return DefaultType$2}static get NAME(){return NAME$3}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e);this._setUpSentinel(),this._maybeAddResizeListener()}dispose(){this._observer?.disconnect(),this._teardownSentinel(),this._disarmSettle(),this._removeResizeListener(),EventHandler.off(this._config.target,EVENT_CLICK$1),super.dispose()}_configAfterMerge(e){return e.target=getElement(e.target)||document.body,"string"==typeof e.threshold&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin??this._getDerivedRootMargin()};return new IntersectionObserver(e=>this._onIntersect(e),e)}_onIntersect(e){for(const t of e)t.isIntersecting?this._intersecting.add(t.target):this._intersecting.delete(t.target);this._computeActive()}_computeActive(){if(!this._element?.isConnected||0===this._sections.length)return;let e=null;if(this._atBottom)e=this._sections.at(-1);else{for(const t of this._sections)this._intersecting.has(t)&&(e=t);e||=this._lastActive??this._sections.at(0)}if(!e)return;this._lastActive=e;const t=this._linkBySection.get(e);t&&this._process(t)}_parseTopMargin(){const e=String(this._config.topMargin);return{value:Number.parseFloat(e)||0,unit:e.endsWith("%")?"%":"px"}}_getDerivedRootMargin(){const{value:e,unit:t}=this._parseTopMargin();let n=e;if("px"===t){const t=this._rootElement?this._rootElement.clientHeight:document.documentElement.clientHeight||window.innerHeight;n=t?e/t*100:12}return`0px 0px -${Math.min(Math.max(100-n,0),100)}% 0px`}_usesPixelMargin(){return!this._config.rootMargin&&"px"===this._parseTopMargin().unit}_setUpSentinel(){if(this._teardownSentinel(),0===this._sections.length)return;const e=document.createElement("div");e.setAttribute("aria-hidden","true"),e.style.cssText="position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;",this._element.append(e),this._sentinel=e,this._sentinelObserver=new IntersectionObserver(e=>this._onSentinel(e),{root:this._rootElement,threshold:[0]}),this._sentinelObserver.observe(e)}_onSentinel(e){const t=e.at(-1);this._atBottom=Boolean(t?.isIntersecting)&&this._isOverflowing(),this._computeActive()}_isOverflowing(){const e=this._rootElement||document.scrollingElement||document.documentElement;return e.scrollHeight>e.clientHeight}_teardownSentinel(){this._sentinelObserver?.disconnect(),this._sentinelObserver=null,this._sentinel?.remove(),this._sentinel=null,this._atBottom=!1}_maybeAddResizeListener(){this._removeResizeListener(),this._usesPixelMargin()&&(this._resizeHandler=()=>{clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(()=>this._rebuildObserver(),100)},EventHandler.on(window,EVENT_RESIZE,this._resizeHandler))}_removeResizeListener(){clearTimeout(this._resizeTimeout),this._resizeTimeout=null,this._resizeHandler&&(EventHandler.off(window,EVENT_RESIZE,this._resizeHandler),this._resizeHandler=null)}_rebuildObserver(){if(this._observer){this._observer.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e)}}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(EventHandler.off(this._config.target,EVENT_CLICK$1),EventHandler.on(this._config.target,EVENT_CLICK$1,"[href]",e=>{const t=e.target.closest("[href]"),n=t&&this._sectionByLink.get(t);if(!n||!this._element)return;e.preventDefault();const s=this._rootElement||window,i=n.offsetTop-this._element.offsetTop,o=this._rootElement?this._rootElement.scrollTop:window.scrollY??window.pageYOffset;if(matchMedia("(prefers-reduced-motion: reduce)").matches||Math.abs(o-i)<=2)return s.scrollTo?s.scrollTo({top:i,behavior:"auto"}):s.scrollTop=i,void this._settleNavigation(t.hash,n);this._pendingNavigation={hash:t.hash,section:n},this._armSettle(),s.scrollTo?s.scrollTo({top:i,behavior:"smooth"}):s.scrollTop=i}))}_armSettle(){this._disarmSettle();const e=this._getSettleTarget();this._settleHandler=()=>this._onSettle(),this._scrollIdleHandler=()=>{clearTimeout(this._settleTimeout),this._settleTimeout=setTimeout(()=>this._onSettle(),100)},EventHandler.on(e,EVENT_SCROLLEND,this._settleHandler),EventHandler.on(e,EVENT_SCROLL,this._scrollIdleHandler)}_disarmSettle(){clearTimeout(this._settleTimeout),this._settleTimeout=null;const e=this._getSettleTarget();this._settleHandler&&(EventHandler.off(e,EVENT_SCROLLEND,this._settleHandler),this._settleHandler=null),this._scrollIdleHandler&&(EventHandler.off(e,EVENT_SCROLL,this._scrollIdleHandler),this._scrollIdleHandler=null)}_getSettleTarget(){return this._rootElement||document}_onSettle(){if(this._disarmSettle(),!this._pendingNavigation)return;const{hash:e,section:t}=this._pendingNavigation;this._settleNavigation(e,t)}_settleNavigation(e,t){this._pendingNavigation=null,window.history?.replaceState&&window.history.replaceState(null,"",e),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.focus({preventScroll:!0})}_initializeTargetsAndObservables(){this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map;const e=SelectorEngine.find("[href]",this._config.target),t=new Set;for(const n of e){if(!n.hash||isDisabled(n))continue;const e=decodeFragment(n.hash.slice(1));if(!e)continue;const s=document.getElementById(e);s&&this._element.contains(s)&&isVisible(s)&&(this._sectionByLink.set(n,s),this._linkBySection.set(s,n),t.has(s)||(t.add(s),this._sections.push(s)))}this._sections.sort((e,t)=>e.getBoundingClientRect().top-t.getBoundingClientRect().top)}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add("active"),this._activateParents(e),EventHandler.trigger(this._element,EVENT_ACTIVATE,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains("menu-item")){const t=e.closest(".menu")?.previousElementSibling;return void(t?.matches(SELECTOR_MENU_TOGGLE$1)&&t.classList.add("active"))}for(const t of SelectorEngine.parents(e,".nav, .list-group"))for(const e of SelectorEngine.prev(t,SELECTOR_LINK_ITEMS))e.classList.add("active")}_clearActiveClass(e){e.classList.remove("active");const t=SelectorEngine.find("[href].active",e);for(const e of t)e.classList.remove("active")}}function decodeFragment(e){try{return decodeURIComponent(e)}catch{return e}}EventHandler.on(window,EVENT_LOAD_DATA_API$1,()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_SPY))ScrollSpy.getOrCreateInstance(e)});const NAME$2="tab",DATA_KEY$2="bs.tab",EVENT_KEY$2=".bs.tab",EVENT_HIDE$1="hide.bs.tab",EVENT_HIDDEN$1="hidden.bs.tab",EVENT_SHOW$1="show.bs.tab",EVENT_SHOWN$1="shown.bs.tab",EVENT_CLICK_DATA_API="click.bs.tab",EVENT_KEYDOWN="keydown.bs.tab",EVENT_LOAD_DATA_API="load.bs.tab",ARROW_LEFT_KEY="ArrowLeft",ARROW_RIGHT_KEY="ArrowRight",ARROW_UP_KEY="ArrowUp",ARROW_DOWN_KEY="ArrowDown",HOME_KEY="Home",END_KEY="End",CLASS_NAME_ACTIVE="active",CLASS_NAME_FADE$1="fade",CLASS_NAME_SHOW$1="show",SELECTOR_MENU_TOGGLE='[data-bs-toggle="menu"]',SELECTOR_MENU=".menu",NOT_SELECTOR_MENU_TOGGLE=`:not(${SELECTOR_MENU_TOGGLE})`,SELECTOR_TAB_PANEL='.list-group, .nav, [role="tablist"]',SELECTOR_OUTER=".nav-item, .list-group-item",SELECTOR_INNER=`.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`,SELECTOR_DATA_TOGGLE$1='[data-bs-toggle="tab"]',SELECTOR_INNER_ELEM=`${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`,SELECTOR_DATA_TOGGLE_ACTIVE='.active[data-bs-toggle="tab"]';class Tab extends BaseComponent{constructor(e){super(e),this._parent=this._element.closest(SELECTOR_TAB_PANEL),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),EventHandler.on(this._element,EVENT_KEYDOWN,e=>this._keydown(e)))}static get NAME(){return"tab"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?EventHandler.trigger(t,EVENT_HIDE$1,{relatedTarget:e}):null;EventHandler.trigger(e,EVENT_SHOW$1,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add("active"),this._activate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleMenu(e,!0),EventHandler.trigger(e,EVENT_SHOWN$1,{relatedTarget:t})):e.classList.add("show")},e,e.classList.contains("fade")))}_deactivate(e,t){e&&(e.classList.remove("active"),e.blur(),this._deactivate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleMenu(e,!1),EventHandler.trigger(e,EVENT_HIDDEN$1,{relatedTarget:t})):e.classList.remove("show")},e,e.classList.contains("fade")))}_keydown(e){if(![ARROW_LEFT_KEY,ARROW_RIGHT_KEY,ARROW_UP_KEY,ARROW_DOWN_KEY,HOME_KEY,END_KEY].includes(e.key))return;if(e.altKey||e.ctrlKey||e.metaKey)return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter(e=>!isDisabled(e));let n;if([HOME_KEY,END_KEY].includes(e.key))n=e.key===HOME_KEY?t[0]:t.at(-1);else{const s=[ARROW_RIGHT_KEY,ARROW_DOWN_KEY].includes(e.key);n=getNextActiveElement(t,e.target,s,!0)}n&&(n.focus({preventScroll:!0}),Tab.getOrCreateInstance(n).show())}_getChildren(){return SelectorEngine.find(SELECTOR_INNER_ELEM,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=SelectorEngine.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleMenu(e,t){const n=this._getOuterElement(e),s=SelectorEngine.findOne(SELECTOR_MENU_TOGGLE,n);if(!s)return;const i=SelectorEngine.findOne(".menu",n);s.classList.toggle("active",t),i&&i.classList.toggle("show",t),s.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains("active")}_getInnerElement(e){return e.matches(SELECTOR_INNER_ELEM)?e:SelectorEngine.findOne(SELECTOR_INNER_ELEM,e)}_getOuterElement(e){return e.closest(SELECTOR_OUTER)||e}}EventHandler.on(document,"click.bs.tab",SELECTOR_DATA_TOGGLE$1,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this)||Tab.getOrCreateInstance(this).show()}),EventHandler.on(window,"load.bs.tab",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE))Tab.getOrCreateInstance(e)});const NAME$1="toast",DATA_KEY$1="bs.toast",EVENT_KEY$1=".bs.toast",EVENT_MOUSEOVER="mouseover.bs.toast",EVENT_MOUSEOUT="mouseout.bs.toast",EVENT_FOCUSIN="focusin.bs.toast",EVENT_FOCUSOUT="focusout.bs.toast",EVENT_HIDE="hide.bs.toast",EVENT_HIDDEN="hidden.bs.toast",EVENT_SHOW="show.bs.toast",EVENT_SHOWN="shown.bs.toast",CLASS_NAME_FADE="fade",CLASS_NAME_HIDE="hide",CLASS_NAME_SHOW="show",CLASS_NAME_SHOWING="showing",DefaultType$1={animation:"boolean",autohide:"boolean",delay:"number"},Default$1={animation:!0,autohide:!0,delay:5e3};class Toast extends BaseComponent{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Default$1}static get DefaultType(){return DefaultType$1}static get NAME(){return NAME$1}show(){EventHandler.trigger(this._element,EVENT_SHOW).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),reflow(this._element),this._element.classList.add("show","showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),EventHandler.trigger(this._element,EVENT_SHOWN),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(EventHandler.trigger(this._element,EVENT_HIDE).defaultPrevented||(this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.add("hide"),this._element.classList.remove("showing","show"),EventHandler.trigger(this._element,EVENT_HIDDEN)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove("show"),super.dispose()}isShown(){return this._element.classList.contains("show")}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":this._hasMouseInteraction=t;break;case"focusin":case"focusout":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){EventHandler.on(this._element,EVENT_MOUSEOVER,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_MOUSEOUT,e=>this._onInteraction(e,!1)),EventHandler.on(this._element,EVENT_FOCUSIN,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_FOCUSOUT,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}}enableDismissTrigger(Toast);const NAME="toggler",DATA_KEY="bs.toggler",EVENT_KEY=`.${DATA_KEY}`,EVENT_TOGGLE=`toggle${EVENT_KEY}`,EVENT_TOGGLED=`toggled${EVENT_KEY}`,EVENT_CLICK="click",SELECTOR_DATA_TOGGLE='[data-bs-toggle="toggler"]',DefaultType={attribute:"string",value:"(string|number|boolean)"},Default={attribute:"class",value:null};class Toggler extends BaseComponent{static get Default(){return Default}static get DefaultType(){return DefaultType}static get NAME(){return NAME}toggle(){EventHandler.trigger(this._element,EVENT_TOGGLE).defaultPrevented||(this._execute(),EventHandler.trigger(this._element,EVENT_TOGGLED))}_execute(){const{attribute:e,value:t}=this._config;"id"!==e&&("class"!==e?this._element.getAttribute(e)!==String(t)?this._element.setAttribute(e,t):this._element.removeAttribute(e):this._element.classList.toggle(t))}}eventActionOnPlugin(Toggler,"click",SELECTOR_DATA_TOGGLE,"toggle");export{Alert,Button,Carousel,Chips,Collapse,Combobox,Datepicker,Dialog,Drawer,Menu,NavOverflow,OtpInput,Popover,Range,ScrollSpy,Strength,Tab,Toast,Toggler,Tooltip}; diff --git a/assets/javascripts/bootstrap.js b/assets/javascripts/bootstrap.js index 59302cc3..3425ed45 100644 --- a/assets/javascripts/bootstrap.js +++ b/assets/javascripts/bootstrap.js @@ -1,4493 +1,7943 @@ /*! - * Bootstrap v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bootstrap = factory(global.Popper)); -})(this, (function (Popper) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/data.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - /** - * Constants - */ - - const elementMap = new Map(); - const Data = { - set(element, key, instance) { - if (!elementMap.has(element)) { - elementMap.set(element, new Map()); - } - const instanceMap = elementMap.get(element); - - // make it clear we only want one instance per element - // can be removed later when multiple key/instances are fine to be used - if (!instanceMap.has(key) && instanceMap.size !== 0) { - // eslint-disable-next-line no-console - console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); - return; - } - instanceMap.set(key, instance); - }, - get(element, key) { - if (elementMap.has(element)) { - return elementMap.get(element).get(key) || null; - } - return null; - }, - remove(element, key) { - if (!elementMap.has(element)) { - return; - } - const instanceMap = elementMap.get(element); - instanceMap.delete(key); - - // free up element references if there are no instances left for an element - if (instanceMap.size === 0) { - elementMap.delete(element); - } - } - }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/index.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const MAX_UID = 1000000; - const MILLISECONDS_MULTIPLIER = 1000; - const TRANSITION_END = 'transitionend'; - - /** - * Properly escape IDs selectors to handle weird IDs - * @param {string} selector - * @returns {string} - */ - const parseSelector = selector => { - if (selector && window.CSS && window.CSS.escape) { - // document.querySelector needs escaping to handle IDs (html5+) containing for instance / - selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); - } - return selector; - }; - - // Shout-out Angus Croll (https://goo.gl/pxwQGp) - const toType = object => { - if (object === null || object === undefined) { - return `${object}`; - } - return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); - }; - - /** - * Public Util API - */ - - const getUID = prefix => { - do { - prefix += Math.floor(Math.random() * MAX_UID); - } while (document.getElementById(prefix)); - return prefix; - }; - const getTransitionDurationFromElement = element => { - if (!element) { - return 0; - } - - // Get transition-duration of the element - let { - transitionDuration, - transitionDelay - } = window.getComputedStyle(element); - const floatTransitionDuration = Number.parseFloat(transitionDuration); - const floatTransitionDelay = Number.parseFloat(transitionDelay); - - // Return 0 if element or transition duration is not found - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0; - } - - // If multiple durations are defined, take the first - transitionDuration = transitionDuration.split(',')[0]; - transitionDelay = transitionDelay.split(',')[0]; - return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; - }; - const triggerTransitionEnd = element => { - element.dispatchEvent(new Event(TRANSITION_END)); - }; - const isElement = object => { - if (!object || typeof object !== 'object') { - return false; - } - if (typeof object.jquery !== 'undefined') { - object = object[0]; - } - return typeof object.nodeType !== 'undefined'; - }; - const getElement = object => { - // it's a jQuery object or a node element - if (isElement(object)) { - return object.jquery ? object[0] : object; +import { computePosition, autoUpdate, offset, flip, shift, arrow } from '@floating-ui/dom'; +import { Calendar } from 'vanilla-calendar-pro'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const elementMap = new Map(); +const Data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; } - if (typeof object === 'string' && object.length > 0) { - return document.querySelector(parseSelector(object)); + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; } return null; - }; - const isVisible = element => { - if (!isElement(element) || element.getClientRects().length === 0) { - return false; - } - const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; - // Handle `details` element as its content may falsie appear visible when it is closed - const closedDetails = element.closest('details:not([open])'); - if (!closedDetails) { - return elementIsVisible; - } - if (closedDetails !== element) { - const summary = element.closest('summary'); - if (summary && summary.parentNode !== closedDetails) { - return false; - } - if (summary === null) { - return false; - } - } - return elementIsVisible; - }; - const isDisabled = element => { - if (!element || element.nodeType !== Node.ELEMENT_NODE) { - return true; - } - if (element.classList.contains('disabled')) { - return true; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; } - if (typeof element.disabled !== 'undefined') { - return element.disabled; - } - return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; - }; - const findShadowRoot = element => { - if (!document.documentElement.attachShadow) { - return null; - } - - // Can find the shadow root otherwise it'll return the document - if (typeof element.getRootNode === 'function') { - const root = element.getRootNode(); - return root instanceof ShadowRoot ? root : null; - } - if (element instanceof ShadowRoot) { - return element; + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); - // when we don't find a shadow root - if (!element.parentNode) { - return null; + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); } - return findShadowRoot(element.parentNode); - }; - const noop = () => {}; - - /** - * Trick to restart an element's animation - * - * @param {HTMLElement} element - * @return void - * - * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation - */ - const reflow = element => { - element.offsetHeight; // eslint-disable-line no-unused-expressions - }; - const getjQuery = () => { - if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { - return window.jQuery; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll', 'scrollend']); + +/** + * Private methods + */ + +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); } - return null; + return fn.apply(element, [event]); }; - const DOMContentLoadedCallbacks = []; - const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { - // add listener on the first call when the document is in loading state - if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { - for (const callback of DOMContentLoadedCallbacks) { - callback(); - } +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); } - DOMContentLoadedCallbacks.push(callback); - } else { - callback(); - } - }; - const isRTL = () => document.documentElement.dir === 'rtl'; - const defineJQueryPlugin = plugin => { - onDOMContentLoaded(() => { - const $ = getjQuery(); - /* istanbul ignore if */ - if ($) { - const name = plugin.NAME; - const JQUERY_NO_CONFLICT = $.fn[name]; - $.fn[name] = plugin.jQueryInterface; - $.fn[name].Constructor = plugin; - $.fn[name].noConflict = () => { - $.fn[name] = JQUERY_NO_CONFLICT; - return plugin.jQueryInterface; - }; - } - }); - }; - const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { - return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; - }; - const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { - if (!waitForTransition) { - execute(callback); - return; } - const durationPadding = 5; - const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; - let called = false; - const handler = ({ - target - }) => { - if (target !== transitionElement) { - return; - } - called = true; - transitionElement.removeEventListener(TRANSITION_END, handler); - execute(callback); - }; - transitionElement.addEventListener(TRANSITION_END, handler); - setTimeout(() => { - if (!called) { - triggerTransitionEnd(transitionElement); - } - }, emulatedDuration); - }; - - /** - * Return the previous/next element of a list. - * - * @param {array} list The list of elements - * @param activeElement The active element - * @param shouldGetNext Choose to get next or previous element - * @param isCycleAllowed - * @return {Element|elem} The proper element - */ - const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { - const listLength = list.length; - let index = list.indexOf(activeElement); - - // if the element does not exist in the list return an element - // depending on the direction and if cycle is allowed - if (index === -1) { - return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; - } - index += shouldGetNext ? 1 : -1; - if (isCycleAllowed) { - index = (index + listLength) % listLength; - } - return list[Math.max(0, Math.min(index, listLength - 1))]; }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/event-handler.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const namespaceRegex = /[^.]*(?=\..*)\.|.*/; - const stripNameRegex = /\..*/; - const stripUidRegex = /::\d+$/; - const eventRegistry = {}; // Events storage - let uidEvent = 1; - const customEvents = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' - }; - const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); - - /** - * Private methods - */ - - function makeEventUid(element, uid) { - return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; } - function getElementEvents(element) { - const uid = makeEventUid(element); - element.uidEvent = uid; - eventRegistry[uid] = eventRegistry[uid] || {}; - return eventRegistry[uid]; + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; } - function bootstrapHandler(element, fn) { - return function handler(event) { - hydrateObj(event, { - delegateTarget: element - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, fn); - } - return fn.apply(element, [event]); - }; - } - function bootstrapDelegationHandler(element, selector, fn) { - return function handler(event) { - const domElements = element.querySelectorAll(selector); - for (let { - target - } = event; target && target !== this; target = target.parentNode) { - for (const domElement of domElements) { - if (domElement !== target) { - continue; - } - hydrateObj(event, { - delegateTarget: target - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, selector, fn); - } - return fn.apply(target, [event]); + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); } - } + }; }; + callable = wrapFunction(callable); + } + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; } - function findHandler(events, callable, delegationSelector = null) { - return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; } - function normalizeParameters(originalTypeEvent, handler, delegationFunction) { - const isDelegated = typeof handler === 'string'; - // TODO: tooltip passes `false` instead of selector, so we need to check - const callable = isDelegated ? delegationFunction : handler || delegationFunction; - let typeEvent = getTypeEvent(originalTypeEvent); - if (!nativeEvents.has(typeEvent)) { - typeEvent = originalTypeEvent; + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } - return [isDelegated, callable, typeEvent]; } - function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { if (typeof originalTypeEvent !== 'string' || !element) { return; } - let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - - // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position - // this prevents the handler from being dispatched the same way as mouseover or mouseout does - if (originalTypeEvent in customEvents) { - const wrapFunction = fn => { - return function (event) { - if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { - return fn.call(this, event); - } - }; - }; - callable = wrapFunction(callable); - } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; const events = getElementEvents(element); - const handlers = events[typeEvent] || (events[typeEvent] = {}); - const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); - if (previousFunction) { - previousFunction.oneOff = previousFunction.oneOff && oneOff; + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); return; } - const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); - const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); - fn.delegationSelector = isDelegated ? handler : null; - fn.callable = callable; - fn.oneOff = oneOff; - fn.uidEvent = uid; - handlers[uid] = fn; - element.addEventListener(typeEvent, fn, isDelegated); - } - function removeHandler(element, events, typeEvent, handler, delegationSelector) { - const fn = findHandler(events[typeEvent], handler, delegationSelector); - if (!fn) { - return; + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } } - element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); - delete events[typeEvent][fn.uidEvent]; - } - function removeNamespacedHandlers(element, events, typeEvent, namespace) { - const storeElementEvent = events[typeEvent] || {}; - for (const [handlerKey, event] of Object.entries(storeElementEvent)) { - if (handlerKey.includes(namespace)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; } - function getTypeEvent(event) { - // allow to get the native events from namespaced events ('click.bs.button' --> 'click') - event = event.replace(stripNameRegex, ''); - return customEvents[event] || event; - } - const EventHandler = { - on(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, false); - }, - one(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, true); - }, - off(element, originalTypeEvent, handler, delegationFunction) { - if (typeof originalTypeEvent !== 'string' || !element) { - return; - } - const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - const inNamespace = typeEvent !== originalTypeEvent; - const events = getElementEvents(element); - const storeElementEvent = events[typeEvent] || {}; - const isNamespace = originalTypeEvent.startsWith('.'); - if (typeof callable !== 'undefined') { - // Simplest case: handler is passed, remove that listener ONLY. - if (!Object.keys(storeElementEvent).length) { - return; - } - removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); - return; - } - if (isNamespace) { - for (const elementEvent of Object.keys(events)) { - removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); - } - } - for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { - const handlerKey = keyHandlers.replace(stripUidRegex, ''); - if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; } - } - }, - trigger(element, event, args) { - if (typeof event !== 'string' || !element) { - return null; - } - const $ = getjQuery(); - const typeEvent = getTypeEvent(event); - const inNamespace = event !== typeEvent; - let jQueryEvent = null; - let bubbles = true; - let nativeDispatch = true; - let defaultPrevented = false; - if (inNamespace && $) { - jQueryEvent = $.Event(event, args); - $(element).trigger(jQueryEvent); - bubbles = !jQueryEvent.isPropagationStopped(); - nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); - defaultPrevented = jQueryEvent.isDefaultPrevented(); - } - const evt = hydrateObj(new Event(event, { - bubbles, - cancelable: true - }), args); - if (defaultPrevented) { - evt.preventDefault(); - } - if (nativeDispatch) { - element.dispatchEvent(evt); - } - if (evt.defaultPrevented && jQueryEvent) { - jQueryEvent.preventDefault(); - } - return evt; + }); } - }; - function hydrateObj(obj, meta = {}) { - for (const [key, value] of Object.entries(meta)) { - try { - obj[key] = value; - } catch (_unused) { - Object.defineProperty(obj, key, { - configurable: true, - get() { - return value; - } - }); - } + } + return obj; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +function normalizeData(value) { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; } - return obj; + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; } - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/manipulator.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } - function normalizeData(value) { - if (value === 'true') { - return true; - } - if (value === 'false') { + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { return false; } - if (value === Number(value).toString()) { - return Number(value); - } - if (value === '' || value === 'null') { - return null; - } - if (typeof value !== 'string') { - return value; - } - try { - return JSON.parse(decodeURIComponent(value)); - } catch (_unused) { - return value; + if (summary === null) { + return false; } } - function normalizeDataKey(key) { - return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; } - const Manipulator = { - setDataAttribute(element, key, value) { - element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); - }, - removeDataAttribute(element, key) { - element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); - }, - getDataAttributes(element) { - if (!element) { - return {}; - } - const attributes = {}; - const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); - for (const key of bsKeys) { - let pureKey = key.replace(/^bs/, ''); - pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); - attributes[pureKey] = normalizeData(element.dataset[key]); - } - return attributes; - }, - getDataAttribute(element, key) { - return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { + return null; + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const isRTL = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { + return; } + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/config.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Class definition - */ - class Config { - // Getters - static get Default() { - return {}; - } - static get DefaultType() { - return {}; - } - static get NAME() { - throw new Error('You have to implement the static method "NAME", for each component!'); - } - _getConfig(config) { - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - return config; - } - _mergeConfigObj(config, element) { - const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse +/** + * Class definition + */ - return { - ...this.constructor.Default, - ...(typeof jsonConfig === 'object' ? jsonConfig : {}), - ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), - ...(typeof config === 'object' ? config : {}) - }; - } - _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const [property, expectedTypes] of Object.entries(configTypes)) { - const value = config[property]; - const valueType = isElement(value) ? 'element' : toType(value); - if (!new RegExp(expectedTypes).test(valueType)) { - throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); - } +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); } } } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap base-component.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const VERSION = '5.3.8'; +const VERSION = '6.0.0-alpha1'; - /** - * Class definition - */ +/** + * Class definition + */ - class BaseComponent extends Config { - constructor(element, config) { - super(); - element = getElement(element); - if (!element) { - return; - } - this._element = element; - this._config = this._getConfig(config); - Data.set(this._element, this.constructor.DATA_KEY, this); +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; } + this._element = element; + this._config = this._getConfig(config); - // Public - dispose() { - Data.remove(this._element, this.constructor.DATA_KEY); - EventHandler.off(this._element, this.constructor.EVENT_KEY); - for (const propertyName of Object.getOwnPropertyNames(this)) { - this[propertyName] = null; - } + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); } + Data.set(this._element, this.constructor.DATA_KEY, this); + } - // Private - _queueCallback(callback, element, isAnimated = true) { - executeAfterTransition(callback, element, isAnimated); + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; } - _getConfig(config) { - config = this._mergeConfigObj(config, this._element); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; + } + + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; } - // Static - static getInstance(element) { - return Data.get(getElement(element), this.DATA_KEY); + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; } - static getOrCreateInstance(element, config = {}) { - return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; + } + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; + } + next = next.nextElementSibling; + } + return []; + }, + focusableChildren(element) { + 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)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); } - static get VERSION() { - return VERSION; + if (isDisabled(this)) { + return; } - static get DATA_KEY() { - return `bs.${this.NAME}`; + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); } - static get EVENT_KEY() { - return `.${this.DATA_KEY}`; + for (const instance of instances) { + instance[method](); } - static eventName(name) { - return `${name}${this.EVENT_KEY}`; + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event + }); + }); +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$l = 'alert'; +const DATA_KEY$h = 'bs.alert'; +const EVENT_KEY$i = `.${DATA_KEY$h}`; +const EVENT_CLOSE = `close${EVENT_KEY$i}`; +const EVENT_CLOSED = `closed${EVENT_KEY$i}`; +const CLASS_NAME_FADE$4 = 'fade'; +const CLASS_NAME_SHOW$6 = 'show'; + +/** + * Class definition + */ + +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME$l; } - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/selector-engine.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const getSelector = element => { - let selector = element.getAttribute('data-bs-target'); - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href'); + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; + } + this._element.classList.remove(CLASS_NAME_SHOW$6); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$4); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + } - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { - return null; - } + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Alert, 'close'); + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$k = 'button'; +const DATA_KEY$g = 'bs.button'; +const EVENT_KEY$h = `.${DATA_KEY$g}`; +const DATA_API_KEY$c = '.data-api'; +const CLASS_NAME_ACTIVE$4 = 'active'; +const SELECTOR_DATA_TOGGLE$a = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API$8 = `click${EVENT_KEY$h}${DATA_API_KEY$c}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME$k; + } - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}`; - } - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$4)); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$8, SELECTOR_DATA_TOGGLE$a, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE$a); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$j = 'carousel'; +const DATA_KEY$f = 'bs.carousel'; +const EVENT_KEY$g = `.${DATA_KEY$f}`; +const DATA_API_KEY$b = '.data-api'; +const ARROW_LEFT_KEY$2 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$2 = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY$g}`; +const EVENT_SLID = `slid${EVENT_KEY$g}`; +const EVENT_KEYDOWN$2 = `keydown${EVENT_KEY$g}`; +const EVENT_MOUSEENTER$2 = `mouseenter${EVENT_KEY$g}`; +const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$g}`; +const EVENT_POINTERDOWN$1 = `pointerdown${EVENT_KEY$g}`; +const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$g}${DATA_API_KEY$b}`; +const EVENT_CLICK_DATA_API$7 = `click${EVENT_KEY$g}${DATA_API_KEY$b}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE$3 = 'active'; +const CLASS_NAME_FADE$3 = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER$1 = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY$2]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY$2]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default$i = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType$i = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER$1, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); } - return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; - }; - const SelectorEngine = { - find(selector, element = document.documentElement) { - return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); - }, - findOne(selector, element = document.documentElement) { - return Element.prototype.querySelector.call(element, selector); - }, - children(element, selector) { - return [].concat(...element.children).filter(child => child.matches(selector)); - }, - parents(element, selector) { - const parents = []; - let ancestor = element.parentNode.closest(selector); - while (ancestor) { - parents.push(ancestor); - ancestor = ancestor.parentNode.closest(selector); - } - return parents; - }, - prev(element, selector) { - let previous = element.previousElementSibling; - while (previous) { - if (previous.matches(selector)) { - return [previous]; - } - previous = previous.previousElementSibling; - } - return []; - }, - // TODO: this is now unused; remove later along with prev() - next(element, selector) { - let next = element.nextElementSibling; - while (next) { - if (next.matches(selector)) { - return [next]; - } - next = next.nextElementSibling; - } - return []; - }, - focusableChildren(element) { - 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)); - }, - getSelectorFromElement(element) { - const selector = getSelector(element); - if (selector) { - return SelectorEngine.findOne(selector) ? selector : null; - } - return null; - }, - getElementFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.findOne(selector) : null; - }, - getMultipleElementsFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.find(selector) : []; + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default$i; + } + static get DefaultType() { + return DefaultType$i; + } + static get NAME() { + return NAME$j; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); } - }; + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); - /** - * -------------------------------------------------------------------------- - * Bootstrap util/component-functions.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const enableDismissTrigger = (component, method = 'hide') => { - const clickEvent = `click.dismiss${component.EVENT_KEY}`; - const name = component.NAME; - EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; } - if (isDisabled(this)) { + if (rawIndex < 0) { + this._loopTransition(false); return; } - const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); - const instance = component.getOrCreateInstance(target); - - // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method - instance[method](); + } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex }); - }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$f = 'alert'; - const DATA_KEY$a = 'bs.alert'; - const EVENT_KEY$b = `.${DATA_KEY$a}`; - const EVENT_CLOSE = `close${EVENT_KEY$b}`; - const EVENT_CLOSED = `closed${EVENT_KEY$b}`; - const CLASS_NAME_FADE$5 = 'fade'; - const CLASS_NAME_SHOW$8 = 'show'; - - /** - * Class definition - */ - - class Alert extends BaseComponent { - // Getters - static get NAME() { - return NAME$f; + if (slideEvent.defaultPrevented) { + return; } - - // Public - close() { - const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); - if (closeEvent.defaultPrevented) { - return; - } - this._element.classList.remove(CLASS_NAME_SHOW$8); - const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5); - this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + if (this._isFade()) { + this._fadeTo(targetIndex); + return; } - // Private - _destroyElement() { - this._element.remove(); - EventHandler.trigger(this._element, EVENT_CLOSED); - this.dispose(); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Alert.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); - } + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); } - - /** - * Data API implementation - */ - - enableDismissTrigger(Alert, 'close'); - - /** - * jQuery - */ - - defineJQueryPlugin(Alert); - - /** - * -------------------------------------------------------------------------- - * Bootstrap button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$e = 'button'; - const DATA_KEY$9 = 'bs.button'; - const EVENT_KEY$a = `.${DATA_KEY$9}`; - const DATA_API_KEY$6 = '.data-api'; - const CLASS_NAME_ACTIVE$3 = 'active'; - const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="button"]'; - const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`; - - /** - * Class definition - */ - - class Button extends BaseComponent { - // Getters - static get NAME() { - return NAME$e; + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); } - - // Public - toggle() { - // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method - this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3)); + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Button.getOrCreateInstance(this); - if (config === 'toggle') { - data[config](); - } - }); + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => { - event.preventDefault(); - const button = event.target.closest(SELECTOR_DATA_TOGGLE$5); - const data = Button.getOrCreateInstance(button); - data.toggle(); - }); + this._viewport.style.scrollSnapType = ''; - /** - * jQuery - */ - - defineJQueryPlugin(Button); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/swipe.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$d = 'swipe'; - const EVENT_KEY$9 = '.bs.swipe'; - const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; - const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; - const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; - const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; - const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; - const POINTER_TYPE_TOUCH = 'touch'; - const POINTER_TYPE_PEN = 'pen'; - const CLASS_NAME_POINTER_EVENT = 'pointer-event'; - const SWIPE_THRESHOLD = 40; - const Default$c = { - endCallback: null, - leftCallback: null, - rightCallback: null - }; - const DefaultType$c = { - endCallback: '(function|null)', - leftCallback: '(function|null)', - rightCallback: '(function|null)' - }; - - /** - * Class definition - */ - - class Swipe extends Config { - constructor(element, config) { - super(); - this._element = element; - if (!element || !Swipe.isSupported()) { - return; - } - this._config = this._getConfig(config); - this._deltaX = 0; - this._supportPointerEvents = Boolean(window.PointerEvent); - this._initEvents(); - } + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY$g); + super.dispose(); + } - // Getters - static get Default() { - return Default$c; + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default$i.ends; } - static get DefaultType() { - return DefaultType$c; + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN$2, event => this._keydown(event)); } - static get NAME() { - return NAME$d; + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER$2, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); } - // Public - dispose() { - EventHandler.off(this._element, EVENT_KEY$9); + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN$1, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; } - - // Private - _start(event) { - if (!this._supportPointerEvents) { - this._deltaX = event.touches[0].clientX; - return; - } - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX; + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { + this.next(); } } - _end(event) { - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX - this._deltaX; - } - this._handleSwipe(); - execute(this._config.endCallback); + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; } - _move(event) { - this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX; + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); } - _handleSwipe() { - const absDeltaX = Math.abs(this._deltaX); - if (absDeltaX <= SWIPE_THRESHOLD) { - return; - } - const direction = absDeltaX / this._deltaX; - this._deltaX = 0; - if (!direction) { - return; - } - execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; } - _initEvents() { - if (this._supportPointerEvents) { - EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); - EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); - this._element.classList.add(CLASS_NAME_POINTER_EVENT); - } else { - EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); - EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); - EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); - } + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); + } + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; + } + } + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; } - _eventIsPointerPenTouch(event) { - return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); - } - - // Static - static isSupported() { - return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; - } - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$c = 'carousel'; - const DATA_KEY$8 = 'bs.carousel'; - const EVENT_KEY$8 = `.${DATA_KEY$8}`; - const DATA_API_KEY$5 = '.data-api'; - const ARROW_LEFT_KEY$1 = 'ArrowLeft'; - const ARROW_RIGHT_KEY$1 = 'ArrowRight'; - const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - const ORDER_NEXT = 'next'; - const ORDER_PREV = 'prev'; - const DIRECTION_LEFT = 'left'; - const DIRECTION_RIGHT = 'right'; - const EVENT_SLIDE = `slide${EVENT_KEY$8}`; - const EVENT_SLID = `slid${EVENT_KEY$8}`; - const EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`; - const EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`; - const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`; - const EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`; - const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; - const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; - const CLASS_NAME_CAROUSEL = 'carousel'; - const CLASS_NAME_ACTIVE$2 = 'active'; - const CLASS_NAME_SLIDE = 'slide'; - const CLASS_NAME_END = 'carousel-item-end'; - const CLASS_NAME_START = 'carousel-item-start'; - const CLASS_NAME_NEXT = 'carousel-item-next'; - const CLASS_NAME_PREV = 'carousel-item-prev'; - const SELECTOR_ACTIVE = '.active'; - const SELECTOR_ITEM = '.carousel-item'; - const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; - const SELECTOR_ITEM_IMG = '.carousel-item img'; - const SELECTOR_INDICATORS = '.carousel-indicators'; - const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; - const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'; - const KEY_TO_DIRECTION = { - [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT, - [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT - }; - const Default$b = { - interval: 5000, - keyboard: true, - pause: 'hover', - ride: false, - touch: true, - wrap: true - }; - const DefaultType$b = { - interval: '(number|boolean)', - // TODO:v6 remove boolean support - keyboard: 'boolean', - pause: '(string|boolean)', - ride: '(boolean|string)', - touch: 'boolean', - wrap: 'boolean' - }; - /** - * Class definition - */ + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); + } + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } - class Carousel extends BaseComponent { - constructor(element, config) { - super(element, config); - this._interval = null; - this._activeElement = null; - this._isSliding = false; - this.touchTimeout = null; - this._swipeHelper = null; - this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); - this._addEventListeners(); - if (this._config.ride === CLASS_NAME_CAROUSEL) { - this.cycle(); - } + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; + } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; + + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; } + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; + } + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; + } - // Getters - static get Default() { - return Default$b; - } - static get DefaultType() { - return DefaultType$b; + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); + } + + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; } - static get NAME() { - return NAME$c; + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE$3); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); + } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); + } + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } - // Public - next() { - this._slide(ORDER_NEXT); + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; } - nextWhenVisible() { - // FIXME TODO use `document.visibilityState` - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && isVisible(this._element)) { - this.next(); - } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE$3, index === this._activeIndex); } - prev() { - this._slide(ORDER_PREV); + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; } - pause() { - if (this._isSliding) { - triggerTransitionEnd(this._element); + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true + }); } - this._clearInterval(); + control.disabled = disabled; } - cycle() { - this._clearInterval(); - this._updateInterval(); - this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval); + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; } - _maybeEnableCycle() { - if (!this._config.ride) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.cycle()); - return; - } - this.cycle(); + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE$3); + active.removeAttribute('aria-current'); } - to(index) { - const items = this._getItems(); - if (index > items.length - 1 || index < 0) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.to(index)); - return; - } - const activeIndex = this._getItemIndex(this._getActive()); - if (activeIndex === index) { - return; - } - const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV; - this._slide(order, items[index]); + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE$3); + newActive.setAttribute('aria-current', 'true'); } - dispose() { - if (this._swipeHelper) { - this._swipeHelper.dispose(); - } - super.dispose(); + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; } - - // Private - _configAfterMerge(config) { - config.defaultInterval = config.interval; - return config; + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; } - _addEventListeners() { - if (this._config.keyboard) { - EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event)); - } - if (this._config.pause === 'hover') { - EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause()); - EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); - } - if (this._config.touch && Swipe.isSupported()) { - this._addTouchEventListeners(); - } + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; } - _addTouchEventListeners() { - for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { - EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()); - } - const endCallBack = () => { - if (this._config.pause !== 'hover') { - return; - } + return index; + } - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } - this.pause(); - if (this.touchTimeout) { - clearTimeout(this.touchTimeout); - } - this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval); - }; - const swipeConfig = { - leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)), - rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)), - endCallback: endCallBack - }; - this._swipeHelper = new Swipe(this._element, swipeConfig); - } - _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - const direction = KEY_TO_DIRECTION[event.key]; - if (direction) { - event.preventDefault(); - this._slide(this._directionToOrder(direction)); - } - } - _getItemIndex(element) { - return this._getItems().indexOf(element); - } - _setActiveIndicatorElement(index) { - if (!this._indicatorsElement) { - return; - } - const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); - activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2); - activeIndicator.removeAttribute('aria-current'); - const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); - if (newActiveIndicator) { - newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2); - newActiveIndicator.setAttribute('aria-current', 'true'); - } + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; } - _updateInterval() { - const element = this._activeElement || this._getActive(); - if (!element) { - return; - } - const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10); - this._config.interval = elementInterval || this._config.defaultInterval; + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } - _slide(order, element = null) { - if (this._isSliding) { - return; - } - const activeElement = this._getActive(); - const isNext = order === ORDER_NEXT; - const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap); - if (nextElement === activeElement) { - return; - } - const nextElementIndex = this._getItemIndex(nextElement); - const triggerEvent = eventName => { - return EventHandler.trigger(this._element, eventName, { - relatedTarget: nextElement, - direction: this._orderToDirection(order), - from: this._getItemIndex(activeElement), - to: nextElementIndex - }); - }; - const slideEvent = triggerEvent(EVENT_SLIDE); - if (slideEvent.defaultPrevented) { - return; - } - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - // TODO: change tests that use empty divs to avoid this check + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); return; } - const isCycling = Boolean(this._interval); - this.pause(); - this._isSliding = true; - this._setActiveIndicatorElement(nextElementIndex); - this._activeElement = nextElement; - const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END; - const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV; - nextElement.classList.add(orderClassName); - reflow(nextElement); - activeElement.classList.add(directionalClassName); - nextElement.classList.add(directionalClassName); - const completeCallBack = () => { - nextElement.classList.remove(directionalClassName, orderClassName); - nextElement.classList.add(CLASS_NAME_ACTIVE$2); - activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName); - this._isSliding = false; - triggerEvent(EVENT_SLID); - }; - this._queueCallback(completeCallBack, activeElement, this._isAnimated()); - if (isCycling) { - this.cycle(); - } - } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_SLIDE); - } - _getActive() { - return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + this._scheduleAutoplay(upcoming); + }, interval); + } + + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { + return; } - _getItems() { - return SelectorEngine.find(SELECTOR_ITEM, this._element); + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); + return; } - _clearInterval() { - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { + return; } - _directionToOrder(direction) { - if (isRTL()) { - return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT; - } - return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV; + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); } - _orderToDirection(order) { - if (isRTL()) { - return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE$3); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; } + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Carousel.getOrCreateInstance(this, config); - if (typeof config === 'number') { - data.to(config); - return; - } - if (typeof config === 'string') { - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); + + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$i = 'collapse'; +const DATA_KEY$e = 'bs.collapse'; +const EVENT_KEY$f = `.${DATA_KEY$e}`; +const DATA_API_KEY$a = '.data-api'; +const EVENT_SHOW$7 = `show${EVENT_KEY$f}`; +const EVENT_SHOWN$6 = `shown${EVENT_KEY$f}`; +const EVENT_HIDE$6 = `hide${EVENT_KEY$f}`; +const EVENT_HIDDEN$8 = `hidden${EVENT_KEY$f}`; +const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$f}${DATA_API_KEY$a}`; +const CLASS_NAME_SHOW$5 = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE$9 = '[data-bs-toggle="collapse"]'; +const Default$h = { + parent: null, + toggle: true +}; +const DefaultType$h = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$9); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); } } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$h; + } + static get DefaultType() { + return DefaultType$h; + } + static get NAME() { + return NAME$i; + } - EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); + } + } + show() { + if (this._isTransitioning || this._isShown()) { return; } - event.preventDefault(); - const carousel = Carousel.getOrCreateInstance(target); - const slideIndex = this.getAttribute('data-bs-slide-to'); - if (slideIndex) { - carousel.to(slideIndex); - carousel._maybeEnableCycle(); + let activeChildren = []; + + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); + } + if (activeChildren.length && activeChildren[0]._isTransitioning) { return; } - if (Manipulator.getDataAttribute(this, 'slide') === 'next') { - carousel.next(); - carousel._maybeEnableCycle(); + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$7); + if (startEvent.defaultPrevented) { return; } - carousel.prev(); - carousel._maybeEnableCycle(); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { - const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE); - for (const carousel of carousels) { - Carousel.getOrCreateInstance(carousel); + for (const activeInstance of activeChildren) { + activeInstance.hide(); } - }); - - /** - * jQuery - */ - - defineJQueryPlugin(Carousel); - - /** - * -------------------------------------------------------------------------- - * Bootstrap collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$b = 'collapse'; - const DATA_KEY$7 = 'bs.collapse'; - const EVENT_KEY$7 = `.${DATA_KEY$7}`; - const DATA_API_KEY$4 = '.data-api'; - const EVENT_SHOW$6 = `show${EVENT_KEY$7}`; - const EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`; - const EVENT_HIDE$6 = `hide${EVENT_KEY$7}`; - const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`; - const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`; - const CLASS_NAME_SHOW$7 = 'show'; - const CLASS_NAME_COLLAPSE = 'collapse'; - const CLASS_NAME_COLLAPSING = 'collapsing'; - const CLASS_NAME_COLLAPSED = 'collapsed'; - const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; - const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; - const WIDTH = 'width'; - const HEIGHT = 'height'; - const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; - const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="collapse"]'; - const Default$a = { - parent: null, - toggle: true - }; - const DefaultType$a = { - parent: '(null|element)', - toggle: 'boolean' - }; - - /** - * Class definition - */ - - class Collapse extends BaseComponent { - constructor(element, config) { - super(element, config); + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { this._isTransitioning = false; - this._triggerArray = []; - const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4); - for (const elem of toggleList) { - const selector = SelectorEngine.getSelectorFromElement(elem); - const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); - if (selector !== null && filterElement.length) { - this._triggerArray.push(elem); - } + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN$6); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); } - this._initializeChildren(); - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN$8); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } + + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW$5); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); + } + } + } + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; + } + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$9, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/floating-ui.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Breakpoints for responsive placement (matches SCSS $breakpoints) + */ +const BREAKPOINTS = { + sm: 576, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536 +}; + +/** + * Parse a placement string that may contain responsive prefixes + * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' } + * + * @param {string} placementString - The placement string to parse + * @param {string} defaultPlacement - The default placement to use for xs/base + * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive + */ +const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => { + // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end") + if (!placementString || !placementString.includes(':')) { + return null; + } + + // Parse the placement string into breakpoint-keyed object + const parts = placementString.split(/\s+/); + const placements = { + xs: defaultPlacement + }; // Default fallback + + for (const part of parts) { + if (part.includes(':')) { + // Responsive placement like "md:top-end" + const [breakpoint, placement] = part.split(':'); + if (BREAKPOINTS[breakpoint] !== undefined) { + placements[breakpoint] = placement; + } + } else { + // Base placement (no prefix = xs/default) + placements.xs = part; + } + } + return placements; +}; + +/** + * Get the active placement for the current viewport width + * + * @param {object} responsivePlacements - Object with breakpoint keys and placement values + * @param {string} defaultPlacement - Fallback placement + * @returns {string} - The active placement for current viewport + */ +const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => { + if (!responsivePlacements) { + return defaultPlacement; + } + + // Get current viewport width + const viewportWidth = window.innerWidth; + + // Find the largest breakpoint that matches + let activePlacement = responsivePlacements.xs || defaultPlacement; + + // Check breakpoints in order (sm, md, lg, xl, 2xl) + const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']; + for (const breakpoint of breakpointOrder) { + const minWidth = BREAKPOINTS[breakpoint]; + if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) { + activePlacement = responsivePlacements[breakpoint]; + } + } + return activePlacement; +}; + +/** + * Create media query listeners for responsive placement changes + * + * @param {Function} callback - Callback to run when breakpoint changes + * @returns {Array} - Array of { mql, handler } objects for cleanup + */ +const createBreakpointListeners = callback => { + const listeners = []; + for (const breakpoint of Object.keys(BREAKPOINTS)) { + const minWidth = BREAKPOINTS[breakpoint]; + const mql = window.matchMedia(`(min-width: ${minWidth}px)`); + mql.addEventListener('change', callback); + listeners.push({ + mql, + handler: callback + }); + } + return listeners; +}; + +/** + * Clean up media query listeners + * + * @param {Array} listeners - Array of { mql, handler } objects + */ +const disposeBreakpointListeners = listeners => { + for (const { + mql, + handler + } of listeners) { + mql.removeEventListener('change', handler); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap menu.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$h = 'menu'; +const DATA_KEY$d = 'bs.menu'; +const EVENT_KEY$e = `.${DATA_KEY$d}`; +const DATA_API_KEY$9 = '.data-api'; +const ESCAPE_KEY$2 = 'Escape'; +const TAB_KEY$1 = 'Tab'; +const ARROW_UP_KEY$2 = 'ArrowUp'; +const ARROW_DOWN_KEY$2 = 'ArrowDown'; +const ARROW_LEFT_KEY$1 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$1 = 'ArrowRight'; +const HOME_KEY$2 = 'Home'; +const END_KEY$2 = 'End'; +const ENTER_KEY$1 = 'Enter'; +const SPACE_KEY$1 = ' '; +const RIGHT_MOUSE_BUTTON = 2; +const SUBMENU_CLOSE_DELAY = 100; +const EVENT_HIDE$5 = `hide${EVENT_KEY$e}`; +const EVENT_HIDDEN$7 = `hidden${EVENT_KEY$e}`; +const EVENT_SHOW$6 = `show${EVENT_KEY$e}`; +const EVENT_SHOWN$5 = `shown${EVENT_KEY$e}`; +const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$e}${DATA_API_KEY$9}`; +const CLASS_NAME_SHOW$4 = 'show'; +const SELECTOR_DATA_TOGGLE$8 = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'; +const SELECTOR_MENU$2 = '.menu'; +const SELECTOR_SUBMENU = '.submenu'; +const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'; +const SELECTOR_NAVBAR_NAV = '.navbar-nav'; +const SELECTOR_VISIBLE_ITEMS$1 = '.menu-item:not(.disabled):not(:disabled)'; +const DEFAULT_PLACEMENT = 'bottom-start'; +const SUBMENU_PLACEMENT = 'end-start'; +const resolveLogicalPlacement = placement => { + if (isRTL()) { + return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left'); + } + return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right'); +}; +const triangleSign = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +const Default$g = { + autoClose: true, + boundary: 'clippingParents', + container: false, + display: 'dynamic', + offset: [0, 2], + floatingConfig: null, + menu: null, + placement: DEFAULT_PLACEMENT, + reference: 'toggle', + strategy: 'absolute', + submenuTrigger: 'both', + submenuDelay: SUBMENU_CLOSE_DELAY +}; +const DefaultType$g = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + container: '(string|element|boolean)', + display: 'string', + offset: '(array|string|function)', + floatingConfig: '(null|object|function)', + menu: '(null|element)', + placement: 'string', + reference: '(string|element|object)', + strategy: 'string', + submenuTrigger: 'string', + submenuDelay: 'number' +}; + +/** + * Class definition + */ + +class Menu extends BaseComponent { + static _openInstances = new Set(); + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)'); + } + super(element, config); + this._floatingCleanup = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + this._parent = this._element.parentNode; // menu wrapper + this._openSubmenus = new Map(); + this._submenuCloseTimeouts = new Map(); + this._hoverIntentData = null; + this._menu = this._config.menu || this._findMenu(); + + // When the menu was discovered from the DOM, refine the wrapper to the closest + // ancestor that actually contains it, so the toggle doesn't have to be a direct + // sibling of `.menu` (e.g. when wrapped by web components). The wrapper still + // receives `.show` and acts as the `reference: 'parent'` positioning anchor. + if (!this._config.menu && this._menu) { + this._parent = this._findWrapper(this._menu); + } + this._isSubmenu = this._parent.classList?.contains('submenu'); + this._menuOriginalParent = this._menu?.parentNode; + this._parseResponsivePlacements(); + this._setupSubmenuListeners(); + } + + // Getters + static get Default() { + return Default$g; + } + static get DefaultType() { + return DefaultType$g; + } + static get NAME() { + return NAME$h; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$6, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._moveMenuToContainer(); + this._createFloating(); + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus({ + focusVisible: false + }); + this._element.setAttribute('aria-expanded', 'true'); + this._menu.classList.add(CLASS_NAME_SHOW$4); + this._element.classList.add(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_SHOW$4); + } + Menu._openInstances.add(this); + EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._disposeMediaQueryListeners(); + this._closeAllSubmenus(); + this._clearAllSubmenuTimeouts(); + Menu._openInstances.delete(this); + super.dispose(); + } + update() { + if (this._floatingCleanup) { + this._updateFloatingPosition(); + } + } + + // Private + _findMenu() { + // Fall back to the closest ancestor that contains a menu so the toggle can be + // nested deeper than a direct sibling of `.menu`. + const wrapper = SelectorEngine.closest(this._element, `:has(${SELECTOR_MENU$2})`); + return SelectorEngine.next(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.findOne(SELECTOR_MENU$2, wrapper || this._parent); + } + _findWrapper(menu) { + let wrapper = this._element.parentNode; + while (wrapper instanceof Element && !wrapper.contains(menu)) { + wrapper = wrapper.parentNode; + } + return wrapper instanceof Element ? wrapper : this._element.parentNode; + } + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + this._closeAllSubmenus(); + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._menu.classList.remove(CLASS_NAME_SHOW$4); + this._element.classList.remove(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_SHOW$4); + } + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'placement'); + Manipulator.removeDataAttribute(this._menu, 'display'); + Menu._openInstances.delete(this); + EventHandler.trigger(this._element, EVENT_HIDDEN$7, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + throw new TypeError(`${NAME$h.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createFloating() { + if (this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'display', 'static'); + return; + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + this._updateFloatingPosition(referenceElement); + this._floatingCleanup = autoUpdate(referenceElement, this._menu, () => this._updateFloatingPosition(referenceElement)); + } + async _updateFloatingPosition(referenceElement = null) { + if (!this._menu) { + return; + } + if (!referenceElement) { + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } else { + referenceElement = this._element; + } + } + const placement = this._getPlacement(); + const middleware = this._getFloatingMiddleware(); + const floatingConfig = this._getFloatingConfig(placement, middleware); + await this._applyFloatingPosition(referenceElement, this._menu, floatingConfig.placement, floatingConfig.middleware, floatingConfig.strategy); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$4); + } + _getPlacement() { + const placement = this._responsivePlacements ? getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) : this._config.placement; + return resolveLogicalPlacement(placement); + } + _parseResponsivePlacements() { + this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + _getOffset() { + const { + offset: offsetConfig + } = this._config; + if (typeof offsetConfig === 'string') { + return offsetConfig.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offsetConfig === 'function') { + return ({ + placement, + rects + }) => { + const result = offsetConfig({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offsetConfig; + } + _getFloatingMiddleware() { + const offsetValue = this._getOffset(); + const middleware = [offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), flip({ + fallbackPlacements: this._getFallbackPlacements() + }), shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + return middleware; + } + _getFallbackPlacements() { + const placement = this._getPlacement(); + const fallbackMap = { + bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'], + 'bottom-start': ['top-start', 'bottom-end', 'top-end'], + 'bottom-end': ['top-end', 'bottom-start', 'top-start'], + top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'], + 'top-start': ['bottom-start', 'top-end', 'bottom-end'], + 'top-end': ['bottom-end', 'top-start', 'bottom-start'], + right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'], + 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'], + 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'], + left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'], + 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'], + 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end'] + }; + return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware, + strategy: this._config.strategy + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + } + _getContainer() { + const { + container + } = this._config; + if (container === false) { + return null; + } + return container === true ? document.body : getElement(container); + } + _moveMenuToContainer() { + const container = this._getContainer(); + if (!container || !this._menu) { + return; + } + if (this._menu.parentNode !== container) { + container.append(this._menu); + } + } + _restoreMenuToOriginalParent() { + if (!this._menuOriginalParent || !this._menu) { + return; + } + if (this._menu.parentNode !== this._menuOriginalParent) { + this._menuOriginalParent.append(this._menu); + } + } + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { + if (!floating.isConnected) { + return null; + } + const { + x, + y, + placement: finalPlacement + } = await computePosition(reference, floating, { + placement, + middleware, + strategy + }); + if (!floating.isConnected) { + return null; + } + Object.assign(floating.style, { + position: strategy, + left: `${x}px`, + top: `${y}px`, + margin: '0' + }); + Manipulator.setDataAttribute(floating, 'placement', finalPlacement); + return finalPlacement; + } + + // ------------------------------------------------------------------------- + // Submenu handling + // ------------------------------------------------------------------------- + + _setupSubmenuListeners() { + if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerEnter(event); + }); + EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => { + this._onSubmenuLeave(event); + }); + EventHandler.on(this._menu, 'mousemove', event => { + this._trackMousePosition(event); + }); + } + if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerClick(event); + }); + } + } + _onSubmenuTriggerEnter(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + this._cancelSubmenuCloseTimeout(submenu); + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + _onSubmenuLeave(event) { + const submenuWrapper = event.target.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu || !this._openSubmenus.has(submenu)) { + return; + } + if (this._isMovingTowardSubmenu(event, submenu)) { + return; + } + this._scheduleSubmenuClose(submenu, submenuWrapper); + } + _onSubmenuTriggerClick(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + event.preventDefault(); + event.stopPropagation(); + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + if (this._openSubmenus.has(submenu)) { + this._closeSubmenu(submenu, submenuWrapper); + } else { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + } + _openSubmenu(trigger, submenu, submenuWrapper) { + if (this._openSubmenus.has(submenu)) { + return; + } + trigger.setAttribute('aria-expanded', 'true'); + trigger.setAttribute('aria-haspopup', 'true'); + + // Keep the submenu transparent until Floating UI applies the first position, so + // it doesn't flash at its CSS fallback position (top: 0, over the parent menu) + // before being moved into place. `opacity` (unlike `visibility`/`display`) keeps + // the submenu measurable for flip/shift and focusable for keyboard navigation. + submenu.style.opacity = '0'; + submenu.classList.add(CLASS_NAME_SHOW$4); + submenuWrapper.classList.add(CLASS_NAME_SHOW$4); + const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper); + this._openSubmenus.set(submenu, cleanup); + EventHandler.on(submenu, 'mouseenter', () => { + this._cancelSubmenuCloseTimeout(submenu); + }); + } + _closeSubmenu(submenu, submenuWrapper) { + if (!this._openSubmenus.has(submenu)) { + return; + } + const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, submenu); + for (const nested of nestedSubmenus) { + const nestedWrapper = nested.closest(SELECTOR_SUBMENU); + this._closeSubmenu(nested, nestedWrapper); + } + const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper); + const cleanup = this._openSubmenus.get(submenu); + if (cleanup) { + cleanup(); + } + this._openSubmenus.delete(submenu); + EventHandler.off(submenu, 'mouseenter'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + submenu.classList.remove(CLASS_NAME_SHOW$4); + submenuWrapper.classList.remove(CLASS_NAME_SHOW$4); + + // Keep the Floating UI position styles in place while the submenu fades out. + // Clearing them here would let the submenu snap back to its CSS fallback + // (`top: 0`, over the parent menu) for the duration of the close transition, + // causing it to flash over the parent. They get recomputed on the next open + // (and the opacity gate in `_openSubmenu` hides any stale position until then). + submenu.style.opacity = ''; + } + _closeAllSubmenus() { + for (const [submenu] of this._openSubmenus) { + const submenuWrapper = submenu.closest(SELECTOR_SUBMENU); + this._closeSubmenu(submenu, submenuWrapper); + } + } + _closeSiblingSubmenus(currentSubmenuWrapper) { + const parent = currentSubmenuWrapper.parentNode; + const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, parent); + for (const siblingMenu of siblingSubmenus) { + const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU); + if (siblingWrapper !== currentSubmenuWrapper) { + this._closeSubmenu(siblingMenu, siblingWrapper); + } + } + } + _createSubmenuFloating(trigger, submenu, submenuWrapper) { + const referenceElement = submenuWrapper; + const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT); + const middleware = [offset({ + mainAxis: 0, + crossAxis: -4 + }), flip({ + fallbackPlacements: [resolveLogicalPlacement('start-start'), resolveLogicalPlacement('end-end'), resolveLogicalPlacement('start-end')] + }), shift({ + padding: 8 + })]; + const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware).then(finalPlacement => { + // Reveal the submenu now that it has been positioned (see `_openSubmenu`); + // clearing the inline opacity lets the CSS fade-in transition take over. + submenu.style.opacity = ''; + return finalPlacement; + }); + updatePosition(); + return autoUpdate(referenceElement, submenu, updatePosition); + } + _scheduleSubmenuClose(submenu, submenuWrapper) { + this._cancelSubmenuCloseTimeout(submenu); + const timeoutId = setTimeout(() => { + this._closeSubmenu(submenu, submenuWrapper); + this._submenuCloseTimeouts.delete(submenu); + }, this._config.submenuDelay); + this._submenuCloseTimeouts.set(submenu, timeoutId); + } + _cancelSubmenuCloseTimeout(submenu) { + const timeoutId = this._submenuCloseTimeouts.get(submenu); + if (timeoutId) { + clearTimeout(timeoutId); + this._submenuCloseTimeouts.delete(submenu); + } + } + _clearAllSubmenuTimeouts() { + for (const timeoutId of this._submenuCloseTimeouts.values()) { + clearTimeout(timeoutId); + } + this._submenuCloseTimeouts.clear(); + } + + // ------------------------------------------------------------------------- + // Hover intent / Safe triangle + // ------------------------------------------------------------------------- + + _trackMousePosition(event) { + this._hoverIntentData = { + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + }; + } + _isMovingTowardSubmenu(event, submenu) { + if (!this._hoverIntentData) { + return false; + } + const submenuRect = submenu.getBoundingClientRect(); + const currentPos = { + x: event.clientX, + y: event.clientY + }; + const lastPos = { + x: this._hoverIntentData.x, + y: this._hoverIntentData.y + }; + const isRtl = isRTL(); + const targetX = isRtl ? submenuRect.right : submenuRect.left; + const topCorner = { + x: targetX, + y: submenuRect.top + }; + const bottomCorner = { + x: targetX, + y: submenuRect.bottom + }; + return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner); + } + _pointInTriangle(point, v1, v2, v3) { + const d1 = triangleSign(point, v1, v2); + const d2 = triangleSign(point, v2, v3); + const d3 = triangleSign(point, v3, v1); + const hasNeg = d1 < 0 || d2 < 0 || d3 < 0; + const hasPos = d1 > 0 || d2 > 0 || d3 > 0; + return !(hasNeg && hasPos); + } + + // ------------------------------------------------------------------------- + // Keyboard navigation + // ------------------------------------------------------------------------- + + _selectMenuItem({ + key, + target + }) { + const currentMenu = target.closest(SELECTOR_MENU$2) || this._menu; + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$2, !items.includes(target)).focus(); + } + _handleSubmenuKeydown(event) { + const { + key, + target + } = event; + const isRtl = isRTL(); + const enterKey = isRtl ? ARROW_LEFT_KEY$1 : ARROW_RIGHT_KEY$1; + const exitKey = isRtl ? ARROW_RIGHT_KEY$1 : ARROW_LEFT_KEY$1; + const submenuWrapper = target.closest(SELECTOR_SUBMENU); + const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE); + if ((key === ENTER_KEY$1 || key === SPACE_KEY$1) && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === enterKey && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === exitKey) { + const currentMenu = target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper) { + event.preventDefault(); + event.stopPropagation(); + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + this._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return true; + } + } + if (key === HOME_KEY$2 || key === END_KEY$2) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = target.closest(SELECTOR_MENU$2); + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (items.length) { + const targetItem = key === HOME_KEY$2 ? items[0] : items.at(-1); + targetItem.focus(); + } + return true; + } + return false; + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { + return; + } + for (const instance of Menu._openInstances) { + if (instance._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(instance._menu); + if (composedPath.includes(instance._element) || instance._config.autoClose === 'inside' && !isMenuTarget || instance._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Don't auto-close when interacting with a form inside the menu — clicks + // on a form's labels, buttons, etc. (not just inputs) should keep it open. + const formAncestor = event.target.closest?.('form'); + const isInsideMenuForm = Boolean(formAncestor) && instance._menu.contains(formAncestor); + if (instance._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName) || isInsideMenuForm)) { + continue; + } + const relatedTarget = { + relatedTarget: instance._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + instance._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the + // menu doesn't hijack their arrow keys. + const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable; + const isEscapeEvent = event.key === ESCAPE_KEY$2; + const isUpOrDownEvent = [ARROW_UP_KEY$2, ARROW_DOWN_KEY$2].includes(event.key); + const isLeftOrRightEvent = [ARROW_LEFT_KEY$1, ARROW_RIGHT_KEY$1].includes(event.key); + const isHomeOrEndEvent = [HOME_KEY$2, END_KEY$2].includes(event.key); + const isEnterOrSpaceEvent = [ENTER_KEY$1, SPACE_KEY$1].includes(event.key); + const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE); + if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent && !(isEnterOrSpaceEvent && isSubmenuTrigger)) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$8) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8, event.delegateTarget.parentNode); + if (!getToggleButton) { + return; + } + const instance = Menu.getOrCreateInstance(getToggleButton); + if ((isLeftOrRightEvent || isHomeOrEndEvent || isEnterOrSpaceEvent && isSubmenuTrigger) && instance._handleSubmenuKeydown(event)) { + return; + } + if (isUpOrDownEvent) { + event.preventDefault(); + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (isEscapeEvent && instance._isShown()) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = event.target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper && instance._openSubmenus.size > 0) { + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + instance._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return; + } + instance.hide(); + getToggleButton.focus(); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$8, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU$2, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, Menu.clearMenus); +EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_TOGGLE$8, function (event) { + event.preventDefault(); + Menu.getOrCreateInstance(this).toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap combobox.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$g = 'combobox'; +const DATA_KEY$c = 'bs.combobox'; +const EVENT_KEY$d = `.${DATA_KEY$c}`; +const DATA_API_KEY$8 = '.data-api'; +const ESCAPE_KEY$1 = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY$1 = 'ArrowUp'; +const ARROW_DOWN_KEY$1 = 'ArrowDown'; +const HOME_KEY$1 = 'Home'; +const END_KEY$1 = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const EVENT_CHANGE$3 = `change${EVENT_KEY$d}`; +const EVENT_SHOW$5 = `show${EVENT_KEY$d}`; +const EVENT_SHOWN$4 = `shown${EVENT_KEY$d}`; +const EVENT_HIDE$4 = `hide${EVENT_KEY$d}`; +const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$d}`; +const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$d}${DATA_API_KEY$8}`; +const CLASS_NAME_SHOW$3 = 'show'; +const CLASS_NAME_SELECTED = 'selected'; +const CLASS_NAME_PLACEHOLDER = 'combobox-placeholder'; +const SELECTOR_DATA_TOGGLE$7 = '[data-bs-toggle="combobox"]'; +const SELECTOR_MENU$1 = '.menu'; +const SELECTOR_MENU_ITEM = '.menu-item[data-bs-value]'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item[data-bs-value]:not(.disabled):not(:disabled)'; +const SELECTOR_VALUE = '.combobox-value'; +const SELECTOR_SEARCH_INPUT = '.combobox-search-input'; +const SELECTOR_NO_RESULTS = '.combobox-no-results'; +const Default$f = { + boundary: 'clippingParents', + multiple: false, + name: null, + offset: [0, 2], + placeholder: '', + placement: 'bottom-start', + search: false, + searchNormalize: false +}; +const DefaultType$f = { + boundary: '(string|element)', + multiple: 'boolean', + name: '(string|null)', + offset: '(array|string|function)', + placeholder: 'string', + placement: 'string', + search: 'boolean', + searchNormalize: 'boolean' +}; + +/** + * Class definition + */ + +class Combobox extends BaseComponent { + constructor(element, config) { + super(element, config); + this._toggle = this._element; + this._menu = SelectorEngine.next(this._toggle, SELECTOR_MENU$1)[0]; + this._valueDisplay = SelectorEngine.findOne(SELECTOR_VALUE, this._toggle); + this._searchInput = SelectorEngine.findOne(SELECTOR_SEARCH_INPUT, this._menu); + this._noResults = SelectorEngine.findOne(SELECTOR_NO_RESULTS, this._menu); + this._hiddenInput = null; + this._menuInstance = null; + this._createHiddenInput(); + this._createMenuInstance(); + this._syncInitialSelection(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$f; + } + static get DefaultType() { + return DefaultType$f; + } + static get NAME() { + return NAME$g; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._toggle) || this._isShown()) { + return; + } + const showEvent = EventHandler.trigger(this._toggle, EVENT_SHOW$5); + if (showEvent.defaultPrevented) { + return; + } + this._menuInstance.show(); + if (this._searchInput) { + this._searchInput.value = ''; + this._filterItems(''); + requestAnimationFrame(() => this._searchInput.focus()); + } + EventHandler.trigger(this._toggle, EVENT_SHOWN$4); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._toggle, EVENT_HIDE$4); + if (hideEvent.defaultPrevented) { + return; + } + this._menuInstance.hide(); + EventHandler.trigger(this._toggle, EVENT_HIDDEN$6); + } + dispose() { + if (this._menuInstance) { + this._menuInstance.dispose(); + this._menuInstance = null; + } + if (this._hiddenInput) { + this._hiddenInput.remove(); + this._hiddenInput = null; + } + EventHandler.off(this._menu, EVENT_KEY$d); + EventHandler.off(this._toggle, EVENT_KEY$d); + super.dispose(); + } + + // Private + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$3); + } + _createHiddenInput() { + const { + name + } = this._config; + if (!name) { + return; + } + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = name; + this._hiddenInput.value = ''; + this._toggle.parentNode.insertBefore(this._hiddenInput, this._toggle); + } + _createMenuInstance() { + this._menuInstance = new Menu(this._toggle, { + menu: this._menu, + autoClose: this._config.multiple ? 'outside' : true, + boundary: this._config.boundary, + offset: this._config.offset, + placement: this._config.placement + }); + } + _syncInitialSelection() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length > 0) { + this._updateToggleText(); + this._updateHiddenInput(); + } else { + this._showPlaceholder(); + } + } + _addEventListeners() { + EventHandler.on(this._menu, 'click', SELECTOR_MENU_ITEM, event => { + const item = event.target.closest(SELECTOR_MENU_ITEM); + if (!item || isDisabled(item)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this._selectItem(item); + }); + EventHandler.on(this._toggle, 'keydown', event => { + this._handleToggleKeydown(event); + }); + EventHandler.on(this._menu, 'keydown', event => { + this._handleMenuKeydown(event); + }); + if (this._searchInput) { + EventHandler.on(this._searchInput, 'input', () => { + this._filterItems(this._searchInput.value); + }); + EventHandler.on(this._searchInput, 'keydown', event => { + if (event.key === ARROW_DOWN_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + items[0].focus(); + } + } + if (event.key === ESCAPE_KEY$1) { + this.hide(); + this._toggle.focus(); + } + }); + } + } + _selectItem(item) { + if (this._config.multiple) { + item.classList.toggle(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', item.classList.contains(CLASS_NAME_SELECTED)); + } else { + const previouslySelected = SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + for (const prev of previouslySelected) { + prev.classList.remove(CLASS_NAME_SELECTED); + prev.setAttribute('aria-selected', 'false'); + } + item.classList.add(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', 'true'); + } + this._updateToggleText(); + this._updateHiddenInput(); + const value = this._config.multiple ? this._getSelectedItems().map(el => el.dataset.bsValue) : item.dataset.bsValue; + EventHandler.trigger(this._toggle, EVENT_CHANGE$3, { + value, + item + }); + if (!this._config.multiple) { + this.hide(); + this._toggle.focus(); + } + } + _updateToggleText() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length === 0) { + this._showPlaceholder(); + return; + } + this._valueDisplay.classList.remove(CLASS_NAME_PLACEHOLDER); + if (this._config.multiple && selectedItems.length > 1) { + this._valueDisplay.textContent = `${selectedItems.length} selected`; + } else { + const item = selectedItems[0]; + const label = SelectorEngine.findOne('.menu-item-content > span:first-child', item); + this._valueDisplay.textContent = label ? label.textContent : item.textContent.trim(); + } + } + _showPlaceholder() { + const { + placeholder + } = this._config; + if (placeholder) { + this._valueDisplay.textContent = placeholder; + this._valueDisplay.classList.add(CLASS_NAME_PLACEHOLDER); + } + } + _updateHiddenInput() { + if (!this._hiddenInput) { + return; + } + const selectedItems = this._getSelectedItems(); + const values = selectedItems.map(el => el.dataset.bsValue); + this._hiddenInput.value = this._config.multiple ? values.join(',') : values[0] || ''; + } + _getSelectedItems() { + return SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + } + _getVisibleItems() { + return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(item => isVisible(item)); + } + _filterItems(query) { + const normalizedQuery = this._normalizeText(query.toLowerCase().trim()); + const items = SelectorEngine.find(SELECTOR_MENU_ITEM, this._menu); + let visibleCount = 0; + for (const item of items) { + const text = this._normalizeText(item.textContent.toLowerCase().trim()); + const matches = !normalizedQuery || text.includes(normalizedQuery); + item.style.display = matches ? '' : 'none'; + if (matches) { + visibleCount++; + } + } + if (this._noResults) { + this._noResults.classList.toggle('d-none', visibleCount > 0); + } + } + _normalizeText(text) { + if (this._config.searchNormalize) { + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + } + return text; + } + _handleToggleKeydown(event) { + const { + key + } = event; + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + if (!this._isShown()) { + this.show(); } - if (this._config.toggle) { - this.toggle(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const target = key === ARROW_DOWN_KEY$1 ? items[0] : items.at(-1); + target.focus(); } + return; } - - // Getters - static get Default() { - return Default$a; + if ((key === ENTER_KEY || key === SPACE_KEY) && !this._isShown()) { + event.preventDefault(); + this.show(); } - static get DefaultType() { - return DefaultType$a; + } + _handleMenuKeydown(event) { + const { + key, + target + } = event; + if (key === ESCAPE_KEY$1) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + this._toggle.focus(); + return; } - static get NAME() { - return NAME$b; + if (key === TAB_KEY) { + this.hide(); + return; } - - // Public - toggle() { - if (this._isShown()) { - this.hide(); - } else { - this.show(); + const isInput = target.matches('input'); + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); } + return; } - show() { - if (this._isTransitioning || this._isShown()) { - return; + if (key === HOME_KEY$1 || key === END_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const targetItem = key === HOME_KEY$1 ? items[0] : items.at(-1); + targetItem.focus(); } - let activeChildren = []; - - // find active children - if (this._config.parent) { - activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { - toggle: false - })); + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !isInput) { + event.preventDefault(); + const item = target.closest(SELECTOR_MENU_ITEM); + if (item && !isDisabled(item)) { + this._selectItem(item); } - if (activeChildren.length && activeChildren[0]._isTransitioning) { + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Combobox.getOrCreateInstance(this, config); + if (typeof config !== 'string') { return; } - const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6); - if (startEvent.defaultPrevented) { - return; + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$7, function (event) { + event.preventDefault(); + Combobox.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const toggle of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7)) { + Combobox.getOrCreateInstance(toggle); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$f = 'datepicker'; +const DATA_KEY$b = 'bs.datepicker'; +const EVENT_KEY$c = `.${DATA_KEY$b}`; +const DATA_API_KEY$7 = '.data-api'; +const EVENT_CHANGE$2 = `change${EVENT_KEY$c}`; +const EVENT_SHOW$4 = `show${EVENT_KEY$c}`; +const EVENT_SHOWN$3 = `shown${EVENT_KEY$c}`; +const EVENT_HIDE$3 = `hide${EVENT_KEY$c}`; +const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$c}`; +const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$c}${DATA_API_KEY$7}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY$c}${DATA_API_KEY$7}`; +const SELECTOR_DATA_TOGGLE$6 = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default$e = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType$e = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default$e; + } + static get DefaultType() { + return DefaultType$e; + } + static get NAME() { + return NAME$f; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN$3); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN$5); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; } - for (const activeInstance of activeChildren) { - activeInstance.hide(); + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; } - const dimension = this._getDimension(); - this._element.classList.remove(CLASS_NAME_COLLAPSE); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.style[dimension] = 0; - this._addAriaAndCollapsedClass(this._triggerArray, true); - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); - this._element.style[dimension] = ''; - EventHandler.trigger(this._element, EVENT_SHOWN$6); - }; - const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - const scrollSize = `scroll${capitalizedDimension}`; - this._queueCallback(complete, this._element, true); - this._element.style[dimension] = `${this._element[scrollSize]}px`; + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); } - hide() { - if (this._isTransitioning || !this._isShown()) { - return; + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; } - const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); - if (startEvent.defaultPrevented) { - return; + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); } - const dimension = this._getDimension(); - this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; - reflow(this._element); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); - for (const trigger of this._triggerArray) { - const element = SelectorEngine.getElementFromSelector(trigger); - if (element && !this._isShown(element)) { - this._addAriaAndCollapsedClass([trigger], false); - } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; } - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE); - EventHandler.trigger(this._element, EVENT_HIDDEN$6); - }; - this._element.style[dimension] = ''; - this._queueCallback(complete, this._element, true); } + EventHandler.trigger(this._element, EVENT_CHANGE$2, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; - // Private - _isShown(element = this._element) { - return element.classList.contains(CLASS_NAME_SHOW$7); + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); } - _configAfterMerge(config) { - config.toggle = Boolean(config.toggle); // Coerce string values - config.parent = getElement(config.parent); - return config; + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); } - _getDimension() { - return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; } - _initializeChildren() { - if (!this._config.parent) { - return; - } - const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4); - for (const element of children) { - const selected = SelectorEngine.getElementFromSelector(element); - if (selected) { - this._addAriaAndCollapsedClass([element], this._isShown(selected)); - } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$6, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE$6, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$c}${DATA_API_KEY$7}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); } } - _getFirstLevelChildren(selector) { - const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); - // remove children if greater depth - return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } } - _addAriaAndCollapsedClass(triggerArray, isOpen) { - if (!triggerArray.length) { + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { return; } - for (const element of triggerArray) { - element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); - element.setAttribute('aria-expanded', isOpen); + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; } + this.hide(); + }); + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$e = 'dialog'; +const DATA_KEY$a = 'bs.dialog'; +const EVENT_KEY$b = `.${DATA_KEY$a}`; +const DATA_API_KEY$6 = '.data-api'; +const EVENT_SHOW$3 = `show${EVENT_KEY$b}`; +const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$b}`; +const EVENT_CANCEL = `cancel${EVENT_KEY$b}`; +const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$b}${DATA_API_KEY$6}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="dialog"]'; +const Default$d = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType$d = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default$d; + } + static get DefaultType() { + return DefaultType$d; + } + static get NAME() { + return NAME$e; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ - // Static - static jQueryInterface(config) { - const _config = {}; - if (typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false; +EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$5, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW$3, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$4, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); } - return this.each(function () { - const data = Collapse.getOrCreateInstance(this, _config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY$b}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN$4, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); }); + currentInstance.hide(); } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$d = 'navoverflow'; +const DATA_KEY$9 = 'bs.navoverflow'; +const EVENT_KEY$a = `.${DATA_KEY$9}`; +const EVENT_UPDATE = `update${EVENT_KEY$a}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY$a}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default$c = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType$c = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$c; + } + static get DefaultType() { + return DefaultType$c; + } + static get NAME() { + return NAME$d; + } - EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { - event.preventDefault(); + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); } - for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { - Collapse.getOrCreateInstance(element, { - toggle: false - }).toggle(); + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); } - }); + super.dispose(); + } - /** - * jQuery - */ - - defineJQueryPlugin(Collapse); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$a = 'dropdown'; - const DATA_KEY$6 = 'bs.dropdown'; - const EVENT_KEY$6 = `.${DATA_KEY$6}`; - const DATA_API_KEY$3 = '.data-api'; - const ESCAPE_KEY$2 = 'Escape'; - const TAB_KEY$1 = 'Tab'; - const ARROW_UP_KEY$1 = 'ArrowUp'; - const ARROW_DOWN_KEY$1 = 'ArrowDown'; - const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button - - const EVENT_HIDE$5 = `hide${EVENT_KEY$6}`; - const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`; - const EVENT_SHOW$5 = `show${EVENT_KEY$6}`; - const EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`; - const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`; - const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`; - const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`; - const CLASS_NAME_SHOW$6 = 'show'; - const CLASS_NAME_DROPUP = 'dropup'; - const CLASS_NAME_DROPEND = 'dropend'; - const CLASS_NAME_DROPSTART = 'dropstart'; - const CLASS_NAME_DROPUP_CENTER = 'dropup-center'; - const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'; - const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'; - const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`; - const SELECTOR_MENU = '.dropdown-menu'; - const SELECTOR_NAVBAR = '.navbar'; - const SELECTOR_NAVBAR_NAV = '.navbar-nav'; - const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; - const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'; - const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'; - const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'; - const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'; - const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'; - const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'; - const PLACEMENT_TOPCENTER = 'top'; - const PLACEMENT_BOTTOMCENTER = 'bottom'; - const Default$9 = { - autoClose: true, - boundary: 'clippingParents', - display: 'dynamic', - offset: [0, 2], - popperConfig: null, - reference: 'toggle' - }; - const DefaultType$9 = { - autoClose: '(boolean|string)', - boundary: '(string|element)', - display: 'string', - offset: '(array|string|function)', - popperConfig: '(null|object|function)', - reference: '(string|element|object)' - }; + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); - /** - * Class definition - */ + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); - class Dropdown extends BaseComponent { - constructor(element, config) { - super(element, config); - this._popper = null; - this._parent = this._element.parentNode; // dropdown wrapper - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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(); - } + // Setup resize observer + this._setupResizeObserver(); - // Getters - static get Default() { - return Default$9; + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; } - static get DefaultType() { - return DefaultType$9; + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; } - static get NAME() { - return NAME$a; + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; } - - // Public - toggle() { - return this._isShown() ? this.hide() : this.show(); + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; } - show() { - if (isDisabled(this._element) || this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget); - if (showEvent.defaultPrevented) { - return; - } - this._createPopper(); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', noop); + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); } } - this._element.focus(); - this._element.setAttribute('aria-expanded', true); - this._menu.classList.add(CLASS_NAME_SHOW$6); - this._element.classList.add(CLASS_NAME_SHOW$6); - EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); - } - hide() { - if (isDisabled(this._element) || !this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - this._completeHide(relatedTarget); - } - dispose() { - if (this._popper) { - this._popper.destroy(); - } - super.dispose(); - } - update() { - this._inNavbar = this._detectNavbar(); - if (this._popper) { - this._popper.update(); + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); } + return; } + const overflowWidth = overflowItem?.offsetWidth || 0; - // Private - _completeHide(relatedTarget) { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); - if (hideEvent.defaultPrevented) { - return; - } + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', noop); - } - } - if (this._popper) { - this._popper.destroy(); - } - this._menu.classList.remove(CLASS_NAME_SHOW$6); - this._element.classList.remove(CLASS_NAME_SHOW$6); - this._element.setAttribute('aria-expanded', 'false'); - Manipulator.removeDataAttribute(this._menu, 'popper'); - EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget); - } - _getConfig(config) { - config = super._getConfig(config); - if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { - // Popper virtual elements require a getBoundingClientRect method - throw new TypeError(`${NAME$a.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); - } - return config; - } - _createPopper() { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)'); + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; } - let referenceElement = this._element; - if (this._config.reference === 'parent') { - referenceElement = this._parent; - } else if (isElement(this._config.reference)) { - referenceElement = getElement(this._config.reference); - } else if (typeof this._config.reference === 'object') { - referenceElement = this._config.reference; + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); } - const popperConfig = this._getPopperConfig(); - this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig); - } - _isShown() { - return this._menu.classList.contains(CLASS_NAME_SHOW$6); } - _getPlacement() { - const parentDropdown = this._parent; - if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { - return PLACEMENT_RIGHT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) { - return PLACEMENT_LEFT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) { - return PLACEMENT_TOPCENTER; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) { - return PLACEMENT_BOTTOMCENTER; - } - // We need to trim the value because custom properties can also include spaces - const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'; - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) { - return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP; - } - return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM; - } - _detectNavbar() { - return this._element.closest(SELECTOR_NAVBAR) !== null; + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _getPopperConfig() { - const defaultBsPopperConfig = { - placement: this._getPlacement(), - modifiers: [{ - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }] - }; - // Disable Popper if we have a static display or Dropdown is in Navbar - if (this._inNavbar || this._config.display === 'static') { - Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove - defaultBsPopperConfig.modifiers = [{ - name: 'applyStyles', - enabled: false - }]; - } - return { - ...defaultBsPopperConfig, - ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; - } - _selectMenuItem({ - key, - target - }) { - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element)); - if (!items.length) { - return; - } + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); - // if target isn't included in items (e.g. when expanding the dropdown) - // allow cycling to get the last item in case key equals ARROW_UP_KEY - getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Dropdown.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length }); } - static clearMenus(event) { - if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { - return; - } - const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN); - for (const toggle of openToggles) { - const context = Dropdown.getInstance(toggle); - if (!context || context._config.autoClose === false) { - continue; - } - 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) { - continue; - } - - // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu - if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) { - continue; - } - const relatedTarget = { - relatedTarget: context._element - }; - if (event.type === 'click') { - relatedTarget.clickEvent = event; - } - context._completeHide(relatedTarget); - } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; } - static dataApiKeydownHandler(event) { - // If not an UP | DOWN | ESCAPE key => not a dropdown command - // If input/textarea && if key is other than ESCAPE => not a dropdown command - const isInput = /input|textarea/i.test(event.target.tagName); - const isEscapeEvent = event.key === ESCAPE_KEY$2; - const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key); - if (!isUpOrDownEvent && !isEscapeEvent) { - return; - } - if (isInput && !isEscapeEvent) { - return; + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; } - event.preventDefault(); - - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode); - const instance = Dropdown.getOrCreateInstance(getToggleButton); - if (isUpOrDownEvent) { - event.stopPropagation(); - instance.show(); - instance._selectMenuItem(event); - return; + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); } - if (instance._isShown()) { - // else is escape and we check if it is shown - event.stopPropagation(); - instance.hide(); - getToggleButton.focus(); + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); } } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus); - EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) { - event.preventDefault(); - Dropdown.getOrCreateInstance(this).toggle(); - }); - - /** - * jQuery - */ - - defineJQueryPlugin(Dropdown); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/backdrop.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$9 = 'backdrop'; - const CLASS_NAME_FADE$4 = 'fade'; - const CLASS_NAME_SHOW$5 = 'show'; - const EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`; - const Default$8 = { - className: 'modal-backdrop', - clickCallback: null, - isAnimated: false, - isVisible: true, - // if false, we use the backdrop helper without adding any element to the dom - rootElement: 'body' // give the choice to place backdrop under different elements - }; - const DefaultType$8 = { - className: 'string', - clickCallback: '(function|null)', - isAnimated: 'boolean', - isVisible: 'boolean', - rootElement: '(element|string)' - }; - - /** - * Class definition - */ +/** + * Data API implementation + */ - class Backdrop extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isAppended = false; - this._element = null; +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$c = 'swipe'; +const EVENT_KEY$9 = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default$b = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType$b = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; + +/** + * Class definition + */ + +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } - // Getters - static get Default() { - return Default$8; - } - static get DefaultType() { - return DefaultType$8; - } - static get NAME() { - return NAME$9; - } + // Getters + static get Default() { + return Default$b; + } + static get DefaultType() { + return DefaultType$b; + } + static get NAME() { + return NAME$c; + } - // Public - show(callback) { - if (!this._config.isVisible) { - execute(callback); - return; - } - this._append(); - const element = this._getElement(); - if (this._config.isAnimated) { - reflow(element); - } - element.classList.add(CLASS_NAME_SHOW$5); - this._emulateAnimation(() => { - execute(callback); - }); + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY$9); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; } - hide(callback) { - if (!this._config.isVisible) { - execute(callback); - return; - } - this._getElement().classList.remove(CLASS_NAME_SHOW$5); - this._emulateAnimation(() => { - this.dispose(); - execute(callback); - }); + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; } - dispose() { - if (!this._isAppended) { - return; - } - EventHandler.off(this._element, EVENT_MOUSEDOWN); - this._element.remove(); - this._isAppended = false; + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; } - - // Private - _getElement() { - if (!this._element) { - const backdrop = document.createElement('div'); - backdrop.className = this._config.className; - if (this._config.isAnimated) { - backdrop.classList.add(CLASS_NAME_FADE$4); - } - this._element = backdrop; - } - return this._element; + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; } - _configAfterMerge(config) { - // use getElement() with the default "body" to get a fresh Element on each instantiation - config.rootElement = getElement(config.rootElement); - return config; + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); + + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; } - _append() { - if (this._isAppended) { + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe + const direction = absDeltaX / this._deltaX; + this._deltaX = 0; + this._deltaY = 0; + if (!direction) { return; } - const element = this._getElement(); - this._config.rootElement.append(element); - EventHandler.on(element, EVENT_MOUSEDOWN, () => { - execute(this._config.clickCallback); - }); - this._isAppended = true; + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; } - _emulateAnimation(callback) { - executeAfterTransition(callback, this._getElement(), this._config.isAnimated); + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); } } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } - /** - * -------------------------------------------------------------------------- - * Bootstrap util/focustrap.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$b = 'drawer'; +const DATA_KEY$8 = 'bs.drawer'; +const EVENT_KEY$8 = `.${DATA_KEY$8}`; +const DATA_API_KEY$5 = '.data-api'; +const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; +const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$8}`; +const EVENT_RESIZE$1 = `resize${EVENT_KEY$8}`; +const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; +const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="drawer"]'; +const Default$a = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType$a = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + // Getters + static get Default() { + return Default$a; + } + static get DefaultType() { + return DefaultType$a; + } + static get NAME() { + return NAME$b; + } - /** - * Constants - */ + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } - const NAME$8 = 'focustrap'; - const DATA_KEY$5 = 'bs.focustrap'; - const EVENT_KEY$5 = `.${DATA_KEY$5}`; - const EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`; - const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`; - const TAB_KEY = 'Tab'; - const TAB_NAV_FORWARD = 'forward'; - const TAB_NAV_BACKWARD = 'backward'; - const Default$7 = { - autofocus: true, - trapElement: null // The element to trap focus inside of - }; - const DefaultType$7 = { - autofocus: 'boolean', - trapElement: 'element' - }; + // Protected — hook overrides - /** - * Class definition - */ + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } - class FocusTrap extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isActive = false; - this._lastTabNavDirection = null; - } + // Private - // Getters - static get Default() { - return Default$7; - } - static get DefaultType() { - return DefaultType$7; - } - static get NAME() { - return NAME$8; + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; } - // Public - activate() { - if (this._isActive) { - return; - } - if (this._config.autofocus) { - this._config.trapElement.focus(); + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); } - EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop - EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event)); - EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)); - this._isActive = true; + } else if (isRTL()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); } - deactivate() { - if (!this._isActive) { - return; - } - this._isActive = false; - EventHandler.off(document, EVENT_KEY$5); + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$4, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$3, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); } + }); - // Private - _handleFocusin(event) { - const { - trapElement - } = this._config; - if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { - return; - } - const elements = SelectorEngine.focusableChildren(trapElement); - if (elements.length === 0) { - trapElement.focus(); - } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { - elements[elements.length - 1].focus(); - } else { - elements[0].focus(); - } + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE$1, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); } - _handleKeydown(event) { - if (event.key !== TAB_KEY) { - return; - } - this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD; + } +}); +enableDismissTrigger(Drawer); + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$a = 'strength'; +const DATA_KEY$7 = 'bs.strength'; +const EVENT_KEY$7 = `.${DATA_KEY$7}`; +const DATA_API_KEY$4 = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY$7}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default$9 = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType$9 = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); } } - /** - * -------------------------------------------------------------------------- - * Bootstrap util/scrollBar.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Getters + static get Default() { + return Default$9; + } + static get DefaultType() { + return DefaultType$9; + } + static get NAME() { + return NAME$a; + } + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } - /** - * Constants - */ + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } - const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; - const SELECTOR_STICKY_CONTENT = '.sticky-top'; - const PROPERTY_PADDING = 'padding-right'; - const PROPERTY_MARGIN = 'margin-right'; + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } - /** - * Class definition - */ + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; - class ScrollBarHelper { - constructor() { - this._element = document.body; + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; } - // Public - getWidth() { - // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes - const documentWidth = document.documentElement.clientWidth; - return Math.abs(window.innerWidth - documentWidth); + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; } - hide() { - const width = this.getWidth(); - this._disableOverFlow(); - // give padding to element to balance the hidden scrollbar width - this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth - this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width); + if (/[A-Z]/.test(password)) { + score += weights.uppercase; } - reset() { - this._resetElementAttributes(this._element, 'overflow'); - this._resetElementAttributes(this._element, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN); + if (/\d/.test(password)) { + score += weights.numbers; } - isOverflowing() { - return this.getWidth() > 0; + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; } - // Private - _disableOverFlow() { - this._saveInitialAttribute(this._element, 'overflow'); - this._element.style.overflow = 'hidden'; - } - _setElementAttributes(selector, styleProperty, callback) { - const scrollbarWidth = this.getWidth(); - const manipulationCallBack = element => { - if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { - return; - } - this._saveInitialAttribute(element, styleProperty); - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty); - element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`); - }; - this._applyManipulationCallback(selector, manipulationCallBack); + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; } - _saveInitialAttribute(element, styleProperty) { - const actualValue = element.style.getPropertyValue(styleProperty); - if (actualValue) { - Manipulator.setDataAttribute(element, styleProperty, actualValue); - } + if (password.length >= 16) { + score += weights.longPassword; } - _resetElementAttributes(selector, styleProperty) { - const manipulationCallBack = element => { - const value = Manipulator.getDataAttribute(element, styleProperty); - // We only want to remove the property if the value is `null`; the value can also be zero - if (value === null) { - element.style.removeProperty(styleProperty); - return; - } - Manipulator.removeDataAttribute(element, styleProperty); - element.style.setProperty(styleProperty, value); - }; - this._applyManipulationCallback(selector, manipulationCallBack); + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; } - _applyManipulationCallback(selector, callBack) { - if (isElement(selector)) { - callBack(selector); - return; + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); } - for (const sel of SelectorEngine.find(selector, this._element)) { - callBack(sel); + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; } } } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$7 = 'modal'; - const DATA_KEY$4 = 'bs.modal'; - const EVENT_KEY$4 = `.${DATA_KEY$4}`; - const DATA_API_KEY$2 = '.data-api'; - const ESCAPE_KEY$1 = 'Escape'; - const EVENT_HIDE$4 = `hide${EVENT_KEY$4}`; - const EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`; - const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`; - const EVENT_SHOW$4 = `show${EVENT_KEY$4}`; - const EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`; - const EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`; - const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`; - const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`; - const EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`; - const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`; - const CLASS_NAME_OPEN = 'modal-open'; - const CLASS_NAME_FADE$3 = 'fade'; - const CLASS_NAME_SHOW$4 = 'show'; - const CLASS_NAME_STATIC = 'modal-static'; - const OPEN_SELECTOR$1 = '.modal.show'; - const SELECTOR_DIALOG = '.modal-dialog'; - const SELECTOR_MODAL_BODY = '.modal-body'; - const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="modal"]'; - const Default$6 = { - backdrop: true, - focus: true, - keyboard: true - }; - const DefaultType$6 = { - backdrop: '(boolean|string)', - focus: 'boolean', - keyboard: 'boolean' - }; +/** + * Data API implementation + */ - /** - * Class definition - */ - - class Modal extends BaseComponent { - constructor(element, config) { - super(element, config); - this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element); - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._isShown = false; - this._isTransitioning = false; - this._scrollBar = new ScrollBarHelper(); - this._addEventListeners(); +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$7}${DATA_API_KEY$4}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$9 = 'otpInput'; +const DATA_KEY$6 = 'bs.otpInput'; +const EVENT_KEY$6 = `.${DATA_KEY$6}`; +const DATA_API_KEY$3 = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY$6}`; +const EVENT_INPUT$1 = `input${EVENT_KEY$6}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$6}${DATA_API_KEY$3}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT$1 = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default$8 = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType$8 = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT$1, this._element); + if (!this._input) { + return; } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default$8; + } + static get DefaultType() { + return DefaultType$8; + } + static get NAME() { + return NAME$9; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } - // Getters - static get Default() { - return Default$6; + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; } - static get DefaultType() { - return DefaultType$6; + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; } - static get NAME() { - return NAME$7; + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); } - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); } - show(relatedTarget) { - if (this._isShown || this._isTransitioning) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._isTransitioning = true; - this._scrollBar.hide(); - document.body.classList.add(CLASS_NAME_OPEN); - this._adjustDialog(); - this._backdrop.show(() => this._showElement(relatedTarget)); - } - hide() { - if (!this._isShown || this._isTransitioning) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4); - if (hideEvent.defaultPrevented) { - return; + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } } - this._isShown = false; - this._isTransitioning = true; - this._focustrap.deactivate(); - this._element.classList.remove(CLASS_NAME_SHOW$4); - this._queueCallback(() => this._hideModal(), this._element, this._isAnimated()); } - dispose() { - EventHandler.off(window, EVENT_KEY$4); - EventHandler.off(this._dialog, EVENT_KEY$4); - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); } - handleUpdate() { - this._adjustDialog(); + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT$1, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); } - - // Private - _initializeBackDrop() { - return new Backdrop({ - isVisible: Boolean(this._config.backdrop), - // 'static' option will be translated to true, and booleans will keep their value, - isAnimated: this._isAnimated() + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value }); } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$8 = 'chips'; +const DATA_KEY$5 = 'bs.chips'; +const EVENT_KEY$5 = `.${DATA_KEY$5}`; +const DATA_API_KEY$2 = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY$5}`; +const EVENT_REMOVE = `remove${EVENT_KEY$5}`; +const EVENT_CHANGE$1 = `change${EVENT_KEY$5}`; +const EVENT_SELECT = `select${EVENT_KEY$5}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE$2 = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default$7 = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType$7 = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$7; + } + static get DefaultType() { + return DefaultType$7; + } + static get NAME() { + return NAME$8; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; } - _showElement(relatedTarget) { - // try to append dynamic modal - if (!document.body.contains(this._element)) { - document.body.append(this._element); - } - this._element.style.display = 'block'; - this._element.removeAttribute('aria-hidden'); - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.scrollTop = 0; - const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog); - if (modalBody) { - modalBody.scrollTop = 0; - } - reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW$4); - const transitionComplete = () => { - if (this._config.focus) { - this._focustrap.activate(); - } - this._isTransitioning = false; - EventHandler.trigger(this._element, EVENT_SHOWN$4, { - relatedTarget - }); - }; - this._queueCallback(transitionComplete, this._dialog, this._isAnimated()); + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => { - if (event.key !== ESCAPE_KEY$1) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - this._triggerBackdropTransition(); - }); - EventHandler.on(window, EVENT_RESIZE$1, () => { - if (this._isShown && !this._isTransitioning) { - this._adjustDialog(); - } - }); - EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => { - // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks - EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => { - if (this._element !== event.target || this._element !== event2.target) { - return; - } - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition(); - return; - } - if (this._config.backdrop) { - this.hide(); - } - }); - }); + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; } - _hideModal() { - this._element.style.display = 'none'; - this._element.setAttribute('aria-hidden', true); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - this._isTransitioning = false; - this._backdrop.hide(() => { - document.body.classList.remove(CLASS_NAME_OPEN); - this._resetAdjustments(); - this._scrollBar.reset(); - EventHandler.trigger(this._element, EVENT_HIDDEN$4); - }); + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_FADE$3); + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); } - _triggerBackdropTransition() { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1); - if (hideEvent.defaultPrevented) { - return; - } - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const initialOverflowY = this._element.style.overflowY; - // return if the following background transition hasn't yet completed - if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { - return; - } - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden'; - } - this._element.classList.add(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.classList.remove(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.style.overflowY = initialOverflowY; - }, this._dialog); - }, this._dialog); - this._element.focus(); - } - - /** - * The following methods are used to handle overflowing modals - */ - - _adjustDialog() { - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const scrollbarWidth = this._scrollBar.getWidth(); - const isBodyOverflowing = scrollbarWidth > 0; - if (isBodyOverflowing && !isModalOverflowing) { - const property = isRTL() ? 'paddingLeft' : 'paddingRight'; - this._element.style[property] = `${scrollbarWidth}px`; - } - if (!isBodyOverflowing && isModalOverflowing) { - const property = isRTL() ? 'paddingRight' : 'paddingLeft'; - this._element.style[property] = `${scrollbarWidth}px`; - } + if (!chip || !value) { + return false; } - _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; } - // Static - static jQueryInterface(config, relatedTarget) { - return this.each(function () { - const data = Modal.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](relatedTarget); - }); + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; } - } - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); } - EventHandler.one(target, EVENT_SHOW$4, showEvent => { - if (showEvent.defaultPrevented) { - // only register focus restorer if modal will actually get shown - return; + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE$2); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; } - EventHandler.one(target, EVENT_HIDDEN$4, () => { - if (isVisible(this)) { - this.focus(); - } - }); + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() }); + } + focus() { + this._input?.focus(); + } - // avoid conflict when clicking modal toggler while another one is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1); - if (alreadyOpen) { - Modal.getInstance(alreadyOpen).hide(); + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } } - const data = Modal.getOrCreateInstance(target); - data.toggle(this); - }); - enableDismissTrigger(Modal); - - /** - * jQuery - */ - - defineJQueryPlugin(Modal); - - /** - * -------------------------------------------------------------------------- - * Bootstrap offcanvas.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$6 = 'offcanvas'; - const DATA_KEY$3 = 'bs.offcanvas'; - const EVENT_KEY$3 = `.${DATA_KEY$3}`; - const DATA_API_KEY$1 = '.data-api'; - const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`; - const ESCAPE_KEY = 'Escape'; - const CLASS_NAME_SHOW$3 = 'show'; - const CLASS_NAME_SHOWING$1 = 'showing'; - const CLASS_NAME_HIDING = 'hiding'; - const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'; - const OPEN_SELECTOR = '.offcanvas.show'; - const EVENT_SHOW$3 = `show${EVENT_KEY$3}`; - const EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`; - const EVENT_HIDE$3 = `hide${EVENT_KEY$3}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`; - const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`; - const EVENT_RESIZE = `resize${EVENT_KEY$3}`; - const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`; - const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="offcanvas"]'; - const Default$5 = { - backdrop: true, - keyboard: true, - scroll: false - }; - const DefaultType$5 = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - scroll: 'boolean' - }; - - /** - * Class definition - */ + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); - class Offcanvas extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isShown = false; - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._addEventListeners(); + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); - // Getters - static get Default() { - return Default$5; + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; } - static get DefaultType() { - return DefaultType$5; + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); } - static get NAME() { - return NAME$6; + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); } - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, { - relatedTarget - }); - if (showEvent.defaultPrevented) { + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { return; } - this._isShown = true; - this._backdrop.show(); - if (!this._config.scroll) { - new ScrollBarHelper().hide(); - } - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.classList.add(CLASS_NAME_SHOWING$1); - const completeCallBack = () => { - if (!this._config.scroll || this._config.backdrop) { - this._focustrap.activate(); - } - this._element.classList.add(CLASS_NAME_SHOW$3); - this._element.classList.remove(CLASS_NAME_SHOWING$1); - EventHandler.trigger(this._element, EVENT_SHOWN$3, { - relatedTarget + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey }); - }; - this._queueCallback(completeCallBack, this._element, true); - } - hide() { - if (!this._isShown) { - return; + chip.focus(); } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); - if (hideEvent.defaultPrevented) { - return; + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); } - this._focustrap.deactivate(); - this._element.blur(); - this._isShown = false; - this._element.classList.add(CLASS_NAME_HIDING); - this._backdrop.hide(); - const completeCallback = () => { - this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - if (!this._config.scroll) { - new ScrollBarHelper().reset(); - } - EventHandler.trigger(this._element, EVENT_HIDDEN$3); - }; - this._queueCallback(completeCallback, this._element, true); - } - dispose() { - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } + }); - // Private - _initializeBackDrop() { - const clickCallback = () => { - if (this._config.backdrop === 'static') { - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - return; + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; } - this.hide(); - }; - // 'static' option will be translated to true, and booleans will keep their value - const isVisible = Boolean(this._config.backdrop); - return new Backdrop({ - className: CLASS_NAME_BACKDROP, - isVisible, - isAnimated: true, - rootElement: this._element.parentNode, - clickCallback: isVisible ? clickCallback : null - }); + // No default } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; } - if (this._config.keyboard) { - this.hide(); - return; + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; } - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - }); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Offcanvas.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; } - data[config](this); - }); + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default } } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; } - if (isDisabled(this)) { + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { return; } - EventHandler.one(target, EVENT_HIDDEN$3, () => { - // focus on trigger when it is closed - if (isVisible(this)) { - this.focus(); - } - }); - - // avoid conflict when clicking a toggler of an offcanvas, while another is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen && alreadyOpen !== target) { - Offcanvas.getInstance(alreadyOpen).hide(); + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; } - const data = Offcanvas.getOrCreateInstance(target); - data.toggle(this); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { - for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { - Offcanvas.getOrCreateInstance(selector).show(); + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE$2); } - }); - EventHandler.on(window, EVENT_RESIZE, () => { - for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) { - if (getComputedStyle(element).position !== 'fixed') { - Offcanvas.getOrCreateInstance(element).hide(); - } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); } - }); - enableDismissTrigger(Offcanvas); - - /** - * jQuery - */ - - defineJQueryPlugin(Offcanvas); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - // js-docs-start allow-list - const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; - const DefaultAllowlist = { - // Global attributes allowed on any supplied element below. - '*': ['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: [] - }; - // js-docs-end allow-list - - const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); - - /** - * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation - * contexts. - * - * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 - */ - const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - const allowedAttribute = (attribute, allowedAttributeList) => { - const attributeName = attribute.nodeName.toLowerCase(); - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)); - } - return true; + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; } - - // Check if a regular expression validates the attribute. - return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); - }; - function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { - if (!unsafeHtml.length) { - return unsafeHtml; - } - if (sanitizeFunction && typeof sanitizeFunction === 'function') { - return sanitizeFunction(unsafeHtml); - } - const domParser = new window.DOMParser(); - const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); - const elements = [].concat(...createdDocument.body.querySelectorAll('*')); - for (const element of elements) { - const elementName = element.nodeName.toLowerCase(); - if (!Object.keys(allowList).includes(elementName)) { - element.remove(); - continue; - } - const attributeList = [].concat(...element.attributes); - const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []); - for (const attribute of attributeList) { - if (!allowedAttribute(attribute, allowedAttributes)) { - element.removeAttribute(attribute.nodeName); - } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); } } - return createdDocument.body.innerHTML; } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap util/template-factory.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Data API implementation + */ +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$5}${DATA_API_KEY$2}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list + +const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); + +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); + } + return true; + } - /** - * Constants - */ + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; + } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); + } + } + } + return createdDocument.body.innerHTML; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$7 = 'TemplateFactory'; +const Default$6 = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType$6 = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; + +/** + * Class definition + */ + +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } - const NAME$5 = 'TemplateFactory'; - const Default$4 = { - allowList: DefaultAllowlist, - content: {}, - // { selector : text , selector2 : text2 , } - extraClass: '', - html: false, - sanitize: true, - sanitizeFn: null, - template: '' - }; - const DefaultType$4 = { - allowList: 'object', - content: 'object', - extraClass: '(string|function)', - html: 'boolean', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - template: 'string' - }; - const DefaultContentType = { - entry: '(string|element|function|null)', - selector: '(string|element)' - }; + // Getters + static get Default() { + return Default$6; + } + static get DefaultType() { + return DefaultType$6; + } + static get NAME() { + return NAME$7; + } - /** - * Class definition - */ + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); + } + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); + } + return template; + } - class TemplateFactory extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); } - - // Getters - static get Default() { - return Default$4; + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; + } + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; + } + if (isElement(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; } - static get DefaultType() { - return DefaultType$4; + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; + } + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; } - static get NAME() { - return NAME$5; + templateElement.textContent = element.textContent; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$6 = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE$2 = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW$2 = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE$2 = 'hide'; +const EVENT_HIDDEN$2 = 'hidden'; +const EVENT_SHOW$2 = 'show'; +const EVENT_SHOWN$2 = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK$3 = 'click'; +const EVENT_FOCUSIN$2 = 'focusin'; +const EVENT_FOCUSOUT$1 = 'focusout'; +const EVENT_MOUSEENTER$1 = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN$1 = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL() ? 'right' : 'left' +}; +const Default$5 = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType$5 = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); + } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); } + } + + // Getters + static get Default() { + return Default$5; + } + static get DefaultType() { + return DefaultType$5; + } + static get NAME() { + return NAME$6; + } - // Public - getContent() { - return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; } - hasContent() { - return this.getContent().length > 0; + if (this._isShown()) { + this._leave(); + return; } - changeContent(content) { - this._checkContent(content); - this._config.content = { - ...this._config.content, - ...content - }; - return this; + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); } - toHtml() { - const templateWrapper = document.createElement('div'); - templateWrapper.innerHTML = this._maybeSanitize(this._config.template); - for (const [selector, text] of Object.entries(this._config.content)) { - this._setContent(templateWrapper, text, selector); - } - const template = templateWrapper.children[0]; - const extraClass = this._resolvePossibleFunction(this._config.extraClass); - if (extraClass) { - template.classList.add(...extraClass.split(' ')); - } - return template; + if (!(this._isWithContent() && this._isEnabled)) { + return; } - - // Private - _typeCheckConfig(config) { - super._typeCheckConfig(config); - this._checkContent(config.content); - } - _checkContent(arg) { - for (const [selector, content] of Object.entries(arg)) { - super._typeCheckConfig({ - selector, - entry: content - }, DefaultContentType); - } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; } - _setContent(template, content, selector) { - const templateElement = SelectorEngine.findOne(selector, template); - if (!templateElement) { - return; - } - content = this._resolvePossibleFunction(content); - if (!content) { - templateElement.remove(); - return; - } - if (isElement(content)) { - this._putElementInTemplate(getElement(content), templateElement); - return; - } - if (this._config.html) { - templateElement.innerHTML = this._maybeSanitize(content); - return; + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW$2); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); + if (this._isHovered === false) { + this._leave(); } - templateElement.textContent = content; - } - _maybeSanitize(arg) { - return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; } - _resolvePossibleFunction(arg) { - return execute(arg, [undefined, this]); + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); + if (hideEvent.defaultPrevented) { + return; } - _putElementInTemplate(element, templateElement) { - if (this._config.html) { - templateElement.innerHTML = ''; - templateElement.append(element); - return; - } - templateElement.textContent = element.textContent; - } - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap tooltip.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$4 = 'tooltip'; - const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); - const CLASS_NAME_FADE$2 = 'fade'; - const CLASS_NAME_MODAL = 'modal'; - const CLASS_NAME_SHOW$2 = 'show'; - const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; - const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; - const EVENT_MODAL_HIDE = 'hide.bs.modal'; - const TRIGGER_HOVER = 'hover'; - const TRIGGER_FOCUS = 'focus'; - const TRIGGER_CLICK = 'click'; - const TRIGGER_MANUAL = 'manual'; - const EVENT_HIDE$2 = 'hide'; - const EVENT_HIDDEN$2 = 'hidden'; - const EVENT_SHOW$2 = 'show'; - const EVENT_SHOWN$2 = 'shown'; - const EVENT_INSERTED = 'inserted'; - const EVENT_CLICK$1 = 'click'; - const EVENT_FOCUSIN$1 = 'focusin'; - const EVENT_FOCUSOUT$1 = 'focusout'; - const EVENT_MOUSEENTER = 'mouseenter'; - const EVENT_MOUSELEAVE = 'mouseleave'; - const AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: isRTL() ? 'left' : 'right', - BOTTOM: 'bottom', - LEFT: isRTL() ? 'right' : 'left' - }; - const Default$3 = { - allowList: DefaultAllowlist, - animation: true, - boundary: 'clippingParents', - container: false, - customClass: '', - delay: 0, - fallbackPlacements: ['top', 'right', 'bottom', 'left'], - html: false, - offset: [0, 6], - placement: 'top', - popperConfig: null, - sanitize: true, - sanitizeFn: null, - selector: false, - template: '' + '' + '' + '', - title: '', - trigger: 'hover focus' - }; - const DefaultType$3 = { - allowList: 'object', - animation: 'boolean', - boundary: '(string|element)', - container: '(string|element|boolean)', - customClass: '(string|function)', - delay: '(number|object)', - fallbackPlacements: 'array', - html: 'boolean', - offset: '(array|string|function)', - placement: '(string|function)', - popperConfig: '(null|object|function)', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - selector: '(string|boolean)', - template: 'string', - title: '(string|element|function)', - trigger: 'string' - }; + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW$2); - /** - * Class definition - */ + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering - class Tooltip extends BaseComponent { - constructor(element, config) { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)'); + const complete = () => { + if (this._isWithActiveTrigger()) { + return; } - super(element, config); - - // Private - this._isEnabled = true; - this._timeout = 0; - this._isHovered = null; - this._activeTrigger = {}; - this._popper = null; - this._templateFactory = null; - this._newContent = null; - - // Protected - this.tip = null; - this._setListeners(); - if (!this._config.selector) { - this._fixTitle(); + if (!this._isHovered) { + this._disposeFloating(); } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); } + } - // Getters - static get Default() { - return Default$3; + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); } - static get DefaultType() { - return DefaultType$3; + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE$2); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); } - static get NAME() { - return NAME$4; + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } - // Public - enable() { - this._isEnabled = true; + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; } - disable() { - this._isEnabled = false; + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; } - toggleEnabled() { - this._isEnabled = !this._isEnabled; + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); } - toggle() { - if (!this._isEnabled) { - return; - } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { if (this._isShown()) { - this._leave(); - return; + this._updateFloatingPosition(); } - this._enter(); + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; } - dispose() { - clearTimeout(this._timeout); - EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); - if (this._element.getAttribute('data-bs-original-title')) { - this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); - } - this._disposePopper(); - super.dispose(); + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; } - show() { - if (this._element.style.display === 'none') { - throw new Error('Please use show on visible elements'); - } - if (!(this._isWithContent() && this._isEnabled)) { - return; - } - const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); - const shadowRoot = findShadowRoot(this._element); - const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); - if (showEvent.defaultPrevented || !isInTheDom) { - return; - } - // TODO: v6 remove this or make it optional - this._disposePopper(); - const tip = this._getTipElement(); - this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { const { - container - } = this._config; - if (!this._element.ownerDocument.documentElement.contains(this.tip)) { - container.append(tip); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); - } - this._popper = this._createPopper(tip); - tip.classList.add(CLASS_NAME_SHOW$2); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', noop); - } - } - const complete = () => { - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); - if (this._isHovered === false) { - this._leave(); - } - this._isHovered = false; + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); + } + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - hide() { - if (!this._isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); - if (hideEvent.defaultPrevented) { - return; - } - const tip = this._getTipElement(); - tip.classList.remove(CLASS_NAME_SHOW$2); - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', noop); - } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); + } + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$3), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER$1) : this.constructor.eventName(EVENT_FOCUSIN$2); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); } - this._activeTrigger[TRIGGER_CLICK] = false; - this._activeTrigger[TRIGGER_FOCUS] = false; - this._activeTrigger[TRIGGER_HOVER] = false; - this._isHovered = null; // it is a trick to support manual triggering - - const complete = () => { - if (this._isWithActiveTrigger()) { - return; - } - if (!this._isHovered) { - this._disposePopper(); - } - this._element.removeAttribute('aria-describedby'); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); - }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - update() { - if (this._popper) { - this._popper.update(); + this._hideModalHandler = () => { + if (this._element) { + this.hide(); } + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; } + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { + return; + } - // Protected - _isWithContent() { - return Boolean(this._getTitle()); + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; } - _getTipElement() { - if (!this.tip) { - this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); - } - return this.tip; + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; } - _createTipElement(content) { - const tip = this._getTemplateFactory(content).toHtml(); - - // TODO: remove this check in v6 - if (!tip) { - return null; - } - tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); - // TODO: v6 the following can be achieved with CSS only - tip.classList.add(`bs-${this.constructor.NAME}-auto`); - const tipId = getUID(this.constructor.NAME).toString(); - tip.setAttribute('id', tipId); - if (this._isAnimated()) { - tip.classList.add(CLASS_NAME_FADE$2); - } - return tip; + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); } - setContent(content) { - this._newContent = content; - if (this._isShown()) { - this._disposePopper(); + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { + this._isHovered = true; + return; + } + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { this.show(); } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; } - _getTemplateFactory(content) { - if (this._templateFactory) { - this._templateFactory.changeContent(content); - } else { - this._templateFactory = new TemplateFactory({ - ...this._config, - // the `content` var has to be after `this._config` - // to override config.content in case of popover - content, - extraClass: this._resolvePossibleFunction(this._config.customClass) - }); + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); + } + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; } - return this._templateFactory; } - _getContentForTemplate() { - return { - [SELECTOR_TOOLTIP_INNER]: this._getTitle() + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay }; } - _getTitle() { - return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; + } + } + config.selector = false; + config.trigger = 'manual'; - // Private - _initializeOnDelegatedTarget(event) { - return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; } - _isAnimated() { - return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + if (this.tip) { + this.tip.remove(); + this.tip = null; } - _isShown() { - return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } +} + +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$3); + if (!target) { + return; + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; + +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN$2, SELECTOR_DATA_TOGGLE$3, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER$1, SELECTOR_DATA_TOGGLE$3, initTooltip); + +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$5 = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="popover"]'; +const EVENT_CLICK$2 = 'click'; +const EVENT_FOCUSIN$1 = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default$4 = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType$4 = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; + +/** + * Class definition + */ + +class Popover extends Tooltip { + // Getters + static get Default() { + return Default$4; + } + static get DefaultType() { + return DefaultType$4; + } + static get NAME() { + return NAME$5; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } + + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} + +/** + * Data API implementation - auto-initialize popovers + */ + +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$2); + if (!target) { + return; + } + + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; + +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK$2, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_FOCUSIN$1, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE$2, initPopover); + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$4 = 'range'; +const DATA_KEY$4 = 'bs.range'; +const EVENT_KEY$4 = `.${DATA_KEY$4}`; +const DATA_API_KEY$1 = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY$4}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$4}${DATA_API_KEY$1}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default$3 = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType$3 = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; } - _createPopper(tip) { - const placement = execute(this._config.placement, [this, tip, this._element]); - const attachment = AttachmentMap[placement.toUpperCase()]; - return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment)); + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _resolvePossibleFunction(arg) { - return execute(arg, [this._element, this._element]); - } - _getPopperConfig(attachment) { - const defaultBsPopperConfig = { - placement: attachment, - modifiers: [{ - name: 'flip', - options: { - fallbackPlacements: this._config.fallbackPlacements - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }, { - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'arrow', - options: { - element: `.${this.constructor.NAME}-arrow` - } - }, { - name: 'preSetPlacement', - enabled: true, - phase: 'beforeMain', - fn: data => { - // Pre-set Popper's placement attribute in order to read the arrow sizes properly. - // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement - this._getTipElement().setAttribute('data-popper-placement', data.state.placement); - } - }] - }; - return { - ...defaultBsPopperConfig, - ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); } - _setListeners() { - const triggers = this._config.trigger.split(' '); - for (const trigger of triggers) { - if (trigger === 'click') { - EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); - context.toggle(); - }); - } else if (trigger !== TRIGGER_MANUAL) { - const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1); - const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); - EventHandler.on(this._element, eventIn, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; - context._enter(); - }); - EventHandler.on(this._element, eventOut, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); - context._leave(); - }); - } - } - this._hideModalHandler = () => { - if (this._element) { - this.hide(); - } - }; - EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default$3; + } + static get DefaultType() { + return DefaultType$3; + } + static get NAME() { + return NAME$4; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; } - _fixTitle() { - const title = this._element.getAttribute('title'); - if (!title) { - return; - } - if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { - this._element.setAttribute('aria-label', title); - } - this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility - this._element.removeAttribute('title'); + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; } - _enter() { - if (this._isShown() || this._isHovered) { - this._isHovered = true; - return; + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); } - this._isHovered = true; - this._setTimeout(() => { - if (this._isHovered) { - this.show(); - } - }, this._config.delay.show); } - _leave() { - if (this._isWithActiveTrigger()) { - return; - } - this._isHovered = false; - this._setTimeout(() => { - if (!this._isHovered) { - this.hide(); - } - }, this._config.delay.hide); + if (points.length === 0) { + return; } - _setTimeout(handler, timeout) { - clearTimeout(this._timeout); - this._timeout = setTimeout(handler, timeout); + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$3 = 'scrollspy'; +const DATA_KEY$3 = 'bs.scrollspy'; +const EVENT_KEY$3 = `.${DATA_KEY$3}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY$3}`; +const EVENT_CLICK$1 = `click${EVENT_KEY$3}`; +const EVENT_SCROLL = `scroll${EVENT_KEY$3}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY$3}`; +const EVENT_RESIZE = `resize${EVENT_KEY$3}`; +const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$3}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE$1 = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE$1 = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default$2 = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType$2 = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default$2; + } + static get DefaultType() { + return DefaultType$2; + } + static get NAME() { + return NAME$3; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } - _isWithActiveTrigger() { - return Object.values(this._activeTrigger).includes(true); + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK$1); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); } - _getConfig(config) { - const dataAttributes = Manipulator.getDataAttributes(this._element); - for (const dataAttribute of Object.keys(dataAttributes)) { - if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { - delete dataAttributes[dataAttribute]; - } - } - config = { - ...dataAttributes, - ...(typeof config === 'object' && config ? config : {}) - }; - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - config.container = config.container === false ? document.body : getElement(config.container); - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - if (typeof config.content === 'number') { - config.content = config.content.toString(); + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- + + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); + } else { + this._intersecting.delete(entry.target); } - return config; } - _getDelegateConfig() { - const config = {}; - for (const [key, value] of Object.entries(this._config)) { - if (this.constructor.Default[key] !== value) { - config[key] = value; + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; } } - config.selector = false; - config.trigger = 'manual'; - - // In the future can be replaced with: - // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) - // `Object.fromEntries(keysWithDifferentValues)` - return config; - } - _disposePopper() { - if (this._popper) { - this._popper.destroy(); - this._popper = null; - } - if (this.tip) { - this.tip.remove(); - this.tip = null; - } - } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tooltip.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); + } + if (!active) { + return; + } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); } } - /** - * jQuery - */ - - defineJQueryPlugin(Tooltip); - - /** - * -------------------------------------------------------------------------- - * Bootstrap popover.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; + + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; + } + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } - /** - * Constants - */ + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } - const NAME$3 = 'popover'; - const SELECTOR_TITLE = '.popover-header'; - const SELECTOR_CONTENT = '.popover-body'; - const Default$2 = { - ...Tooltip.Default, - content: '', - offset: [0, 8], - placement: 'right', - template: '' + '' + '' + '' + '', - trigger: 'click' - }; - const DefaultType$2 = { - ...Tooltip.DefaultType, - content: '(null|string|element|function)' - }; + // --- Bottom sentinel ----------------------------------------------------- - /** - * Class definition - */ + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; + } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } - class Popover extends Tooltip { - // Getters - static get Default() { - return Default$2; - } - static get DefaultType() { - return DefaultType$2; - } - static get NAME() { - return NAME$3; - } + // --- Resize (px activation lines only) ----------------------------------- - // Overrides - _isWithContent() { - return this._getTitle() || this._getContent(); + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; } - - // Private - _getContentForTemplate() { - return { - [SELECTOR_TITLE]: this._getTitle(), - [SELECTOR_CONTENT]: this._getContent() - }; + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; } - _getContent() { - return this._resolvePossibleFunction(this._config.content); + } + _rebuildObserver() { + if (!this._observer) { + return; } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Popover.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } } - /** - * jQuery - */ - - defineJQueryPlugin(Popover); - - /** - * -------------------------------------------------------------------------- - * Bootstrap scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$2 = 'scrollspy'; - const DATA_KEY$2 = 'bs.scrollspy'; - const EVENT_KEY$2 = `.${DATA_KEY$2}`; - const DATA_API_KEY = '.data-api'; - const EVENT_ACTIVATE = `activate${EVENT_KEY$2}`; - const EVENT_CLICK = `click${EVENT_KEY$2}`; - const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`; - const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; - const CLASS_NAME_ACTIVE$1 = 'active'; - const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; - const SELECTOR_TARGET_LINKS = '[href]'; - const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; - const SELECTOR_NAV_LINKS = '.nav-link'; - const SELECTOR_NAV_ITEMS = '.nav-item'; - const SELECTOR_LIST_ITEMS = '.list-group-item'; - const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; - const SELECTOR_DROPDOWN = '.dropdown'; - const SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle'; - const Default$1 = { - offset: null, - // TODO: v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: '0px 0px -25%', - smoothScroll: false, - target: null, - threshold: [0.1, 0.5, 1] - }; - const DefaultType$1 = { - offset: '(number|null)', - // TODO v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: 'string', - smoothScroll: 'boolean', - target: 'element', - threshold: 'array' - }; + // --- Smooth-scroll settle (hash + focus) --------------------------------- - /** - * Class definition - */ - - class ScrollSpy extends BaseComponent { - constructor(element, config) { - super(element, config); - - // this._element is the observablesContainer and config.target the menu links wrapper - this._targetLinks = new Map(); - this._observableSections = new Map(); - this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; - this._activeTarget = null; - this._observer = null; - this._previousScrollData = { - visibleEntryTop: 0, - parentScrollTop: 0 - }; - this.refresh(); // initialize + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; } - // Getters - static get Default() { - return Default$1; - } - static get DefaultType() { - return DefaultType$1; - } - static get NAME() { - return NAME$2; - } + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK$1); + EventHandler.on(this._config.target, EVENT_CLICK$1, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { + return; + } + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); + return; + } - // Public - refresh() { - this._initializeTargetsAndObservables(); - this._maybeEnableSmoothScroll(); - if (this._observer) { - this._observer.disconnect(); + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); } else { - this._observer = this._getNewObserver(); - } - for (const section of this._observableSections.values()) { - this._observer.observe(section); + root.scrollTop = height; } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; + } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; } - dispose() { - this._observer.disconnect(); - super.dispose(); + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; - // Private - _configAfterMerge(config) { - // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case - config.target = getElement(config.target) || document.body; - - // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only - config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin; - if (typeof config.threshold === 'string') { - config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); - } - return config; + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); } - _maybeEnableSmoothScroll() { - if (!this._config.smoothScroll) { - return; - } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); + } + section.focus({ + preventScroll: true + }); + } - // unregister any previous listeners - EventHandler.off(this._config.target, EVENT_CLICK); - EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { - const observableSection = this._observableSections.get(event.target.hash); - if (observableSection) { - event.preventDefault(); - const root = this._rootElement || window; - const height = observableSection.offsetTop - this._element.offsetTop; - if (root.scrollTo) { - root.scrollTo({ - top: height, - behavior: 'smooth' - }); - return; - } + // --- Targets / observables ---------------------------------------------- - // Chrome 60 doesn't support `scrollTo` - root.scrollTop = height; - } - }); - } - _getNewObserver() { - const options = { - root: this._rootElement, - threshold: this._config.threshold, - rootMargin: this._config.rootMargin - }; - return new IntersectionObserver(entries => this._observerCallback(entries), options); - } + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } - // The logic of selection - _observerCallback(entries) { - const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); - const activate = entry => { - this._previousScrollData.visibleEntryTop = entry.target.offsetTop; - this._process(targetElement(entry)); - }; - const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; - const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; - this._previousScrollData.parentScrollTop = parentScrollTop; - for (const entry of entries) { - if (!entry.isIntersecting) { - this._activeTarget = null; - this._clearActiveClass(targetElement(entry)); - continue; - } - const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; - // if we are scrolling down, pick the bigger offsetTop - if (userScrollsDown && entryIsLowerThanPrevious) { - activate(entry); - // if parent isn't scrolled, let's keep the first visible item, breaking the iteration - if (!parentScrollTop) { - return; - } - continue; - } + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section - // if we are scrolling up, pick the smallest offsetTop - if (!userScrollsDown && !entryIsLowerThanPrevious) { - activate(entry); - } + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); } } - _initializeTargetsAndObservables() { - this._targetLinks = new Map(); - this._observableSections = new Map(); - const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); - for (const anchor of targetLinks) { - // ensure that the anchor has an id and is not disabled - if (!anchor.hash || isDisabled(anchor)) { - continue; - } - const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element); - // ensure that the observableSection exists & is visible - if (isVisible(observableSection)) { - this._targetLinks.set(decodeURI(anchor.hash), anchor); - this._observableSections.set(anchor.hash, observableSection); - } - } + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; } - _process(target) { - if (this._activeTarget === target) { - return; + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE$1); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE$1)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE$1); } - this._clearActiveClass(this._config.target); - this._activeTarget = target; - target.classList.add(CLASS_NAME_ACTIVE$1); - this._activateParents(target); - EventHandler.trigger(this._element, EVENT_ACTIVATE, { - relatedTarget: target - }); + return; } - _activateParents(target) { - // Activate dropdown parents - if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { - SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1); - return; - } - for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { - // Set triggered links parents as active - // With both and markup a parent is the previous sibling of any nav ancestor - for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { - item.classList.add(CLASS_NAME_ACTIVE$1); - } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE$1); } } - _clearActiveClass(parent) { - parent.classList.remove(CLASS_NAME_ACTIVE$1); - const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); - for (const node of activeNodes) { - node.classList.remove(CLASS_NAME_ACTIVE$1); - } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE$1); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE$1); } + } +} + +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = ScrollSpy.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); +/** + * Data API implementation + */ + +EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$2 = 'tab'; +const DATA_KEY$2 = 'bs.tab'; +const EVENT_KEY$2 = `.${DATA_KEY$2}`; +const EVENT_HIDE$1 = `hide${EVENT_KEY$2}`; +const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$2}`; +const EVENT_SHOW$1 = `show${EVENT_KEY$2}`; +const EVENT_SHOWN$1 = `shown${EVENT_KEY$2}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY$2}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY$2}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY$2}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE$1 = 'fade'; +const CLASS_NAME_SHOW$1 = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; + +/** + * Class definition + */ + +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) } + + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); } - /** - * Data API implementation - */ + // Getters + static get NAME() { + return NAME$2; + } - EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { - for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { - ScrollSpy.getOrCreateInstance(spy); + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; } - }); - - /** - * jQuery - */ - - defineJQueryPlugin(ScrollSpy); - - /** - * -------------------------------------------------------------------------- - * Bootstrap tab.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$1 = 'tab'; - const DATA_KEY$1 = 'bs.tab'; - const EVENT_KEY$1 = `.${DATA_KEY$1}`; - const EVENT_HIDE$1 = `hide${EVENT_KEY$1}`; - const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$1}`; - const EVENT_SHOW$1 = `show${EVENT_KEY$1}`; - const EVENT_SHOWN$1 = `shown${EVENT_KEY$1}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY$1}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY$1}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY$1}`; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const HOME_KEY = 'Home'; - const END_KEY = 'End'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_FADE$1 = 'fade'; - const CLASS_NAME_SHOW$1 = 'show'; - const CLASS_DROPDOWN = 'dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; - const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; - const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; - const SELECTOR_OUTER = '.nav-item, .list-group-item'; - const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6 - const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; - const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`; - - /** - * Class definition - */ - - class Tab extends BaseComponent { - constructor(element) { - super(element); - this._parent = this._element.closest(SELECTOR_TAB_PANEL); - if (!this._parent) { - return; - // TODO: should throw exception in v6 - // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) - } - // Set up initial aria attributes - this._setInitialAttributes(this._parent, this._getChildren()); - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } - // Getters - static get NAME() { - return NAME$1; + // Private + _activate(element, relatedElem) { + if (!element) { + return; } + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section - // Public - show() { - // Shows this elem and deactivate the active sibling if exists - const innerElem = this._element; - if (this._elemIsActive(innerElem)) { + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW$1); return; } - - // Search for active tab on same parent to deactivate it - const active = this._getActiveElem(); - const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { - relatedTarget: innerElem - }) : null; - const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { - relatedTarget: active + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN$1, { + relatedTarget: relatedElem }); - if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { - return; - } - this._deactivate(active, innerElem); - this._activate(innerElem, active); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; } + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - // Private - _activate(element, relatedElem) { - if (!element) { + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW$1); return; } - element.classList.add(CLASS_NAME_ACTIVE); - this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; + } - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.add(CLASS_NAME_SHOW$1); - return; - } - element.removeAttribute('tabindex'); - element.setAttribute('aria-selected', true); - this._toggleDropDown(element, true); - EventHandler.trigger(element, EVENT_SHOWN$1, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; } - _deactivate(element, relatedElem) { - if (!element) { - return; - } - element.classList.remove(CLASS_NAME_ACTIVE); - element.blur(); - this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.remove(CLASS_NAME_SHOW$1); - return; - } - element.setAttribute('aria-selected', false); - element.setAttribute('tabindex', '-1'); - this._toggleDropDown(element, false); - EventHandler.trigger(element, EVENT_HIDDEN$1, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); } - _keydown(event) { - if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { - return; - } - event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page - event.preventDefault(); - const children = this._getChildren().filter(element => !isDisabled(element)); - let nextActiveElement; - if ([HOME_KEY, END_KEY].includes(event.key)) { - nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]; - } else { - const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); - nextActiveElement = getNextActiveElement(children, event.target, isNext, true); - } - if (nextActiveElement) { - nextActiveElement.focus({ - preventScroll: true - }); - Tab.getOrCreateInstance(nextActiveElement).show(); - } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); } - _getChildren() { - // collection of inner elements - return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); } - _getActiveElem() { - return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); } - _setInitialAttributes(parent, children) { - this._setAttributeIfNotExists(parent, 'role', 'tablist'); - for (const child of children) { - this._setInitialAttributesOnChild(child); - } + if (!isActive) { + child.setAttribute('tabindex', '-1'); } - _setInitialAttributesOnChild(child) { - child = this._getInnerElement(child); - const isActive = this._elemIsActive(child); - const outerElem = this._getOuterElement(child); - child.setAttribute('aria-selected', isActive); - if (outerElem !== child) { - this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); - } - if (!isActive) { - child.setAttribute('tabindex', '-1'); - } - this._setAttributeIfNotExists(child, 'role', 'tab'); + this._setAttributeIfNotExists(child, 'role', 'tab'); - // set attributes to the related panel too - this._setInitialAttributesOnTargetPanel(child); + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; } - _setInitialAttributesOnTargetPanel(child) { - const target = SelectorEngine.getElementFromSelector(child); - if (!target) { - return; - } - this._setAttributeIfNotExists(target, 'role', 'tabpanel'); - if (child.id) { - this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); - } + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); } - _toggleDropDown(element, open) { - const outerElem = this._getOuterElement(element); - if (!outerElem.classList.contains(CLASS_DROPDOWN)) { - return; - } - const toggle = (selector, className) => { - const element = SelectorEngine.findOne(selector, outerElem); - if (element) { - element.classList.toggle(className, open); - } - }; - toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE); - toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW$1); - outerElem.setAttribute('aria-expanded', open); + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; } - _setAttributeIfNotExists(element, attribute, value) { - if (!element.hasAttribute(attribute)) { - element.setAttribute(attribute, value); - } + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW$1, open); } - _elemIsActive(elem) { - return elem.classList.contains(CLASS_NAME_ACTIVE); + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); } + } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } - // Try to get the inner element (usually the .nav-link) - _getInnerElement(elem) { - return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); - } + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } - // Try to get the outer element (usually the .nav-item) - _getOuterElement(elem) { - return elem.closest(SELECTOR_OUTER) || elem; - } + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tab.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE$1, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); + +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$1 = 'toast'; +const DATA_KEY$1 = 'bs.toast'; +const EVENT_KEY$1 = `.${DATA_KEY$1}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY$1}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY$1}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY$1}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY$1}`; +const EVENT_HIDE = `hide${EVENT_KEY$1}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY$1}`; +const EVENT_SHOW = `show${EVENT_KEY$1}`; +const EVENT_SHOWN = `shown${EVENT_KEY$1}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType$1 = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default$1 = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$1; + } + static get DefaultType() { + return DefaultType$1; + } + static get NAME() { + return NAME$1; + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (isDisabled(this)) { + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { return; } - Tab.getOrCreateInstance(this).show(); - }); - - /** - * Initialize on focus - */ - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { - Tab.getOrCreateInstance(element); - } - }); - /** - * jQuery - */ - - defineJQueryPlugin(Tab); - - /** - * -------------------------------------------------------------------------- - * Bootstrap toast.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'toast'; - const DATA_KEY = 'bs.toast'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; - const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const DefaultType = { - animation: 'boolean', - autohide: 'boolean', - delay: 'number' - }; - const Default = { - animation: true, - autohide: true, - delay: 5000 - }; - - /** - * Class definition - */ - - class Toast extends BaseComponent { - constructor(element, config) { - super(element, config); - this._timeout = null; - this._hasMouseInteraction = false; - this._hasKeyboardInteraction = false; - this._setListeners(); + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); } - - // Getters - static get Default() { - return Default; + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; } - static get DefaultType() { - return DefaultType; + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; } - static get NAME() { - return NAME; + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } - // Public - show() { - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (showEvent.defaultPrevented) { - return; - } - this._clearTimeout(); - if (this._config.animation) { - this._element.classList.add(CLASS_NAME_FADE); - } - const complete = () => { - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN); - this._maybeScheduleHide(); - }; - this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated - reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; } - hide() { - if (!this.isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - const complete = () => { - this._element.classList.add(CLASS_NAME_HIDE); // @deprecated - this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.classList.add(CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; + } + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } } - dispose() { + if (isInteracting) { this._clearTimeout(); - if (this.isShown()) { - this._element.classList.remove(CLASS_NAME_SHOW); - } - super.dispose(); + return; } - isShown() { - return this._element.classList.contains(CLASS_NAME_SHOW); + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; } + this._maybeScheduleHide(); + } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Toast); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Private - _maybeScheduleHide() { - if (!this._config.autohide) { - return; - } - if (this._hasMouseInteraction || this._hasKeyboardInteraction) { - return; - } - this._timeout = setTimeout(() => { - this.hide(); - }, this._config.delay); - } - _onInteraction(event, isInteracting) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - { - this._hasMouseInteraction = isInteracting; - break; - } - case 'focusin': - case 'focusout': - { - this._hasKeyboardInteraction = isInteracting; - break; - } - } - if (isInteracting) { - this._clearTimeout(); - return; - } - const nextElement = event.relatedTarget; - if (this._element === nextElement || this._element.contains(nextElement)) { - return; - } - this._maybeScheduleHide(); - } - _setListeners() { - EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); - EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; } - _clearTimeout() { - clearTimeout(this._timeout); - this._timeout = null; + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Toast.getOrCreateInstance(this, config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - } - }); + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; } + this._element.setAttribute(attribute, value); } +} - /** - * Data API implementation - */ - - enableDismissTrigger(Toast); - - /** - * jQuery - */ - - defineJQueryPlugin(Toast); - - /** - * -------------------------------------------------------------------------- - * Bootstrap index.umd.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const index_umd = { - Alert, - Button, - Carousel, - Collapse, - Dropdown, - Modal, - Offcanvas, - Popover, - ScrollSpy, - Tab, - Toast, - Tooltip - }; +/** + * Data API implementation + */ - return index_umd; +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); -})); +export { Alert, Button, Carousel, Chips, Collapse, Combobox, Datepicker, Dialog, Drawer, Menu, NavOverflow, OtpInput, Popover, Range, ScrollSpy, Strength, Tab, Toast, Toggler, Tooltip }; diff --git a/assets/javascripts/bootstrap.min.js b/assets/javascripts/bootstrap.min.js index 8c0e1727..0609a811 100644 --- a/assets/javascripts/bootstrap.min.js +++ b/assets/javascripts/bootstrap.min.js @@ -1,6 +1,6 @@ /*! - * Bootstrap v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),a=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),l=t=>{t.dispatchEvent(new Event(o))},c=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),h=t=>c(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,d=t=>{if(!c(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},u=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),_=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?_(t.parentNode):null},g=()=>{},f=t=>{t.offsetHeight},m=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,p=[],b=()=>"rtl"===document.documentElement.dir,v=t=>{var e;e=()=>{const e=m();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of p)t()}),p.push(e)):e()},y=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,w=(t,e,i=!0)=>{if(!i)return void y(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),y(t))};e.addEventListener(o,r),setTimeout(()=>{n||l(e)},s)},A=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},E=/[^.]*(?=\..*)\.|.*/,C=/\..*/,T=/::\d+$/,k={};let $=1;const S={mouseenter:"mouseover",mouseleave:"mouseout"},L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${$++}`||t.uidEvent||$++}function I(t){const e=O(t);return t.uidEvent=e,k[e]=k[e]||{},k[e]}function D(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function N(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=j(t);return L.has(o)||(o=t),[s,n,o]}function P(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=N(e,i,s);if(e in S){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=I(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=O(r,e.replace(E,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return z(n,{delegateTarget:r}),s.oneOff&&F.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return z(s,{delegateTarget:t}),i.oneOff&&F.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function x(t,e,i,s,n){const o=D(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function M(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&x(t,e,i,r.callable,r.delegationSelector)}function j(t){return t=t.replace(C,""),S[t]||t}const F={on(t,e,i,s){P(t,e,i,s,!1)},one(t,e,i,s){P(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=N(e,i,s),a=r!==e,l=I(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))M(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(T,"");a&&!e.includes(n)||x(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;x(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=m();let n=null,o=!0,r=!0,a=!1;e!==j(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=z(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function z(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function H(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function B(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const q={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${B(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${B(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=H(t.dataset[s])}return e},getDataAttribute:(t,e)=>H(t.getAttribute(`data-bs-${B(e)}`))};class W{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=c(e)?q.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...c(e)?q.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,s]of Object.entries(e)){const e=t[i],n=c(e)?"element":a(e);if(!new RegExp(s).test(n))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${n}" but expected type "${s}".`)}}}class R extends W{constructor(t,e){super(),(t=h(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),F.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){w(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(h(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.8"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const K=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map(t=>r(t)).join(","):null},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!u(t)&&d(t))},getSelectorFromElement(t){const e=K(t);return e&&V.findOne(e)?e:null},getElementFromSelector(t){const e=K(t);return e?V.findOne(e):null},getMultipleElementsFromSelector(t){const e=K(t);return e?V.find(e):[]}},Q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;F.on(document,i,`[data-bs-dismiss="${s}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),u(this))return;const n=V.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()})},X=".bs.alert",Y=`close${X}`,U=`closed${X}`;class G extends R{static get NAME(){return"alert"}close(){if(F.trigger(this._element,Y).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),F.trigger(this._element,U),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=G.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}Q(G,"close"),v(G);const J='[data-bs-toggle="button"]';class Z extends R{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=Z.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}F.on(document,"click.bs.button.data-api",J,t=>{t.preventDefault();const e=t.target.closest(J);Z.getOrCreateInstance(e).toggle()}),v(Z);const tt=".bs.swipe",et=`touchstart${tt}`,it=`touchmove${tt}`,st=`touchend${tt}`,nt=`pointerdown${tt}`,ot=`pointerup${tt}`,rt={endCallback:null,leftCallback:null,rightCallback:null},at={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class lt extends W{constructor(t,e){super(),this._element=t,t&<.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return rt}static get DefaultType(){return at}static get NAME(){return"swipe"}dispose(){F.off(this._element,tt)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&y(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(F.on(this._element,nt,t=>this._start(t)),F.on(this._element,ot,t=>this._end(t)),this._element.classList.add("pointer-event")):(F.on(this._element,et,t=>this._start(t)),F.on(this._element,it,t=>this._move(t)),F.on(this._element,st,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ct=".bs.carousel",ht=".data-api",dt="ArrowLeft",ut="ArrowRight",_t="next",gt="prev",ft="left",mt="right",pt=`slide${ct}`,bt=`slid${ct}`,vt=`keydown${ct}`,yt=`mouseenter${ct}`,wt=`mouseleave${ct}`,At=`dragstart${ct}`,Et=`load${ct}${ht}`,Ct=`click${ct}${ht}`,Tt="carousel",kt="active",$t=".active",St=".carousel-item",Lt=$t+St,Ot={[dt]:mt,[ut]:ft},It={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Dt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Nt extends R{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Tt&&this.cycle()}static get Default(){return It}static get DefaultType(){return Dt}static get NAME(){return"carousel"}next(){this._slide(_t)}nextWhenVisible(){!document.hidden&&d(this._element)&&this.next()}prev(){this._slide(gt)}pause(){this._isSliding&&l(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?F.one(this._element,bt,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void F.one(this._element,bt,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?_t:gt;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&F.on(this._element,vt,t=>this._keydown(t)),"hover"===this._config.pause&&(F.on(this._element,yt,()=>this.pause()),F.on(this._element,wt,()=>this._maybeEnableCycle())),this._config.touch&<.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of V.find(".carousel-item img",this._element))F.on(t,At,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(ft)),rightCallback:()=>this._slide(this._directionToOrder(mt)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new lt(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Ot[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=V.findOne($t,this._indicatorsElement);e.classList.remove(kt),e.removeAttribute("aria-current");const i=V.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(kt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===_t,n=e||A(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>F.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(pt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),f(n),i.classList.add(l),n.classList.add(l),this._queueCallback(()=>{n.classList.remove(l,c),n.classList.add(kt),i.classList.remove(kt,c,l),this._isSliding=!1,r(bt)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return V.findOne(Lt,this._element)}_getItems(){return V.find(St,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return b()?t===ft?gt:_t:t===ft?_t:gt}_orderToDirection(t){return b()?t===gt?ft:mt:t===gt?mt:ft}static jQueryInterface(t){return this.each(function(){const e=Nt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}F.on(document,Ct,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=V.getElementFromSelector(this);if(!e||!e.classList.contains(Tt))return;t.preventDefault();const i=Nt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===q.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),F.on(window,Et,()=>{const t=V.find('[data-bs-ride="carousel"]');for(const e of t)Nt.getOrCreateInstance(e)}),v(Nt);const Pt=".bs.collapse",xt=`show${Pt}`,Mt=`shown${Pt}`,jt=`hide${Pt}`,Ft=`hidden${Pt}`,zt=`click${Pt}.data-api`,Ht="show",Bt="collapse",qt="collapsing",Wt=`:scope .${Bt} .${Bt}`,Rt='[data-bs-toggle="collapse"]',Kt={parent:null,toggle:!0},Vt={parent:"(null|element)",toggle:"boolean"};class Qt extends R{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=V.find(Rt);for(const t of i){const e=V.getSelectorFromElement(t),i=V.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Kt}static get DefaultType(){return Vt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>Qt.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(F.trigger(this._element,xt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Bt),this._element.classList.add(qt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(qt),this._element.classList.add(Bt,Ht),this._element.style[e]="",F.trigger(this._element,Mt)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(F.trigger(this._element,jt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,f(this._element),this._element.classList.add(qt),this._element.classList.remove(Bt,Ht);for(const t of this._triggerArray){const e=V.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(qt),this._element.classList.add(Bt),F.trigger(this._element,Ft)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(Ht)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=h(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Rt);for(const e of t){const t=V.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=V.find(Wt,this._config.parent);return V.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=Qt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}F.on(document,zt,Rt,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of V.getMultipleElementsFromSelector(this))Qt.getOrCreateInstance(t,{toggle:!1}).toggle()}),v(Qt);const Xt="dropdown",Yt=".bs.dropdown",Ut=".data-api",Gt="ArrowUp",Jt="ArrowDown",Zt=`hide${Yt}`,te=`hidden${Yt}`,ee=`show${Yt}`,ie=`shown${Yt}`,se=`click${Yt}${Ut}`,ne=`keydown${Yt}${Ut}`,oe=`keyup${Yt}${Ut}`,re="show",ae='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',le=`${ae}.${re}`,ce=".dropdown-menu",he=b()?"top-end":"top-start",de=b()?"top-start":"top-end",ue=b()?"bottom-end":"bottom-start",_e=b()?"bottom-start":"bottom-end",ge=b()?"left-start":"right-start",fe=b()?"right-start":"left-start",me={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},pe={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class be extends R{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=V.next(this._element,ce)[0]||V.prev(this._element,ce)[0]||V.findOne(ce,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return me}static get DefaultType(){return pe}static get NAME(){return Xt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(u(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!F.trigger(this._element,ee,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))F.on(t,"mouseover",g);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(re),this._element.classList.add(re),F.trigger(this._element,ie,t)}}hide(){if(u(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!F.trigger(this._element,Zt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.off(t,"mouseover",g);this._popper&&this._popper.destroy(),this._menu.classList.remove(re),this._element.classList.remove(re),this._element.setAttribute("aria-expanded","false"),q.removeDataAttribute(this._menu,"popper"),F.trigger(this._element,te,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!c(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Xt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:c(this._config.reference)?t=h(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(re)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return ge;if(t.classList.contains("dropstart"))return fe;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?de:he:e?_e:ue}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(q.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...y(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>d(t));i.length&&A(i,e,t===Jt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=be.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=V.find(le);for(const i of e){const e=be.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Gt,Jt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(ae)?this:V.prev(this,ae)[0]||V.next(this,ae)[0]||V.findOne(ae,t.delegateTarget.parentNode),o=be.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}F.on(document,ne,ae,be.dataApiKeydownHandler),F.on(document,ne,ce,be.dataApiKeydownHandler),F.on(document,se,be.clearMenus),F.on(document,oe,be.clearMenus),F.on(document,se,ae,function(t){t.preventDefault(),be.getOrCreateInstance(this).toggle()}),v(be);const ve="backdrop",ye="show",we=`mousedown.bs.${ve}`,Ae={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ee={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ce extends W{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Ae}static get DefaultType(){return Ee}static get NAME(){return ve}show(t){if(!this._config.isVisible)return void y(t);this._append();const e=this._getElement();this._config.isAnimated&&f(e),e.classList.add(ye),this._emulateAnimation(()=>{y(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(ye),this._emulateAnimation(()=>{this.dispose(),y(t)})):y(t)}dispose(){this._isAppended&&(F.off(this._element,we),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=h(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),F.on(t,we,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){w(t,this._getElement(),this._config.isAnimated)}}const Te=".bs.focustrap",ke=`focusin${Te}`,$e=`keydown.tab${Te}`,Se="backward",Le={autofocus:!0,trapElement:null},Oe={autofocus:"boolean",trapElement:"element"};class Ie extends W{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Le}static get DefaultType(){return Oe}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),F.off(document,Te),F.on(document,ke,t=>this._handleFocusin(t)),F.on(document,$e,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,F.off(document,Te))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=V.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Se?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Se:"forward")}}const De=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ne=".sticky-top",Pe="padding-right",xe="margin-right";class Me{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Pe,e=>e+t),this._setElementAttributes(De,Pe,e=>e+t),this._setElementAttributes(Ne,xe,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Pe),this._resetElementAttributes(De,Pe),this._resetElementAttributes(Ne,xe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&q.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=q.getDataAttribute(t,e);null!==i?(q.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(c(t))e(t);else for(const i of V.find(t,this._element))e(i)}}const je=".bs.modal",Fe=`hide${je}`,ze=`hidePrevented${je}`,He=`hidden${je}`,Be=`show${je}`,qe=`shown${je}`,We=`resize${je}`,Re=`click.dismiss${je}`,Ke=`mousedown.dismiss${je}`,Ve=`keydown.dismiss${je}`,Qe=`click${je}.data-api`,Xe="modal-open",Ye="show",Ue="modal-static",Ge={backdrop:!0,focus:!0,keyboard:!0},Je={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ze extends R{constructor(t,e){super(t,e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Me,this._addEventListeners()}static get Default(){return Ge}static get DefaultType(){return Je}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||F.trigger(this._element,Be,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Xe),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(F.trigger(this._element,Fe).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Ye),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){F.off(window,je),F.off(this._dialog,je),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ce({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ie({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=V.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),f(this._element),this._element.classList.add(Ye),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,F.trigger(this._element,qe,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){F.on(this._element,Ve,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),F.on(window,We,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),F.on(this._element,Ke,t=>{F.one(this._element,Re,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Xe),this._resetAdjustments(),this._scrollBar.reset(),F.trigger(this._element,He)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(F.trigger(this._element,ze).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ue)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ue),this._queueCallback(()=>{this._element.classList.remove(Ue),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=b()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=b()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Ze.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}F.on(document,Qe,'[data-bs-toggle="modal"]',function(t){const e=V.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),F.one(e,Be,t=>{t.defaultPrevented||F.one(e,He,()=>{d(this)&&this.focus()})});const i=V.findOne(".modal.show");i&&Ze.getInstance(i).hide(),Ze.getOrCreateInstance(e).toggle(this)}),Q(Ze),v(Ze);const ti=".bs.offcanvas",ei=".data-api",ii=`load${ti}${ei}`,si="show",ni="showing",oi="hiding",ri=".offcanvas.show",ai=`show${ti}`,li=`shown${ti}`,ci=`hide${ti}`,hi=`hidePrevented${ti}`,di=`hidden${ti}`,ui=`resize${ti}`,_i=`click${ti}${ei}`,gi=`keydown.dismiss${ti}`,fi={backdrop:!0,keyboard:!0,scroll:!1},mi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class pi extends R{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return fi}static get DefaultType(){return mi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||F.trigger(this._element,ai,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Me).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ni),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(si),this._element.classList.remove(ni),F.trigger(this._element,li,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(F.trigger(this._element,ci).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(oi),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(si,oi),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Me).reset(),F.trigger(this._element,di)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ce({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():F.trigger(this._element,hi)}:null})}_initializeFocusTrap(){return new Ie({trapElement:this._element})}_addEventListeners(){F.on(this._element,gi,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():F.trigger(this._element,hi))})}static jQueryInterface(t){return this.each(function(){const e=pi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}F.on(document,_i,'[data-bs-toggle="offcanvas"]',function(t){const e=V.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this))return;F.one(e,di,()=>{d(this)&&this.focus()});const i=V.findOne(ri);i&&i!==e&&pi.getInstance(i).hide(),pi.getOrCreateInstance(e).toggle(this)}),F.on(window,ii,()=>{for(const t of V.find(ri))pi.getOrCreateInstance(t).show()}),F.on(window,ui,()=>{for(const t of V.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&pi.getOrCreateInstance(t).hide()}),Q(pi),v(pi);const bi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],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:[]},vi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),yi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,wi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!vi.has(i)||Boolean(yi.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},Ai={allowList:bi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},Ei={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ci={entry:"(string|element|function|null)",selector:"(string|element)"};class Ti extends W{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Ai}static get DefaultType(){return Ei}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ci)}_setContent(t,e,i){const s=V.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?c(e)?this._putElementInTemplate(h(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)wi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return y(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const ki=new Set(["sanitize","allowList","sanitizeFn"]),$i="fade",Si="show",Li=".tooltip-inner",Oi=".modal",Ii="hide.bs.modal",Di="hover",Ni="focus",Pi="click",xi={AUTO:"auto",TOP:"top",RIGHT:b()?"left":"right",BOTTOM:"bottom",LEFT:b()?"right":"left"},Mi={allowList:bi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ji={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Fi extends R{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Mi}static get DefaultType(){return ji}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),F.off(this._element.closest(Oi),Ii,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=F.trigger(this._element,this.constructor.eventName("show")),e=(_(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),F.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(Si),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.on(t,"mouseover",g);this._queueCallback(()=>{F.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!F.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(Si),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.off(t,"mouseover",g);this._activeTrigger[Pi]=!1,this._activeTrigger[Ni]=!1,this._activeTrigger[Di]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),F.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove($i,Si),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add($i),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ti({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Li]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains($i)}_isShown(){return this.tip&&this.tip.classList.contains(Si)}_createPopper(t){const e=y(this._config.placement,[this,t,this._element]),s=xi[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return y(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...y(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)F.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[Pi]=!(e._isShown()&&e._activeTrigger[Pi]),e.toggle()});else if("manual"!==e){const t=e===Di?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Di?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");F.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Ni:Di]=!0,e._enter()}),F.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Ni:Di]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},F.on(this._element.closest(Oi),Ii,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=q.getDataAttributes(this._element);for(const t of Object.keys(e))ki.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:h(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}v(Fi);const zi=".popover-header",Hi=".popover-body",Bi={...Fi.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},qi={...Fi.DefaultType,content:"(null|string|element|function)"};class Wi extends Fi{static get Default(){return Bi}static get DefaultType(){return qi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[zi]:this._getTitle(),[Hi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=Wi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}v(Wi);const Ri=".bs.scrollspy",Ki=`activate${Ri}`,Vi=`click${Ri}`,Qi=`load${Ri}.data-api`,Xi="active",Yi="[href]",Ui=".nav-link",Gi=`${Ui}, .nav-item > ${Ui}, .list-group-item`,Ji={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Zi={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class ts extends R{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ji}static get DefaultType(){return Zi}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=h(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(F.off(this._config.target,Vi),F.on(this._config.target,Vi,Yi,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=V.find(Yi,this._config.target);for(const e of t){if(!e.hash||u(e))continue;const t=V.findOne(decodeURI(e.hash),this._element);d(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Xi),this._activateParents(t),F.trigger(this._element,Ki,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))V.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Xi);else for(const e of V.parents(t,".nav, .list-group"))for(const t of V.prev(e,Gi))t.classList.add(Xi)}_clearActiveClass(t){t.classList.remove(Xi);const e=V.find(`${Yi}.${Xi}`,t);for(const t of e)t.classList.remove(Xi)}static jQueryInterface(t){return this.each(function(){const e=ts.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}F.on(window,Qi,()=>{for(const t of V.find('[data-bs-spy="scroll"]'))ts.getOrCreateInstance(t)}),v(ts);const es=".bs.tab",is=`hide${es}`,ss=`hidden${es}`,ns=`show${es}`,os=`shown${es}`,rs=`click${es}`,as=`keydown${es}`,ls=`load${es}`,cs="ArrowLeft",hs="ArrowRight",ds="ArrowUp",us="ArrowDown",_s="Home",gs="End",fs="active",ms="fade",ps="show",bs=".dropdown-toggle",vs=`:not(${bs})`,ys='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',ws=`.nav-link${vs}, .list-group-item${vs}, [role="tab"]${vs}, ${ys}`,As=`.${fs}[data-bs-toggle="tab"], .${fs}[data-bs-toggle="pill"], .${fs}[data-bs-toggle="list"]`;class Es extends R{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),F.on(this._element,as,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?F.trigger(e,is,{relatedTarget:t}):null;F.trigger(t,ns,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(fs),this._activate(V.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),F.trigger(t,os,{relatedTarget:e})):t.classList.add(ps)},t,t.classList.contains(ms)))}_deactivate(t,e){t&&(t.classList.remove(fs),t.blur(),this._deactivate(V.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),F.trigger(t,ss,{relatedTarget:e})):t.classList.remove(ps)},t,t.classList.contains(ms)))}_keydown(t){if(![cs,hs,ds,us,_s,gs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!u(t));let i;if([_s,gs].includes(t.key))i=e[t.key===_s?0:e.length-1];else{const s=[hs,us].includes(t.key);i=A(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),Es.getOrCreateInstance(i).show())}_getChildren(){return V.find(ws,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=V.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=V.findOne(t,i);n&&n.classList.toggle(s,e)};s(bs,fs),s(".dropdown-menu",ps),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(fs)}_getInnerElement(t){return t.matches(ws)?t:V.findOne(ws,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=Es.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}F.on(document,rs,ys,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this)||Es.getOrCreateInstance(this).show()}),F.on(window,ls,()=>{for(const t of V.find(As))Es.getOrCreateInstance(t)}),v(Es);const Cs=".bs.toast",Ts=`mouseover${Cs}`,ks=`mouseout${Cs}`,$s=`focusin${Cs}`,Ss=`focusout${Cs}`,Ls=`hide${Cs}`,Os=`hidden${Cs}`,Is=`show${Cs}`,Ds=`shown${Cs}`,Ns="hide",Ps="show",xs="showing",Ms={animation:"boolean",autohide:"boolean",delay:"number"},js={animation:!0,autohide:!0,delay:5e3};class Fs extends R{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return js}static get DefaultType(){return Ms}static get NAME(){return"toast"}show(){F.trigger(this._element,Is).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ns),f(this._element),this._element.classList.add(Ps,xs),this._queueCallback(()=>{this._element.classList.remove(xs),F.trigger(this._element,Ds),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(F.trigger(this._element,Ls).defaultPrevented||(this._element.classList.add(xs),this._queueCallback(()=>{this._element.classList.add(Ns),this._element.classList.remove(xs,Ps),F.trigger(this._element,Os)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ps),super.dispose()}isShown(){return this._element.classList.contains(Ps)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){F.on(this._element,Ts,t=>this._onInteraction(t,!0)),F.on(this._element,ks,t=>this._onInteraction(t,!1)),F.on(this._element,$s,t=>this._onInteraction(t,!0)),F.on(this._element,Ss,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=Fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}return Q(Fs),v(Fs),{Alert:G,Button:Z,Carousel:Nt,Collapse:Qt,Dropdown:be,Modal:Ze,Offcanvas:pi,Popover:Wi,ScrollSpy:ts,Tab:Es,Toast:Fs,Tooltip:Fi}}); +import{computePosition,autoUpdate,offset,flip,shift,arrow}from"@floating-ui/dom";import{Calendar}from"vanilla-calendar-pro";const elementMap=new Map,Data={set(e,t,n){elementMap.has(e)||elementMap.set(e,new Map);const s=elementMap.get(e);s.has(t)||0===s.size?s.set(t,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...s.keys()][0]}.`)},get:(e,t)=>elementMap.has(e)&&elementMap.get(e).get(t)||null,getAny:e=>elementMap.has(e)&&elementMap.get(e).values().next().value||null,remove(e,t){if(!elementMap.has(e))return;const n=elementMap.get(e);n.delete(t),0===n.size&&elementMap.delete(e)}},namespaceRegex=/[^.]*(?=\..*)\.|.*/,stripNameRegex=/\..*/,stripUidRegex=/::\d+$/,eventRegistry={};let uidEvent=1;const customEvents={mouseenter:"mouseover",mouseleave:"mouseout"},nativeEvents=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll","scrollend"]);function makeEventUid(e,t){return t&&`${t}::${uidEvent++}`||e.uidEvent||uidEvent++}function getElementEvents(e){const t=makeEventUid(e);return e.uidEvent=t,eventRegistry[t]=eventRegistry[t]||{},eventRegistry[t]}function bootstrapHandler(e,t){return function n(s){return hydrateObj(s,{delegateTarget:e}),n.oneOff&&EventHandler.off(e,s.type,t),t.apply(e,[s])}}function bootstrapDelegationHandler(e,t,n){return function s(i){const o=e.querySelectorAll(t);for(let{target:r}=i;r&&r!==this;r=r.parentNode)for(const l of o)if(l===r)return hydrateObj(i,{delegateTarget:r}),s.oneOff&&EventHandler.off(e,i.type,t,n),n.apply(r,[i])}}function findHandler(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function normalizeParameters(e,t,n){const s="string"==typeof t,i=s?n:t||n;let o=getTypeEvent(e);return nativeEvents.has(o)||(o=e),[s,i,o]}function addHandler(e,t,n,s,i){if("string"!=typeof t||!e)return;let[o,r,l]=normalizeParameters(t,n,s);if(t in customEvents){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};r=e(r)}const a=getElementEvents(e),c=a[l]||(a[l]={}),_=findHandler(c,r,o?n:null);if(_)return void(_.oneOff=_.oneOff&&i);const h=makeEventUid(r,t.replace(namespaceRegex,"")),u=o?bootstrapDelegationHandler(e,n,r):bootstrapHandler(e,r);u.delegationSelector=o?n:null,u.callable=r,u.oneOff=i,u.uidEvent=h,c[h]=u,e.addEventListener(l,u,o)}function removeHandler(e,t,n,s,i){const o=findHandler(t[n],s,i);o&&(e.removeEventListener(n,o,Boolean(i)),delete t[n][o.uidEvent])}function removeNamespacedHandlers(e,t,n,s){const i=t[n]||{};for(const[o,r]of Object.entries(i))o.includes(s)&&removeHandler(e,t,n,r.callable,r.delegationSelector)}function getTypeEvent(e){return e=e.replace(stripNameRegex,""),customEvents[e]||e}const EventHandler={on(e,t,n,s){addHandler(e,t,n,s,!1)},one(e,t,n,s){addHandler(e,t,n,s,!0)},off(e,t,n,s){if("string"!=typeof t||!e)return;const[i,o,r]=normalizeParameters(t,n,s),l=r!==t,a=getElementEvents(e),c=a[r]||{},_=t.startsWith(".");if(void 0===o){if(_)for(const n of Object.keys(a))removeNamespacedHandlers(e,a,n,t.slice(1));for(const[n,s]of Object.entries(c)){const i=n.replace(stripUidRegex,"");l&&!t.includes(i)||removeHandler(e,a,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;removeHandler(e,a,r,o,i?n:null)}},trigger(e,t,n){if("string"!=typeof t||!e)return null;const s=hydrateObj(new Event(t,{bubbles:!0,cancelable:!0}),n);return e.dispatchEvent(s),s}};function hydrateObj(e,t={}){for(const[n,s]of Object.entries(t))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get:()=>s})}return e}function normalizeData(e){if("true"===e)return!0;if("false"===e)return!1;if(e===Number(e).toString())return Number(e);if(""===e||"null"===e)return null;if("string"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function normalizeDataKey(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const Manipulator={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${normalizeDataKey(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${normalizeDataKey(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const s of n){let n=s.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=normalizeData(e.dataset[s])}return t},getDataAttribute:(e,t)=>normalizeData(e.getAttribute(`data-bs-${normalizeDataKey(t)}`))},MAX_UID=1e6,MILLISECONDS_MULTIPLIER=1e3,TRANSITION_END="transitionend",parseSelector=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),e),toType=e=>null==e?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),getUID=e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e},getTransitionDurationFromElement=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),i=Number.parseFloat(n);return s||i?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0},triggerTransitionEnd=e=>{e.dispatchEvent(new Event(TRANSITION_END))},isElement=e=>!(!e||"object"!=typeof e)&&void 0!==e.nodeType,getElement=e=>isElement(e)?e:"string"==typeof e&&e.length>0?document.querySelector(parseSelector(e)):null,isVisible=e=>{if(!isElement(e)||0===e.getClientRects().length)return!1;const t="visible"===getComputedStyle(e).getPropertyValue("visibility"),n=e.closest("details:not([open])");if(!n)return t;if(n!==e){const t=e.closest("summary");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},isDisabled=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(void 0!==e.disabled?e.disabled:e.hasAttribute("disabled")&&"false"!==e.getAttribute("disabled")),findShadowRoot=e=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?findShadowRoot(e.parentNode):null},noop=()=>{},reflow=e=>{e.offsetHeight},isRTL=()=>"rtl"===document.documentElement.dir,execute=(e,t=[],n=e)=>"function"==typeof e?e.call(...t):n,executeAfterTransition=(e,t,n=!0)=>{if(!n)return void execute(e);const s=getTransitionDurationFromElement(t)+5;let i=!1;const o=({target:n})=>{n===t&&(i=!0,t.removeEventListener(TRANSITION_END,o),execute(e))};t.addEventListener(TRANSITION_END,o),setTimeout(()=>{i||triggerTransitionEnd(t)},s)},getNextActiveElement=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return-1===o?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])};class Config{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=isElement(t)?Manipulator.getDataAttribute(t,"config"):{};return{...this.constructor.Default,..."object"==typeof n?n:{},...isElement(t)?Manipulator.getDataAttributes(t):{},..."object"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[n,s]of Object.entries(t)){const t=e[n],i=isElement(t)?"element":toType(t);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const VERSION="6.0.0-alpha1";class BaseComponent extends Config{constructor(e,t){if(super(),!(e=getElement(e)))return;this._element=e,this._config=this._getConfig(t);const n=Data.get(this._element,this.constructor.DATA_KEY);n&&n.dispose(),Data.set(this._element,this.constructor.DATA_KEY,this)}dispose(){Data.remove(this._element,this.constructor.DATA_KEY),EventHandler.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){executeAfterTransition(()=>{this._element&&e()},t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return Data.get(getElement(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return VERSION}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const getSelector=e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&"#"!==n?n.trim():null}return t?t.split(",").map(e=>parseSelector(e)).join(","):null},SelectorEngine={find:(e,t=document.documentElement)=>[...Element.prototype.querySelectorAll.call(t,e)],findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[...e.children].filter(e=>e.matches(t)),parents(e,t){const n=[];let s=e.parentNode.closest(t);for(;s;)n.push(s),s=s.parentNode.closest(t);return n},closest:(e,t)=>Element.prototype.closest.call(e,t),prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!isDisabled(e)&&isVisible(e))},getSelectorFromElement(e){const t=getSelector(e);return t&&SelectorEngine.findOne(t)?t:null},getElementFromSelector(e){const t=getSelector(e);return t?SelectorEngine.findOne(t):null},getMultipleElementsFromSelector(e){const t=getSelector(e);return t?SelectorEngine.find(t):[]}},enableDismissTrigger=(e,t="hide")=>{const n=`click.dismiss${e.EVENT_KEY}`,s=e.NAME;EventHandler.on(document,n,`[data-bs-dismiss="${s}"]`,function(n){if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),isDisabled(this))return;const i=SelectorEngine.getElementFromSelector(this)||this.closest(`.${s}`);e.getOrCreateInstance(i)[t]()})},eventActionOnPlugin=(e,t,n,s,i=null)=>{eventAction(`${t}.${e.NAME}`,n,t=>{const n=t.targets.filter(Boolean).map(t=>e.getOrCreateInstance(t));"function"==typeof i&&i({...t,instances:n});for(const e of n)e[s]()})},eventAction=(e,t,n)=>{const s=`${t}:not(.disabled):not(:disabled)`;EventHandler.on(document,e,s,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault();const t=SelectorEngine.getSelectorFromElement(this),s=t?SelectorEngine.find(t):[this];n({targets:s,event:e})})},NAME$l="alert",DATA_KEY$h="bs.alert",EVENT_KEY$i=".bs.alert",EVENT_CLOSE="close.bs.alert",EVENT_CLOSED="closed.bs.alert",CLASS_NAME_FADE$4="fade",CLASS_NAME_SHOW$6="show";class Alert extends BaseComponent{static get NAME(){return NAME$l}close(){if(EventHandler.trigger(this._element,EVENT_CLOSE).defaultPrevented)return;this._element.classList.remove("show");const e=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,e)}_destroyElement(){this._element.remove(),EventHandler.trigger(this._element,EVENT_CLOSED),this.dispose()}}enableDismissTrigger(Alert,"close");const NAME$k="button",DATA_KEY$g="bs.button",EVENT_KEY$h=`.${DATA_KEY$g}`,DATA_API_KEY$c=".data-api",CLASS_NAME_ACTIVE$4="active",SELECTOR_DATA_TOGGLE$a='[data-bs-toggle="button"]',EVENT_CLICK_DATA_API$8=`click${EVENT_KEY$h}.data-api`;class Button extends BaseComponent{static get NAME(){return NAME$k}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}}EventHandler.on(document,EVENT_CLICK_DATA_API$8,SELECTOR_DATA_TOGGLE$a,e=>{e.preventDefault();const t=e.target.closest(SELECTOR_DATA_TOGGLE$a);Button.getOrCreateInstance(t).toggle()});const NAME$j="carousel",DATA_KEY$f="bs.carousel",EVENT_KEY$g=`.${DATA_KEY$f}`,DATA_API_KEY$b=".data-api",ARROW_LEFT_KEY$2="ArrowLeft",ARROW_RIGHT_KEY$2="ArrowRight",DIRECTION_LEFT="left",DIRECTION_RIGHT="right",EVENT_SLIDE=`slide${EVENT_KEY$g}`,EVENT_SLID=`slid${EVENT_KEY$g}`,EVENT_KEYDOWN$2=`keydown${EVENT_KEY$g}`,EVENT_MOUSEENTER$2=`mouseenter${EVENT_KEY$g}`,EVENT_MOUSELEAVE$1=`mouseleave${EVENT_KEY$g}`,EVENT_POINTERDOWN$1=`pointerdown${EVENT_KEY$g}`,EVENT_LOAD_DATA_API$3=`load${EVENT_KEY$g}.data-api`,EVENT_CLICK_DATA_API$7=`click${EVENT_KEY$g}.data-api`,CLASS_NAME_CAROUSEL="carousel",CLASS_NAME_ACTIVE$3="active",CLASS_NAME_FADE$3="carousel-fade",CLASS_NAME_CENTER="carousel-center",CLASS_NAME_AUTO="carousel-auto",CLASS_NAME_CLONE="carousel-item-clone",CLASS_NAME_PAUSED="paused",CLASS_NAME_PLAYING="carousel-playing",PROPERTY_INTERVAL="--bs-carousel-interval",SCROLL_DURATION=300,ACTIVE_RATIO_TOLERANCE=.05,SELECTOR_ACTIVE=".active",SELECTOR_ITEM=`.carousel-item:not(.${CLASS_NAME_CLONE})`,SELECTOR_ACTIVE_ITEM=".active"+SELECTOR_ITEM,SELECTOR_INNER$1=".carousel-inner",SELECTOR_INDICATORS=".carousel-indicators",SELECTOR_PLAY_PAUSE=".carousel-control-play-pause",SELECTOR_DATA_SLIDE="[data-bs-slide], [data-bs-slide-to]",SELECTOR_DATA_SLIDE_PREV='[data-bs-slide="prev"]',SELECTOR_DATA_SLIDE_NEXT='[data-bs-slide="next"]',SELECTOR_DATA_AUTOPLAY='[data-bs-autoplay="true"]',KEY_TO_DIRECTION={[ARROW_LEFT_KEY$2]:"right",[ARROW_RIGHT_KEY$2]:"left"},ENDS_STOP="stop",ENDS_WRAP="wrap",ENDS_LOOP="loop",Default$i={autoplay:!1,ends:ENDS_LOOP,interval:5e3,keyboard:!0,pause:"hover"},DefaultType$i={autoplay:"boolean",ends:"string",interval:"number",keyboard:"boolean",pause:"(string|boolean)"},easeInOutCubic=e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2;class Carousel extends BaseComponent{constructor(e,t){super(e,t),this._viewport=SelectorEngine.findOne(SELECTOR_INNER$1,this._element)||this._element,this._indicatorsElement=SelectorEngine.findOne(SELECTOR_INDICATORS,this._element),this._playPauseElement=SelectorEngine.findOne(SELECTOR_PLAY_PAUSE,this._element),this._prevControls=SelectorEngine.find('[data-bs-slide="prev"]',this._element),this._nextControls=SelectorEngine.find('[data-bs-slide="next"]',this._element),this._interval=null,this._observer=null,this._scrollFrame=null,this._looping=!1,this._visibility=new Map,this._playing=this._config.autoplay,this._activeIndex=this._initialActiveIndex(),this._addEventListeners(),this._observeItems(),this._refreshActiveState(),this._playing&&this.cycle(),this._updatePlayPauseControl()}static get Default(){return Default$i}static get DefaultType(){return DefaultType$i}static get NAME(){return NAME$j}next(){this.to(this._navIndex()+1)}nextWhenVisible(){"visible"===document.visibilityState&&isVisible(this._element)&&this.next()}prev(){this.to(this._navIndex()-1)}pause(){this._clearInterval(),this._element.classList.remove("carousel-playing")}cycle(){this._clearInterval(),this._scheduleAutoplay(),this._element.classList.add("carousel-playing")}to(e){if(this._looping)return;const t=this._getItems(),n=Number.parseInt(e,10);if(this._config.ends===ENDS_LOOP&&!this._prefersReducedMotion()&&this._canLoop()){if(n>t.length-1)return void this._loopTransition(!0);if(n<0)return void this._loopTransition(!1)}const s=this._normalizeIndex(n,t.length),i=this._navIndex();null!==s&&s!==i&&(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[s],direction:this._direction(i,s),from:i,to:s}).defaultPrevented||(this._isFade()?this._fadeTo(s):this._scrollToIndex(s)))}dispose(){this._clearInterval(),this._observer&&this._observer.disconnect(),null!==this._scrollFrame&&cancelAnimationFrame(this._scrollFrame);for(const e of SelectorEngine.find(`.${CLASS_NAME_CLONE}`,this._viewport))e.remove();this._viewport.style.scrollSnapType="",EventHandler.off(this._viewport,EVENT_KEY$g),super.dispose()}_configAfterMerge(e){return[ENDS_STOP,ENDS_WRAP,ENDS_LOOP].includes(e.ends)||(e.ends=Default$i.ends),e}_initialActiveIndex(){const e=SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM,this._element),t=e?this._getItems().indexOf(e):0;return Math.max(t,0)}_addEventListeners(){this._config.keyboard&&EventHandler.on(this._element,EVENT_KEYDOWN$2,e=>this._keydown(e)),"hover"===this._config.pause&&(EventHandler.on(this._element,EVENT_MOUSEENTER$2,()=>this.pause()),EventHandler.on(this._element,EVENT_MOUSELEAVE$1,()=>this._maybeEnableCycle())),EventHandler.on(this._viewport,EVENT_POINTERDOWN$1,()=>this._pauseFromInteraction())}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=KEY_TO_DIRECTION[e.key];t&&(e.preventDefault(),this._pauseFromInteraction(),"right"===t?this.prev():this.next())}_observeItems(){if(!this._isFade()&&"undefined"!=typeof IntersectionObserver){this._observer=new IntersectionObserver(e=>this._handleIntersection(e),{root:this._viewport,threshold:[0,.25,.5,.75,1]});for(const e of this._getItems())this._observer.observe(e)}}_handleIntersection(e){if(this._looping)return;for(const t of e)this._visibility.set(t.target,t.isIntersecting?t.intersectionRatio:0);const t=this._getItems().map(e=>this._visibility.get(e)??0),n=Math.max(...t);let s=this._activeIndex;n>0&&(s=t.findIndex(e=>e>=n-.05)),this._setActive(s),this._updateEndControls()}_navIndex(){if(this._isFade()||this._viewport.scrollWidth-this._viewport.clientWidth<=0)return this._activeIndex;let e=this._activeIndex,t=Number.POSITIVE_INFINITY;for(const[n,s]of this._getItems().entries()){const i=Math.abs(this._scrollDelta(s));i{this._viewport.style.scrollSnapType="",this._observer||this._setActive(e),this._updateEndControls()})}_animateScroll(e,t){null!==this._scrollFrame&&(cancelAnimationFrame(this._scrollFrame),this._scrollFrame=null);const n=this._viewport.scrollLeft,s=e-n;if(this._prefersReducedMotion()||"undefined"==typeof requestAnimationFrame)return this._viewport.scrollTo({left:e,behavior:"instant"}),void t();let i=null;const o=r=>{null===i&&(i=r);const l=Math.min((r-i)/300,1);this._viewport.scrollTo({left:n+s*easeInOutCubic(l),behavior:"instant"}),l<1?this._scrollFrame=requestAnimationFrame(o):(this._viewport.scrollTo({left:e,behavior:"instant"}),this._scrollFrame=null,t())};this._scrollFrame=requestAnimationFrame(o)}_scrollDelta(e){const t=this._viewport.getBoundingClientRect(),n=e.getBoundingClientRect();if(this._element.classList.contains("carousel-center"))return n.left+n.width/2-(t.left+t.width/2);const s=Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart)||0;return isRTL()?n.right-(t.right-s):n.left-(t.left+s)}_loopTransition(e){const t=this._getItems(),n=t.length-1,s=this._activeIndex,i=e?0:n,o=this._loopDirection(e);if(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[i],direction:o,from:s,to:i}).defaultPrevented)return;this._looping=!0;const r=(e?t[0]:t[n]).cloneNode(!0);r.classList.add(CLASS_NAME_CLONE),r.classList.remove("active"),r.removeAttribute("id");for(const e of SelectorEngine.find("[id]",r))e.removeAttribute("id");r.setAttribute("aria-hidden","true"),r.inert=!0,this._viewport.style.scrollSnapType="none",e?this._viewport.append(r):(this._viewport.prepend(r),this._jumpScroll(this._scrollDelta(t[s]))),this._animateScroll(this._viewport.scrollLeft+this._scrollDelta(r),()=>{r.remove(),this._jumpScroll(this._scrollDelta(t[i])),this._activeIndex=i,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[i],direction:o,from:s,to:i}),this._viewport.style.scrollSnapType="",this._looping=!1})}_loopDirection(e){return isRTL()?e?"right":"left":e?"left":"right"}_jumpScroll(e){this._viewport.style.scrollSnapType="none",this._viewport.scrollBy({left:e,top:0,behavior:"instant"})}_fadeTo(e){this._setActive(e)}_setActive(e){const t=this._getItems();if(e===this._activeIndex||!t[e])return;const n=this._activeIndex;this._activeIndex=e,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[e],direction:this._direction(n,e),from:n,to:e})}_refreshActiveState(){const e=this._getItems();for(const[t,n]of e.entries())n.classList.toggle("active",t===this._activeIndex);this._setActiveIndicatorElement(this._activeIndex),this._updateEndControls()}_updateEndControls(){if(this._config.ends!==ENDS_STOP)return;const e=this._viewport,t=e.scrollWidth-e.clientWidth;let n,s;if(t>0){const i=Math.abs(e.scrollLeft);n=i<=1,s=i>=t-1}else{const e=this._getItems().length-1;n=this._activeIndex<=0,s=this._activeIndex>=e}this._setControlsDisabled(this._prevControls,n),this._setControlsDisabled(this._nextControls,s)}_setControlsDisabled(e,t){for(const n of e)t&&n===document.activeElement&&((e===this._prevControls?this._nextControls:this._prevControls)[0]??this._viewport).focus({preventScroll:!0}),n.disabled=t}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=SelectorEngine.findOne(".active",this._indicatorsElement);t&&(t.classList.remove("active"),t.removeAttribute("aria-current"));const n=SelectorEngine.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add("active"),n.setAttribute("aria-current","true"))}_normalizeIndex(e,t){return Number.isNaN(e)||0===t?null:e<0?this._wrapsAround()?t-1:null:e>t-1?this._wrapsAround()?0:null:e}_wrapsAround(){return this._config.ends===ENDS_WRAP||this._config.ends===ENDS_LOOP}_canLoop(){if(this._isFade()||this._getItems().length<2)return!1;const e=getComputedStyle(this._element),t=t=>Number.parseFloat(e.getPropertyValue(t))||0;return 1===(t("--bs-carousel-items")||1)&&0===t("--bs-carousel-items-peek")&&!this._element.classList.contains("carousel-center")&&!this._element.classList.contains("carousel-auto")}_direction(e,t){const n=t>e;return isRTL()?n?"right":"left":n?"left":"right"}_scheduleAutoplay(e=this._activeIndex){const t=this._itemInterval(e);this._element.style.setProperty(PROPERTY_INTERVAL,`${t}ms`),this._interval=setTimeout(()=>{const e=this._upcomingIndex();this.nextWhenVisible(),null!==e?this._scheduleAutoplay(e):this.pause()},t)}_upcomingIndex(){return this._normalizeIndex(this._navIndex()+1,this._getItems().length)}_itemInterval(e=this._activeIndex){const t=this._getItems()[e],n=t?Number.parseInt(t.getAttribute("data-bs-interval"),10):Number.NaN;return Number.isNaN(n)?this._config.interval:n}_maybeEnableCycle(){this._playing&&this.cycle()}_pauseFromInteraction(){this._playing=!1,this.pause(),this._updatePlayPauseControl()}_togglePlayPause(){this._playing?this._pauseFromInteraction():(this._playing=!0,this.cycle(),this._updatePlayPauseControl())}_updatePlayPauseControl(){if(!this._playPauseElement)return;this._playPauseElement.classList.toggle("paused",!this._playing);const e=this._playPauseElement.getAttribute(this._playing?"data-bs-pause-label":"data-bs-play-label");e&&this._playPauseElement.setAttribute("aria-label",e)}_isFade(){return this._element.classList.contains("carousel-fade")}_prefersReducedMotion(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}_getItems(){return SelectorEngine.find(SELECTOR_ITEM,this._element)}_clearInterval(){this._interval&&(clearTimeout(this._interval),this._interval=null)}}EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_DATA_SLIDE,function(e){const t=SelectorEngine.getElementFromSelector(this);if(!t||!t.classList.contains("carousel"))return;e.preventDefault();const n=Carousel.getOrCreateInstance(t);n._pauseFromInteraction();const s=this.getAttribute("data-bs-slide-to");s?n.to(s):"next"!==Manipulator.getDataAttribute(this,"slide")?n.prev():n.next()}),EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_PLAY_PAUSE,function(e){const t=SelectorEngine.getElementFromSelector(this);t&&t.classList.contains("carousel")&&(e.preventDefault(),Carousel.getOrCreateInstance(t)._togglePlayPause())}),EventHandler.on(window,EVENT_LOAD_DATA_API$3,()=>{const e=SelectorEngine.find(SELECTOR_DATA_AUTOPLAY);for(const t of e)Carousel.getOrCreateInstance(t)});const NAME$i="collapse",DATA_KEY$e="bs.collapse",EVENT_KEY$f=`.${DATA_KEY$e}`,DATA_API_KEY$a=".data-api",EVENT_SHOW$7=`show${EVENT_KEY$f}`,EVENT_SHOWN$6=`shown${EVENT_KEY$f}`,EVENT_HIDE$6=`hide${EVENT_KEY$f}`,EVENT_HIDDEN$8=`hidden${EVENT_KEY$f}`,EVENT_CLICK_DATA_API$6=`click${EVENT_KEY$f}.data-api`,CLASS_NAME_SHOW$5="show",CLASS_NAME_COLLAPSE="collapse",CLASS_NAME_COLLAPSING="collapsing",CLASS_NAME_COLLAPSED="collapsed",CLASS_NAME_DEEPER_CHILDREN=":scope .collapse .collapse",CLASS_NAME_HORIZONTAL="collapse-horizontal",WIDTH="width",HEIGHT="height",SELECTOR_ACTIVES=".collapse.show, .collapse.collapsing",SELECTOR_DATA_TOGGLE$9='[data-bs-toggle="collapse"]',Default$h={parent:null,toggle:!0},DefaultType$h={parent:"(null|element)",toggle:"boolean"};class Collapse extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=SelectorEngine.find(SELECTOR_DATA_TOGGLE$9);for(const e of n){const t=SelectorEngine.getSelectorFromElement(e),n=SelectorEngine.find(t).filter(e=>e===this._element);null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Default$h}static get DefaultType(){return DefaultType$h}static get NAME(){return NAME$i}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(e=>e!==this._element).map(e=>Collapse.getOrCreateInstance(e,{toggle:!1}))),e.length&&e[0]._isTransitioning)return;if(EventHandler.trigger(this._element,EVENT_SHOW$7).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[t]="",EventHandler.trigger(this._element,EVENT_SHOWN$6)},this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(EventHandler.trigger(this._element,EVENT_HIDE$6).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,reflow(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");for(const e of this._triggerArray){const t=SelectorEngine.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),EventHandler.trigger(this._element,EVENT_HIDDEN$8)},this._element,!0)}_isShown(e=this._element){return e.classList.contains("show")}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=getElement(e.parent),e}_getDimension(){return this._element.classList.contains("collapse-horizontal")?WIDTH:HEIGHT}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9);for(const t of e){const e=SelectorEngine.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN,this._config.parent);return SelectorEngine.find(e,this._config.parent).filter(e=>!t.includes(e))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle("collapsed",!t),n.setAttribute("aria-expanded",t)}}EventHandler.on(document,EVENT_CLICK_DATA_API$6,SELECTOR_DATA_TOGGLE$9,function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of SelectorEngine.getMultipleElementsFromSelector(this))Collapse.getOrCreateInstance(e,{toggle:!1}).toggle()});const BREAKPOINTS={sm:576,md:768,lg:1024,xl:1280,"2xl":1536},parseResponsivePlacement=(e,t="bottom")=>{if(!e||!e.includes(":"))return null;const n=e.split(/\s+/),s={xs:t};for(const e of n)if(e.includes(":")){const[t,n]=e.split(":");void 0!==BREAKPOINTS[t]&&(s[t]=n)}else s.xs=e;return s},getResponsivePlacement=(e,t="bottom")=>{if(!e)return t;const n=window.innerWidth;let s=e.xs||t;const i=["sm","md","lg","xl","2xl"];for(const t of i)n>=BREAKPOINTS[t]&&e[t]&&(s=e[t]);return s},createBreakpointListeners=e=>{const t=[];for(const n of Object.keys(BREAKPOINTS)){const s=BREAKPOINTS[n],i=window.matchMedia(`(min-width: ${s}px)`);i.addEventListener("change",e),t.push({mql:i,handler:e})}return t},disposeBreakpointListeners=e=>{for(const{mql:t,handler:n}of e)t.removeEventListener("change",n)},NAME$h="menu",DATA_KEY$d="bs.menu",EVENT_KEY$e=".bs.menu",DATA_API_KEY$9=".data-api",ESCAPE_KEY$2="Escape",TAB_KEY$1="Tab",ARROW_UP_KEY$2="ArrowUp",ARROW_DOWN_KEY$2="ArrowDown",ARROW_LEFT_KEY$1="ArrowLeft",ARROW_RIGHT_KEY$1="ArrowRight",HOME_KEY$2="Home",END_KEY$2="End",ENTER_KEY$1="Enter",SPACE_KEY$1=" ",RIGHT_MOUSE_BUTTON=2,SUBMENU_CLOSE_DELAY=100,EVENT_HIDE$5="hide.bs.menu",EVENT_HIDDEN$7="hidden.bs.menu",EVENT_SHOW$6="show.bs.menu",EVENT_SHOWN$5="shown.bs.menu",EVENT_CLICK_DATA_API$5="click.bs.menu.data-api",EVENT_KEYDOWN_DATA_API="keydown.bs.menu.data-api",EVENT_KEYUP_DATA_API="keyup.bs.menu.data-api",CLASS_NAME_SHOW$4="show",SELECTOR_DATA_TOGGLE$8='[data-bs-toggle="menu"]:not(.disabled):not(:disabled)',SELECTOR_MENU$2=".menu",SELECTOR_SUBMENU=".submenu",SELECTOR_SUBMENU_TOGGLE=".submenu > .menu-item",SELECTOR_NAVBAR_NAV=".navbar-nav",SELECTOR_VISIBLE_ITEMS$1=".menu-item:not(.disabled):not(:disabled)",DEFAULT_PLACEMENT="bottom-start",SUBMENU_PLACEMENT="end-start",resolveLogicalPlacement=e=>isRTL()?e.replace(/^start(?=-|$)/,"right").replace(/^end(?=-|$)/,"left"):e.replace(/^start(?=-|$)/,"left").replace(/^end(?=-|$)/,"right"),triangleSign=(e,t,n)=>(e.x-n.x)*(t.y-n.y)-(t.x-n.x)*(e.y-n.y),Default$g={autoClose:!0,boundary:"clippingParents",container:!1,display:"dynamic",offset:[0,2],floatingConfig:null,menu:null,placement:"bottom-start",reference:"toggle",strategy:"absolute",submenuTrigger:"both",submenuDelay:100},DefaultType$g={autoClose:"(boolean|string)",boundary:"(string|element)",container:"(string|element|boolean)",display:"string",offset:"(array|string|function)",floatingConfig:"(null|object|function)",menu:"(null|element)",placement:"string",reference:"(string|element|object)",strategy:"string",submenuTrigger:"string",submenuDelay:"number"};class Menu extends BaseComponent{static _openInstances=new Set;constructor(e,t){if(void 0===computePosition)throw new TypeError("Bootstrap's menus require Floating UI (https://floating-ui.com)");super(e,t),this._floatingCleanup=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this._parent=this._element.parentNode,this._openSubmenus=new Map,this._submenuCloseTimeouts=new Map,this._hoverIntentData=null,this._menu=this._config.menu||this._findMenu(),!this._config.menu&&this._menu&&(this._parent=this._findWrapper(this._menu)),this._isSubmenu=this._parent.classList?.contains("submenu"),this._menuOriginalParent=this._menu?.parentNode,this._parseResponsivePlacements(),this._setupSubmenuListeners()}static get Default(){return Default$g}static get DefaultType(){return DefaultType$g}static get NAME(){return"menu"}toggle(){return this._isShown()?this.hide():this.show()}show(){if(isDisabled(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!EventHandler.trigger(this._element,EVENT_SHOW$6,e).defaultPrevented){if(this._moveMenuToContainer(),this._createFloating(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._element.focus({focusVisible:!1}),this._element.setAttribute("aria-expanded","true"),this._menu.classList.add("show"),this._element.classList.add("show"),this._parent&&this._parent.classList.add("show"),Menu._openInstances.add(this),EventHandler.trigger(this._element,EVENT_SHOWN$5,e)}}hide(){if(isDisabled(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._disposeFloating(),this._restoreMenuToOriginalParent(),this._disposeMediaQueryListeners(),this._closeAllSubmenus(),this._clearAllSubmenuTimeouts(),Menu._openInstances.delete(this),super.dispose()}update(){this._floatingCleanup&&this._updateFloatingPosition()}_findMenu(){const e=SelectorEngine.closest(this._element,":has(.menu)");return SelectorEngine.next(this._element,".menu")[0]||SelectorEngine.prev(this._element,".menu")[0]||SelectorEngine.findOne(".menu",e||this._parent)}_findWrapper(e){let t=this._element.parentNode;for(;t instanceof Element&&!t.contains(e);)t=t.parentNode;return t instanceof Element?t:this._element.parentNode}_completeHide(e){if(!EventHandler.trigger(this._element,EVENT_HIDE$5,e).defaultPrevented){if(this._closeAllSubmenus(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._disposeFloating(),this._restoreMenuToOriginalParent(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._parent&&this._parent.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),Manipulator.removeDataAttribute(this._menu,"placement"),Manipulator.removeDataAttribute(this._menu,"display"),Menu._openInstances.delete(this),EventHandler.trigger(this._element,EVENT_HIDDEN$7,e)}}_getConfig(e){if("object"==typeof(e=super._getConfig(e)).reference&&!isElement(e.reference)&&"function"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${"menu".toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createFloating(){if("static"===this._config.display)return void Manipulator.setDataAttribute(this._menu,"display","static");let e=this._element;"parent"===this._config.reference?e=this._parent:isElement(this._config.reference)?e=getElement(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference),this._updateFloatingPosition(e),this._floatingCleanup=autoUpdate(e,this._menu,()=>this._updateFloatingPosition(e))}async _updateFloatingPosition(e=null){if(!this._menu)return;e||(e="parent"===this._config.reference?this._parent:isElement(this._config.reference)?getElement(this._config.reference):"object"==typeof this._config.reference?this._config.reference:this._element);const t=this._getPlacement(),n=this._getFloatingMiddleware(),s=this._getFloatingConfig(t,n);await this._applyFloatingPosition(e,this._menu,s.placement,s.middleware,s.strategy)}_isShown(){return this._menu.classList.contains("show")}_getPlacement(){const e=this._responsivePlacements?getResponsivePlacement(this._responsivePlacements,"bottom-start"):this._config.placement;return resolveLogicalPlacement(e)}_parseResponsivePlacements(){this._responsivePlacements=parseResponsivePlacement(this._config.placement,"bottom-start"),this._responsivePlacements&&this._setupMediaQueryListeners()}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_getFloatingMiddleware(){const e=this._getOffset();return[offset("function"==typeof e?e:{mainAxis:e[1]||0,crossAxis:e[0]||0}),flip({fallbackPlacements:this._getFallbackPlacements()}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})]}_getFallbackPlacements(){return{bottom:["top","bottom-start","bottom-end","top-start","top-end"],"bottom-start":["top-start","bottom-end","top-end"],"bottom-end":["top-end","bottom-start","top-start"],top:["bottom","top-start","top-end","bottom-start","bottom-end"],"top-start":["bottom-start","top-end","bottom-end"],"top-end":["bottom-end","top-start","bottom-start"],right:["left","right-start","right-end","left-start","left-end"],"right-start":["left-start","right-end","left-end","top-start","bottom-start"],"right-end":["left-end","right-start","left-start","top-end","bottom-end"],left:["right","left-start","left-end","right-start","right-end"],"left-start":["right-start","left-end","right-end","top-start","bottom-start"],"left-end":["right-end","left-start","right-start","top-end","bottom-end"]}[this._getPlacement()]||["top","bottom","right","left"]}_getFloatingConfig(e,t){const n={placement:e,middleware:t,strategy:this._config.strategy};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null)}_getContainer(){const{container:e}=this._config;return!1===e?null:!0===e?document.body:getElement(e)}_moveMenuToContainer(){const e=this._getContainer();e&&this._menu&&this._menu.parentNode!==e&&e.append(this._menu)}_restoreMenuToOriginalParent(){this._menuOriginalParent&&this._menu&&this._menu.parentNode!==this._menuOriginalParent&&this._menuOriginalParent.append(this._menu)}async _applyFloatingPosition(e,t,n,s,i="absolute"){if(!t.isConnected)return null;const{x:o,y:r,placement:l}=await computePosition(e,t,{placement:n,middleware:s,strategy:i});return t.isConnected?(Object.assign(t.style,{position:i,left:`${o}px`,top:`${r}px`,margin:"0"}),Manipulator.setDataAttribute(t,"placement",l),l):null}_setupSubmenuListeners(){"hover"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||(EventHandler.on(this._menu,"mouseenter",".submenu > .menu-item",e=>{this._onSubmenuTriggerEnter(e)}),EventHandler.on(this._menu,"mouseleave",".submenu",e=>{this._onSubmenuLeave(e)}),EventHandler.on(this._menu,"mousemove",e=>{this._trackMousePosition(e)})),"click"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||EventHandler.on(this._menu,"click",".submenu > .menu-item",e=>{this._onSubmenuTriggerClick(e)})}_onSubmenuTriggerEnter(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._cancelSubmenuCloseTimeout(s),this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n))}_onSubmenuLeave(e){const t=e.target.closest(".submenu"),n=SelectorEngine.findOne(".menu",t);n&&this._openSubmenus.has(n)&&(this._isMovingTowardSubmenu(e,n)||this._scheduleSubmenuClose(n,t))}_onSubmenuTriggerClick(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;e.preventDefault(),e.stopPropagation();const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._openSubmenus.has(s)?this._closeSubmenu(s,n):(this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n)))}_openSubmenu(e,t,n){if(this._openSubmenus.has(t))return;e.setAttribute("aria-expanded","true"),e.setAttribute("aria-haspopup","true"),t.style.opacity="0",t.classList.add("show"),n.classList.add("show");const s=this._createSubmenuFloating(e,t,n);this._openSubmenus.set(t,s),EventHandler.on(t,"mouseenter",()=>{this._cancelSubmenuCloseTimeout(t)})}_closeSubmenu(e,t){if(!this._openSubmenus.has(e))return;const n=SelectorEngine.find(".submenu .menu.show",e);for(const e of n){const t=e.closest(".submenu");this._closeSubmenu(e,t)}const s=SelectorEngine.findOne(".submenu > .menu-item",t),i=this._openSubmenus.get(e);i&&i(),this._openSubmenus.delete(e),EventHandler.off(e,"mouseenter"),s&&s.setAttribute("aria-expanded","false"),e.classList.remove("show"),t.classList.remove("show"),e.style.opacity=""}_closeAllSubmenus(){for(const[e]of this._openSubmenus){const t=e.closest(".submenu");this._closeSubmenu(e,t)}}_closeSiblingSubmenus(e){const t=e.parentNode,n=SelectorEngine.find(".submenu > .menu.show",t);for(const t of n){const n=t.closest(".submenu");n!==e&&this._closeSubmenu(t,n)}}_createSubmenuFloating(e,t,n){const s=n,i=resolveLogicalPlacement("end-start"),o=[offset({mainAxis:0,crossAxis:-4}),flip({fallbackPlacements:[resolveLogicalPlacement("start-start"),resolveLogicalPlacement("end-end"),resolveLogicalPlacement("start-end")]}),shift({padding:8})],r=()=>this._applyFloatingPosition(s,t,i,o).then(e=>(t.style.opacity="",e));return r(),autoUpdate(s,t,r)}_scheduleSubmenuClose(e,t){this._cancelSubmenuCloseTimeout(e);const n=setTimeout(()=>{this._closeSubmenu(e,t),this._submenuCloseTimeouts.delete(e)},this._config.submenuDelay);this._submenuCloseTimeouts.set(e,n)}_cancelSubmenuCloseTimeout(e){const t=this._submenuCloseTimeouts.get(e);t&&(clearTimeout(t),this._submenuCloseTimeouts.delete(e))}_clearAllSubmenuTimeouts(){for(const e of this._submenuCloseTimeouts.values())clearTimeout(e);this._submenuCloseTimeouts.clear()}_trackMousePosition(e){this._hoverIntentData={x:e.clientX,y:e.clientY,timestamp:Date.now()}}_isMovingTowardSubmenu(e,t){if(!this._hoverIntentData)return!1;const n=t.getBoundingClientRect(),s={x:e.clientX,y:e.clientY},i={x:this._hoverIntentData.x,y:this._hoverIntentData.y},o=isRTL()?n.right:n.left,r={x:o,y:n.top},l={x:o,y:n.bottom};return this._pointInTriangle(s,i,r,l)}_pointInTriangle(e,t,n,s){const i=triangleSign(e,t,n),o=triangleSign(e,n,s),r=triangleSign(e,s,t);return!((i<0||o<0||r<0)&&(i>0||o>0||r>0))}_selectMenuItem({key:e,target:t}){const n=t.closest(".menu")||this._menu,s=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,n).filter(e=>isVisible(e));s.length&&getNextActiveElement(s,t,e===ARROW_DOWN_KEY$2,!s.includes(t)).focus()}_handleSubmenuKeydown(e){const{key:t,target:n}=e,s=isRTL(),i=s?ARROW_LEFT_KEY$1:ARROW_RIGHT_KEY$1,o=s?ARROW_RIGHT_KEY$1:ARROW_LEFT_KEY$1,r=n.closest(".submenu"),l=r&&n.matches(".submenu > .menu-item");if((t===ENTER_KEY$1||t===SPACE_KEY$1)&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",r);return t&&(this._closeSiblingSubmenus(r),this._openSubmenu(n,t,r),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===i&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",r);return t&&(this._closeSiblingSubmenus(r),this._openSubmenu(n,t,r),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===o){const t=n.closest(".menu"),s=t?.closest(".submenu");if(s){e.preventDefault(),e.stopPropagation();const n=SelectorEngine.findOne(".submenu > .menu-item",s);return this._closeSubmenu(t,s),n&&n.focus(),!0}}if(t===HOME_KEY$2||t===END_KEY$2){e.preventDefault(),e.stopPropagation();const s=n.closest(".menu"),i=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,s).filter(e=>isVisible(e));return i.length&&(t===HOME_KEY$2?i[0]:i.at(-1)).focus(),!0}return!1}static clearMenus(e){if(2!==e.button&&("keyup"!==e.type||"Tab"===e.key))for(const t of Menu._openInstances){if(!1===t._config.autoClose)continue;const n=e.composedPath(),s=n.includes(t._menu);if(n.includes(t._element)||"inside"===t._config.autoClose&&!s||"outside"===t._config.autoClose&&s)continue;const i=e.target.closest?.("form"),o=Boolean(i)&&t._menu.contains(i);if(t._menu.contains(e.target)&&("keyup"===e.type&&"Tab"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)||o))continue;const r={relatedTarget:t._element};"click"===e.type&&(r.clickEvent=e),t._completeHide(r)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName)||e.target.isContentEditable,n="Escape"===e.key,s=[ARROW_UP_KEY$2,ARROW_DOWN_KEY$2].includes(e.key),i=[ARROW_LEFT_KEY$1,ARROW_RIGHT_KEY$1].includes(e.key),o=[HOME_KEY$2,END_KEY$2].includes(e.key),r=[ENTER_KEY$1,SPACE_KEY$1].includes(e.key),l=e.target.matches(".submenu > .menu-item");if(!(s||n||i||o||r&&l))return;if(t&&!n)return;const a=this.matches(SELECTOR_DATA_TOGGLE$8)?this:SelectorEngine.prev(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.next(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8,e.delegateTarget.parentNode);if(!a)return;const c=Menu.getOrCreateInstance(a);if(!(i||o||r&&l)||!c._handleSubmenuKeydown(e)){if(s)return e.preventDefault(),e.stopPropagation(),c.show(),void c._selectMenuItem(e);if(n&&c._isShown()){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".menu"),n=t?.closest(".submenu");if(n&&c._openSubmenus.size>0){const e=SelectorEngine.findOne(".submenu > .menu-item",n);return c._closeSubmenu(t,n),void(e&&e.focus())}c.hide(),a.focus()}}}}EventHandler.on(document,EVENT_KEYDOWN_DATA_API,SELECTOR_DATA_TOGGLE$8,Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_KEYDOWN_DATA_API,".menu",Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_CLICK_DATA_API$5,Menu.clearMenus),EventHandler.on(document,EVENT_KEYUP_DATA_API,Menu.clearMenus),EventHandler.on(document,EVENT_CLICK_DATA_API$5,SELECTOR_DATA_TOGGLE$8,function(e){e.preventDefault(),Menu.getOrCreateInstance(this).toggle()});const NAME$g="combobox",DATA_KEY$c="bs.combobox",EVENT_KEY$d=`.${DATA_KEY$c}`,DATA_API_KEY$8=".data-api",ESCAPE_KEY$1="Escape",TAB_KEY="Tab",ARROW_UP_KEY$1="ArrowUp",ARROW_DOWN_KEY$1="ArrowDown",HOME_KEY$1="Home",END_KEY$1="End",ENTER_KEY="Enter",SPACE_KEY=" ",EVENT_CHANGE$3=`change${EVENT_KEY$d}`,EVENT_SHOW$5=`show${EVENT_KEY$d}`,EVENT_SHOWN$4=`shown${EVENT_KEY$d}`,EVENT_HIDE$4=`hide${EVENT_KEY$d}`,EVENT_HIDDEN$6=`hidden${EVENT_KEY$d}`,EVENT_CLICK_DATA_API$4=`click${EVENT_KEY$d}.data-api`,CLASS_NAME_SHOW$3="show",CLASS_NAME_SELECTED="selected",CLASS_NAME_PLACEHOLDER="combobox-placeholder",SELECTOR_DATA_TOGGLE$7='[data-bs-toggle="combobox"]',SELECTOR_MENU$1=".menu",SELECTOR_MENU_ITEM=".menu-item[data-bs-value]",SELECTOR_VISIBLE_ITEMS=".menu-item[data-bs-value]:not(.disabled):not(:disabled)",SELECTOR_VALUE=".combobox-value",SELECTOR_SEARCH_INPUT=".combobox-search-input",SELECTOR_NO_RESULTS=".combobox-no-results",Default$f={boundary:"clippingParents",multiple:!1,name:null,offset:[0,2],placeholder:"",placement:"bottom-start",search:!1,searchNormalize:!1},DefaultType$f={boundary:"(string|element)",multiple:"boolean",name:"(string|null)",offset:"(array|string|function)",placeholder:"string",placement:"string",search:"boolean",searchNormalize:"boolean"};class Combobox extends BaseComponent{constructor(e,t){super(e,t),this._toggle=this._element,this._menu=SelectorEngine.next(this._toggle,".menu")[0],this._valueDisplay=SelectorEngine.findOne(SELECTOR_VALUE,this._toggle),this._searchInput=SelectorEngine.findOne(SELECTOR_SEARCH_INPUT,this._menu),this._noResults=SelectorEngine.findOne(SELECTOR_NO_RESULTS,this._menu),this._hiddenInput=null,this._menuInstance=null,this._createHiddenInput(),this._createMenuInstance(),this._syncInitialSelection(),this._addEventListeners()}static get Default(){return Default$f}static get DefaultType(){return DefaultType$f}static get NAME(){return NAME$g}toggle(){return this._isShown()?this.hide():this.show()}show(){isDisabled(this._toggle)||this._isShown()||EventHandler.trigger(this._toggle,EVENT_SHOW$5).defaultPrevented||(this._menuInstance.show(),this._searchInput&&(this._searchInput.value="",this._filterItems(""),requestAnimationFrame(()=>this._searchInput.focus())),EventHandler.trigger(this._toggle,EVENT_SHOWN$4))}hide(){this._isShown()&&(EventHandler.trigger(this._toggle,EVENT_HIDE$4).defaultPrevented||(this._menuInstance.hide(),EventHandler.trigger(this._toggle,EVENT_HIDDEN$6)))}dispose(){this._menuInstance&&(this._menuInstance.dispose(),this._menuInstance=null),this._hiddenInput&&(this._hiddenInput.remove(),this._hiddenInput=null),EventHandler.off(this._menu,EVENT_KEY$d),EventHandler.off(this._toggle,EVENT_KEY$d),super.dispose()}_isShown(){return this._menu.classList.contains("show")}_createHiddenInput(){const{name:e}=this._config;e&&(this._hiddenInput=document.createElement("input"),this._hiddenInput.type="hidden",this._hiddenInput.name=e,this._hiddenInput.value="",this._toggle.parentNode.insertBefore(this._hiddenInput,this._toggle))}_createMenuInstance(){this._menuInstance=new Menu(this._toggle,{menu:this._menu,autoClose:!this._config.multiple||"outside",boundary:this._config.boundary,offset:this._config.offset,placement:this._config.placement})}_syncInitialSelection(){this._getSelectedItems().length>0?(this._updateToggleText(),this._updateHiddenInput()):this._showPlaceholder()}_addEventListeners(){EventHandler.on(this._menu,"click",SELECTOR_MENU_ITEM,e=>{const t=e.target.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&(e.preventDefault(),e.stopPropagation(),this._selectItem(t))}),EventHandler.on(this._toggle,"keydown",e=>{this._handleToggleKeydown(e)}),EventHandler.on(this._menu,"keydown",e=>{this._handleMenuKeydown(e)}),this._searchInput&&(EventHandler.on(this._searchInput,"input",()=>{this._filterItems(this._searchInput.value)}),EventHandler.on(this._searchInput,"keydown",e=>{if("ArrowDown"===e.key){e.preventDefault();const t=this._getVisibleItems();t.length>0&&t[0].focus()}"Escape"===e.key&&(this.hide(),this._toggle.focus())}))}_selectItem(e){if(this._config.multiple)e.classList.toggle("selected"),e.setAttribute("aria-selected",e.classList.contains("selected"));else{const t=SelectorEngine.find(".selected",this._menu);for(const e of t)e.classList.remove("selected"),e.setAttribute("aria-selected","false");e.classList.add("selected"),e.setAttribute("aria-selected","true")}this._updateToggleText(),this._updateHiddenInput();const t=this._config.multiple?this._getSelectedItems().map(e=>e.dataset.bsValue):e.dataset.bsValue;EventHandler.trigger(this._toggle,EVENT_CHANGE$3,{value:t,item:e}),this._config.multiple||(this.hide(),this._toggle.focus())}_updateToggleText(){const e=this._getSelectedItems();if(0!==e.length)if(this._valueDisplay.classList.remove("combobox-placeholder"),this._config.multiple&&e.length>1)this._valueDisplay.textContent=`${e.length} selected`;else{const t=e[0],n=SelectorEngine.findOne(".menu-item-content > span:first-child",t);this._valueDisplay.textContent=n?n.textContent:t.textContent.trim()}else this._showPlaceholder()}_showPlaceholder(){const{placeholder:e}=this._config;e&&(this._valueDisplay.textContent=e,this._valueDisplay.classList.add("combobox-placeholder"))}_updateHiddenInput(){if(!this._hiddenInput)return;const e=this._getSelectedItems().map(e=>e.dataset.bsValue);this._hiddenInput.value=this._config.multiple?e.join(","):e[0]||""}_getSelectedItems(){return SelectorEngine.find(".selected",this._menu)}_getVisibleItems(){return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS,this._menu).filter(e=>isVisible(e))}_filterItems(e){const t=this._normalizeText(e.toLowerCase().trim()),n=SelectorEngine.find(SELECTOR_MENU_ITEM,this._menu);let s=0;for(const e of n){const n=this._normalizeText(e.textContent.toLowerCase().trim()),i=!t||n.includes(t);e.style.display=i?"":"none",i&&s++}this._noResults&&this._noResults.classList.toggle("d-none",s>0)}_normalizeText(e){return this._config.searchNormalize?e.normalize("NFD").replace(/[\u0300-\u036F]/g,""):e}_handleToggleKeydown(e){const{key:t}=e;if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault(),this._isShown()||this.show();const n=this._getVisibleItems();return void(n.length>0&&("ArrowDown"===t?n[0]:n.at(-1)).focus())}"Enter"!==t&&" "!==t||this._isShown()||(e.preventDefault(),this.show())}_handleMenuKeydown(e){const{key:t,target:n}=e;if("Escape"===t)return e.preventDefault(),e.stopPropagation(),this.hide(),void this._toggle.focus();if("Tab"===t)return void this.hide();const s=n.matches("input");if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault();const s=this._getVisibleItems();return void(s.length>0&&getNextActiveElement(s,n,"ArrowDown"===t,!s.includes(n)).focus())}if("Home"===t||"End"===t){e.preventDefault();const n=this._getVisibleItems();return void(n.length>0&&("Home"===t?n[0]:n.at(-1)).focus())}if(("Enter"===t||" "===t)&&!s){e.preventDefault();const t=n.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&this._selectItem(t)}}static jQueryInterface(e){return this.each(function(){const t=Combobox.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}})}}EventHandler.on(document,EVENT_CLICK_DATA_API$4,SELECTOR_DATA_TOGGLE$7,function(e){e.preventDefault(),Combobox.getOrCreateInstance(this).toggle()}),EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7))Combobox.getOrCreateInstance(e)});const NAME$f="datepicker",DATA_KEY$b="bs.datepicker",EVENT_KEY$c=`.${DATA_KEY$b}`,DATA_API_KEY$7=".data-api",EVENT_CHANGE$2=`change${EVENT_KEY$c}`,EVENT_SHOW$4=`show${EVENT_KEY$c}`,EVENT_SHOWN$3=`shown${EVENT_KEY$c}`,EVENT_HIDE$3=`hide${EVENT_KEY$c}`,EVENT_HIDDEN$5=`hidden${EVENT_KEY$c}`,EVENT_CLICK_DATA_API$3=`click${EVENT_KEY$c}.data-api`,EVENT_FOCUSIN_DATA_API=`focusin${EVENT_KEY$c}.data-api`,SELECTOR_DATA_TOGGLE$6='[data-bs-toggle="datepicker"]',HIDE_DELAY=100,Default$e={datepickerTheme:null,dateMin:null,dateMax:null,dateFormat:null,displayElement:null,displayMonthsCount:1,firstWeekday:1,inline:!1,locale:"default",positionElement:null,selectedDates:[],selectionMode:"single",placement:"left",vcpOptions:{}},DefaultType$e={datepickerTheme:"(null|string)",dateMin:"(null|string|number|object)",dateMax:"(null|string|number|object)",dateFormat:"(null|object|function)",displayElement:"(null|string|element|boolean)",displayMonthsCount:"number",firstWeekday:"number",inline:"boolean",locale:"string",positionElement:"(null|string|element)",selectedDates:"array",selectionMode:"string",placement:"string",vcpOptions:"object"};class Datepicker extends BaseComponent{constructor(e,t){super(e,t),this._calendar=null,this._isShown=!1,this._initCalendar()}static get Default(){return Default$e}static get DefaultType(){return DefaultType$e}static get NAME(){return NAME$f}toggle(){if(!this._config.inline)return this._isShown?this.hide():this.show()}show(){this._config.inline||!this._calendar||isDisabled(this._element)||this._isShown||EventHandler.trigger(this._element,EVENT_SHOW$4).defaultPrevented||(this._calendar.show(),this._isShown=!0,EventHandler.trigger(this._element,EVENT_SHOWN$3))}hide(){this._config.inline||this._calendar&&this._isShown&&(EventHandler.trigger(this._element,EVENT_HIDE$3).defaultPrevented||(this._calendar.hide(),this._isShown=!1,EventHandler.trigger(this._element,EVENT_HIDDEN$5)))}dispose(){this._themeObserver&&(this._themeObserver.disconnect(),this._themeObserver=null),this._calendar&&this._calendar.destroy(),this._calendar=null,super.dispose()}getSelectedDates(){const e=this._calendar?.context?.selectedDates;return e?[...e]:[]}setSelectedDates(e){this._calendar&&this._calendar.set({selectedDates:e})}_initCalendar(){this._isInput="INPUT"===this._element.tagName,this._isInline=this._config.inline,this._isInline&&!this._isInput&&(this._boundInput=this._element.querySelector('input[type="hidden"], input[name]')),this._positionElement=this._resolvePositionElement(),this._displayElement=this._resolveDisplayElement();const e=this._buildCalendarOptions();this._calendar=new Calendar(this._positionElement,e),this._calendar.init(),this._setupThemeObserver(),this._isInput&&this._element.value&&this._parseInputValue(),this._updateDisplayWithSelectedDates()}_updateDisplayWithSelectedDates(){const{selectedDates:e}=this._config;if(!e||0===e.length)return;const t=this._formatDateForInput(e);this._isInput&&(this._element.value=t),this._boundInput&&(this._boundInput.value=e.join(",")),this._displayElement&&(this._displayElement.textContent=t)}_resolvePositionElement(){let{positionElement:e}=this._config;if("string"==typeof e&&(e=document.querySelector(e)),!e&&this._isInput&&!this._isInline){const t=this._element.closest(".form-adorn");t&&(e=t)}return e||this._element}_resolveDisplayElement(){const{displayElement:e}=this._config;return"string"==typeof e?document.querySelector(e):!0===e||null===e&&!this._isInput&&!this._isInline?this._element.querySelector("[data-bs-datepicker-display]")||this._element:e}_getThemeAncestor(){return this._element.closest("[data-bs-theme]")}_getEffectiveTheme(){const{datepickerTheme:e}=this._config;if(e)return e;const t=this._getThemeAncestor();return t?.getAttribute("data-bs-theme")||null}_syncThemeAttribute(e){if(!e)return;const t=this._getEffectiveTheme();t?e.setAttribute("data-bs-theme",t):e.removeAttribute("data-bs-theme")}_setupThemeObserver(){const e=this._getThemeAncestor();e&&!this._config.datepickerTheme&&(this._themeObserver=new MutationObserver(()=>{this._syncThemeAttribute(this._calendar?.context?.mainElement)}),this._themeObserver.observe(e,{attributes:!0,attributeFilter:["data-bs-theme"]}))}_buildCalendarOptions(){const e=this._getEffectiveTheme(),t=e&&"auto"!==e?e:"system",n={...this._config.vcpOptions,inputMode:!this._isInline,positionToInput:this._config.placement,firstWeekday:this._config.firstWeekday,locale:this._config.locale,selectionDatesMode:this._config.selectionMode,selectedDates:this._config.selectedDates,displayMonthsCount:this._config.displayMonthsCount,type:this._config.displayMonthsCount>1?"multiple":"default",selectedTheme:t,themeAttrDetect:"[data-bs-theme]",onClickDate:(e,t)=>this._handleDateClick(e,t),onInit:e=>{this._syncThemeAttribute(e.context.mainElement)},onShow:()=>{this._isShown=!0,this._syncThemeAttribute(this._calendar.context.mainElement)},onHide:()=>{this._isShown=!1}};if(this._config.selectedDates.length>0){const e=this._parseDate(this._config.selectedDates[0]);n.selectedMonth=e.getMonth(),n.selectedYear=e.getFullYear()}return this._config.dateMin&&(n.dateMin=this._config.dateMin),this._config.dateMax&&(n.dateMax=this._config.dateMax),n}_handleDateClick(e,t){const n=[...e.context.selectedDates];if(n.length>0){const e=this._formatDateForInput(n);this._isInput&&(this._element.value=e),this._boundInput&&(this._boundInput.value=n.join(",")),this._displayElement&&(this._displayElement.textContent=e)}EventHandler.trigger(this._element,EVENT_CHANGE$2,{dates:n,event:t}),this._maybeHideAfterSelection(n)}_maybeHideAfterSelection(e){this._isInline||("single"===this._config.selectionMode&&e.length>0||"multiple-ranged"===this._config.selectionMode&&e.length>=2)&&setTimeout(()=>this.hide(),100)}_parseDate(e){const[t,n,s]=e.split("-");return new Date(t,n-1,s)}_formatDate(e){const t=this._parseDate(e),n="default"===this._config.locale?void 0:this._config.locale,{dateFormat:s}=this._config;return"function"==typeof s?s(t,n):s&&"object"==typeof s?new Intl.DateTimeFormat(n,s).format(t):t.toLocaleDateString(n)}_formatDateForInput(e){if(0===e.length)return"";if(1===e.length)return this._formatDate(e[0]);const t="multiple-ranged"===this._config.selectionMode?" – ":", ";return e.map(e=>this._formatDate(e)).join(t)}_parseInputValue(){const e=this._element.value.trim();if(!e)return;const t=new Date(e);if(!Number.isNaN(t.getTime())){const e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;this._calendar.set({selectedDates:[e]})}}}EventHandler.on(document,EVENT_CLICK_DATA_API$3,SELECTOR_DATA_TOGGLE$6,function(e){"INPUT"!==this.tagName&&"true"!==this.dataset.bsInline&&(e.preventDefault(),Datepicker.getOrCreateInstance(this).toggle())}),EventHandler.on(document,EVENT_FOCUSIN_DATA_API,SELECTOR_DATA_TOGGLE$6,function(){"INPUT"===this.tagName&&Datepicker.getOrCreateInstance(this).show()}),EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$c}.data-api`,()=>{for(const e of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`))Datepicker.getOrCreateInstance(e)});const CLASS_NAME_OPEN="dialog-open";class DialogBase extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._openedAsModal=!1,this._addDialogListeners()}static get NAME(){return"dialogbase"}toggle(e){return this._element.open?this.hide():this.show(e)}show(e){if(this._element.open||this._isTransitioning)return;if(EventHandler.trigger(this._element,this.constructor.eventName("show"),{relatedTarget:e}).defaultPrevented)return;this._isTransitioning=!0,this._onBeforeShow();const{modal:t,preventBodyScroll:n}=this._getShowOptions();this._showElement({modal:t,preventBodyScroll:n}),this._queueCallback(()=>{this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("shown"),{relatedTarget:e})},this._element,this._isAnimated())}hide(){this._element.open&&!this._isTransitioning&&(EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented||(this._isTransitioning=!0,this._hideElement(),this._queueCallback(()=>{this._element.open&&this._closeAndCleanup(),this._element.classList.remove("hiding"),this._onAfterHide(),this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("hidden"))},this._element,this._isAnimated())))}dispose(){this._element.open&&this._closeAndCleanup(),super.dispose()}_getShowOptions(){return{modal:!0,preventBodyScroll:!0}}_onBeforeShow(){}_onAfterHide(){}_isAnimated(){return!this._element.classList.contains(this._getInstantClassName())}_getInstantClassName(){return"dialog-instant"}_getStaticClassName(){return"dialog-static"}_onCancel(){}_showElement({modal:e=!0,preventBodyScroll:t=!0}={}){this._openedAsModal=e,e?this._element.showModal():this._element.show(),t&&document.documentElement.classList.add("dialog-open")}_hideElement(){this._hideChildComponents(),this._element.classList.add("hiding"),this._shouldDeferClose()||this._closeAndCleanup()}_closeAndCleanup(){this._element.close(),this._openedAsModal=!1,document.querySelector("dialog[open]:modal")||document.documentElement.classList.remove("dialog-open")}_shouldDeferClose(){return!1}_triggerBackdropTransition(){if(EventHandler.trigger(this._element,this.constructor.eventName("hidePrevented")).defaultPrevented)return;const e=this._getStaticClassName();this._element.classList.add(e),this._queueCallback(()=>{this._element.classList.remove(e)},this._element)}_hideChildComponents(){for(const e of SelectorEngine.find('[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]',this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}for(const e of SelectorEngine.find(".toast.show",this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}}_addDialogListeners(){const e=this.constructor.EVENT_KEY;EventHandler.on(this._element,"cancel",e=>{e.preventDefault(),this._config.keyboard?(this._onCancel(),this.hide()):this._triggerBackdropTransition()}),EventHandler.on(this._element,`keydown${e}`,e=>{"Escape"!==e.key||this._openedAsModal||(e.preventDefault(),this._config.keyboard&&(this._onCancel(),this.hide()))}),EventHandler.on(this._element,`click${e}`,e=>{e.target===this._element&&this._openedAsModal&&("static"!==this._config.backdrop?this.hide():this._triggerBackdropTransition())})}}const NAME$e="dialog",DATA_KEY$a="bs.dialog",EVENT_KEY$b=`.${DATA_KEY$a}`,DATA_API_KEY$6=".data-api",EVENT_SHOW$3=`show${EVENT_KEY$b}`,EVENT_HIDDEN$4=`hidden${EVENT_KEY$b}`,EVENT_CANCEL=`cancel${EVENT_KEY$b}`,EVENT_CLICK_DATA_API$2=`click${EVENT_KEY$b}.data-api`,CLASS_NAME_NONMODAL="dialog-nonmodal",CLASS_NAME_INSTANT="dialog-instant",CLASS_NAME_SWAP_IN="dialog-swap-in",SELECTOR_DATA_TOGGLE$5='[data-bs-toggle="dialog"]',Default$d={backdrop:!0,keyboard:!0,modal:!0},DefaultType$d={backdrop:"(boolean|string)",keyboard:"boolean",modal:"boolean"};class Dialog extends DialogBase{static get Default(){return Default$d}static get DefaultType(){return DefaultType$d}static get NAME(){return NAME$e}handleUpdate(){}_getShowOptions(){return{modal:this._config.modal,preventBodyScroll:this._config.modal}}_onBeforeShow(){this._config.modal||this._element.classList.add("dialog-nonmodal")}_onAfterHide(){this._element.classList.remove("dialog-nonmodal")}_shouldDeferClose(){return this._isAnimated()}_onCancel(){EventHandler.trigger(this._element,EVENT_CANCEL)}}EventHandler.on(document,EVENT_CLICK_DATA_API$2,SELECTOR_DATA_TOGGLE$5,function(e){const t=SelectorEngine.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&e.preventDefault(),EventHandler.one(t,EVENT_SHOW$3,e=>{e.defaultPrevented||EventHandler.one(t,EVENT_HIDDEN$4,()=>{isVisible(this)&&this.focus({preventScroll:!0})})});const n=Manipulator.getDataAttributes(this),s=this.closest("dialog[open]");if(s&&s!==t){const e=Dialog.getOrCreateInstance(t,n);t.classList.add("dialog-swap-in"),e.show(this),EventHandler.one(t,`shown${EVENT_KEY$b}`,()=>{t.classList.remove("dialog-swap-in")});const i=Dialog.getInstance(s);return void(i&&(s.classList.add("dialog-instant"),EventHandler.one(s,EVENT_HIDDEN$4,()=>{s.classList.remove("dialog-instant")}),i.hide()))}Dialog.getOrCreateInstance(t,n).toggle(this)}),enableDismissTrigger(Dialog);const NAME$d="navoverflow",DATA_KEY$9="bs.navoverflow",EVENT_KEY$a=`.${DATA_KEY$9}`,EVENT_UPDATE=`update${EVENT_KEY$a}`,EVENT_OVERFLOW=`overflow${EVENT_KEY$a}`,CLASS_NAME_OVERFLOW="nav-overflow",CLASS_NAME_OVERFLOW_MENU="nav-overflow-menu",CLASS_NAME_HIDDEN="d-none",SELECTOR_NAV_ITEM=".nav-item",SELECTOR_NAV_LINK=".nav-link",SELECTOR_OVERFLOW_TOGGLE=".nav-overflow-toggle",SELECTOR_OVERFLOW_MENU=".nav-overflow-menu",SELECTOR_CUSTOM_ICON="[data-bs-overflow-icon]",CLASS_NAME_KEEP="nav-overflow-keep",Default$c={collapseBelow:0,iconPlacement:"start",menuPlacement:"bottom-end",moreText:"More",moreIcon:'',threshold:0},DefaultType$c={collapseBelow:"(number|string)",iconPlacement:"string",menuPlacement:"string",moreText:"string",moreIcon:"string",threshold:"number"};class NavOverflow extends BaseComponent{constructor(e,t){super(e,t),this._items=[],this._overflowItems=[],this._overflowMenu=null,this._overflowToggle=null,this._resizeObserver=null,this._collapseBelow=0,this._isInitialized=!1,this._init()}static get Default(){return Default$c}static get DefaultType(){return DefaultType$c}static get NAME(){return NAME$d}update(){this._calculateOverflow(),EventHandler.trigger(this._element,EVENT_UPDATE)}dispose(){this._resizeObserver&&this._resizeObserver.disconnect(),this._restoreItems(),this._overflowToggle&&this._overflowToggle.parentElement&&this._overflowToggle.parentElement.remove(),super.dispose()}_init(){this._element.classList.add("nav-overflow"),this._items=[...SelectorEngine.find(".nav-item",this._element)];for(const[e,t]of this._items.entries())t.dataset.bsNavOrder=e;this._collapseBelow=this._resolveCollapseBelow(),this._createOverflowMenu(),this._setupResizeObserver(),this._calculateOverflow(),this._isInitialized=!0}_createOverflowMenu(){if(this._overflowToggle=SelectorEngine.findOne(".nav-overflow-toggle",this._element),this._overflowToggle)return void(this._overflowMenu=SelectorEngine.findOne(".nav-overflow-menu",this._element));const e=`${this._resolveIcon()}`,t=`${this._config.moreText}`,n="end"===this._config.iconPlacement?`${t}${e}`:`${e}${t}`,s=document.createElement("li");s.className="nav-item nav-overflow-item",s.innerHTML=`\n \n ${n}\n \n \n `,this._element.append(s),this._overflowToggle=s.querySelector(".nav-overflow-toggle"),this._overflowMenu=s.querySelector(".nav-overflow-menu")}_resolveIcon(){const e=SelectorEngine.findOne(SELECTOR_CUSTOM_ICON,this._element);if(!e)return this._config.moreIcon;const t=e.cloneNode(!0);t.removeAttribute("data-bs-overflow-icon");const n=t.outerHTML;return e.remove(),n}_resolveCollapseBelow(){const e=this._config.collapseBelow;if("number"==typeof e)return e;if("string"==typeof e&&""!==e){const t=getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${e}`);return Number.parseFloat(t)||0}return 0}_setupResizeObserver(){"undefined"!=typeof ResizeObserver?(this._resizeObserver=new ResizeObserver(()=>{this._calculateOverflow()}),this._resizeObserver.observe(this._element)):EventHandler.on(window,"resize",()=>this._calculateOverflow())}_calculateOverflow(){this._restoreItems();const e=this._element.offsetWidth,t=this._overflowToggle?.closest(".nav-item");if(this._collapseBelow>0&&e!e.classList.contains(CLASS_NAME_KEEP));return this._moveToOverflow(e),t&&(e.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),void(e.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:e.length,visibleCount:this._items.length-e.length}))}let n=0;const s=[],i=e-(t?.offsetWidth||0)-this._items.filter(e=>e.classList.contains(CLASS_NAME_KEEP)).reduce((e,t)=>e+t.offsetWidth,0)-10;for(const e of this._items)e.classList.contains(CLASS_NAME_KEEP)||(n+=e.offsetWidth,n>i&&s.push(e));if(this._items.length-s.lengththis._config.threshold){const e=this._items.slice(this._config.threshold).filter(e=>!e.classList.contains(CLASS_NAME_KEEP));s.length=0,s.push(...e)}this._moveToOverflow(s),t&&(s.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),s.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:s.length,visibleCount:this._items.length-s.length})}_moveToOverflow(e){if(this._overflowMenu){this._overflowMenu.innerHTML="",this._overflowItems=[];for(const t of e){const e=SelectorEngine.findOne(".nav-link",t);if(!e)continue;const n=e.cloneNode(!0);n.className="menu-item",e.classList.contains("active")&&n.classList.add("active"),(e.classList.contains("disabled")||e.hasAttribute("disabled"))&&n.classList.add("disabled"),this._overflowMenu.append(n),t.classList.add("d-none"),t.dataset.bsNavOverflow="true",this._overflowItems.push(t)}}}_restoreItems(){for(const e of this._items)e.classList.remove("d-none"),delete e.dataset.bsNavOverflow;this._overflowMenu&&(this._overflowMenu.innerHTML=""),this._overflowItems=[]}}EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find('[data-bs-toggle="nav-overflow"]'))NavOverflow.getOrCreateInstance(e)});const NAME$c="swipe",EVENT_KEY$9=".bs.swipe",EVENT_TOUCHSTART="touchstart.bs.swipe",EVENT_TOUCHMOVE="touchmove.bs.swipe",EVENT_TOUCHEND="touchend.bs.swipe",EVENT_POINTERDOWN="pointerdown.bs.swipe",EVENT_POINTERUP="pointerup.bs.swipe",POINTER_TYPE_TOUCH="touch",POINTER_TYPE_PEN="pen",CLASS_NAME_POINTER_EVENT="pointer-event",SWIPE_THRESHOLD=40,Default$b={endCallback:null,leftCallback:null,rightCallback:null,upCallback:null,downCallback:null},DefaultType$b={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)",upCallback:"(function|null)",downCallback:"(function|null)"};class Swipe extends Config{constructor(e,t){super(),this._element=e,e&&Swipe.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._deltaY=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Default$b}static get DefaultType(){return DefaultType$b}static get NAME(){return NAME$c}dispose(){EventHandler.off(this._element,".bs.swipe")}_start(e){if(!this._supportPointerEvents)return this._deltaX=e.touches[0].clientX,void(this._deltaY=e.touches[0].clientY);this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX,this._deltaY=e.clientY)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX,this._deltaY=e.clientY-this._deltaY),this._handleSwipe(),execute(this._config.endCallback)}_move(e){if(e.touches&&e.touches.length>1)return this._deltaX=0,void(this._deltaY=0);this._deltaX=e.touches[0].clientX-this._deltaX,this._deltaY=e.touches[0].clientY-this._deltaY}_handleSwipe(){const e=Math.abs(this._deltaX),t=Math.abs(this._deltaY);if(t>e&&t>40){const e=this._deltaY>0?"down":"up";return this._deltaX=0,this._deltaY=0,void execute("down"===e?this._config.downCallback:this._config.upCallback)}if(e>40){const t=e/this._deltaX;if(this._deltaX=0,this._deltaY=0,!t)return;return void execute(t>0?this._config.rightCallback:this._config.leftCallback)}this._deltaX=0,this._deltaY=0}_initEvents(){this._supportPointerEvents?(EventHandler.on(this._element,EVENT_POINTERDOWN,e=>this._start(e)),EventHandler.on(this._element,EVENT_POINTERUP,e=>this._end(e)),this._element.classList.add("pointer-event")):(EventHandler.on(this._element,EVENT_TOUCHSTART,e=>this._start(e)),EventHandler.on(this._element,EVENT_TOUCHMOVE,e=>this._move(e)),EventHandler.on(this._element,EVENT_TOUCHEND,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&("pen"===e.pointerType||"touch"===e.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const NAME$b="drawer",DATA_KEY$8="bs.drawer",EVENT_KEY$8=`.${DATA_KEY$8}`,DATA_API_KEY$5=".data-api",EVENT_LOAD_DATA_API$2=`load${EVENT_KEY$8}.data-api`,EVENT_HIDDEN$3=`hidden${EVENT_KEY$8}`,EVENT_RESIZE$1=`resize${EVENT_KEY$8}`,EVENT_CLICK_DATA_API$1=`click${EVENT_KEY$8}.data-api`,SELECTOR_DATA_TOGGLE$4='[data-bs-toggle="drawer"]',Default$a={backdrop:!0,keyboard:!0,scroll:!1},DefaultType$a={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Drawer extends DialogBase{constructor(e,t){super(e,t),this._swipeHelper=null}static get Default(){return Default$a}static get DefaultType(){return DefaultType$a}static get NAME(){return NAME$b}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_getShowOptions(){return{modal:Boolean(this._config.backdrop)||!this._config.scroll,preventBodyScroll:!this._config.scroll}}_onBeforeShow(){this._initSwipe()}_getInstantClassName(){return"drawer-instant"}_getStaticClassName(){return"drawer-static"}_initSwipe(){if(this._swipeHelper||!Swipe.isSupported())return;const e={},t=this._element;t.classList.contains("drawer-bottom")?e.downCallback=()=>this.hide():t.classList.contains("drawer-top")?e.upCallback=()=>this.hide():t.classList.contains("drawer-end")?isRTL()?e.leftCallback=()=>this.hide():e.rightCallback=()=>this.hide():isRTL()?e.rightCallback=()=>this.hide():e.leftCallback=()=>this.hide(),this._swipeHelper=new Swipe(t,e)}}EventHandler.on(document,EVENT_CLICK_DATA_API$1,SELECTOR_DATA_TOGGLE$4,function(e){const t=SelectorEngine.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this))return;EventHandler.one(t,EVENT_HIDDEN$3,()=>{isVisible(this)&&this.focus({preventScroll:!0})});const n=SelectorEngine.findOne("dialog.drawer[open]");n&&n!==t&&Drawer.getInstance(n).hide(),Drawer.getOrCreateInstance(t).toggle(this)}),EventHandler.on(window,EVENT_LOAD_DATA_API$2,()=>{for(const e of SelectorEngine.find("dialog.drawer[open]"))Drawer.getOrCreateInstance(e).show()}),EventHandler.on(window,EVENT_RESIZE$1,()=>{for(const e of SelectorEngine.find('dialog[open][class*="\\:drawer"]'))"fixed"!==getComputedStyle(e).position&&Drawer.getOrCreateInstance(e).hide()}),enableDismissTrigger(Drawer);const NAME$a="strength",DATA_KEY$7="bs.strength",EVENT_KEY$7=`.${DATA_KEY$7}`,DATA_API_KEY$4=".data-api",EVENT_STRENGTH_CHANGE=`strengthChange${EVENT_KEY$7}`,SELECTOR_DATA_STRENGTH="[data-bs-strength]",STRENGTH_LEVELS=["weak","fair","good","strong"],Default$9={input:null,minLength:8,messages:{weak:"Weak",fair:"Fair",good:"Good",strong:"Strong"},weights:{minLength:1,extraLength:1,lowercase:1,uppercase:1,numbers:1,special:1,multipleSpecial:1,longPassword:1},thresholds:[2,4,6],scorer:null},DefaultType$9={input:"(string|element|null)",minLength:"number",messages:"object",weights:"object",thresholds:"array",scorer:"(function|null)"};class Strength extends BaseComponent{constructor(e,t){super(e,t),this._input=this._getInput(),this._segments=SelectorEngine.find(".strength-segment",this._element),this._textElement=SelectorEngine.findOne(".strength-text",this._element.parentElement),this._currentStrength=null,this._input&&(this._addEventListeners(),this._evaluate())}static get Default(){return Default$9}static get DefaultType(){return DefaultType$9}static get NAME(){return NAME$a}getStrength(){return this._currentStrength}evaluate(){this._evaluate()}_getInput(){if(this._config.input)return"string"==typeof this._config.input?SelectorEngine.findOne(this._config.input):this._config.input;const e=this._element.parentElement;return SelectorEngine.findOne('input[type="password"]',e)}_addEventListeners(){EventHandler.on(this._input,"input",()=>this._evaluate()),EventHandler.on(this._input,"change",()=>this._evaluate())}_evaluate(){const e=this._input.value,t=this._calculateScore(e),n=this._scoreToStrength(t);n!==this._currentStrength&&(this._currentStrength=n,this._updateUI(n,t),EventHandler.trigger(this._element,EVENT_STRENGTH_CHANGE,{strength:n,score:t,password:e.length>0?"***":""}))}_calculateScore(e){if(!e)return 0;if("function"==typeof this._config.scorer)return this._config.scorer(e);const{weights:t}=this._config;let n=0;return e.length>=this._config.minLength&&(n+=t.minLength),e.length>=this._config.minLength+4&&(n+=t.extraLength),/[a-z]/.test(e)&&(n+=t.lowercase),/[A-Z]/.test(e)&&(n+=t.uppercase),/\d/.test(e)&&(n+=t.numbers),/[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.special),/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.multipleSpecial),e.length>=16&&(n+=t.longPassword),n}_scoreToStrength(e){if(0===e)return null;const[t,n,s]=this._config.thresholds;return e<=t?"weak":e<=n?"fair":e<=s?"good":"strong"}_updateUI(e){e?this._element.dataset.bsStrength=e:delete this._element.dataset.bsStrength;const t=e?STRENGTH_LEVELS.indexOf(e):-1;for(const[e,n]of this._segments.entries())e<=t?n.classList.add("active"):n.classList.remove("active");if(this._textElement)if(e&&this._config.messages[e]){this._textElement.textContent=this._config.messages[e],this._textElement.dataset.bsStrength=e;const t={weak:"danger",fair:"warning",good:"info",strong:"success"};this._textElement.style.setProperty("--strength-color",`var(--${t[e]}-text)`)}else this._textElement.textContent="",delete this._textElement.dataset.bsStrength}}EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$7}.data-api`,()=>{for(const e of SelectorEngine.find("[data-bs-strength]"))Strength.getOrCreateInstance(e)});const NAME$9="otpInput",DATA_KEY$6="bs.otpInput",EVENT_KEY$6=`.${DATA_KEY$6}`,DATA_API_KEY$3=".data-api",EVENT_COMPLETE=`complete${EVENT_KEY$6}`,EVENT_INPUT$1=`input${EVENT_KEY$6}`,EVENT_DOMCONTENT_LOADED=`DOMContentLoaded${EVENT_KEY$6}.data-api`,SELECTOR_DATA_OTP="[data-bs-otp]",SELECTOR_INPUT$1="input",SYNC_EVENTS=["blur","keyup","click","select"],CLASS_NAME_INPUT="otp-input",CLASS_NAME_RENDERED="otp-rendered",CLASS_NAME_SLOTS="otp-slots",CLASS_NAME_SLOT="otp-slot",CLASS_NAME_SLOT_FILLED="otp-slot-filled",CLASS_NAME_SLOT_ACTIVE="otp-slot-active",CLASS_NAME_SEPARATOR="otp-separator",MASK_CHARACTER="•",TYPES={numeric:{inputmode:"numeric",pattern:"[0-9]*",filter:/[^0-9]/g},alphanumeric:{inputmode:"text",pattern:"[A-Za-z0-9]*",filter:/[^A-Za-z0-9]/g},alpha:{inputmode:"text",pattern:"[A-Za-z]*",filter:/[^A-Za-z]/g}},Default$8={groups:null,length:null,mask:!1,separator:"·",type:"numeric"},DefaultType$8={groups:"(array|null)",length:"(number|null)",mask:"boolean",separator:"string",type:"string"};class OtpInput extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne("input",this._element),this._input&&(this._type=TYPES[this._config.type]||TYPES.numeric,this._length=this._resolveLength(),this._slots=[],this._setupInput(),this._renderSlots(),this._addEventListeners(),this._render())}static get Default(){return Default$8}static get DefaultType(){return DefaultType$8}static get NAME(){return NAME$9}getValue(){return this._input.value}setValue(e){this._input.value=this._sanitize(String(e)),this._render(),this._checkComplete()}clear(){this._input.value="",this._render(),this._input.focus()}focus(){this._input.focus();const e=this._input.value.length;this._input.setSelectionRange(e,e),this._render()}dispose(){EventHandler.off(this._input,"input",this._onInput),EventHandler.off(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.off(this._input,e,this._onSync);this._slotsContainer?.remove(),this._element.classList.remove("otp-rendered"),super.dispose()}_resolveLength(){if(this._config.length)return this._config.length;const e=Number.parseInt(this._input.getAttribute("maxlength"),10);return Number.isNaN(e)||e<1?6:e}_setupInput(){const e=this._input;"number"!==e.type&&"password"!==e.type||(e.type="text"),e.classList.add("otp-input"),e.setAttribute("maxlength",String(this._length)),e.setAttribute("inputmode",this._type.inputmode),e.setAttribute("pattern",this._type.pattern),e.getAttribute("autocomplete")||e.setAttribute("autocomplete","one-time-code"),e.value&&(e.value=this._sanitize(e.value))}_renderSlots(){const e=document.createElement("div");e.className="otp-slots",e.setAttribute("aria-hidden","true");const{groups:t}=this._config;let n=0,s=0;for(let i=0;i0&&(s++,s===t[n]&&ithis._handleInput(),this._onFocus=()=>this.focus(),this._onSync=()=>this._render(),EventHandler.on(this._input,"input",this._onInput),EventHandler.on(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.on(this._input,e,this._onSync)}_handleInput(){const e=this._sanitize(this._input.value);e!==this._input.value&&(this._input.value=e),this._render(),EventHandler.trigger(this._element,EVENT_INPUT$1,{value:this._input.value}),this._checkComplete()}_sanitize(e){return e.replace(this._type.filter,"").slice(0,this._length)}_render(){const{value:e}=this._input,t=document.activeElement===this._input,n=Math.min(this._input.selectionStart??e.length,this._length-1);for(const[s,i]of this._slots.entries()){const o=e[s]??"";i.textContent=o&&this._config.mask?"•":o,i.classList.toggle("otp-slot-filled",Boolean(o)),i.classList.toggle("otp-slot-active",t&&s===n)}}_checkComplete(){const{value:e}=this._input;e.length===this._length&&EventHandler.trigger(this._element,EVENT_COMPLETE,{value:e})}}EventHandler.on(document,EVENT_DOMCONTENT_LOADED,()=>{for(const e of SelectorEngine.find("[data-bs-otp]"))OtpInput.getOrCreateInstance(e)});const NAME$8="chips",DATA_KEY$5="bs.chips",EVENT_KEY$5=".bs.chips",DATA_API_KEY$2=".data-api",EVENT_ADD="add.bs.chips",EVENT_REMOVE="remove.bs.chips",EVENT_CHANGE$1="change.bs.chips",EVENT_SELECT="select.bs.chips",SELECTOR_DATA_CHIPS="[data-bs-chips]",SELECTOR_GHOST_INPUT=".form-ghost",SELECTOR_CHIP=".chip",SELECTOR_CHIP_DISMISS=".chip-dismiss",CLASS_NAME_CHIP="chip",CLASS_NAME_CHIP_DISMISS="chip-dismiss",CLASS_NAME_ACTIVE$2="active",DEFAULT_DISMISS_ICON='',Default$7={separator:",",allowDuplicates:!1,maxChips:null,placeholder:"",dismissible:!0,dismissIcon:DEFAULT_DISMISS_ICON,createOnBlur:!0},DefaultType$7={separator:"(string|null)",allowDuplicates:"boolean",maxChips:"(number|null)",placeholder:"string",dismissible:"boolean",dismissIcon:"string",createOnBlur:"boolean"};class Chips extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne(".form-ghost",this._element),this._chips=[],this._selectedChips=new Set,this._anchorChip=null,this._input||this._createInput(),this._initializeExistingChips(),this._addEventListeners()}static get Default(){return Default$7}static get DefaultType(){return DefaultType$7}static get NAME(){return NAME$8}add(e){const t=String(e).trim();if(!t)return null;if(!this._config.allowDuplicates&&this._chips.includes(t))return null;if(null!==this._config.maxChips&&this._chips.length>=this._config.maxChips)return null;if(EventHandler.trigger(this._element,EVENT_ADD,{value:t,relatedTarget:this._input}).defaultPrevented)return null;const n=this._createChip(t);return this._element.insertBefore(n,this._input),this._chips.push(t),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),n}remove(e){let t,n;return"string"==typeof e?(n=e,t=this._findChipByValue(n)):(t=e,n=this._getChipValue(t)),!(!t||!n)&&(!EventHandler.trigger(this._element,EVENT_REMOVE,{value:n,chip:t,relatedTarget:this._input}).defaultPrevented&&(this._selectedChips.delete(t),this._anchorChip===t&&(this._anchorChip=null),t.remove(),this._chips=this._chips.filter(e=>e!==n),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),!0))}removeSelected(){const e=[...this._selectedChips];for(const t of e)this.remove(t);this._input?.focus()}getValues(){return[...this._chips]}getSelectedValues(){return[...this._selectedChips].map(e=>this._getChipValue(e))}clear(){const e=SelectorEngine.find(".chip",this._element);for(const t of e)t.remove();this._chips=[],this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:[]})}clearSelection(){for(const e of this._selectedChips)e.classList.remove("active");this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_SELECT,{selected:[]})}selectChip(e,t={}){const{addToSelection:n=!1,rangeSelect:s=!1}=t,i=this._getChipElements();if(i.includes(e)){if(s&&this._anchorChip){const t=i.indexOf(this._anchorChip),s=i.indexOf(e),o=Math.min(t,s),r=Math.max(t,s);n||this.clearSelection();for(let e=o;e<=r;e++)this._selectedChips.add(i[e]),i[e].classList.add("active")}else n?this._selectedChips.has(e)?(this._selectedChips.delete(e),e.classList.remove("active")):(this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e):(this.clearSelection(),this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e);EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}focus(){this._input?.focus()}_getChipElements(){return SelectorEngine.find(".chip",this._element)}_createInput(){const e=document.createElement("input");e.type="text",e.className="form-ghost",this._config.placeholder&&(e.placeholder=this._config.placeholder),this._element.append(e),this._input=e}_initializeExistingChips(){const e=SelectorEngine.find(".chip",this._element);for(const t of e){const e=this._getChipValue(t);e&&(this._chips.push(e),this._setupChip(t))}}_setupChip(e){e.setAttribute("tabindex","0"),this._config.dismissible&&!SelectorEngine.findOne(".chip-dismiss",e)&&e.append(this._createDismissButton())}_createChip(e){const t=document.createElement("span");return t.className="chip",t.dataset.bsChipValue=e,t.append(document.createTextNode(e)),this._setupChip(t),t}_createDismissButton(){const e=document.createElement("button");return e.type="button",e.className="chip-dismiss",e.setAttribute("aria-label","Remove"),e.setAttribute("tabindex","-1"),e.innerHTML=this._config.dismissIcon,e}_findChipByValue(e){return this._getChipElements().find(t=>this._getChipValue(t)===e)}_getChipValue(e){if(e.dataset.bsChipValue)return e.dataset.bsChipValue;const t=e.cloneNode(!0),n=SelectorEngine.findOne(".chip-dismiss",t);return n&&n.remove(),t.textContent?.trim()||""}_addEventListeners(){EventHandler.on(this._input,"keydown",e=>this._handleInputKeydown(e)),EventHandler.on(this._input,"input",e=>this._handleInput(e)),EventHandler.on(this._input,"paste",e=>this._handlePaste(e)),EventHandler.on(this._input,"focus",()=>this.clearSelection()),this._config.createOnBlur&&EventHandler.on(this._input,"blur",e=>{e.relatedTarget?.closest(".chip")||this._createChipFromInput()}),EventHandler.on(this._element,"click",".chip",e=>{if(e.target.closest(".chip-dismiss"))return;const t=e.target.closest(".chip");t&&(e.preventDefault(),this.selectChip(t,{addToSelection:e.metaKey||e.ctrlKey,rangeSelect:e.shiftKey}),t.focus())}),EventHandler.on(this._element,"click",".chip-dismiss",e=>{e.stopPropagation();const t=e.target.closest(".chip");t&&(this.remove(t),this._input?.focus())}),EventHandler.on(this._element,"keydown",".chip",e=>{this._handleChipKeydown(e)}),EventHandler.on(this._element,"click",e=>{e.target===this._element&&(this.clearSelection(),this._input?.focus())})}_handleInputKeydown(e){const{key:t}=e;switch(t){case"Enter":e.preventDefault(),this._createChipFromInput();break;case"Backspace":case"Delete":if(""===this._input.value){e.preventDefault();const t=this._getChipElements();if(t.length>0){const e=t.at(-1);this.selectChip(e),e.focus()}}break;case"ArrowLeft":if(0===this._input.selectionStart&&0===this._input.selectionEnd){e.preventDefault();const t=this._getChipElements();if(t.length>0){const n=t.at(-1);e.shiftKey?this.selectChip(n,{addToSelection:!0}):this.selectChip(n),n.focus()}}break;case"Escape":this._input.value="",this.clearSelection(),this._input.blur()}}_handleChipKeydown(e){const{key:t}=e,n=e.target.closest(".chip");if(!n)return;const s=this._getChipElements(),i=s.indexOf(n);switch(t){case"Backspace":case"Delete":e.preventDefault(),this._handleChipDelete(i,s);break;case"ArrowLeft":e.preventDefault(),this._navigateChip(s,i,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._navigateChip(s,i,1,e.shiftKey);break;case"Home":e.preventDefault(),this._navigateToEdge(s,0,e.shiftKey);break;case"End":case"Escape":e.preventDefault(),this.clearSelection(),this._input?.focus();break;case"a":this._handleSelectAll(e,s)}}_handleChipDelete(e,t){if(0===this._selectedChips.size)return;const n=Math.min(e,t.length-this._selectedChips.size-1);this.removeSelected();const s=this._getChipElements();if(s.length>0){const e=Math.max(0,Math.min(n,s.length-1));s[e].focus(),this.selectChip(s[e])}else this._input?.focus()}_navigateChip(e,t,n,s){const i=t+n;if(n<0&&i>=0){const t=e[i];this.selectChip(t,s?{addToSelection:!0,rangeSelect:!0}:{}),t.focus()}else if(n>0&&i0&&(this.clearSelection(),this._input?.focus())}_navigateToEdge(e,t,n){if(0===e.length)return;const s=e[t];this.selectChip(s,n?{rangeSelect:!0}:{}),s.focus()}_handleSelectAll(e,t){if(e.metaKey||e.ctrlKey){e.preventDefault();for(const e of t)this._selectedChips.add(e),e.classList.add("active");EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}_handleInput(e){const{value:t}=e.target,{separator:n}=this._config;if(n&&t.includes(n)){const e=t.split(n);for(const t of e.slice(0,-1))this.add(t.trim());this._input.value=e.at(-1)}}_handlePaste(e){const{separator:t}=this._config;if(!t)return;const n=(e.clipboardData||window.clipboardData).getData("text");if(n.includes(t)){e.preventDefault();const s=n.split(t);for(const e of s)this.add(e.trim())}}_createChipFromInput(){const e=this._input.value.trim();e&&(this.add(e),this._input.value="")}}EventHandler.on(document,"DOMContentLoaded.bs.chips.data-api",()=>{for(const e of SelectorEngine.find("[data-bs-chips]"))Chips.getOrCreateInstance(e)});const ARIA_ATTRIBUTE_PATTERN=/^aria-[\w-]*$/i,DefaultAllowlist={"*":["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:[]},uriAttributes=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),SAFE_URL_PATTERN=/^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,DATA_URL_PATTERN=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i,allowedAttribute=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!uriAttributes.has(n)||Boolean(SAFE_URL_PATTERN.test(e.nodeValue)||DATA_URL_PATTERN.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))};function sanitizeHtml(e,t,n){if(!e.length)return e;if(n&&"function"==typeof n)return n(e);const s=(new window.DOMParser).parseFromString(e,"text/html"),i=[...s.body.querySelectorAll("*")];for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[...e.attributes],i=[...t["*"]||[],...t[n]||[]];for(const t of s)allowedAttribute(t,i)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const NAME$7="TemplateFactory",Default$6={allowList:DefaultAllowlist,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},DefaultType$6={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},DefaultContentType={entry:"(string|element|function|null)",selector:"(string|element)"};class TemplateFactory extends Config{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Default$6}static get DefaultType(){return DefaultType$6}static get NAME(){return NAME$7}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},DefaultContentType)}_setContent(e,t,n){const s=SelectorEngine.findOne(n,e);s&&((t=this._resolvePossibleFunction(t))?isElement(t)?this._putElementInTemplate(getElement(t),s):this._config.html?s.innerHTML=this._maybeSanitize(t):s.textContent=t:s.remove())}_maybeSanitize(e){return this._config.sanitize?sanitizeHtml(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return execute(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML="",void t.append(e);t.textContent=e.textContent}}const NAME$6="tooltip",DISALLOWED_ATTRIBUTES=new Set(["sanitize","allowList","sanitizeFn"]),ESCAPE_KEY="Escape",CLASS_NAME_FADE$2="fade",CLASS_NAME_MODAL="modal",CLASS_NAME_SHOW$2="show",SELECTOR_TOOLTIP_INNER=".tooltip-inner",SELECTOR_MODAL=".modal",SELECTOR_DATA_TOGGLE$3='[data-bs-toggle="tooltip"]',EVENT_MODAL_HIDE="hide.bs.modal",TRIGGER_HOVER="hover",TRIGGER_FOCUS="focus",TRIGGER_CLICK="click",TRIGGER_MANUAL="manual",EVENT_HIDE$2="hide",EVENT_HIDDEN$2="hidden",EVENT_SHOW$2="show",EVENT_SHOWN$2="shown",EVENT_INSERTED="inserted",EVENT_CLICK$3="click",EVENT_FOCUSIN$2="focusin",EVENT_FOCUSOUT$1="focusout",EVENT_MOUSEENTER$1="mouseenter",EVENT_MOUSELEAVE="mouseleave",EVENT_KEYDOWN$1="keydown",AttachmentMap={AUTO:"auto",TOP:"top",RIGHT:isRTL()?"left":"right",BOTTOM:"bottom",LEFT:isRTL()?"right":"left"},Default$5={allowList:DefaultAllowlist,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",floatingConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},DefaultType$5={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",floatingConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Tooltip extends BaseComponent{constructor(e,t){if(void 0===computePosition)throw new TypeError("Bootstrap's tooltips require Floating UI (https://floating-ui.com)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._floatingCleanup=null,this._keydownHandler=null,this._templateFactory=null,this._newContent=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this.tip=null,this._parseResponsivePlacements(),this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Default$5}static get DefaultType(){return DefaultType$5}static get NAME(){return NAME$6}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),this._removeEscapeListener(),EventHandler.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposeFloating(),this._disposeMediaQueryListeners(),super.dispose()}async show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const e=EventHandler.trigger(this._element,this.constructor.eventName("show")),t=(findShadowRoot(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return void(this._isHovered=!1);this._disposeFloating();const n=this._getTipElement();this._element.setAttribute("aria-describedby",n.getAttribute("id"));let{container:s}=this._config;const i=this._element.closest("dialog[open]");if(i&&s===document.body&&(s=i),this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(n),EventHandler.trigger(this._element,this.constructor.eventName("inserted"))),await this._createFloating(n),n.classList.add("show"),this._setEscapeListener(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._queueCallback(()=>{EventHandler.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._removeEscapeListener(),this._getTipElement().classList.remove("show"),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposeFloating(),this._element.removeAttribute("aria-describedby"),EventHandler.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._floatingCleanup&&this.tip&&this._updateFloatingPosition()}_isWithContent(){return Boolean(this._getTitle())||this._hasNewContent()}_hasNewContent(){return Boolean(this._newContent)&&Object.values(this._newContent).some(Boolean)}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();t.classList.remove("fade","show"),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=getUID(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add("fade"),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposeFloating(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new TemplateFactory({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[SELECTOR_TOOLTIP_INNER]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains("fade")}_isShown(){return this.tip&&this.tip.classList.contains("show")}_getPlacement(e){if(this._responsivePlacements){const e=getResponsivePlacement(this._responsivePlacements,"top");return AttachmentMap[e.toUpperCase()]||e}const t=execute(this._config.placement,[this,e,this._element]);return AttachmentMap[t.toUpperCase()]||t}_parseResponsivePlacements(){"string"==typeof this._config.placement?(this._responsivePlacements=parseResponsivePlacement(this._config.placement,"top"),this._responsivePlacements&&this._setupMediaQueryListeners()):this._responsivePlacements=null}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}async _createFloating(e){const t=this._getPlacement(e),n=e.querySelector(`.${this.constructor.NAME}-arrow`);await this._updateFloatingPosition(e,t,n),this._floatingCleanup=autoUpdate(this._element,e,()=>this._updateFloatingPosition(e,null,n))}async _updateFloatingPosition(e=this.tip,t=null,n=null){if(!e)return;t||(t=this._getPlacement(e)),n||(n=e.querySelector(`.${this.constructor.NAME}-arrow`));const s=this._getFloatingMiddleware(n),i=this._getFloatingConfig(t,s),{x:o,y:r,placement:l,middlewareData:a}=await computePosition(this._element,e,i);if(Object.assign(e.style,{position:"absolute",left:`${o}px`,top:`${r}px`}),n&&(n.style.position="absolute"),Manipulator.setDataAttribute(e,"placement",l),n&&a.arrow){const{x:e,y:t}=a.arrow,s=l.startsWith("top")||l.startsWith("bottom");Object.assign(n.style,{left:s&&null!==e?`${e}px`:"",top:s||null===t?"":`${t}px`,right:"",bottom:""})}}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_resolvePossibleFunction(e){return execute(e,[this._element,this._element])}_getFloatingMiddleware(e){const t=this._getOffset(),n=[offset("function"==typeof t?t:{mainAxis:t[1]||0,crossAxis:t[0]||0}),flip({fallbackPlacements:this._config.fallbackPlacements}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})];return e&&n.push(arrow({element:e})),n}_getFloatingConfig(e,t){const n={placement:e,middleware:t};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_setListeners(){const e=this._config.trigger.split(" ");for(const t of e)if("click"===t)EventHandler.on(this._element,this.constructor.eventName("click"),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger.click=!(t._isShown()&&t._activeTrigger.click),t.toggle()});else if("manual"!==t){const e="hover"===t?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),n="hover"===t?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");EventHandler.on(this._element,e,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusin"===e.type?"focus":"hover"]=!0,t._enter()}),EventHandler.on(this._element,n,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusout"===e.type?"focus":"hover"]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},EventHandler.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler)}_setEscapeListener(){this._keydownHandler||(this._keydownHandler=e=>{"Escape"===e.key&&this._isShown()&&this.tip.isConnected&&(e.preventDefault(),e.stopPropagation(),this.hide())},this._element.ownerDocument.addEventListener("keydown",this._keydownHandler,!0))}_removeEscapeListener(){this._keydownHandler&&(this._element.ownerDocument.removeEventListener("keydown",this._keydownHandler,!0),this._keydownHandler=null)}_fixTitle(){const e=this._element.getAttribute("title");e&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=Manipulator.getDataAttributes(this._element);for(const e of Object.keys(t))DISALLOWED_ATTRIBUTES.has(e)&&delete t[e];return e={...t,..."object"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:getElement(e.container),"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),"number"!=typeof e.title&&"boolean"!=typeof e.title||(e.title=e.title.toString()),"number"!=typeof e.content&&"boolean"!=typeof e.content||(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger="manual",e}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null),this.tip&&(this.tip.remove(),this.tip=null)}}const initTooltip=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$3);t&&Tooltip.getOrCreateInstance(t)};EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$3,initTooltip),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$3,initTooltip);const NAME$5="popover",SELECTOR_TITLE=".popover-header",SELECTOR_CONTENT=".popover-body",SELECTOR_DATA_TOGGLE$2='[data-bs-toggle="popover"]',EVENT_CLICK$2="click",EVENT_FOCUSIN$1="focusin",EVENT_MOUSEENTER="mouseenter",Default$4={...Tooltip.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},DefaultType$4={...Tooltip.DefaultType,content:"(null|string|element|function)"};class Popover extends Tooltip{static get Default(){return Default$4}static get DefaultType(){return DefaultType$4}static get NAME(){return NAME$5}_isWithContent(){return Boolean(this._getTitle()||this._getContent())||this._hasNewContent()}_getContentForTemplate(){return{[SELECTOR_TITLE]:this._getTitle(),[SELECTOR_CONTENT]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}}const initPopover=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$2);t&&("click"===e.type&&e.preventDefault(),Popover.getOrCreateInstance(t))};EventHandler.on(document,"click",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$2,initPopover);const NAME$4="range",DATA_KEY$4="bs.range",EVENT_KEY$4=".bs.range",DATA_API_KEY$1=".data-api",EVENT_CHANGED="changed.bs.range",EVENT_DOM_CONTENT_LOADED="DOMContentLoaded.bs.range.data-api",EVENT_INPUT="input",EVENT_CHANGE="change",SELECTOR_RANGE=".form-range",SELECTOR_INPUT=".form-range-input",CLASS_NAME_BUBBLE="form-range-bubble",CLASS_NAME_TICKS="form-range-ticks",CLASS_NAME_TICK="form-range-tick",CLASS_NAME_TICK_LABEL="form-range-tick-label",PROPERTY_FILL="--bs-range-fill",Default$3={bubble:!1,formatter:null},DefaultType$3={bubble:"(boolean|null)",formatter:"(function|null)"};class Range extends BaseComponent{constructor(e,t){super(e,t),this._element&&(this._input=SelectorEngine.findOne(SELECTOR_INPUT,this._element),this._input&&(this._bubble=null,this._bubbleText=null,this._ticks=null,this._updateHandler=()=>this._update(),this._config.bubble&&this._createBubble(),this._createTicks(),this._addEventListeners(),this._update()))}static get Default(){return Default$3}static get DefaultType(){return DefaultType$3}static get NAME(){return NAME$4}update(){this._update()}dispose(){EventHandler.off(this._input,"input",this._updateHandler),EventHandler.off(this._input,"change",this._updateHandler),this._bubble?.remove(),this._ticks?.remove(),super.dispose()}_configAfterMerge(e){return null===e.bubble&&(e.bubble=!0),e}_addEventListeners(){EventHandler.on(this._input,"input",this._updateHandler),EventHandler.on(this._input,"change",this._updateHandler)}_min(){return""===this._input.min?0:Number.parseFloat(this._input.min)}_max(){return""===this._input.max?100:Number.parseFloat(this._input.max)}_value(){return Number.parseFloat(this._input.value)}_ratio(){const e=this._max()-this._min();return e>0?(this._value()-this._min())/e:0}_update(){this._element.style.setProperty(PROPERTY_FILL,`${this._ratio()}`),this._bubbleText&&(this._bubbleText.textContent=this._format(this._value())),EventHandler.trigger(this._input,EVENT_CHANGED,{value:this._value()})}_format(e){return"function"==typeof this._config.formatter?this._config.formatter(e):String(e)}_createBubble(){this._bubble=document.createElement("output"),this._bubble.className=`${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`,this._bubble.setAttribute("aria-hidden","true");const e=document.createElement("div");e.className="tooltip-arrow",this._bubbleText=document.createElement("div"),this._bubbleText.className="tooltip-inner",this._bubble.append(e,this._bubbleText),this._input.insertAdjacentElement("afterend",this._bubble)}_createTicks(){const e=this._input.getAttribute("list"),t=e?document.getElementById(e):null;if(!t)return;const n=this._min(),s=this._max()-n||1,i=[];for(const e of SelectorEngine.find("option",t)){const t=Number.parseFloat(e.value);if(!Number.isNaN(t)){const o=Math.min(Math.max((t-n)/s,0),1);i.push({ratio:o,label:e.label})}}if(0===i.length)return;i.sort((e,t)=>e.ratio-t.ratio),this._ticks=document.createElement("div"),this._ticks.className=CLASS_NAME_TICKS,this._ticks.setAttribute("aria-hidden","true");const o=[0,...i.map(e=>e.ratio),1];this._ticks.style.gridTemplateColumns=o.slice(1).map((e,t)=>e-o[t]+"fr").join(" ");for(const[e,t]of i.entries()){const n=document.createElement("span");if(n.className=CLASS_NAME_TICK,n.style.gridColumnStart=`${e+2}`,t.label){const e=document.createElement("span");e.className=CLASS_NAME_TICK_LABEL,e.textContent=t.label,n.append(e)}this._ticks.append(n)}this._element.append(this._ticks)}}EventHandler.on(document,EVENT_DOM_CONTENT_LOADED,()=>{for(const e of SelectorEngine.find(".form-range"))Range.getOrCreateInstance(e)});const NAME$3="scrollspy",DATA_KEY$3="bs.scrollspy",EVENT_KEY$3=`.${DATA_KEY$3}`,DATA_API_KEY=".data-api",EVENT_ACTIVATE=`activate${EVENT_KEY$3}`,EVENT_CLICK$1=`click${EVENT_KEY$3}`,EVENT_SCROLL=`scroll${EVENT_KEY$3}`,EVENT_SCROLLEND=`scrollend${EVENT_KEY$3}`,EVENT_RESIZE=`resize${EVENT_KEY$3}`,EVENT_LOAD_DATA_API$1=`load${EVENT_KEY$3}.data-api`,CLASS_NAME_MENU_ITEM="menu-item",CLASS_NAME_ACTIVE$1="active",SELECTOR_DATA_SPY='[data-bs-spy="scroll"]',SELECTOR_TARGET_LINKS="[href]",SELECTOR_NAV_LIST_GROUP=".nav, .list-group",SELECTOR_NAV_LINKS=".nav-link",SELECTOR_NAV_ITEMS=".nav-item",SELECTOR_LIST_ITEMS=".list-group-item",SELECTOR_LINK_ITEMS=".nav-link, .nav-item > .nav-link, .list-group-item",SELECTOR_MENU_TOGGLE$1='[data-bs-toggle="menu"]',SCROLL_IDLE_TIMEOUT=100,RESIZE_DEBOUNCE=100,Default$2={rootMargin:null,smoothScroll:!1,target:null,threshold:[0],topMargin:"12%"},DefaultType$2={rootMargin:"(string|null)",smoothScroll:"boolean",target:"element",threshold:"array",topMargin:"string"};class ScrollSpy extends BaseComponent{constructor(e,t){super(e,t),this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map,this._intersecting=new Set,this._activeTarget=null,this._lastActive=null,this._atBottom=!1,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._observer=null,this._sentinel=null,this._sentinelObserver=null,this._pendingNavigation=null,this._settleTimeout=null,this._settleHandler=null,this._scrollIdleHandler=null,this._resizeHandler=null,this._resizeTimeout=null,this.refresh()}static get Default(){return Default$2}static get DefaultType(){return DefaultType$2}static get NAME(){return NAME$3}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e);this._setUpSentinel(),this._maybeAddResizeListener()}dispose(){this._observer?.disconnect(),this._teardownSentinel(),this._disarmSettle(),this._removeResizeListener(),EventHandler.off(this._config.target,EVENT_CLICK$1),super.dispose()}_configAfterMerge(e){return e.target=getElement(e.target)||document.body,"string"==typeof e.threshold&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin??this._getDerivedRootMargin()};return new IntersectionObserver(e=>this._onIntersect(e),e)}_onIntersect(e){for(const t of e)t.isIntersecting?this._intersecting.add(t.target):this._intersecting.delete(t.target);this._computeActive()}_computeActive(){if(!this._element?.isConnected||0===this._sections.length)return;let e=null;if(this._atBottom)e=this._sections.at(-1);else{for(const t of this._sections)this._intersecting.has(t)&&(e=t);e||=this._lastActive??this._sections.at(0)}if(!e)return;this._lastActive=e;const t=this._linkBySection.get(e);t&&this._process(t)}_parseTopMargin(){const e=String(this._config.topMargin);return{value:Number.parseFloat(e)||0,unit:e.endsWith("%")?"%":"px"}}_getDerivedRootMargin(){const{value:e,unit:t}=this._parseTopMargin();let n=e;if("px"===t){const t=this._rootElement?this._rootElement.clientHeight:document.documentElement.clientHeight||window.innerHeight;n=t?e/t*100:12}return`0px 0px -${Math.min(Math.max(100-n,0),100)}% 0px`}_usesPixelMargin(){return!this._config.rootMargin&&"px"===this._parseTopMargin().unit}_setUpSentinel(){if(this._teardownSentinel(),0===this._sections.length)return;const e=document.createElement("div");e.setAttribute("aria-hidden","true"),e.style.cssText="position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;",this._element.append(e),this._sentinel=e,this._sentinelObserver=new IntersectionObserver(e=>this._onSentinel(e),{root:this._rootElement,threshold:[0]}),this._sentinelObserver.observe(e)}_onSentinel(e){const t=e.at(-1);this._atBottom=Boolean(t?.isIntersecting)&&this._isOverflowing(),this._computeActive()}_isOverflowing(){const e=this._rootElement||document.scrollingElement||document.documentElement;return e.scrollHeight>e.clientHeight}_teardownSentinel(){this._sentinelObserver?.disconnect(),this._sentinelObserver=null,this._sentinel?.remove(),this._sentinel=null,this._atBottom=!1}_maybeAddResizeListener(){this._removeResizeListener(),this._usesPixelMargin()&&(this._resizeHandler=()=>{clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(()=>this._rebuildObserver(),100)},EventHandler.on(window,EVENT_RESIZE,this._resizeHandler))}_removeResizeListener(){clearTimeout(this._resizeTimeout),this._resizeTimeout=null,this._resizeHandler&&(EventHandler.off(window,EVENT_RESIZE,this._resizeHandler),this._resizeHandler=null)}_rebuildObserver(){if(this._observer){this._observer.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e)}}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(EventHandler.off(this._config.target,EVENT_CLICK$1),EventHandler.on(this._config.target,EVENT_CLICK$1,"[href]",e=>{const t=e.target.closest("[href]"),n=t&&this._sectionByLink.get(t);if(!n||!this._element)return;e.preventDefault();const s=this._rootElement||window,i=n.offsetTop-this._element.offsetTop,o=this._rootElement?this._rootElement.scrollTop:window.scrollY??window.pageYOffset;if(matchMedia("(prefers-reduced-motion: reduce)").matches||Math.abs(o-i)<=2)return s.scrollTo?s.scrollTo({top:i,behavior:"auto"}):s.scrollTop=i,void this._settleNavigation(t.hash,n);this._pendingNavigation={hash:t.hash,section:n},this._armSettle(),s.scrollTo?s.scrollTo({top:i,behavior:"smooth"}):s.scrollTop=i}))}_armSettle(){this._disarmSettle();const e=this._getSettleTarget();this._settleHandler=()=>this._onSettle(),this._scrollIdleHandler=()=>{clearTimeout(this._settleTimeout),this._settleTimeout=setTimeout(()=>this._onSettle(),100)},EventHandler.on(e,EVENT_SCROLLEND,this._settleHandler),EventHandler.on(e,EVENT_SCROLL,this._scrollIdleHandler)}_disarmSettle(){clearTimeout(this._settleTimeout),this._settleTimeout=null;const e=this._getSettleTarget();this._settleHandler&&(EventHandler.off(e,EVENT_SCROLLEND,this._settleHandler),this._settleHandler=null),this._scrollIdleHandler&&(EventHandler.off(e,EVENT_SCROLL,this._scrollIdleHandler),this._scrollIdleHandler=null)}_getSettleTarget(){return this._rootElement||document}_onSettle(){if(this._disarmSettle(),!this._pendingNavigation)return;const{hash:e,section:t}=this._pendingNavigation;this._settleNavigation(e,t)}_settleNavigation(e,t){this._pendingNavigation=null,window.history?.replaceState&&window.history.replaceState(null,"",e),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.focus({preventScroll:!0})}_initializeTargetsAndObservables(){this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map;const e=SelectorEngine.find("[href]",this._config.target),t=new Set;for(const n of e){if(!n.hash||isDisabled(n))continue;const e=decodeFragment(n.hash.slice(1));if(!e)continue;const s=document.getElementById(e);s&&this._element.contains(s)&&isVisible(s)&&(this._sectionByLink.set(n,s),this._linkBySection.set(s,n),t.has(s)||(t.add(s),this._sections.push(s)))}this._sections.sort((e,t)=>e.getBoundingClientRect().top-t.getBoundingClientRect().top)}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add("active"),this._activateParents(e),EventHandler.trigger(this._element,EVENT_ACTIVATE,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains("menu-item")){const t=e.closest(".menu")?.previousElementSibling;return void(t?.matches(SELECTOR_MENU_TOGGLE$1)&&t.classList.add("active"))}for(const t of SelectorEngine.parents(e,".nav, .list-group"))for(const e of SelectorEngine.prev(t,SELECTOR_LINK_ITEMS))e.classList.add("active")}_clearActiveClass(e){e.classList.remove("active");const t=SelectorEngine.find("[href].active",e);for(const e of t)e.classList.remove("active")}}function decodeFragment(e){try{return decodeURIComponent(e)}catch{return e}}EventHandler.on(window,EVENT_LOAD_DATA_API$1,()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_SPY))ScrollSpy.getOrCreateInstance(e)});const NAME$2="tab",DATA_KEY$2="bs.tab",EVENT_KEY$2=".bs.tab",EVENT_HIDE$1="hide.bs.tab",EVENT_HIDDEN$1="hidden.bs.tab",EVENT_SHOW$1="show.bs.tab",EVENT_SHOWN$1="shown.bs.tab",EVENT_CLICK_DATA_API="click.bs.tab",EVENT_KEYDOWN="keydown.bs.tab",EVENT_LOAD_DATA_API="load.bs.tab",ARROW_LEFT_KEY="ArrowLeft",ARROW_RIGHT_KEY="ArrowRight",ARROW_UP_KEY="ArrowUp",ARROW_DOWN_KEY="ArrowDown",HOME_KEY="Home",END_KEY="End",CLASS_NAME_ACTIVE="active",CLASS_NAME_FADE$1="fade",CLASS_NAME_SHOW$1="show",SELECTOR_MENU_TOGGLE='[data-bs-toggle="menu"]',SELECTOR_MENU=".menu",NOT_SELECTOR_MENU_TOGGLE=`:not(${SELECTOR_MENU_TOGGLE})`,SELECTOR_TAB_PANEL='.list-group, .nav, [role="tablist"]',SELECTOR_OUTER=".nav-item, .list-group-item",SELECTOR_INNER=`.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`,SELECTOR_DATA_TOGGLE$1='[data-bs-toggle="tab"]',SELECTOR_INNER_ELEM=`${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`,SELECTOR_DATA_TOGGLE_ACTIVE='.active[data-bs-toggle="tab"]';class Tab extends BaseComponent{constructor(e){super(e),this._parent=this._element.closest(SELECTOR_TAB_PANEL),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),EventHandler.on(this._element,EVENT_KEYDOWN,e=>this._keydown(e)))}static get NAME(){return"tab"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?EventHandler.trigger(t,EVENT_HIDE$1,{relatedTarget:e}):null;EventHandler.trigger(e,EVENT_SHOW$1,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add("active"),this._activate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleMenu(e,!0),EventHandler.trigger(e,EVENT_SHOWN$1,{relatedTarget:t})):e.classList.add("show")},e,e.classList.contains("fade")))}_deactivate(e,t){e&&(e.classList.remove("active"),e.blur(),this._deactivate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleMenu(e,!1),EventHandler.trigger(e,EVENT_HIDDEN$1,{relatedTarget:t})):e.classList.remove("show")},e,e.classList.contains("fade")))}_keydown(e){if(![ARROW_LEFT_KEY,ARROW_RIGHT_KEY,ARROW_UP_KEY,ARROW_DOWN_KEY,HOME_KEY,END_KEY].includes(e.key))return;if(e.altKey||e.ctrlKey||e.metaKey)return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter(e=>!isDisabled(e));let n;if([HOME_KEY,END_KEY].includes(e.key))n=e.key===HOME_KEY?t[0]:t.at(-1);else{const s=[ARROW_RIGHT_KEY,ARROW_DOWN_KEY].includes(e.key);n=getNextActiveElement(t,e.target,s,!0)}n&&(n.focus({preventScroll:!0}),Tab.getOrCreateInstance(n).show())}_getChildren(){return SelectorEngine.find(SELECTOR_INNER_ELEM,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=SelectorEngine.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleMenu(e,t){const n=this._getOuterElement(e),s=SelectorEngine.findOne(SELECTOR_MENU_TOGGLE,n);if(!s)return;const i=SelectorEngine.findOne(".menu",n);s.classList.toggle("active",t),i&&i.classList.toggle("show",t),s.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains("active")}_getInnerElement(e){return e.matches(SELECTOR_INNER_ELEM)?e:SelectorEngine.findOne(SELECTOR_INNER_ELEM,e)}_getOuterElement(e){return e.closest(SELECTOR_OUTER)||e}}EventHandler.on(document,"click.bs.tab",SELECTOR_DATA_TOGGLE$1,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this)||Tab.getOrCreateInstance(this).show()}),EventHandler.on(window,"load.bs.tab",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE))Tab.getOrCreateInstance(e)});const NAME$1="toast",DATA_KEY$1="bs.toast",EVENT_KEY$1=".bs.toast",EVENT_MOUSEOVER="mouseover.bs.toast",EVENT_MOUSEOUT="mouseout.bs.toast",EVENT_FOCUSIN="focusin.bs.toast",EVENT_FOCUSOUT="focusout.bs.toast",EVENT_HIDE="hide.bs.toast",EVENT_HIDDEN="hidden.bs.toast",EVENT_SHOW="show.bs.toast",EVENT_SHOWN="shown.bs.toast",CLASS_NAME_FADE="fade",CLASS_NAME_HIDE="hide",CLASS_NAME_SHOW="show",CLASS_NAME_SHOWING="showing",DefaultType$1={animation:"boolean",autohide:"boolean",delay:"number"},Default$1={animation:!0,autohide:!0,delay:5e3};class Toast extends BaseComponent{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Default$1}static get DefaultType(){return DefaultType$1}static get NAME(){return NAME$1}show(){EventHandler.trigger(this._element,EVENT_SHOW).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),reflow(this._element),this._element.classList.add("show","showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),EventHandler.trigger(this._element,EVENT_SHOWN),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(EventHandler.trigger(this._element,EVENT_HIDE).defaultPrevented||(this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.add("hide"),this._element.classList.remove("showing","show"),EventHandler.trigger(this._element,EVENT_HIDDEN)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove("show"),super.dispose()}isShown(){return this._element.classList.contains("show")}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":this._hasMouseInteraction=t;break;case"focusin":case"focusout":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){EventHandler.on(this._element,EVENT_MOUSEOVER,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_MOUSEOUT,e=>this._onInteraction(e,!1)),EventHandler.on(this._element,EVENT_FOCUSIN,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_FOCUSOUT,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}}enableDismissTrigger(Toast);const NAME="toggler",DATA_KEY="bs.toggler",EVENT_KEY=`.${DATA_KEY}`,EVENT_TOGGLE=`toggle${EVENT_KEY}`,EVENT_TOGGLED=`toggled${EVENT_KEY}`,EVENT_CLICK="click",SELECTOR_DATA_TOGGLE='[data-bs-toggle="toggler"]',DefaultType={attribute:"string",value:"(string|number|boolean)"},Default={attribute:"class",value:null};class Toggler extends BaseComponent{static get Default(){return Default}static get DefaultType(){return DefaultType}static get NAME(){return NAME}toggle(){EventHandler.trigger(this._element,EVENT_TOGGLE).defaultPrevented||(this._execute(),EventHandler.trigger(this._element,EVENT_TOGGLED))}_execute(){const{attribute:e,value:t}=this._config;"id"!==e&&("class"!==e?this._element.getAttribute(e)!==String(t)?this._element.setAttribute(e,t):this._element.removeAttribute(e):this._element.classList.toggle(t))}}eventActionOnPlugin(Toggler,"click",SELECTOR_DATA_TOGGLE,"toggle");export{Alert,Button,Carousel,Chips,Collapse,Combobox,Datepicker,Dialog,Drawer,Menu,NavOverflow,OtpInput,Popover,Range,ScrollSpy,Strength,Tab,Toast,Toggler,Tooltip}; diff --git a/assets/javascripts/bootstrap/alert.js b/assets/javascripts/bootstrap/alert.js index 4e3d7db8..7a31f2d6 100644 --- a/assets/javascripts/bootstrap/alert.js +++ b/assets/javascripts/bootstrap/alert.js @@ -1,89 +1,65 @@ /*! - * Bootstrap alert.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap alert.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Alert = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index)); -})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { enableDismissTrigger } from './util/component-functions.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'alert'; - const DATA_KEY = 'bs.alert'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_CLOSE = `close${EVENT_KEY}`; - const EVENT_CLOSED = `closed${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; +const NAME = 'alert'; +const DATA_KEY = 'bs.alert'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_CLOSE = `close${EVENT_KEY}`; +const EVENT_CLOSED = `closed${EVENT_KEY}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_SHOW = 'show'; - /** - * Class definition - */ +/** + * Class definition + */ - class Alert extends BaseComponent { - // Getters - static get NAME() { - return NAME; - } - - // Public - close() { - const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); - if (closeEvent.defaultPrevented) { - return; - } - this._element.classList.remove(CLASS_NAME_SHOW); - const isAnimated = this._element.classList.contains(CLASS_NAME_FADE); - this._queueCallback(() => this._destroyElement(), this._element, isAnimated); - } - - // Private - _destroyElement() { - this._element.remove(); - EventHandler.trigger(this._element, EVENT_CLOSED); - this.dispose(); - } +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME; + } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Alert.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; } + this._element.classList.remove(CLASS_NAME_SHOW); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); } - /** - * Data API implementation - */ - - componentFunctions_js.enableDismissTrigger(Alert, 'close'); - - /** - * jQuery - */ + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} - index_js.defineJQueryPlugin(Alert); +/** + * Data API implementation + */ - return Alert; +enableDismissTrigger(Alert, 'close'); -})); +export { Alert as default }; diff --git a/assets/javascripts/bootstrap/base-component.js b/assets/javascripts/bootstrap/base-component.js index 991b190d..98a1c98b 100644 --- a/assets/javascripts/bootstrap/base-component.js +++ b/assets/javascripts/bootstrap/base-component.js @@ -1,85 +1,95 @@ /*! - * Bootstrap base-component.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap base-component.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/data.js'), require('./dom/event-handler.js'), require('./util/config.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./dom/data', './dom/event-handler', './util/config', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BaseComponent = factory(global.Data, global.EventHandler, global.Config, global.Index)); -})(this, (function (Data, EventHandler, Config, index_js) { 'use strict'; +import Data from './dom/data.js'; +import EventHandler from './dom/event-handler.js'; +import Config from './util/config.js'; +import { getElement, executeAfterTransition } from './util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap base-component.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const VERSION = '5.3.8'; +const VERSION = '6.0.0-alpha1'; - /** - * Class definition - */ +/** + * Class definition + */ - class BaseComponent extends Config { - constructor(element, config) { - super(); - element = index_js.getElement(element); - if (!element) { - return; - } - this._element = element; - this._config = this._getConfig(config); - Data.set(this._element, this.constructor.DATA_KEY, this); +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; } + this._element = element; + this._config = this._getConfig(config); - // Public - dispose() { - Data.remove(this._element, this.constructor.DATA_KEY); - EventHandler.off(this._element, this.constructor.EVENT_KEY); - for (const propertyName of Object.getOwnPropertyNames(this)) { - this[propertyName] = null; - } + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); } + Data.set(this._element, this.constructor.DATA_KEY, this); + } - // Private - _queueCallback(callback, element, isAnimated = true) { - index_js.executeAfterTransition(callback, element, isAnimated); - } - _getConfig(config) { - config = this._mergeConfigObj(config, this._element); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; } + } - // Static - static getInstance(element) { - return Data.get(index_js.getElement(element), this.DATA_KEY); - } - static getOrCreateInstance(element, config = {}) { - return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); - } - static get VERSION() { - return VERSION; - } - static get DATA_KEY() { - return `bs.${this.NAME}`; - } - static get EVENT_KEY() { - return `.${this.DATA_KEY}`; - } - static eventName(name) { - return `${name}${this.EVENT_KEY}`; - } + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; } - return BaseComponent; + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} -})); +export { BaseComponent as default }; diff --git a/assets/javascripts/bootstrap/button.js b/assets/javascripts/bootstrap/button.js index 8514e9b0..69288ab1 100644 --- a/assets/javascripts/bootstrap/button.js +++ b/assets/javascripts/bootstrap/button.js @@ -1,78 +1,57 @@ /*! - * Bootstrap button.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap button.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Button = factory(global.BaseComponent, global.EventHandler, global.Index)); -})(this, (function (BaseComponent, EventHandler, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'button'; - const DATA_KEY = 'bs.button'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const CLASS_NAME_ACTIVE = 'active'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]'; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - - /** - * Class definition - */ - - class Button extends BaseComponent { - // Getters - static get NAME() { - return NAME; - } - - // Public - toggle() { - // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method - this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE)); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Button.getOrCreateInstance(this); - if (config === 'toggle') { - data[config](); - } - }); - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'button'; +const DATA_KEY = 'bs.button'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const CLASS_NAME_ACTIVE = 'active'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME; } - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { - event.preventDefault(); - const button = event.target.closest(SELECTOR_DATA_TOGGLE); - const data = Button.getOrCreateInstance(button); - data.toggle(); - }); - - /** - * jQuery - */ + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE)); + } +} - index_js.defineJQueryPlugin(Button); +/** + * Data API implementation + */ - return Button; +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); -})); +export { Button as default }; diff --git a/assets/javascripts/bootstrap/carousel.js b/assets/javascripts/bootstrap/carousel.js index 9263da81..61196040 100644 --- a/assets/javascripts/bootstrap/carousel.js +++ b/assets/javascripts/bootstrap/carousel.js @@ -1,387 +1,804 @@ /*! - * Bootstrap carousel.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap carousel.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js'), require('./util/swipe.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index', './util/swipe'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Carousel = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index, global.Swipe)); -})(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js, Swipe) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'carousel'; - const DATA_KEY = 'bs.carousel'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - const ORDER_NEXT = 'next'; - const ORDER_PREV = 'prev'; - const DIRECTION_LEFT = 'left'; - const DIRECTION_RIGHT = 'right'; - const EVENT_SLIDE = `slide${EVENT_KEY}`; - const EVENT_SLID = `slid${EVENT_KEY}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; - const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`; - const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`; - const EVENT_DRAG_START = `dragstart${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_CAROUSEL = 'carousel'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_SLIDE = 'slide'; - const CLASS_NAME_END = 'carousel-item-end'; - const CLASS_NAME_START = 'carousel-item-start'; - const CLASS_NAME_NEXT = 'carousel-item-next'; - const CLASS_NAME_PREV = 'carousel-item-prev'; - const SELECTOR_ACTIVE = '.active'; - const SELECTOR_ITEM = '.carousel-item'; - const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; - const SELECTOR_ITEM_IMG = '.carousel-item img'; - const SELECTOR_INDICATORS = '.carousel-indicators'; - const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; - const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'; - const KEY_TO_DIRECTION = { - [ARROW_LEFT_KEY]: DIRECTION_RIGHT, - [ARROW_RIGHT_KEY]: DIRECTION_LEFT - }; - const Default = { - interval: 5000, - keyboard: true, - pause: 'hover', - ride: false, - touch: true, - wrap: true - }; - const DefaultType = { - interval: '(number|boolean)', - // TODO:v6 remove boolean support - keyboard: 'boolean', - pause: '(string|boolean)', - ride: '(boolean|string)', - touch: 'boolean', - wrap: 'boolean' - }; - - /** - * Class definition - */ - - class Carousel extends BaseComponent { - constructor(element, config) { - super(element, config); - this._interval = null; - this._activeElement = null; - this._isSliding = false; - this.touchTimeout = null; - this._swipeHelper = null; - this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); - this._addEventListeners(); - if (this._config.ride === CLASS_NAME_CAROUSEL) { - this.cycle(); +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isVisible, isRTL } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'carousel'; +const DATA_KEY = 'bs.carousel'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY}`; +const EVENT_SLID = `slid${EVENT_KEY}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; +const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`; +const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); + } + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); + } + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); + + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; + } + if (rawIndex < 0) { + this._loopTransition(false); + return; } } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + if (this._isFade()) { + this._fadeTo(targetIndex); + return; + } - // Getters - static get Default() { - return Default; + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); + } + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); } - static get DefaultType() { - return DefaultType; + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); } - static get NAME() { - return NAME; + + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); } + this._viewport.style.scrollSnapType = ''; - // Public - next() { - this._slide(ORDER_NEXT); + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY); + super.dispose(); + } + + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default.ends; + } + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle()); } - nextWhenVisible() { - // FIXME TODO use `document.visibilityState` - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && index_js.isVisible(this._element)) { + + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { this.next(); } } - prev() { - this._slide(ORDER_PREV); + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; } - pause() { - if (this._isSliding) { - index_js.triggerTransitionEnd(this._element); - } - this._clearInterval(); + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); } - cycle() { - this._clearInterval(); - this._updateInterval(); - this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval); + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; } - _maybeEnableCycle() { - if (!this._config.ride) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.cycle()); - return; - } - this.cycle(); + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); } - to(index) { - const items = this._getItems(); - if (index > items.length - 1 || index < 0) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.to(index)); - return; - } - const activeIndex = this._getItemIndex(this._getActive()); - if (activeIndex === index) { - return; + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; } - const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV; - this._slide(order, items[index]); } - dispose() { - if (this._swipeHelper) { - this._swipeHelper.dispose(); + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; + } + + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); } - super.dispose(); + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } + + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; - // Private - _configAfterMerge(config) { - config.defaultInterval = config.interval; - return config; + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; } - _addEventListeners() { - if (this._config.keyboard) { - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; } - if (this._config.pause === 'hover') { - EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause()); - EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle()); - } - if (this._config.touch && Swipe.isSupported()) { - this._addTouchEventListeners(); + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; } + + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); } - _addTouchEventListeners() { - for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { - EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()); - } - const endCallBack = () => { - if (this._config.pause !== 'hover') { - return; - } - - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - this.pause(); - if (this.touchTimeout) { - clearTimeout(this.touchTimeout); - } - this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval); - }; - const swipeConfig = { - leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)), - rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)), - endCallback: endCallBack - }; - this._swipeHelper = new Swipe(this._element, swipeConfig); - } - _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - const direction = KEY_TO_DIRECTION[event.key]; - if (direction) { - event.preventDefault(); - this._slide(this._directionToOrder(direction)); - } + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; } - _getItemIndex(element) { - return this._getItems().indexOf(element); + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); } - _setActiveIndicatorElement(index) { - if (!this._indicatorsElement) { - return; - } - const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); - activeIndicator.classList.remove(CLASS_NAME_ACTIVE); - activeIndicator.removeAttribute('aria-current'); - const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); - if (newActiveIndicator) { - newActiveIndicator.classList.add(CLASS_NAME_ACTIVE); - newActiveIndicator.setAttribute('aria-current', 'true'); - } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); } - _updateInterval() { - const element = this._activeElement || this._getActive(); - if (!element) { - return; - } - const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10); - this._config.interval = elementInterval || this._config.defaultInterval; + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } - _slide(order, element = null) { - if (this._isSliding) { - return; - } - const activeElement = this._getActive(); - const isNext = order === ORDER_NEXT; - const nextElement = element || index_js.getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap); - if (nextElement === activeElement) { - return; - } - const nextElementIndex = this._getItemIndex(nextElement); - const triggerEvent = eventName => { - return EventHandler.trigger(this._element, eventName, { - relatedTarget: nextElement, - direction: this._orderToDirection(order), - from: this._getItemIndex(activeElement), - to: nextElementIndex + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } + + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; + } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE, index === this._activeIndex); + } + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; + } + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true }); - }; - const slideEvent = triggerEvent(EVENT_SLIDE); - if (slideEvent.defaultPrevented) { - return; - } - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - // TODO: change tests that use empty divs to avoid this check - return; - } - const isCycling = Boolean(this._interval); - this.pause(); - this._isSliding = true; - this._setActiveIndicatorElement(nextElementIndex); - this._activeElement = nextElement; - const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END; - const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV; - nextElement.classList.add(orderClassName); - index_js.reflow(nextElement); - activeElement.classList.add(directionalClassName); - nextElement.classList.add(directionalClassName); - const completeCallBack = () => { - nextElement.classList.remove(directionalClassName, orderClassName); - nextElement.classList.add(CLASS_NAME_ACTIVE); - activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName); - this._isSliding = false; - triggerEvent(EVENT_SLID); - }; - this._queueCallback(completeCallBack, activeElement, this._isAnimated()); - if (isCycling) { - this.cycle(); } + control.disabled = disabled; } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_SLIDE); + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; } - _getActive() { - return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE); + active.removeAttribute('aria-current'); } - _getItems() { - return SelectorEngine.find(SELECTOR_ITEM, this._element); + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE); + newActive.setAttribute('aria-current', 'true'); } - _clearInterval() { - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; } - _directionToOrder(direction) { - if (index_js.isRTL()) { - return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT; - } - return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV; + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; } - _orderToDirection(order) { - if (index_js.isRTL()) { - return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT; - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Carousel.getOrCreateInstance(this, config); - if (typeof config === 'number') { - data.to(config); - return; - } - if (typeof config === 'string') { - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; } + return index; } - /** - * Data API implementation - */ + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } + + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; + } + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); + return; + } + this._scheduleAutoplay(upcoming); + }, interval); + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { return; } - event.preventDefault(); - const carousel = Carousel.getOrCreateInstance(target); - const slideIndex = this.getAttribute('data-bs-slide-to'); - if (slideIndex) { - carousel.to(slideIndex); - carousel._maybeEnableCycle(); + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); return; } - if (Manipulator.getDataAttribute(this, 'slide') === 'next') { - carousel.next(); - carousel._maybeEnableCycle(); + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { return; } - carousel.prev(); - carousel._maybeEnableCycle(); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE); - for (const carousel of carousels) { - Carousel.getOrCreateInstance(carousel); + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); + } + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; } - }); + } +} - /** - * jQuery - */ +/** + * Data API implementation + */ - index_js.defineJQueryPlugin(Carousel); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); - return Carousel; + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); -})); +export { Carousel as default }; diff --git a/assets/javascripts/bootstrap/chips.js b/assets/javascripts/bootstrap/chips.js new file mode 100644 index 00000000..26727f5a --- /dev/null +++ b/assets/javascripts/bootstrap/chips.js @@ -0,0 +1,584 @@ +/*! + * Bootstrap chips.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'chips'; +const DATA_KEY = 'bs.chips'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY}`; +const EVENT_REMOVE = `remove${EVENT_KEY}`; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SELECT = `select${EVENT_KEY}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; + } + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; + } + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; + } + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; + } + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); + } + if (!chip || !value) { + return false; + } + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; + } + + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; + } + + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); + } + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE); + this._anchorChip = chip; + } + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + focus() { + this._input?.focus(); + } + + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } + } + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); + + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); + } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); + + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; + } + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); + } + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); + } + + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { + return; + } + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey + }); + chip.focus(); + } + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); + } + }); + + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; + } + + // No default + } + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; + } + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; + } + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; + } + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; + } + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; + } + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default + } + } + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; + } + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { + return; + } + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; + } + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE); + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); + } + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; + } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); + } + } + } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +export { Chips as default }; diff --git a/assets/javascripts/bootstrap/collapse.js b/assets/javascripts/bootstrap/collapse.js index 7465cf02..933c36c3 100644 --- a/assets/javascripts/bootstrap/collapse.js +++ b/assets/javascripts/bootstrap/collapse.js @@ -1,248 +1,222 @@ /*! - * Bootstrap collapse.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap collapse.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Collapse = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'collapse'; - const DATA_KEY = 'bs.collapse'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_COLLAPSE = 'collapse'; - const CLASS_NAME_COLLAPSING = 'collapsing'; - const CLASS_NAME_COLLAPSED = 'collapsed'; - const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; - const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; - const WIDTH = 'width'; - const HEIGHT = 'height'; - const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'; - const Default = { - parent: null, - toggle: true - }; - const DefaultType = { - parent: '(null|element)', - toggle: 'boolean' - }; - - /** - * Class definition - */ - - class Collapse extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isTransitioning = false; - this._triggerArray = []; - const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE); - for (const elem of toggleList) { - const selector = SelectorEngine.getSelectorFromElement(elem); - const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); - if (selector !== null && filterElement.length) { - this._triggerArray.push(elem); - } - } - this._initializeChildren(); - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); - } - if (this._config.toggle) { - this.toggle(); - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { reflow, getElement } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'collapse'; +const DATA_KEY = 'bs.collapse'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'; +const Default = { + parent: null, + toggle: true +}; +const DefaultType = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); } + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Public - toggle() { - if (this._isShown()) { - this.hide(); - } else { - this.show(); - } + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); } - show() { - if (this._isTransitioning || this._isShown()) { - return; - } - let activeChildren = []; - - // find active children - if (this._config.parent) { - activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { - toggle: false - })); - } - if (activeChildren.length && activeChildren[0]._isTransitioning) { - return; - } - const startEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (startEvent.defaultPrevented) { - return; - } - for (const activeInstance of activeChildren) { - activeInstance.hide(); - } - const dimension = this._getDimension(); - this._element.classList.remove(CLASS_NAME_COLLAPSE); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.style[dimension] = 0; - this._addAriaAndCollapsedClass(this._triggerArray, true); - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); - this._element.style[dimension] = ''; - EventHandler.trigger(this._element, EVENT_SHOWN); - }; - const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - const scrollSize = `scroll${capitalizedDimension}`; - this._queueCallback(complete, this._element, true); - this._element.style[dimension] = `${this._element[scrollSize]}px`; - } - hide() { - if (this._isTransitioning || !this._isShown()) { - return; - } - const startEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (startEvent.defaultPrevented) { - return; - } - const dimension = this._getDimension(); - this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); - for (const trigger of this._triggerArray) { - const element = SelectorEngine.getElementFromSelector(trigger); - if (element && !this._isShown(element)) { - this._addAriaAndCollapsedClass([trigger], false); - } - } - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.style[dimension] = ''; - this._queueCallback(complete, this._element, true); + } + show() { + if (this._isTransitioning || this._isShown()) { + return; } + let activeChildren = []; - // Private - _isShown(element = this._element) { - return element.classList.contains(CLASS_NAME_SHOW); + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); } - _configAfterMerge(config) { - config.toggle = Boolean(config.toggle); // Coerce string values - config.parent = index_js.getElement(config.parent); - return config; + if (activeChildren.length && activeChildren[0]._isTransitioning) { + return; } - _getDimension() { - return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (startEvent.defaultPrevented) { + return; } - _initializeChildren() { - if (!this._config.parent) { - return; - } - const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE); - for (const element of children) { - const selected = SelectorEngine.getElementFromSelector(element); - if (selected) { - this._addAriaAndCollapsedClass([element], this._isShown(selected)); - } - } - } - _getFirstLevelChildren(selector) { - const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); - // remove children if greater depth - return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); - } - _addAriaAndCollapsedClass(triggerArray, isOpen) { - if (!triggerArray.length) { - return; - } - for (const element of triggerArray) { - element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); - element.setAttribute('aria-expanded', isOpen); - } + for (const activeInstance of activeChildren) { + activeInstance.hide(); } + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); + } + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } - // Static - static jQueryInterface(config) { - const _config = {}; - if (typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false; + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); } - return this.each(function () { - const data = Collapse.getOrCreateInstance(this, _config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); } } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { - event.preventDefault(); + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; } - for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { - Collapse.getOrCreateInstance(element, { - toggle: false - }).toggle(); + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); } - }); - - /** - * jQuery - */ + } +} - index_js.defineJQueryPlugin(Collapse); +/** + * Data API implementation + */ - return Collapse; +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); -})); +export { Collapse as default }; diff --git a/assets/javascripts/bootstrap/combobox.js b/assets/javascripts/bootstrap/combobox.js new file mode 100644 index 00000000..542e30ab --- /dev/null +++ b/assets/javascripts/bootstrap/combobox.js @@ -0,0 +1,397 @@ +/*! + * Bootstrap combobox.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import Menu from './menu.js'; +import { isDisabled, isVisible, getNextActiveElement } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap combobox.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'combobox'; +const DATA_KEY = 'bs.combobox'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ESCAPE_KEY = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SELECTED = 'selected'; +const CLASS_NAME_PLACEHOLDER = 'combobox-placeholder'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="combobox"]'; +const SELECTOR_MENU = '.menu'; +const SELECTOR_MENU_ITEM = '.menu-item[data-bs-value]'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item[data-bs-value]:not(.disabled):not(:disabled)'; +const SELECTOR_VALUE = '.combobox-value'; +const SELECTOR_SEARCH_INPUT = '.combobox-search-input'; +const SELECTOR_NO_RESULTS = '.combobox-no-results'; +const Default = { + boundary: 'clippingParents', + multiple: false, + name: null, + offset: [0, 2], + placeholder: '', + placement: 'bottom-start', + search: false, + searchNormalize: false +}; +const DefaultType = { + boundary: '(string|element)', + multiple: 'boolean', + name: '(string|null)', + offset: '(array|string|function)', + placeholder: 'string', + placement: 'string', + search: 'boolean', + searchNormalize: 'boolean' +}; + +/** + * Class definition + */ + +class Combobox extends BaseComponent { + constructor(element, config) { + super(element, config); + this._toggle = this._element; + this._menu = SelectorEngine.next(this._toggle, SELECTOR_MENU)[0]; + this._valueDisplay = SelectorEngine.findOne(SELECTOR_VALUE, this._toggle); + this._searchInput = SelectorEngine.findOne(SELECTOR_SEARCH_INPUT, this._menu); + this._noResults = SelectorEngine.findOne(SELECTOR_NO_RESULTS, this._menu); + this._hiddenInput = null; + this._menuInstance = null; + this._createHiddenInput(); + this._createMenuInstance(); + this._syncInitialSelection(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._toggle) || this._isShown()) { + return; + } + const showEvent = EventHandler.trigger(this._toggle, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._menuInstance.show(); + if (this._searchInput) { + this._searchInput.value = ''; + this._filterItems(''); + requestAnimationFrame(() => this._searchInput.focus()); + } + EventHandler.trigger(this._toggle, EVENT_SHOWN); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._toggle, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + this._menuInstance.hide(); + EventHandler.trigger(this._toggle, EVENT_HIDDEN); + } + dispose() { + if (this._menuInstance) { + this._menuInstance.dispose(); + this._menuInstance = null; + } + if (this._hiddenInput) { + this._hiddenInput.remove(); + this._hiddenInput = null; + } + EventHandler.off(this._menu, EVENT_KEY); + EventHandler.off(this._toggle, EVENT_KEY); + super.dispose(); + } + + // Private + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW); + } + _createHiddenInput() { + const { + name + } = this._config; + if (!name) { + return; + } + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = name; + this._hiddenInput.value = ''; + this._toggle.parentNode.insertBefore(this._hiddenInput, this._toggle); + } + _createMenuInstance() { + this._menuInstance = new Menu(this._toggle, { + menu: this._menu, + autoClose: this._config.multiple ? 'outside' : true, + boundary: this._config.boundary, + offset: this._config.offset, + placement: this._config.placement + }); + } + _syncInitialSelection() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length > 0) { + this._updateToggleText(); + this._updateHiddenInput(); + } else { + this._showPlaceholder(); + } + } + _addEventListeners() { + EventHandler.on(this._menu, 'click', SELECTOR_MENU_ITEM, event => { + const item = event.target.closest(SELECTOR_MENU_ITEM); + if (!item || isDisabled(item)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this._selectItem(item); + }); + EventHandler.on(this._toggle, 'keydown', event => { + this._handleToggleKeydown(event); + }); + EventHandler.on(this._menu, 'keydown', event => { + this._handleMenuKeydown(event); + }); + if (this._searchInput) { + EventHandler.on(this._searchInput, 'input', () => { + this._filterItems(this._searchInput.value); + }); + EventHandler.on(this._searchInput, 'keydown', event => { + if (event.key === ARROW_DOWN_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + items[0].focus(); + } + } + if (event.key === ESCAPE_KEY) { + this.hide(); + this._toggle.focus(); + } + }); + } + } + _selectItem(item) { + if (this._config.multiple) { + item.classList.toggle(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', item.classList.contains(CLASS_NAME_SELECTED)); + } else { + const previouslySelected = SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + for (const prev of previouslySelected) { + prev.classList.remove(CLASS_NAME_SELECTED); + prev.setAttribute('aria-selected', 'false'); + } + item.classList.add(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', 'true'); + } + this._updateToggleText(); + this._updateHiddenInput(); + const value = this._config.multiple ? this._getSelectedItems().map(el => el.dataset.bsValue) : item.dataset.bsValue; + EventHandler.trigger(this._toggle, EVENT_CHANGE, { + value, + item + }); + if (!this._config.multiple) { + this.hide(); + this._toggle.focus(); + } + } + _updateToggleText() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length === 0) { + this._showPlaceholder(); + return; + } + this._valueDisplay.classList.remove(CLASS_NAME_PLACEHOLDER); + if (this._config.multiple && selectedItems.length > 1) { + this._valueDisplay.textContent = `${selectedItems.length} selected`; + } else { + const item = selectedItems[0]; + const label = SelectorEngine.findOne('.menu-item-content > span:first-child', item); + this._valueDisplay.textContent = label ? label.textContent : item.textContent.trim(); + } + } + _showPlaceholder() { + const { + placeholder + } = this._config; + if (placeholder) { + this._valueDisplay.textContent = placeholder; + this._valueDisplay.classList.add(CLASS_NAME_PLACEHOLDER); + } + } + _updateHiddenInput() { + if (!this._hiddenInput) { + return; + } + const selectedItems = this._getSelectedItems(); + const values = selectedItems.map(el => el.dataset.bsValue); + this._hiddenInput.value = this._config.multiple ? values.join(',') : values[0] || ''; + } + _getSelectedItems() { + return SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + } + _getVisibleItems() { + return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(item => isVisible(item)); + } + _filterItems(query) { + const normalizedQuery = this._normalizeText(query.toLowerCase().trim()); + const items = SelectorEngine.find(SELECTOR_MENU_ITEM, this._menu); + let visibleCount = 0; + for (const item of items) { + const text = this._normalizeText(item.textContent.toLowerCase().trim()); + const matches = !normalizedQuery || text.includes(normalizedQuery); + item.style.display = matches ? '' : 'none'; + if (matches) { + visibleCount++; + } + } + if (this._noResults) { + this._noResults.classList.toggle('d-none', visibleCount > 0); + } + } + _normalizeText(text) { + if (this._config.searchNormalize) { + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + } + return text; + } + _handleToggleKeydown(event) { + const { + key + } = event; + if (key === ARROW_DOWN_KEY || key === ARROW_UP_KEY) { + event.preventDefault(); + if (!this._isShown()) { + this.show(); + } + const items = this._getVisibleItems(); + if (items.length > 0) { + const target = key === ARROW_DOWN_KEY ? items[0] : items.at(-1); + target.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !this._isShown()) { + event.preventDefault(); + this.show(); + } + } + _handleMenuKeydown(event) { + const { + key, + target + } = event; + if (key === ESCAPE_KEY) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + this._toggle.focus(); + return; + } + if (key === TAB_KEY) { + this.hide(); + return; + } + const isInput = target.matches('input'); + if (key === ARROW_DOWN_KEY || key === ARROW_UP_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); + } + return; + } + if (key === HOME_KEY || key === END_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const targetItem = key === HOME_KEY ? items[0] : items.at(-1); + targetItem.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !isInput) { + event.preventDefault(); + const item = target.closest(SELECTOR_MENU_ITEM); + if (item && !isDisabled(item)) { + this._selectItem(item); + } + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Combobox.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + event.preventDefault(); + Combobox.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const toggle of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) { + Combobox.getOrCreateInstance(toggle); + } +}); + +export { Combobox as default }; diff --git a/assets/javascripts/bootstrap/datepicker.js b/assets/javascripts/bootstrap/datepicker.js new file mode 100644 index 00000000..3f93c4da --- /dev/null +++ b/assets/javascripts/bootstrap/datepicker.js @@ -0,0 +1,439 @@ +/*! + * Bootstrap datepicker.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { Calendar } from 'vanilla-calendar-pro'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { isDisabled } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'datepicker'; +const DATA_KEY = 'bs.datepicker'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; + } + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; + } + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); + } + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + EventHandler.trigger(this._element, EVENT_CHANGE, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; + + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); + } + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); + } + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; + } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +export { Datepicker as default }; diff --git a/assets/javascripts/bootstrap/dialog-base.js b/assets/javascripts/bootstrap/dialog-base.js new file mode 100644 index 00000000..9381df05 --- /dev/null +++ b/assets/javascripts/bootstrap/dialog-base.js @@ -0,0 +1,277 @@ +/*! + * Bootstrap dialog-base.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import Data from './dom/data.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { + return; + } + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; + } + this.hide(); + }); + } +} + +export { DialogBase as default }; diff --git a/assets/javascripts/bootstrap/dialog.js b/assets/javascripts/bootstrap/dialog.js new file mode 100644 index 00000000..1c5cccd1 --- /dev/null +++ b/assets/javascripts/bootstrap/dialog.js @@ -0,0 +1,168 @@ +/*! + * Bootstrap dialog.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import DialogBase from './dialog-base.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { isVisible } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'dialog'; +const DATA_KEY = 'bs.dialog'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CANCEL = `cancel${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dialog"]'; +const Default = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); + } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); + }); + currentInstance.hide(); + } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +export { Dialog as default }; diff --git a/assets/javascripts/bootstrap/dom/data.js b/assets/javascripts/bootstrap/dom/data.js index 3ab4bdbd..9955e319 100644 --- a/assets/javascripts/bootstrap/dom/data.js +++ b/assets/javascripts/bootstrap/dom/data.js @@ -1,62 +1,60 @@ /*! - * Bootstrap data.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap data.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Data = factory()); -})(this, (function () { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/data.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Constants + */ - /** - * Constants - */ - - const elementMap = new Map(); - const data = { - set(element, key, instance) { - if (!elementMap.has(element)) { - elementMap.set(element, new Map()); - } - const instanceMap = elementMap.get(element); - - // make it clear we only want one instance per element - // can be removed later when multiple key/instances are fine to be used - if (!instanceMap.has(key) && instanceMap.size !== 0) { - // eslint-disable-next-line no-console - console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); - return; - } - instanceMap.set(key, instance); - }, - get(element, key) { - if (elementMap.has(element)) { - return elementMap.get(element).get(key) || null; - } - return null; - }, - remove(element, key) { - if (!elementMap.has(element)) { - return; - } - const instanceMap = elementMap.get(element); - instanceMap.delete(key); +const elementMap = new Map(); +const data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); - // free up element references if there are no instances left for an element - if (instanceMap.size === 0) { - elementMap.delete(element); - } + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; + } + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; } - }; + return null; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; + } + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; + } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); - return data; + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); + } + } +}; -})); +export { data as default }; diff --git a/assets/javascripts/bootstrap/dom/event-handler.js b/assets/javascripts/bootstrap/dom/event-handler.js index 8d84431b..131ce8e0 100644 --- a/assets/javascripts/bootstrap/dom/event-handler.js +++ b/assets/javascripts/bootstrap/dom/event-handler.js @@ -1,236 +1,204 @@ /*! - * Bootstrap event-handler.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap event-handler.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) : - typeof define === 'function' && define.amd ? define(['../util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.EventHandler = factory(global.Index)); -})(this, (function (index_js) { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/event-handler.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Constants + */ +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll', 'scrollend']); - /** - * Constants - */ +/** + * Private methods + */ - const namespaceRegex = /[^.]*(?=\..*)\.|.*/; - const stripNameRegex = /\..*/; - const stripUidRegex = /::\d+$/; - const eventRegistry = {}; // Events storage - let uidEvent = 1; - const customEvents = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + return fn.apply(element, [event]); }; - const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); - - /** - * Private methods - */ - - function makeEventUid(element, uid) { - return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; - } - function getElementEvents(element) { - const uid = makeEventUid(element); - element.uidEvent = uid; - eventRegistry[uid] = eventRegistry[uid] || {}; - return eventRegistry[uid]; - } - function bootstrapHandler(element, fn) { - return function handler(event) { - hydrateObj(event, { - delegateTarget: element - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, fn); +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); } - return fn.apply(element, [event]); - }; + } + }; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; } - function bootstrapDelegationHandler(element, selector, fn) { - return function handler(event) { - const domElements = element.querySelectorAll(selector); - for (let { - target - } = event; target && target !== this; target = target.parentNode) { - for (const domElement of domElements) { - if (domElement !== target) { - continue; - } - hydrateObj(event, { - delegateTarget: target - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, selector, fn); - } - return fn.apply(target, [event]); + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); } - } + }; }; + callable = wrapFunction(callable); } - function findHandler(events, callable, delegationSelector = null) { - return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; } - function normalizeParameters(originalTypeEvent, handler, delegationFunction) { - const isDelegated = typeof handler === 'string'; - // TODO: tooltip passes `false` instead of selector, so we need to check - const callable = isDelegated ? delegationFunction : handler || delegationFunction; - let typeEvent = getTypeEvent(originalTypeEvent); - if (!nativeEvents.has(typeEvent)) { - typeEvent = originalTypeEvent; + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; + } + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } - return [isDelegated, callable, typeEvent]; } - function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { if (typeof originalTypeEvent !== 'string' || !element) { return; } - let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - - // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position - // this prevents the handler from being dispatched the same way as mouseover or mouseout does - if (originalTypeEvent in customEvents) { - const wrapFunction = fn => { - return function (event) { - if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { - return fn.call(this, event); - } - }; - }; - callable = wrapFunction(callable); - } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; const events = getElementEvents(element); - const handlers = events[typeEvent] || (events[typeEvent] = {}); - const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); - if (previousFunction) { - previousFunction.oneOff = previousFunction.oneOff && oneOff; + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); return; } - const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); - const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); - fn.delegationSelector = isDelegated ? handler : null; - fn.callable = callable; - fn.oneOff = oneOff; - fn.uidEvent = uid; - handlers[uid] = fn; - element.addEventListener(typeEvent, fn, isDelegated); - } - function removeHandler(element, events, typeEvent, handler, delegationSelector) { - const fn = findHandler(events[typeEvent], handler, delegationSelector); - if (!fn) { - return; + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } } - element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); - delete events[typeEvent][fn.uidEvent]; - } - function removeNamespacedHandlers(element, events, typeEvent, namespace) { - const storeElementEvent = events[typeEvent] || {}; - for (const [handlerKey, event] of Object.entries(storeElementEvent)) { - if (handlerKey.includes(namespace)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; } - function getTypeEvent(event) { - // allow to get the native events from namespaced events ('click.bs.button' --> 'click') - event = event.replace(stripNameRegex, ''); - return customEvents[event] || event; - } - const EventHandler = { - on(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, false); - }, - one(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, true); - }, - off(element, originalTypeEvent, handler, delegationFunction) { - if (typeof originalTypeEvent !== 'string' || !element) { - return; - } - const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - const inNamespace = typeEvent !== originalTypeEvent; - const events = getElementEvents(element); - const storeElementEvent = events[typeEvent] || {}; - const isNamespace = originalTypeEvent.startsWith('.'); - if (typeof callable !== 'undefined') { - // Simplest case: handler is passed, remove that listener ONLY. - if (!Object.keys(storeElementEvent).length) { - return; - } - removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); - return; - } - if (isNamespace) { - for (const elementEvent of Object.keys(events)) { - removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); - } - } - for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { - const handlerKey = keyHandlers.replace(stripUidRegex, ''); - if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; } - } - }, - trigger(element, event, args) { - if (typeof event !== 'string' || !element) { - return null; - } - const $ = index_js.getjQuery(); - const typeEvent = getTypeEvent(event); - const inNamespace = event !== typeEvent; - let jQueryEvent = null; - let bubbles = true; - let nativeDispatch = true; - let defaultPrevented = false; - if (inNamespace && $) { - jQueryEvent = $.Event(event, args); - $(element).trigger(jQueryEvent); - bubbles = !jQueryEvent.isPropagationStopped(); - nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); - defaultPrevented = jQueryEvent.isDefaultPrevented(); - } - const evt = hydrateObj(new Event(event, { - bubbles, - cancelable: true - }), args); - if (defaultPrevented) { - evt.preventDefault(); - } - if (nativeDispatch) { - element.dispatchEvent(evt); - } - if (evt.defaultPrevented && jQueryEvent) { - jQueryEvent.preventDefault(); - } - return evt; - } - }; - function hydrateObj(obj, meta = {}) { - for (const [key, value] of Object.entries(meta)) { - try { - obj[key] = value; - } catch (_unused) { - Object.defineProperty(obj, key, { - configurable: true, - get() { - return value; - } - }); - } + }); } - return obj; } + return obj; +} - return EventHandler; - -})); +export { EventHandler as default }; diff --git a/assets/javascripts/bootstrap/dom/manipulator.js b/assets/javascripts/bootstrap/dom/manipulator.js index 4ccad124..fcc13ea9 100644 --- a/assets/javascripts/bootstrap/dom/manipulator.js +++ b/assets/javascripts/bootstrap/dom/manipulator.js @@ -1,71 +1,63 @@ /*! - * Bootstrap manipulator.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap manipulator.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Manipulator = factory()); -})(this, (function () { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/manipulator.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - function normalizeData(value) { - if (value === 'true') { - return true; - } - if (value === 'false') { - return false; - } - if (value === Number(value).toString()) { - return Number(value); - } - if (value === '' || value === 'null') { - return null; - } - if (typeof value !== 'string') { - return value; - } - try { - return JSON.parse(decodeURIComponent(value)); - } catch (_unused) { - return value; - } +function normalizeData(value) { + if (value === 'true') { + return true; } - function normalizeDataKey(key) { - return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); + if (value === 'false') { + return false; } - const Manipulator = { - setDataAttribute(element, key, value) { - element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); - }, - removeDataAttribute(element, key) { - element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); - }, - getDataAttributes(element) { - if (!element) { - return {}; - } - const attributes = {}; - const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); - for (const key of bsKeys) { - let pureKey = key.replace(/^bs/, ''); - pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); - attributes[pureKey] = normalizeData(element.dataset[key]); - } - return attributes; - }, - getDataAttribute(element, key) { - return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; } - }; - - return Manipulator; + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; -})); +export { Manipulator as default }; diff --git a/assets/javascripts/bootstrap/dom/selector-engine.js b/assets/javascripts/bootstrap/dom/selector-engine.js index fedaed54..57b229d0 100644 --- a/assets/javascripts/bootstrap/dom/selector-engine.js +++ b/assets/javascripts/bootstrap/dom/selector-engine.js @@ -1,103 +1,100 @@ /*! - * Bootstrap selector-engine.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap selector-engine.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) : - typeof define === 'function' && define.amd ? define(['../util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SelectorEngine = factory(global.Index)); -})(this, (function (index_js) { 'use strict'; +import { isDisabled, isVisible, parseSelector } from '../util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/selector-engine.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - const getSelector = element => { - let selector = element.getAttribute('data-bs-target'); - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href'); +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { - return null; - } + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; + } - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}`; - } - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; } - return selector ? selector.split(',').map(sel => index_js.parseSelector(sel)).join(',') : null; - }; - const SelectorEngine = { - find(selector, element = document.documentElement) { - return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); - }, - findOne(selector, element = document.documentElement) { - return Element.prototype.querySelector.call(element, selector); - }, - children(element, selector) { - return [].concat(...element.children).filter(child => child.matches(selector)); - }, - parents(element, selector) { - const parents = []; - let ancestor = element.parentNode.closest(selector); - while (ancestor) { - parents.push(ancestor); - ancestor = ancestor.parentNode.closest(selector); - } - return parents; - }, - prev(element, selector) { - let previous = element.previousElementSibling; - while (previous) { - if (previous.matches(selector)) { - return [previous]; - } - previous = previous.previousElementSibling; - } - return []; - }, - // TODO: this is now unused; remove later along with prev() - next(element, selector) { - let next = element.nextElementSibling; - while (next) { - if (next.matches(selector)) { - return [next]; - } - next = next.nextElementSibling; + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; } - return []; - }, - focusableChildren(element) { - const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map(selector => `${selector}:not([tabindex^="-"])`).join(','); - return this.find(focusables, element).filter(el => !index_js.isDisabled(el) && index_js.isVisible(el)); - }, - getSelectorFromElement(element) { - const selector = getSelector(element); - if (selector) { - return SelectorEngine.findOne(selector) ? selector : null; + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; } - return null; - }, - getElementFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.findOne(selector) : null; - }, - getMultipleElementsFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.find(selector) : []; + next = next.nextElementSibling; } - }; - - return SelectorEngine; + return []; + }, + focusableChildren(element) { + 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)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; -})); +export { SelectorEngine as default }; diff --git a/assets/javascripts/bootstrap/drawer.js b/assets/javascripts/bootstrap/drawer.js new file mode 100644 index 00000000..fb1f701a --- /dev/null +++ b/assets/javascripts/bootstrap/drawer.js @@ -0,0 +1,167 @@ +/*! + * Bootstrap drawer.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import DialogBase from './dialog-base.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import Swipe from './util/swipe.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { isDisabled, isVisible, isRTL } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'drawer'; +const DATA_KEY = 'bs.drawer'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_RESIZE = `resize${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="drawer"]'; +const Default = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } + + // Protected — hook overrides + + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } + + // Private + + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; + } + + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); + } + } else if (isRTL()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); + } + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); + } + } +}); +enableDismissTrigger(Drawer); + +export { Drawer as default }; diff --git a/assets/javascripts/bootstrap/dropdown.js b/assets/javascripts/bootstrap/dropdown.js deleted file mode 100644 index 7bd5ae0c..00000000 --- a/assets/javascripts/bootstrap/dropdown.js +++ /dev/null @@ -1,401 +0,0 @@ -/*! - * Bootstrap dropdown.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dropdown = factory(global["@popperjs/core"], global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index)); -})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'dropdown'; - const DATA_KEY = 'bs.dropdown'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ESCAPE_KEY = 'Escape'; - const TAB_KEY = 'Tab'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button - - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_DROPUP = 'dropup'; - const CLASS_NAME_DROPEND = 'dropend'; - const CLASS_NAME_DROPSTART = 'dropstart'; - const CLASS_NAME_DROPUP_CENTER = 'dropup-center'; - const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'; - const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`; - const SELECTOR_MENU = '.dropdown-menu'; - const SELECTOR_NAVBAR = '.navbar'; - const SELECTOR_NAVBAR_NAV = '.navbar-nav'; - const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; - const PLACEMENT_TOP = index_js.isRTL() ? 'top-end' : 'top-start'; - const PLACEMENT_TOPEND = index_js.isRTL() ? 'top-start' : 'top-end'; - const PLACEMENT_BOTTOM = index_js.isRTL() ? 'bottom-end' : 'bottom-start'; - const PLACEMENT_BOTTOMEND = index_js.isRTL() ? 'bottom-start' : 'bottom-end'; - const PLACEMENT_RIGHT = index_js.isRTL() ? 'left-start' : 'right-start'; - const PLACEMENT_LEFT = index_js.isRTL() ? 'right-start' : 'left-start'; - const PLACEMENT_TOPCENTER = 'top'; - const PLACEMENT_BOTTOMCENTER = 'bottom'; - const Default = { - autoClose: true, - boundary: 'clippingParents', - display: 'dynamic', - offset: [0, 2], - popperConfig: null, - reference: 'toggle' - }; - const DefaultType = { - autoClose: '(boolean|string)', - boundary: '(string|element)', - display: 'string', - offset: '(array|string|function)', - popperConfig: '(null|object|function)', - reference: '(string|element|object)' - }; - - /** - * Class definition - */ - - class Dropdown extends BaseComponent { - constructor(element, config) { - super(element, config); - this._popper = null; - this._parent = this._element.parentNode; // dropdown wrapper - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle() { - return this._isShown() ? this.hide() : this.show(); - } - show() { - if (index_js.isDisabled(this._element) || this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget); - if (showEvent.defaultPrevented) { - return; - } - this._createPopper(); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', index_js.noop); - } - } - this._element.focus(); - this._element.setAttribute('aria-expanded', true); - this._menu.classList.add(CLASS_NAME_SHOW); - this._element.classList.add(CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget); - } - hide() { - if (index_js.isDisabled(this._element) || !this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - this._completeHide(relatedTarget); - } - dispose() { - if (this._popper) { - this._popper.destroy(); - } - super.dispose(); - } - update() { - this._inNavbar = this._detectNavbar(); - if (this._popper) { - this._popper.update(); - } - } - - // Private - _completeHide(relatedTarget) { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget); - if (hideEvent.defaultPrevented) { - return; - } - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', index_js.noop); - } - } - if (this._popper) { - this._popper.destroy(); - } - this._menu.classList.remove(CLASS_NAME_SHOW); - this._element.classList.remove(CLASS_NAME_SHOW); - this._element.setAttribute('aria-expanded', 'false'); - Manipulator.removeDataAttribute(this._menu, 'popper'); - EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget); - } - _getConfig(config) { - config = super._getConfig(config); - if (typeof config.reference === 'object' && !index_js.isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { - // Popper virtual elements require a getBoundingClientRect method - throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); - } - return config; - } - _createPopper() { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)'); - } - let referenceElement = this._element; - if (this._config.reference === 'parent') { - referenceElement = this._parent; - } else if (index_js.isElement(this._config.reference)) { - referenceElement = index_js.getElement(this._config.reference); - } else if (typeof this._config.reference === 'object') { - referenceElement = this._config.reference; - } - const popperConfig = this._getPopperConfig(); - this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig); - } - _isShown() { - return this._menu.classList.contains(CLASS_NAME_SHOW); - } - _getPlacement() { - const parentDropdown = this._parent; - if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { - return PLACEMENT_RIGHT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) { - return PLACEMENT_LEFT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) { - return PLACEMENT_TOPCENTER; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) { - return PLACEMENT_BOTTOMCENTER; - } - - // We need to trim the value because custom properties can also include spaces - const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'; - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) { - return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP; - } - return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM; - } - _detectNavbar() { - return this._element.closest(SELECTOR_NAVBAR) !== null; - } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _getPopperConfig() { - const defaultBsPopperConfig = { - placement: this._getPlacement(), - modifiers: [{ - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }] - }; - - // Disable Popper if we have a static display or Dropdown is in Navbar - if (this._inNavbar || this._config.display === 'static') { - Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove - defaultBsPopperConfig.modifiers = [{ - name: 'applyStyles', - enabled: false - }]; - } - return { - ...defaultBsPopperConfig, - ...index_js.execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; - } - _selectMenuItem({ - key, - target - }) { - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => index_js.isVisible(element)); - if (!items.length) { - return; - } - - // if target isn't included in items (e.g. when expanding the dropdown) - // allow cycling to get the last item in case key equals ARROW_UP_KEY - index_js.getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Dropdown.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } - static clearMenus(event) { - if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) { - return; - } - const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN); - for (const toggle of openToggles) { - const context = Dropdown.getInstance(toggle); - if (!context || context._config.autoClose === false) { - continue; - } - 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) { - continue; - } - - // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu - if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName))) { - continue; - } - const relatedTarget = { - relatedTarget: context._element - }; - if (event.type === 'click') { - relatedTarget.clickEvent = event; - } - context._completeHide(relatedTarget); - } - } - static dataApiKeydownHandler(event) { - // If not an UP | DOWN | ESCAPE key => not a dropdown command - // If input/textarea && if key is other than ESCAPE => not a dropdown command - - const isInput = /input|textarea/i.test(event.target.tagName); - const isEscapeEvent = event.key === ESCAPE_KEY; - const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key); - if (!isUpOrDownEvent && !isEscapeEvent) { - return; - } - if (isInput && !isEscapeEvent) { - return; - } - event.preventDefault(); - - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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.delegateTarget.parentNode); - const instance = Dropdown.getOrCreateInstance(getToggleButton); - if (isUpOrDownEvent) { - event.stopPropagation(); - instance.show(); - instance._selectMenuItem(event); - return; - } - if (instance._isShown()) { - // else is escape and we check if it is shown - event.stopPropagation(); - instance.hide(); - getToggleButton.focus(); - } - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - event.preventDefault(); - Dropdown.getOrCreateInstance(this).toggle(); - }); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Dropdown); - - return Dropdown; - -})); diff --git a/assets/javascripts/bootstrap/menu.js b/assets/javascripts/bootstrap/menu.js new file mode 100644 index 00000000..e0746aad --- /dev/null +++ b/assets/javascripts/bootstrap/menu.js @@ -0,0 +1,818 @@ +/*! + * Bootstrap menu.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isDisabled, noop, isElement, getElement, execute, isRTL, isVisible, getNextActiveElement } from './util/index.js'; +import { getResponsivePlacement, parseResponsivePlacement, createBreakpointListeners, disposeBreakpointListeners } from './util/floating-ui.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap menu.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'menu'; +const DATA_KEY = 'bs.menu'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ESCAPE_KEY = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const RIGHT_MOUSE_BUTTON = 2; +const SUBMENU_CLOSE_DELAY = 100; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'; +const SELECTOR_MENU = '.menu'; +const SELECTOR_SUBMENU = '.submenu'; +const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'; +const SELECTOR_NAVBAR_NAV = '.navbar-nav'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item:not(.disabled):not(:disabled)'; +const DEFAULT_PLACEMENT = 'bottom-start'; +const SUBMENU_PLACEMENT = 'end-start'; +const resolveLogicalPlacement = placement => { + if (isRTL()) { + return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left'); + } + return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right'); +}; +const triangleSign = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +const Default = { + autoClose: true, + boundary: 'clippingParents', + container: false, + display: 'dynamic', + offset: [0, 2], + floatingConfig: null, + menu: null, + placement: DEFAULT_PLACEMENT, + reference: 'toggle', + strategy: 'absolute', + submenuTrigger: 'both', + submenuDelay: SUBMENU_CLOSE_DELAY +}; +const DefaultType = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + container: '(string|element|boolean)', + display: 'string', + offset: '(array|string|function)', + floatingConfig: '(null|object|function)', + menu: '(null|element)', + placement: 'string', + reference: '(string|element|object)', + strategy: 'string', + submenuTrigger: 'string', + submenuDelay: 'number' +}; + +/** + * Class definition + */ + +class Menu extends BaseComponent { + static _openInstances = new Set(); + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)'); + } + super(element, config); + this._floatingCleanup = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + this._parent = this._element.parentNode; // menu wrapper + this._openSubmenus = new Map(); + this._submenuCloseTimeouts = new Map(); + this._hoverIntentData = null; + this._menu = this._config.menu || this._findMenu(); + + // When the menu was discovered from the DOM, refine the wrapper to the closest + // ancestor that actually contains it, so the toggle doesn't have to be a direct + // sibling of `.menu` (e.g. when wrapped by web components). The wrapper still + // receives `.show` and acts as the `reference: 'parent'` positioning anchor. + if (!this._config.menu && this._menu) { + this._parent = this._findWrapper(this._menu); + } + this._isSubmenu = this._parent.classList?.contains('submenu'); + this._menuOriginalParent = this._menu?.parentNode; + this._parseResponsivePlacements(); + this._setupSubmenuListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._moveMenuToContainer(); + this._createFloating(); + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus({ + focusVisible: false + }); + this._element.setAttribute('aria-expanded', 'true'); + this._menu.classList.add(CLASS_NAME_SHOW); + this._element.classList.add(CLASS_NAME_SHOW); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_SHOW); + } + Menu._openInstances.add(this); + EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._disposeMediaQueryListeners(); + this._closeAllSubmenus(); + this._clearAllSubmenuTimeouts(); + Menu._openInstances.delete(this); + super.dispose(); + } + update() { + if (this._floatingCleanup) { + this._updateFloatingPosition(); + } + } + + // Private + _findMenu() { + // Fall back to the closest ancestor that contains a menu so the toggle can be + // nested deeper than a direct sibling of `.menu`. + const wrapper = SelectorEngine.closest(this._element, `:has(${SELECTOR_MENU})`); + return SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, wrapper || this._parent); + } + _findWrapper(menu) { + let wrapper = this._element.parentNode; + while (wrapper instanceof Element && !wrapper.contains(menu)) { + wrapper = wrapper.parentNode; + } + return wrapper instanceof Element ? wrapper : this._element.parentNode; + } + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + this._closeAllSubmenus(); + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._menu.classList.remove(CLASS_NAME_SHOW); + this._element.classList.remove(CLASS_NAME_SHOW); + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_SHOW); + } + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'placement'); + Manipulator.removeDataAttribute(this._menu, 'display'); + Menu._openInstances.delete(this); + EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createFloating() { + if (this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'display', 'static'); + return; + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + this._updateFloatingPosition(referenceElement); + this._floatingCleanup = autoUpdate(referenceElement, this._menu, () => this._updateFloatingPosition(referenceElement)); + } + async _updateFloatingPosition(referenceElement = null) { + if (!this._menu) { + return; + } + if (!referenceElement) { + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } else { + referenceElement = this._element; + } + } + const placement = this._getPlacement(); + const middleware = this._getFloatingMiddleware(); + const floatingConfig = this._getFloatingConfig(placement, middleware); + await this._applyFloatingPosition(referenceElement, this._menu, floatingConfig.placement, floatingConfig.middleware, floatingConfig.strategy); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW); + } + _getPlacement() { + const placement = this._responsivePlacements ? getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) : this._config.placement; + return resolveLogicalPlacement(placement); + } + _parseResponsivePlacements() { + this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + _getOffset() { + const { + offset: offsetConfig + } = this._config; + if (typeof offsetConfig === 'string') { + return offsetConfig.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offsetConfig === 'function') { + return ({ + placement, + rects + }) => { + const result = offsetConfig({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offsetConfig; + } + _getFloatingMiddleware() { + const offsetValue = this._getOffset(); + const middleware = [offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), flip({ + fallbackPlacements: this._getFallbackPlacements() + }), shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + return middleware; + } + _getFallbackPlacements() { + const placement = this._getPlacement(); + const fallbackMap = { + bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'], + 'bottom-start': ['top-start', 'bottom-end', 'top-end'], + 'bottom-end': ['top-end', 'bottom-start', 'top-start'], + top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'], + 'top-start': ['bottom-start', 'top-end', 'bottom-end'], + 'top-end': ['bottom-end', 'top-start', 'bottom-start'], + right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'], + 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'], + 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'], + left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'], + 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'], + 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end'] + }; + return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware, + strategy: this._config.strategy + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + } + _getContainer() { + const { + container + } = this._config; + if (container === false) { + return null; + } + return container === true ? document.body : getElement(container); + } + _moveMenuToContainer() { + const container = this._getContainer(); + if (!container || !this._menu) { + return; + } + if (this._menu.parentNode !== container) { + container.append(this._menu); + } + } + _restoreMenuToOriginalParent() { + if (!this._menuOriginalParent || !this._menu) { + return; + } + if (this._menu.parentNode !== this._menuOriginalParent) { + this._menuOriginalParent.append(this._menu); + } + } + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { + if (!floating.isConnected) { + return null; + } + const { + x, + y, + placement: finalPlacement + } = await computePosition(reference, floating, { + placement, + middleware, + strategy + }); + if (!floating.isConnected) { + return null; + } + Object.assign(floating.style, { + position: strategy, + left: `${x}px`, + top: `${y}px`, + margin: '0' + }); + Manipulator.setDataAttribute(floating, 'placement', finalPlacement); + return finalPlacement; + } + + // ------------------------------------------------------------------------- + // Submenu handling + // ------------------------------------------------------------------------- + + _setupSubmenuListeners() { + if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerEnter(event); + }); + EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => { + this._onSubmenuLeave(event); + }); + EventHandler.on(this._menu, 'mousemove', event => { + this._trackMousePosition(event); + }); + } + if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerClick(event); + }); + } + } + _onSubmenuTriggerEnter(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu) { + return; + } + this._cancelSubmenuCloseTimeout(submenu); + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + _onSubmenuLeave(event) { + const submenuWrapper = event.target.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu || !this._openSubmenus.has(submenu)) { + return; + } + if (this._isMovingTowardSubmenu(event, submenu)) { + return; + } + this._scheduleSubmenuClose(submenu, submenuWrapper); + } + _onSubmenuTriggerClick(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + event.preventDefault(); + event.stopPropagation(); + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu) { + return; + } + if (this._openSubmenus.has(submenu)) { + this._closeSubmenu(submenu, submenuWrapper); + } else { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + } + _openSubmenu(trigger, submenu, submenuWrapper) { + if (this._openSubmenus.has(submenu)) { + return; + } + trigger.setAttribute('aria-expanded', 'true'); + trigger.setAttribute('aria-haspopup', 'true'); + + // Keep the submenu transparent until Floating UI applies the first position, so + // it doesn't flash at its CSS fallback position (top: 0, over the parent menu) + // before being moved into place. `opacity` (unlike `visibility`/`display`) keeps + // the submenu measurable for flip/shift and focusable for keyboard navigation. + submenu.style.opacity = '0'; + submenu.classList.add(CLASS_NAME_SHOW); + submenuWrapper.classList.add(CLASS_NAME_SHOW); + const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper); + this._openSubmenus.set(submenu, cleanup); + EventHandler.on(submenu, 'mouseenter', () => { + this._cancelSubmenuCloseTimeout(submenu); + }); + } + _closeSubmenu(submenu, submenuWrapper) { + if (!this._openSubmenus.has(submenu)) { + return; + } + const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, submenu); + for (const nested of nestedSubmenus) { + const nestedWrapper = nested.closest(SELECTOR_SUBMENU); + this._closeSubmenu(nested, nestedWrapper); + } + const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper); + const cleanup = this._openSubmenus.get(submenu); + if (cleanup) { + cleanup(); + } + this._openSubmenus.delete(submenu); + EventHandler.off(submenu, 'mouseenter'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + submenu.classList.remove(CLASS_NAME_SHOW); + submenuWrapper.classList.remove(CLASS_NAME_SHOW); + + // Keep the Floating UI position styles in place while the submenu fades out. + // Clearing them here would let the submenu snap back to its CSS fallback + // (`top: 0`, over the parent menu) for the duration of the close transition, + // causing it to flash over the parent. They get recomputed on the next open + // (and the opacity gate in `_openSubmenu` hides any stale position until then). + submenu.style.opacity = ''; + } + _closeAllSubmenus() { + for (const [submenu] of this._openSubmenus) { + const submenuWrapper = submenu.closest(SELECTOR_SUBMENU); + this._closeSubmenu(submenu, submenuWrapper); + } + } + _closeSiblingSubmenus(currentSubmenuWrapper) { + const parent = currentSubmenuWrapper.parentNode; + const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, parent); + for (const siblingMenu of siblingSubmenus) { + const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU); + if (siblingWrapper !== currentSubmenuWrapper) { + this._closeSubmenu(siblingMenu, siblingWrapper); + } + } + } + _createSubmenuFloating(trigger, submenu, submenuWrapper) { + const referenceElement = submenuWrapper; + const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT); + const middleware = [offset({ + mainAxis: 0, + crossAxis: -4 + }), flip({ + fallbackPlacements: [resolveLogicalPlacement('start-start'), resolveLogicalPlacement('end-end'), resolveLogicalPlacement('start-end')] + }), shift({ + padding: 8 + })]; + const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware).then(finalPlacement => { + // Reveal the submenu now that it has been positioned (see `_openSubmenu`); + // clearing the inline opacity lets the CSS fade-in transition take over. + submenu.style.opacity = ''; + return finalPlacement; + }); + updatePosition(); + return autoUpdate(referenceElement, submenu, updatePosition); + } + _scheduleSubmenuClose(submenu, submenuWrapper) { + this._cancelSubmenuCloseTimeout(submenu); + const timeoutId = setTimeout(() => { + this._closeSubmenu(submenu, submenuWrapper); + this._submenuCloseTimeouts.delete(submenu); + }, this._config.submenuDelay); + this._submenuCloseTimeouts.set(submenu, timeoutId); + } + _cancelSubmenuCloseTimeout(submenu) { + const timeoutId = this._submenuCloseTimeouts.get(submenu); + if (timeoutId) { + clearTimeout(timeoutId); + this._submenuCloseTimeouts.delete(submenu); + } + } + _clearAllSubmenuTimeouts() { + for (const timeoutId of this._submenuCloseTimeouts.values()) { + clearTimeout(timeoutId); + } + this._submenuCloseTimeouts.clear(); + } + + // ------------------------------------------------------------------------- + // Hover intent / Safe triangle + // ------------------------------------------------------------------------- + + _trackMousePosition(event) { + this._hoverIntentData = { + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + }; + } + _isMovingTowardSubmenu(event, submenu) { + if (!this._hoverIntentData) { + return false; + } + const submenuRect = submenu.getBoundingClientRect(); + const currentPos = { + x: event.clientX, + y: event.clientY + }; + const lastPos = { + x: this._hoverIntentData.x, + y: this._hoverIntentData.y + }; + const isRtl = isRTL(); + const targetX = isRtl ? submenuRect.right : submenuRect.left; + const topCorner = { + x: targetX, + y: submenuRect.top + }; + const bottomCorner = { + x: targetX, + y: submenuRect.bottom + }; + return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner); + } + _pointInTriangle(point, v1, v2, v3) { + const d1 = triangleSign(point, v1, v2); + const d2 = triangleSign(point, v2, v3); + const d3 = triangleSign(point, v3, v1); + const hasNeg = d1 < 0 || d2 < 0 || d3 < 0; + const hasPos = d1 > 0 || d2 > 0 || d3 > 0; + return !(hasNeg && hasPos); + } + + // ------------------------------------------------------------------------- + // Keyboard navigation + // ------------------------------------------------------------------------- + + _selectMenuItem({ + key, + target + }) { + const currentMenu = target.closest(SELECTOR_MENU) || this._menu; + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); + } + _handleSubmenuKeydown(event) { + const { + key, + target + } = event; + const isRtl = isRTL(); + const enterKey = isRtl ? ARROW_LEFT_KEY : ARROW_RIGHT_KEY; + const exitKey = isRtl ? ARROW_RIGHT_KEY : ARROW_LEFT_KEY; + const submenuWrapper = target.closest(SELECTOR_SUBMENU); + const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE); + if ((key === ENTER_KEY || key === SPACE_KEY) && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === enterKey && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === exitKey) { + const currentMenu = target.closest(SELECTOR_MENU); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper) { + event.preventDefault(); + event.stopPropagation(); + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + this._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return true; + } + } + if (key === HOME_KEY || key === END_KEY) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = target.closest(SELECTOR_MENU); + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu).filter(element => isVisible(element)); + if (items.length) { + const targetItem = key === HOME_KEY ? items[0] : items.at(-1); + targetItem.focus(); + } + return true; + } + return false; + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) { + return; + } + for (const instance of Menu._openInstances) { + if (instance._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(instance._menu); + if (composedPath.includes(instance._element) || instance._config.autoClose === 'inside' && !isMenuTarget || instance._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Don't auto-close when interacting with a form inside the menu — clicks + // on a form's labels, buttons, etc. (not just inputs) should keep it open. + const formAncestor = event.target.closest?.('form'); + const isInsideMenuForm = Boolean(formAncestor) && instance._menu.contains(formAncestor); + if (instance._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName) || isInsideMenuForm)) { + continue; + } + const relatedTarget = { + relatedTarget: instance._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + instance._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the + // menu doesn't hijack their arrow keys. + const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable; + const isEscapeEvent = event.key === ESCAPE_KEY; + const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key); + const isLeftOrRightEvent = [ARROW_LEFT_KEY, ARROW_RIGHT_KEY].includes(event.key); + const isHomeOrEndEvent = [HOME_KEY, END_KEY].includes(event.key); + const isEnterOrSpaceEvent = [ENTER_KEY, SPACE_KEY].includes(event.key); + const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE); + if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent && !(isEnterOrSpaceEvent && isSubmenuTrigger)) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + 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.delegateTarget.parentNode); + if (!getToggleButton) { + return; + } + const instance = Menu.getOrCreateInstance(getToggleButton); + if ((isLeftOrRightEvent || isHomeOrEndEvent || isEnterOrSpaceEvent && isSubmenuTrigger) && instance._handleSubmenuKeydown(event)) { + return; + } + if (isUpOrDownEvent) { + event.preventDefault(); + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (isEscapeEvent && instance._isShown()) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = event.target.closest(SELECTOR_MENU); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper && instance._openSubmenus.size > 0) { + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + instance._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return; + } + instance.hide(); + getToggleButton.focus(); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_CLICK_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + event.preventDefault(); + Menu.getOrCreateInstance(this).toggle(); +}); + +export { Menu as default }; diff --git a/assets/javascripts/bootstrap/modal.js b/assets/javascripts/bootstrap/modal.js deleted file mode 100644 index 8427cc75..00000000 --- a/assets/javascripts/bootstrap/modal.js +++ /dev/null @@ -1,319 +0,0 @@ -/*! - * Bootstrap modal.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'modal'; - const DATA_KEY = 'bs.modal'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ESCAPE_KEY = 'Escape'; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_RESIZE = `resize${EVENT_KEY}`; - const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`; - const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_OPEN = 'modal-open'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_STATIC = 'modal-static'; - const OPEN_SELECTOR = '.modal.show'; - const SELECTOR_DIALOG = '.modal-dialog'; - const SELECTOR_MODAL_BODY = '.modal-body'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'; - const Default = { - backdrop: true, - focus: true, - keyboard: true - }; - const DefaultType = { - backdrop: '(boolean|string)', - focus: 'boolean', - keyboard: 'boolean' - }; - - /** - * Class definition - */ - - class Modal extends BaseComponent { - constructor(element, config) { - super(element, config); - this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element); - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._isShown = false; - this._isTransitioning = false; - this._scrollBar = new ScrollBarHelper(); - this._addEventListeners(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown || this._isTransitioning) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._isTransitioning = true; - this._scrollBar.hide(); - document.body.classList.add(CLASS_NAME_OPEN); - this._adjustDialog(); - this._backdrop.show(() => this._showElement(relatedTarget)); - } - hide() { - if (!this._isShown || this._isTransitioning) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - this._isShown = false; - this._isTransitioning = true; - this._focustrap.deactivate(); - this._element.classList.remove(CLASS_NAME_SHOW); - this._queueCallback(() => this._hideModal(), this._element, this._isAnimated()); - } - dispose() { - EventHandler.off(window, EVENT_KEY); - EventHandler.off(this._dialog, EVENT_KEY); - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } - handleUpdate() { - this._adjustDialog(); - } - - // Private - _initializeBackDrop() { - return new Backdrop({ - isVisible: Boolean(this._config.backdrop), - // 'static' option will be translated to true, and booleans will keep their value, - isAnimated: this._isAnimated() - }); - } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); - } - _showElement(relatedTarget) { - // try to append dynamic modal - if (!document.body.contains(this._element)) { - document.body.append(this._element); - } - this._element.style.display = 'block'; - this._element.removeAttribute('aria-hidden'); - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.scrollTop = 0; - const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog); - if (modalBody) { - modalBody.scrollTop = 0; - } - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW); - const transitionComplete = () => { - if (this._config.focus) { - this._focustrap.activate(); - } - this._isTransitioning = false; - EventHandler.trigger(this._element, EVENT_SHOWN, { - relatedTarget - }); - }; - this._queueCallback(transitionComplete, this._dialog, this._isAnimated()); - } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - this._triggerBackdropTransition(); - }); - EventHandler.on(window, EVENT_RESIZE, () => { - if (this._isShown && !this._isTransitioning) { - this._adjustDialog(); - } - }); - EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => { - // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks - EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => { - if (this._element !== event.target || this._element !== event2.target) { - return; - } - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition(); - return; - } - if (this._config.backdrop) { - this.hide(); - } - }); - }); - } - _hideModal() { - this._element.style.display = 'none'; - this._element.setAttribute('aria-hidden', true); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - this._isTransitioning = false; - this._backdrop.hide(() => { - document.body.classList.remove(CLASS_NAME_OPEN); - this._resetAdjustments(); - this._scrollBar.reset(); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }); - } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_FADE); - } - _triggerBackdropTransition() { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - if (hideEvent.defaultPrevented) { - return; - } - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const initialOverflowY = this._element.style.overflowY; - // return if the following background transition hasn't yet completed - if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { - return; - } - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden'; - } - this._element.classList.add(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.classList.remove(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.style.overflowY = initialOverflowY; - }, this._dialog); - }, this._dialog); - this._element.focus(); - } - - /** - * The following methods are used to handle overflowing modals - */ - - _adjustDialog() { - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const scrollbarWidth = this._scrollBar.getWidth(); - const isBodyOverflowing = scrollbarWidth > 0; - if (isBodyOverflowing && !isModalOverflowing) { - const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight'; - this._element.style[property] = `${scrollbarWidth}px`; - } - if (!isBodyOverflowing && isModalOverflowing) { - const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft'; - this._element.style[property] = `${scrollbarWidth}px`; - } - } - _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; - } - - // Static - static jQueryInterface(config, relatedTarget) { - return this.each(function () { - const data = Modal.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](relatedTarget); - }); - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - EventHandler.one(target, EVENT_SHOW, showEvent => { - if (showEvent.defaultPrevented) { - // only register focus restorer if modal will actually get shown - return; - } - EventHandler.one(target, EVENT_HIDDEN, () => { - if (index_js.isVisible(this)) { - this.focus(); - } - }); - }); - - // avoid conflict when clicking modal toggler while another one is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen) { - Modal.getInstance(alreadyOpen).hide(); - } - const data = Modal.getOrCreateInstance(target); - data.toggle(this); - }); - componentFunctions_js.enableDismissTrigger(Modal); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Modal); - - return Modal; - -})); diff --git a/assets/javascripts/bootstrap/nav-overflow.js b/assets/javascripts/bootstrap/nav-overflow.js new file mode 100644 index 00000000..0954cbc8 --- /dev/null +++ b/assets/javascripts/bootstrap/nav-overflow.js @@ -0,0 +1,309 @@ +/*! + * Bootstrap nav-overflow.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'navoverflow'; +const DATA_KEY = 'bs.navoverflow'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_UPDATE = `update${EVENT_KEY}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); + } + super.dispose(); + } + + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); + + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); + + // Setup resize observer + this._setupResizeObserver(); + + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; + } + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; + } + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; + } + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + return; + } + const overflowWidth = overflowItem?.offsetWidth || 0; + + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer + + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; + } + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); + } + } + + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); + } + + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); + + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; + } + + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; + } + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); + } + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); + } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); + } + } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +export { NavOverflow as default }; diff --git a/assets/javascripts/bootstrap/offcanvas.js b/assets/javascripts/bootstrap/offcanvas.js deleted file mode 100644 index a6aa6c74..00000000 --- a/assets/javascripts/bootstrap/offcanvas.js +++ /dev/null @@ -1,245 +0,0 @@ -/*! - * Bootstrap offcanvas.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Offcanvas = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap offcanvas.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'offcanvas'; - const DATA_KEY = 'bs.offcanvas'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const ESCAPE_KEY = 'Escape'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const CLASS_NAME_HIDING = 'hiding'; - const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'; - const OPEN_SELECTOR = '.offcanvas.show'; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_RESIZE = `resize${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'; - const Default = { - backdrop: true, - keyboard: true, - scroll: false - }; - const DefaultType = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - scroll: 'boolean' - }; - - /** - * Class definition - */ - - class Offcanvas extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isShown = false; - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._addEventListeners(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._backdrop.show(); - if (!this._config.scroll) { - new ScrollBarHelper().hide(); - } - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.classList.add(CLASS_NAME_SHOWING); - const completeCallBack = () => { - if (!this._config.scroll || this._config.backdrop) { - this._focustrap.activate(); - } - this._element.classList.add(CLASS_NAME_SHOW); - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN, { - relatedTarget - }); - }; - this._queueCallback(completeCallBack, this._element, true); - } - hide() { - if (!this._isShown) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - this._focustrap.deactivate(); - this._element.blur(); - this._isShown = false; - this._element.classList.add(CLASS_NAME_HIDING); - this._backdrop.hide(); - const completeCallback = () => { - this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - if (!this._config.scroll) { - new ScrollBarHelper().reset(); - } - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._queueCallback(completeCallback, this._element, true); - } - dispose() { - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } - - // Private - _initializeBackDrop() { - const clickCallback = () => { - if (this._config.backdrop === 'static') { - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - return; - } - this.hide(); - }; - - // 'static' option will be translated to true, and booleans will keep their value - const isVisible = Boolean(this._config.backdrop); - return new Backdrop({ - className: CLASS_NAME_BACKDROP, - isVisible, - isAnimated: true, - rootElement: this._element.parentNode, - clickCallback: isVisible ? clickCallback : null - }); - } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); - } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - }); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Offcanvas.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - EventHandler.one(target, EVENT_HIDDEN, () => { - // focus on trigger when it is closed - if (index_js.isVisible(this)) { - this.focus(); - } - }); - - // avoid conflict when clicking a toggler of an offcanvas, while another is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen && alreadyOpen !== target) { - Offcanvas.getInstance(alreadyOpen).hide(); - } - const data = Offcanvas.getOrCreateInstance(target); - data.toggle(this); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { - Offcanvas.getOrCreateInstance(selector).show(); - } - }); - EventHandler.on(window, EVENT_RESIZE, () => { - for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) { - if (getComputedStyle(element).position !== 'fixed') { - Offcanvas.getOrCreateInstance(element).hide(); - } - } - }); - componentFunctions_js.enableDismissTrigger(Offcanvas); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Offcanvas); - - return Offcanvas; - -})); diff --git a/assets/javascripts/bootstrap/otp-input.js b/assets/javascripts/bootstrap/otp-input.js new file mode 100644 index 00000000..8b39f168 --- /dev/null +++ b/assets/javascripts/bootstrap/otp-input.js @@ -0,0 +1,265 @@ +/*! + * Bootstrap otp-input.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'otpInput'; +const DATA_KEY = 'bs.otpInput'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY}`; +const EVENT_INPUT = `input${EVENT_KEY}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } + + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; + } + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; + } + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); + } + + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); + } + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } + } + } + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); + } + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); + } + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +export { OtpInput as default }; diff --git a/assets/javascripts/bootstrap/popover.js b/assets/javascripts/bootstrap/popover.js index b81306fa..2c599706 100644 --- a/assets/javascripts/bootstrap/popover.js +++ b/assets/javascripts/bootstrap/popover.js @@ -1,95 +1,101 @@ /*! - * Bootstrap popover.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap popover.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./tooltip.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./tooltip', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Popover = factory(global.Tooltip, global.Index)); -})(this, (function (Tooltip, index_js) { 'use strict'; +import Tooltip from './tooltip.js'; +import EventHandler from './dom/event-handler.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap popover.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'popover'; - const SELECTOR_TITLE = '.popover-header'; - const SELECTOR_CONTENT = '.popover-body'; - const Default = { - ...Tooltip.Default, - content: '', - offset: [0, 8], - placement: 'right', - template: '' + '' + '' + '' + '', - trigger: 'click' - }; - const DefaultType = { - ...Tooltip.DefaultType, - content: '(null|string|element|function)' - }; +const NAME = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="popover"]'; +const EVENT_CLICK = 'click'; +const EVENT_FOCUSIN = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class Popover extends Tooltip { - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } +class Popover extends Tooltip { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } - // Overrides - _isWithContent() { - return this._getTitle() || this._getContent(); - } + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} - // Private - _getContentForTemplate() { - return { - [SELECTOR_TITLE]: this._getTitle(), - [SELECTOR_CONTENT]: this._getContent() - }; - } - _getContent() { - return this._resolvePossibleFunction(this._config.content); - } +/** + * Data API implementation - auto-initialize popovers + */ - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Popover.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE); + if (!target) { + return; } - /** - * jQuery - */ + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } - index_js.defineJQueryPlugin(Popover); + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; - return Popover; +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK, SELECTOR_DATA_TOGGLE, initPopover); +EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initPopover); -})); +export { Popover as default }; diff --git a/assets/javascripts/bootstrap/range.js b/assets/javascripts/bootstrap/range.js new file mode 100644 index 00000000..4985a2c8 --- /dev/null +++ b/assets/javascripts/bootstrap/range.js @@ -0,0 +1,213 @@ +/*! + * Bootstrap range.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'range'; +const DATA_KEY = 'bs.range'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; + } + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); + } + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; + } + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; + } + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); + } + } + if (points.length === 0) { + return; + } + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +export { Range as default }; diff --git a/assets/javascripts/bootstrap/scrollspy.js b/assets/javascripts/bootstrap/scrollspy.js index af342ccc..144f992d 100644 --- a/assets/javascripts/bootstrap/scrollspy.js +++ b/assets/javascripts/bootstrap/scrollspy.js @@ -1,274 +1,520 @@ /*! - * Bootstrap scrollspy.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap scrollspy.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollspy = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'scrollspy'; - const DATA_KEY = 'bs.scrollspy'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_ACTIVATE = `activate${EVENT_KEY}`; - const EVENT_CLICK = `click${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; - const CLASS_NAME_ACTIVE = 'active'; - const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; - const SELECTOR_TARGET_LINKS = '[href]'; - const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; - const SELECTOR_NAV_LINKS = '.nav-link'; - const SELECTOR_NAV_ITEMS = '.nav-item'; - const SELECTOR_LIST_ITEMS = '.list-group-item'; - const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; - const SELECTOR_DROPDOWN = '.dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const Default = { - offset: null, - // TODO: v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: '0px 0px -25%', - smoothScroll: false, - target: null, - threshold: [0.1, 0.5, 1] - }; - const DefaultType = { - offset: '(number|null)', - // TODO v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: 'string', - smoothScroll: 'boolean', - target: 'element', - threshold: 'array' - }; - - /** - * Class definition - */ - - class ScrollSpy extends BaseComponent { - constructor(element, config) { - super(element, config); - - // this._element is the observablesContainer and config.target the menu links wrapper - this._targetLinks = new Map(); - this._observableSections = new Map(); - this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; - this._activeTarget = null; - this._observer = null; - this._previousScrollData = { - visibleEntryTop: 0, - parentScrollTop: 0 - }; - this.refresh(); // initialize - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { getElement, isDisabled, isVisible } from './util/index.js'; - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'scrollspy'; +const DATA_KEY = 'bs.scrollspy'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY}`; +const EVENT_CLICK = `click${EVENT_KEY}`; +const EVENT_SCROLL = `scroll${EVENT_KEY}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY}`; +const EVENT_RESIZE = `resize${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } - static get NAME() { - return NAME; + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); } + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- - // Public - refresh() { - this._initializeTargetsAndObservables(); - this._maybeEnableSmoothScroll(); - if (this._observer) { - this._observer.disconnect(); + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); } else { - this._observer = this._getNewObserver(); + this._intersecting.delete(entry.target); } - for (const section of this._observableSections.values()) { - this._observer.observe(section); + } + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; + } } + + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); } - dispose() { - this._observer.disconnect(); - super.dispose(); + if (!active) { + return; } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); + } + } - // Private - _configAfterMerge(config) { - // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case - config.target = index_js.getElement(config.target) || document.body; + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } - // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only - config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin; - if (typeof config.threshold === 'string') { - config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); - } - return config; - } - _maybeEnableSmoothScroll() { - if (!this._config.smoothScroll) { - return; - } + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; - // unregister any previous listeners - EventHandler.off(this._config.target, EVENT_CLICK); - EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { - const observableSection = this._observableSections.get(event.target.hash); - if (observableSection) { - event.preventDefault(); - const root = this._rootElement || window; - const height = observableSection.offsetTop - this._element.offsetTop; - if (root.scrollTo) { - root.scrollTo({ - top: height, - behavior: 'smooth' - }); - return; - } - - // Chrome 60 doesn't support `scrollTo` - root.scrollTop = height; - } - }); + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; } - _getNewObserver() { - const options = { - root: this._rootElement, - threshold: this._config.threshold, - rootMargin: this._config.rootMargin - }; - return new IntersectionObserver(entries => this._observerCallback(entries), options); + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } + + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } + + // --- Bottom sentinel ----------------------------------------------------- + + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } - // The logic of selection - _observerCallback(entries) { - const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); - const activate = entry => { - this._previousScrollData.visibleEntryTop = entry.target.offsetTop; - this._process(targetElement(entry)); - }; - const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; - const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; - this._previousScrollData.parentScrollTop = parentScrollTop; - for (const entry of entries) { - if (!entry.isIntersecting) { - this._activeTarget = null; - this._clearActiveClass(targetElement(entry)); - continue; - } - const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; - // if we are scrolling down, pick the bigger offsetTop - if (userScrollsDown && entryIsLowerThanPrevious) { - activate(entry); - // if parent isn't scrolled, let's keep the first visible item, breaking the iteration - if (!parentScrollTop) { - return; - } - continue; - } + // --- Resize (px activation lines only) ----------------------------------- - // if we are scrolling up, pick the smallest offsetTop - if (!userScrollsDown && !entryIsLowerThanPrevious) { - activate(entry); - } - } + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; } - _initializeTargetsAndObservables() { - this._targetLinks = new Map(); - this._observableSections = new Map(); - const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); - for (const anchor of targetLinks) { - // ensure that the anchor has an id and is not disabled - if (!anchor.hash || index_js.isDisabled(anchor)) { - continue; - } - const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element); + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; + } + } + _rebuildObserver() { + if (!this._observer) { + return; + } + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + } - // ensure that the observableSection exists & is visible - if (index_js.isVisible(observableSection)) { - this._targetLinks.set(decodeURI(anchor.hash), anchor); - this._observableSections.set(anchor.hash, observableSection); - } - } + // --- Smooth-scroll settle (hash + focus) --------------------------------- + + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; } - _process(target) { - if (this._activeTarget === target) { + + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK); + EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { return; } - this._clearActiveClass(this._config.target); - this._activeTarget = target; - target.classList.add(CLASS_NAME_ACTIVE); - this._activateParents(target); - EventHandler.trigger(this._element, EVENT_ACTIVATE, { - relatedTarget: target - }); - } - _activateParents(target) { - // Activate dropdown parents - if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { - SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE); + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); return; } - for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { - // Set triggered links parents as active - // With both and markup a parent is the previous sibling of any nav ancestor - for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { - item.classList.add(CLASS_NAME_ACTIVE); - } + + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + } else { + root.scrollTop = height; } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; } - _clearActiveClass(parent) { - parent.classList.remove(CLASS_NAME_ACTIVE); - const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent); - for (const node of activeNodes) { - node.classList.remove(CLASS_NAME_ACTIVE); - } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; } + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; + } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = ScrollSpy.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); + } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); } + section.focus({ + preventScroll: true + }); } - /** - * Data API implementation - */ + // --- Targets / observables ---------------------------------------------- + + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } + + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section + + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); + } + } - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { - ScrollSpy.getOrCreateInstance(spy); + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; } - }); + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE); + } + return; + } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE); + } + } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE); + } + } +} - /** - * jQuery - */ +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} - index_js.defineJQueryPlugin(ScrollSpy); +/** + * Data API implementation + */ - return ScrollSpy; +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); -})); +export { ScrollSpy as default }; diff --git a/assets/javascripts/bootstrap/strength.js b/assets/javascripts/bootstrap/strength.js new file mode 100644 index 00000000..033f8d6f --- /dev/null +++ b/assets/javascripts/bootstrap/strength.js @@ -0,0 +1,240 @@ +/*! + * Bootstrap strength.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'strength'; +const DATA_KEY = 'bs.strength'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); + } + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } + + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } + + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } + + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; + + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; + } + + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; + } + if (/[A-Z]/.test(password)) { + score += weights.uppercase; + } + if (/\d/.test(password)) { + score += weights.numbers; + } + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; + } + + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; + } + if (password.length >= 16) { + score += weights.longPassword; + } + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; + } + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); + } + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; + } + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +export { Strength as default }; diff --git a/assets/javascripts/bootstrap/tab.js b/assets/javascripts/bootstrap/tab.js index 94088201..7a77c0e9 100644 --- a/assets/javascripts/bootstrap/tab.js +++ b/assets/javascripts/bootstrap/tab.js @@ -1,284 +1,265 @@ /*! - * Bootstrap tab.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap tab.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tab = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isDisabled, getNextActiveElement } from './util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap tab.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'tab'; - const DATA_KEY = 'bs.tab'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const HOME_KEY = 'Home'; - const END_KEY = 'End'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_DROPDOWN = 'dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; - const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; - const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; - const SELECTOR_OUTER = '.nav-item, .list-group-item'; - const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6 - const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; - const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`; +const NAME = 'tab'; +const DATA_KEY = 'bs.tab'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; - /** - * Class definition - */ +/** + * Class definition + */ - class Tab extends BaseComponent { - constructor(element) { - super(element); - this._parent = this._element.closest(SELECTOR_TAB_PANEL); - if (!this._parent) { - return; - // TODO: should throw exception in v6 - // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) - } - - // Set up initial aria attributes - this._setInitialAttributes(this._parent, this._getChildren()); - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) } - // Getters - static get NAME() { - return NAME; - } + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } - // Public - show() { - // Shows this elem and deactivate the active sibling if exists - const innerElem = this._element; - if (this._elemIsActive(innerElem)) { - return; - } + // Getters + static get NAME() { + return NAME; + } - // Search for active tab on same parent to deactivate it - const active = this._getActiveElem(); - const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, { - relatedTarget: innerElem - }) : null; - const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { - relatedTarget: active - }); - if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { - return; - } - this._deactivate(active, innerElem); - this._activate(innerElem, active); + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; } - // Private - _activate(element, relatedElem) { - if (!element) { - return; - } - element.classList.add(CLASS_NAME_ACTIVE); - this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; + } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.add(CLASS_NAME_SHOW); - return; - } - element.removeAttribute('tabindex'); - element.setAttribute('aria-selected', true); - this._toggleDropDown(element, true); - EventHandler.trigger(element, EVENT_SHOWN, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + // Private + _activate(element, relatedElem) { + if (!element) { + return; } - _deactivate(element, relatedElem) { - if (!element) { + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW); return; } - element.classList.remove(CLASS_NAME_ACTIVE); - element.blur(); - this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.remove(CLASS_NAME_SHOW); - return; - } - element.setAttribute('aria-selected', false); - element.setAttribute('tabindex', '-1'); - this._toggleDropDown(element, false); - EventHandler.trigger(element, EVENT_HIDDEN, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; } - _keydown(event) { - if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW); return; } - event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page - event.preventDefault(); - const children = this._getChildren().filter(element => !index_js.isDisabled(element)); - let nextActiveElement; - if ([HOME_KEY, END_KEY].includes(event.key)) { - nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]; - } else { - const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); - nextActiveElement = index_js.getNextActiveElement(children, event.target, isNext, true); - } - if (nextActiveElement) { - nextActiveElement.focus({ - preventScroll: true - }); - Tab.getOrCreateInstance(nextActiveElement).show(); - } + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; } - _getChildren() { - // collection of inner elements - return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; } - _getActiveElem() { - return this._getChildren().find(child => this._elemIsActive(child)) || null; + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); } - _setInitialAttributes(parent, children) { - this._setAttributeIfNotExists(parent, 'role', 'tablist'); - for (const child of children) { - this._setInitialAttributesOnChild(child); - } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); } - _setInitialAttributesOnChild(child) { - child = this._getInnerElement(child); - const isActive = this._elemIsActive(child); - const outerElem = this._getOuterElement(child); - child.setAttribute('aria-selected', isActive); - if (outerElem !== child) { - this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); - } - if (!isActive) { - child.setAttribute('tabindex', '-1'); - } - this._setAttributeIfNotExists(child, 'role', 'tab'); - - // set attributes to the related panel too - this._setInitialAttributesOnTargetPanel(child); + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); } - _setInitialAttributesOnTargetPanel(child) { - const target = SelectorEngine.getElementFromSelector(child); - if (!target) { - return; - } - this._setAttributeIfNotExists(target, 'role', 'tabpanel'); - if (child.id) { - this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); - } + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); } - _toggleDropDown(element, open) { - const outerElem = this._getOuterElement(element); - if (!outerElem.classList.contains(CLASS_DROPDOWN)) { - return; - } - const toggle = (selector, className) => { - const element = SelectorEngine.findOne(selector, outerElem); - if (element) { - element.classList.toggle(className, open); - } - }; - toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE); - toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW); - outerElem.setAttribute('aria-expanded', open); + if (!isActive) { + child.setAttribute('tabindex', '-1'); } - _setAttributeIfNotExists(element, attribute, value) { - if (!element.hasAttribute(attribute)) { - element.setAttribute(attribute, value); - } + this._setAttributeIfNotExists(child, 'role', 'tab'); + + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; } - _elemIsActive(elem) { - return elem.classList.contains(CLASS_NAME_ACTIVE); + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); } - - // Try to get the inner element (usually the .nav-link) - _getInnerElement(elem) { - return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; } - - // Try to get the outer element (usually the .nav-item) - _getOuterElement(elem) { - return elem.closest(SELECTOR_OUTER) || elem; + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW, open); } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tab.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); } } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } - /** - * Data API implementation - */ + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - Tab.getOrCreateInstance(this).show(); - }); + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} - /** - * Initialize on focus - */ - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { - Tab.getOrCreateInstance(element); - } - }); - /** - * jQuery - */ +/** + * Data API implementation + */ - index_js.defineJQueryPlugin(Tab); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); - return Tab; +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); -})); +export { Tab as default }; diff --git a/assets/javascripts/bootstrap/toast.js b/assets/javascripts/bootstrap/toast.js index 3e8903fc..5df6e268 100644 --- a/assets/javascripts/bootstrap/toast.js +++ b/assets/javascripts/bootstrap/toast.js @@ -1,197 +1,175 @@ /*! - * Bootstrap toast.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap toast.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Toast = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index)); -})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap toast.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'toast'; - const DATA_KEY = 'bs.toast'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; - const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const DefaultType = { - animation: 'boolean', - autohide: 'boolean', - delay: 'number' - }; - const Default = { - animation: true, - autohide: true, - delay: 5000 - }; - - /** - * Class definition - */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { reflow } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toast'; +const DATA_KEY = 'bs.toast'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); + } - class Toast extends BaseComponent { - constructor(element, config) { - super(element, config); - this._timeout = null; - this._hasMouseInteraction = false; - this._hasKeyboardInteraction = false; - this._setListeners(); - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; } - - // Public - show() { - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (showEvent.defaultPrevented) { - return; - } - this._clearTimeout(); - if (this._config.animation) { - this._element.classList.add(CLASS_NAME_FADE); - } - const complete = () => { - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN); - this._maybeScheduleHide(); - }; - this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); } - hide() { - if (!this.isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - const complete = () => { - this._element.classList.add(CLASS_NAME_HIDE); // @deprecated - this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.classList.add(CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; } - dispose() { - this._clearTimeout(); - if (this.isShown()) { - this._element.classList.remove(CLASS_NAME_SHOW); - } - super.dispose(); + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; } - isShown() { - return this._element.classList.contains(CLASS_NAME_SHOW); + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } - // Private - _maybeScheduleHide() { - if (!this._config.autohide) { - return; - } - if (this._hasMouseInteraction || this._hasKeyboardInteraction) { - return; - } - this._timeout = setTimeout(() => { - this.hide(); - }, this._config.delay); + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; } - _onInteraction(event, isInteracting) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - { - this._hasMouseInteraction = isInteracting; - break; - } - case 'focusin': - case 'focusout': - { - this._hasKeyboardInteraction = isInteracting; - break; - } - } - if (isInteracting) { - this._clearTimeout(); - return; - } - const nextElement = event.relatedTarget; - if (this._element === nextElement || this._element.contains(nextElement)) { - return; - } - this._maybeScheduleHide(); + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; } - _setListeners() { - EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); - EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } } - _clearTimeout() { - clearTimeout(this._timeout); - this._timeout = null; + if (isInteracting) { + this._clearTimeout(); + return; } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Toast.getOrCreateInstance(this, config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - } - }); + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; } + this._maybeScheduleHide(); } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} - /** - * Data API implementation - */ - - componentFunctions_js.enableDismissTrigger(Toast); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Toast); +/** + * Data API implementation + */ - return Toast; +enableDismissTrigger(Toast); -})); +export { Toast as default }; diff --git a/assets/javascripts/bootstrap/toggler.js b/assets/javascripts/bootstrap/toggler.js new file mode 100644 index 00000000..589f72e1 --- /dev/null +++ b/assets/javascripts/bootstrap/toggler.js @@ -0,0 +1,93 @@ +/*! + * Bootstrap toggler.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { eventActionOnPlugin } from './util/component-functions.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; + } + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; + } + + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; + } + this._element.setAttribute(attribute, value); + } +} + +/** + * Data API implementation + */ + +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); + +export { Toggler as default }; diff --git a/assets/javascripts/bootstrap/tooltip.js b/assets/javascripts/bootstrap/tooltip.js index d0f823d8..1f5bfcca 100644 --- a/assets/javascripts/bootstrap/tooltip.js +++ b/assets/javascripts/bootstrap/tooltip.js @@ -1,545 +1,688 @@ /*! - * Bootstrap tooltip.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap tooltip.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./util/index.js'), require('./util/sanitizer.js'), require('./util/template-factory.js')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './util/index', './util/sanitizer', './util/template-factory'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tooltip = factory(global["@popperjs/core"], global.BaseComponent, global.EventHandler, global.Manipulator, global.Index, global.Sanitizer, global.TemplateFactory)); -})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, index_js, sanitizer_js, TemplateFactory) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap tooltip.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'tooltip'; - const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_MODAL = 'modal'; - const CLASS_NAME_SHOW = 'show'; - const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; - const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; - const EVENT_MODAL_HIDE = 'hide.bs.modal'; - const TRIGGER_HOVER = 'hover'; - const TRIGGER_FOCUS = 'focus'; - const TRIGGER_CLICK = 'click'; - const TRIGGER_MANUAL = 'manual'; - const EVENT_HIDE = 'hide'; - const EVENT_HIDDEN = 'hidden'; - const EVENT_SHOW = 'show'; - const EVENT_SHOWN = 'shown'; - const EVENT_INSERTED = 'inserted'; - const EVENT_CLICK = 'click'; - const EVENT_FOCUSIN = 'focusin'; - const EVENT_FOCUSOUT = 'focusout'; - const EVENT_MOUSEENTER = 'mouseenter'; - const EVENT_MOUSELEAVE = 'mouseleave'; - const AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: index_js.isRTL() ? 'left' : 'right', - BOTTOM: 'bottom', - LEFT: index_js.isRTL() ? 'right' : 'left' - }; - const Default = { - allowList: sanitizer_js.DefaultAllowlist, - animation: true, - boundary: 'clippingParents', - container: false, - customClass: '', - delay: 0, - fallbackPlacements: ['top', 'right', 'bottom', 'left'], - html: false, - offset: [0, 6], - placement: 'top', - popperConfig: null, - sanitize: true, - sanitizeFn: null, - selector: false, - template: '' + '' + '' + '', - title: '', - trigger: 'hover focus' - }; - const DefaultType = { - allowList: 'object', - animation: 'boolean', - boundary: '(string|element)', - container: '(string|element|boolean)', - customClass: '(string|function)', - delay: '(number|object)', - fallbackPlacements: 'array', - html: 'boolean', - offset: '(array|string|function)', - placement: '(string|function)', - popperConfig: '(null|object|function)', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - selector: '(string|boolean)', - template: 'string', - title: '(string|element|function)', - trigger: 'string' - }; - - /** - * Class definition - */ - - class Tooltip extends BaseComponent { - constructor(element, config) { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)'); - } - super(element, config); - - // Private - this._isEnabled = true; - this._timeout = 0; - this._isHovered = null; - this._activeTrigger = {}; - this._popper = null; - this._templateFactory = null; - this._newContent = null; - - // Protected - this.tip = null; - this._setListeners(); - if (!this._config.selector) { - this._fixTitle(); - } - } +import { computePosition, autoUpdate, offset, flip, shift, arrow } from '@floating-ui/dom'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import { isRTL, findShadowRoot, noop, getUID, execute, getElement } from './util/index.js'; +import { DefaultAllowlist } from './util/sanitizer.js'; +import TemplateFactory from './util/template-factory.js'; +import { getResponsivePlacement, parseResponsivePlacement, createBreakpointListeners, disposeBreakpointListeners } from './util/floating-ui.js'; - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE = 'hide'; +const EVENT_HIDDEN = 'hidden'; +const EVENT_SHOW = 'show'; +const EVENT_SHOWN = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK = 'click'; +const EVENT_FOCUSIN = 'focusin'; +const EVENT_FOCUSOUT = 'focusout'; +const EVENT_MOUSEENTER = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL() ? 'right' : 'left' +}; +const Default = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; - // Public - enable() { - this._isEnabled = true; + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); } - disable() { - this._isEnabled = false; + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; } - toggleEnabled() { - this._isEnabled = !this._isEnabled; + if (this._isShown()) { + this._leave(); + return; } - toggle() { - if (!this._isEnabled) { - return; - } - if (this._isShown()) { + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (!(this._isWithContent() && this._isEnabled)) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; + } + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN)); + if (this._isHovered === false) { this._leave(); - return; } - this._enter(); + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; } - dispose() { - clearTimeout(this._timeout); - EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); - if (this._element.getAttribute('data-bs-original-title')) { - this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); - } - this._disposePopper(); - super.dispose(); + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)); + if (hideEvent.defaultPrevented) { + return; } - show() { - if (this._element.style.display === 'none') { - throw new Error('Please use show on visible elements'); - } - if (!(this._isWithContent() && this._isEnabled)) { - return; - } - const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW)); - const shadowRoot = index_js.findShadowRoot(this._element); - const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); - if (showEvent.defaultPrevented || !isInTheDom) { - return; - } + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW); - // TODO: v6 remove this or make it optional - this._disposePopper(); - const tip = this._getTipElement(); - this._element.setAttribute('aria-describedby', tip.getAttribute('id')); - const { - container - } = this._config; - if (!this._element.ownerDocument.documentElement.contains(this.tip)) { - container.append(tip); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); - } - this._popper = this._createPopper(tip); - tip.classList.add(CLASS_NAME_SHOW); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', index_js.noop); - } + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); } - const complete = () => { - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN)); - if (this._isHovered === false) { - this._leave(); - } - this._isHovered = false; - }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - hide() { - if (!this._isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)); - if (hideEvent.defaultPrevented) { + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering + + const complete = () => { + if (this._isWithActiveTrigger()) { return; } - const tip = this._getTipElement(); - tip.classList.remove(CLASS_NAME_SHOW); - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', index_js.noop); - } - } - this._activeTrigger[TRIGGER_CLICK] = false; - this._activeTrigger[TRIGGER_FOCUS] = false; - this._activeTrigger[TRIGGER_HOVER] = false; - this._isHovered = null; // it is a trick to support manual triggering - - const complete = () => { - if (this._isWithActiveTrigger()) { - return; - } - if (!this._isHovered) { - this._disposePopper(); - } - this._element.removeAttribute('aria-describedby'); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN)); - }; - this._queueCallback(complete, this.tip, this._isAnimated()); - } - update() { - if (this._popper) { - this._popper.update(); + if (!this._isHovered) { + this._disposeFloating(); } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); } + } - // Protected - _isWithContent() { - return Boolean(this._getTitle()); + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); } - _getTipElement() { - if (!this.tip) { - this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); - } - return this.tip; + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); } - _createTipElement(content) { - const tip = this._getTemplateFactory(content).toHtml(); + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); + } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } - // TODO: remove this check in v6 - if (!tip) { - return null; - } - tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW); - // TODO: v6 the following can be achieved with CSS only - tip.classList.add(`bs-${this.constructor.NAME}-auto`); - const tipId = index_js.getUID(this.constructor.NAME).toString(); - tip.setAttribute('id', tipId); - if (this._isAnimated()) { - tip.classList.add(CLASS_NAME_FADE); - } - return tip; + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; } - setContent(content) { - this._newContent = content; + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; + } + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { if (this._isShown()) { - this._disposePopper(); - this.show(); + this._updateFloatingPosition(); } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; + } + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; } - _getTemplateFactory(content) { - if (this._templateFactory) { - this._templateFactory.changeContent(content); - } else { - this._templateFactory = new TemplateFactory({ - ...this._config, - // the `content` var has to be after `this._config` - // to override config.content in case of popover - content, - extraClass: this._resolvePossibleFunction(this._config.customClass) - }); - } - return this._templateFactory; + + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { + const { + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); } - _getContentForTemplate() { - return { - [SELECTOR_TOOLTIP_INNER]: this._getTitle() + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; }; } - _getTitle() { - return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); - } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; - // Private - _initializeOnDelegatedTarget(event) { - return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); - } - _isAnimated() { - return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE); + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); } - _isShown() { - return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW); - } - _createPopper(tip) { - const placement = index_js.execute(this._config.placement, [this, tip, this._element]); - const attachment = AttachmentMap[placement.toUpperCase()]; - return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment)); - } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); } - return offset; - } - _resolvePossibleFunction(arg) { - return index_js.execute(arg, [this._element, this._element]); - } - _getPopperConfig(attachment) { - const defaultBsPopperConfig = { - placement: attachment, - modifiers: [{ - name: 'flip', - options: { - fallbackPlacements: this._config.fallbackPlacements - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }, { - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'arrow', - options: { - element: `.${this.constructor.NAME}-arrow` - } - }, { - name: 'preSetPlacement', - enabled: true, - phase: 'beforeMain', - fn: data => { - // Pre-set Popper's placement attribute in order to read the arrow sizes properly. - // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement - this._getTipElement().setAttribute('data-popper-placement', data.state.placement); - } - }] - }; - return { - ...defaultBsPopperConfig, - ...index_js.execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; } - _setListeners() { - const triggers = this._config.trigger.split(' '); - for (const trigger of triggers) { - if (trigger === 'click') { - EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); - context.toggle(); - }); - } else if (trigger !== TRIGGER_MANUAL) { - const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN); - const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT); - EventHandler.on(this._element, eventIn, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; - context._enter(); - }); - EventHandler.on(this._element, eventOut, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); - context._leave(); - }); - } + this._hideModalHandler = () => { + if (this._element) { + this.hide(); } - this._hideModalHandler = () => { - if (this._element) { - this.hide(); - } - }; - EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; } - _fixTitle() { - const title = this._element.getAttribute('title'); - if (!title) { + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { return; } - if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { - this._element.setAttribute('aria-label', title); - } - this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility - this._element.removeAttribute('title'); + + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; } - _enter() { - if (this._isShown() || this._isHovered) { - this._isHovered = true; - return; - } + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; + } + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); + } + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { this._isHovered = true; - this._setTimeout(() => { - if (this._isHovered) { - this.show(); - } - }, this._config.delay.show); + return; } - _leave() { - if (this._isWithActiveTrigger()) { - return; + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { + this.show(); } - this._isHovered = false; - this._setTimeout(() => { - if (!this._isHovered) { - this.hide(); - } - }, this._config.delay.hide); - } - _setTimeout(handler, timeout) { - clearTimeout(this._timeout); - this._timeout = setTimeout(handler, timeout); - } - _isWithActiveTrigger() { - return Object.values(this._activeTrigger).includes(true); - } - _getConfig(config) { - const dataAttributes = Manipulator.getDataAttributes(this._element); - for (const dataAttribute of Object.keys(dataAttributes)) { - if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { - delete dataAttributes[dataAttribute]; - } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; + } + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); } - config = { - ...dataAttributes, - ...(typeof config === 'object' && config ? config : {}) + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; + } + } + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay }; - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - config.container = config.container === false ? document.body : index_js.getElement(config.container); - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - if (typeof config.content === 'number') { - config.content = config.content.toString(); - } - return config; - } - _getDelegateConfig() { - const config = {}; - for (const [key, value] of Object.entries(this._config)) { - if (this.constructor.Default[key] !== value) { - config[key] = value; - } - } - config.selector = false; - config.trigger = 'manual'; - - // In the future can be replaced with: - // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) - // `Object.fromEntries(keysWithDifferentValues)` - return config; - } - _disposePopper() { - if (this._popper) { - this._popper.destroy(); - this._popper = null; - } - if (this.tip) { - this.tip.remove(); - this.tip = null; + } + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); + } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; } } + config.selector = false; + config.trigger = 'manual'; - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tooltip.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + if (this.tip) { + this.tip.remove(); + this.tip = null; } } +} - /** - * jQuery - */ +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE); + if (!target) { + return; + } - index_js.defineJQueryPlugin(Tooltip); + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; - return Tooltip; +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initTooltip); -})); +export { Tooltip as default }; diff --git a/assets/javascripts/bootstrap/util/backdrop.js b/assets/javascripts/bootstrap/util/backdrop.js deleted file mode 100644 index dad1188a..00000000 --- a/assets/javascripts/bootstrap/util/backdrop.js +++ /dev/null @@ -1,138 +0,0 @@ -/*! - * Bootstrap backdrop.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Backdrop = factory(global.EventHandler, global.Config, global.Index)); -})(this, (function (EventHandler, Config, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/backdrop.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'backdrop'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`; - const Default = { - className: 'modal-backdrop', - clickCallback: null, - isAnimated: false, - isVisible: true, - // if false, we use the backdrop helper without adding any element to the dom - rootElement: 'body' // give the choice to place backdrop under different elements - }; - const DefaultType = { - className: 'string', - clickCallback: '(function|null)', - isAnimated: 'boolean', - isVisible: 'boolean', - rootElement: '(element|string)' - }; - - /** - * Class definition - */ - - class Backdrop extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isAppended = false; - this._element = null; - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - show(callback) { - if (!this._config.isVisible) { - index_js.execute(callback); - return; - } - this._append(); - const element = this._getElement(); - if (this._config.isAnimated) { - index_js.reflow(element); - } - element.classList.add(CLASS_NAME_SHOW); - this._emulateAnimation(() => { - index_js.execute(callback); - }); - } - hide(callback) { - if (!this._config.isVisible) { - index_js.execute(callback); - return; - } - this._getElement().classList.remove(CLASS_NAME_SHOW); - this._emulateAnimation(() => { - this.dispose(); - index_js.execute(callback); - }); - } - dispose() { - if (!this._isAppended) { - return; - } - EventHandler.off(this._element, EVENT_MOUSEDOWN); - this._element.remove(); - this._isAppended = false; - } - - // Private - _getElement() { - if (!this._element) { - const backdrop = document.createElement('div'); - backdrop.className = this._config.className; - if (this._config.isAnimated) { - backdrop.classList.add(CLASS_NAME_FADE); - } - this._element = backdrop; - } - return this._element; - } - _configAfterMerge(config) { - // use getElement() with the default "body" to get a fresh Element on each instantiation - config.rootElement = index_js.getElement(config.rootElement); - return config; - } - _append() { - if (this._isAppended) { - return; - } - const element = this._getElement(); - this._config.rootElement.append(element); - EventHandler.on(element, EVENT_MOUSEDOWN, () => { - index_js.execute(this._config.clickCallback); - }); - this._isAppended = true; - } - _emulateAnimation(callback) { - index_js.executeAfterTransition(callback, this._getElement(), this._config.isAnimated); - } - } - - return Backdrop; - -})); diff --git a/assets/javascripts/bootstrap/util/component-functions.js b/assets/javascripts/bootstrap/util/component-functions.js index d2cd7744..11ccac7b 100644 --- a/assets/javascripts/bootstrap/util/component-functions.js +++ b/assets/javascripts/bootstrap/util/component-functions.js @@ -1,41 +1,63 @@ /*! - * Bootstrap component-functions.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap component-functions.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['exports', '../dom/event-handler', '../dom/selector-engine', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ComponentFunctions = {}, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (exports, EventHandler, SelectorEngine, index_js) { 'use strict'; +import EventHandler from '../dom/event-handler.js'; +import SelectorEngine from '../dom/selector-engine.js'; +import { isDisabled } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/component-functions.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - const enableDismissTrigger = (component, method = 'hide') => { - const clickEvent = `click.dismiss${component.EVENT_KEY}`; - const name = component.NAME; - EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); - const instance = component.getOrCreateInstance(target); +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); - // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); + } + for (const instance of instances) { instance[method](); + } + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event }); - }; + }); +}; - exports.enableDismissTrigger = enableDismissTrigger; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); +export { enableDismissTrigger, eventActionOnPlugin }; diff --git a/assets/javascripts/bootstrap/util/config.js b/assets/javascripts/bootstrap/util/config.js index b35fc078..a9c141c1 100644 --- a/assets/javascripts/bootstrap/util/config.js +++ b/assets/javascripts/bootstrap/util/config.js @@ -1,67 +1,62 @@ /*! - * Bootstrap config.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap config.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/manipulator', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Config = factory(global.Manipulator, global.Index)); -})(this, (function (Manipulator, index_js) { 'use strict'; +import Manipulator from '../dom/manipulator.js'; +import { isElement, toType } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/config.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Class definition - */ +/** + * Class definition + */ - class Config { - // Getters - static get Default() { - return {}; - } - static get DefaultType() { - return {}; - } - static get NAME() { - throw new Error('You have to implement the static method "NAME", for each component!'); - } - _getConfig(config) { - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - return config; - } - _mergeConfigObj(config, element) { - const jsonConfig = index_js.isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse - return { - ...this.constructor.Default, - ...(typeof jsonConfig === 'object' ? jsonConfig : {}), - ...(index_js.isElement(element) ? Manipulator.getDataAttributes(element) : {}), - ...(typeof config === 'object' ? config : {}) - }; - } - _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const [property, expectedTypes] of Object.entries(configTypes)) { - const value = config[property]; - const valueType = index_js.isElement(value) ? 'element' : index_js.toType(value); - if (!new RegExp(expectedTypes).test(valueType)) { - throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); - } + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); } } } +} - return Config; - -})); +export { Config as default }; diff --git a/assets/javascripts/bootstrap/util/floating-ui.js b/assets/javascripts/bootstrap/util/floating-ui.js new file mode 100644 index 00000000..62e06bd2 --- /dev/null +++ b/assets/javascripts/bootstrap/util/floating-ui.js @@ -0,0 +1,137 @@ +/*! + * Bootstrap floating-ui.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { isRTL } from './index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/floating-ui.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Breakpoints for responsive placement (matches SCSS $breakpoints) + */ +const BREAKPOINTS = { + sm: 576, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536 +}; + +/** + * Default placement with RTL support + */ +const getDefaultPlacement = (fallback = 'bottom') => { + if (fallback.includes('-start') || fallback.includes('-end')) { + const [side, alignment] = fallback.split('-'); + const flippedAlignment = alignment === 'start' ? 'end' : 'start'; + return isRTL() ? `${side}-${flippedAlignment}` : fallback; + } + return fallback; +}; + +/** + * Parse a placement string that may contain responsive prefixes + * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' } + * + * @param {string} placementString - The placement string to parse + * @param {string} defaultPlacement - The default placement to use for xs/base + * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive + */ +const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => { + // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end") + if (!placementString || !placementString.includes(':')) { + return null; + } + + // Parse the placement string into breakpoint-keyed object + const parts = placementString.split(/\s+/); + const placements = { + xs: defaultPlacement + }; // Default fallback + + for (const part of parts) { + if (part.includes(':')) { + // Responsive placement like "md:top-end" + const [breakpoint, placement] = part.split(':'); + if (BREAKPOINTS[breakpoint] !== undefined) { + placements[breakpoint] = placement; + } + } else { + // Base placement (no prefix = xs/default) + placements.xs = part; + } + } + return placements; +}; + +/** + * Get the active placement for the current viewport width + * + * @param {object} responsivePlacements - Object with breakpoint keys and placement values + * @param {string} defaultPlacement - Fallback placement + * @returns {string} - The active placement for current viewport + */ +const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => { + if (!responsivePlacements) { + return defaultPlacement; + } + + // Get current viewport width + const viewportWidth = window.innerWidth; + + // Find the largest breakpoint that matches + let activePlacement = responsivePlacements.xs || defaultPlacement; + + // Check breakpoints in order (sm, md, lg, xl, 2xl) + const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']; + for (const breakpoint of breakpointOrder) { + const minWidth = BREAKPOINTS[breakpoint]; + if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) { + activePlacement = responsivePlacements[breakpoint]; + } + } + return activePlacement; +}; + +/** + * Create media query listeners for responsive placement changes + * + * @param {Function} callback - Callback to run when breakpoint changes + * @returns {Array} - Array of { mql, handler } objects for cleanup + */ +const createBreakpointListeners = callback => { + const listeners = []; + for (const breakpoint of Object.keys(BREAKPOINTS)) { + const minWidth = BREAKPOINTS[breakpoint]; + const mql = window.matchMedia(`(min-width: ${minWidth}px)`); + mql.addEventListener('change', callback); + listeners.push({ + mql, + handler: callback + }); + } + return listeners; +}; + +/** + * Clean up media query listeners + * + * @param {Array} listeners - Array of { mql, handler } objects + */ +const disposeBreakpointListeners = listeners => { + for (const { + mql, + handler + } of listeners) { + mql.removeEventListener('change', handler); + } +}; + +export { BREAKPOINTS, createBreakpointListeners, disposeBreakpointListeners, getDefaultPlacement, getResponsivePlacement, parseResponsivePlacement }; diff --git a/assets/javascripts/bootstrap/util/focustrap.js b/assets/javascripts/bootstrap/util/focustrap.js deleted file mode 100644 index efad33db..00000000 --- a/assets/javascripts/bootstrap/util/focustrap.js +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * Bootstrap focustrap.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./config.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', '../dom/selector-engine', './config'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Focustrap = factory(global.EventHandler, global.SelectorEngine, global.Config)); -})(this, (function (EventHandler, SelectorEngine, Config) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/focustrap.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'focustrap'; - const DATA_KEY = 'bs.focustrap'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`; - const TAB_KEY = 'Tab'; - const TAB_NAV_FORWARD = 'forward'; - const TAB_NAV_BACKWARD = 'backward'; - const Default = { - autofocus: true, - trapElement: null // The element to trap focus inside of - }; - const DefaultType = { - autofocus: 'boolean', - trapElement: 'element' - }; - - /** - * Class definition - */ - - class FocusTrap extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isActive = false; - this._lastTabNavDirection = null; - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - activate() { - if (this._isActive) { - return; - } - if (this._config.autofocus) { - this._config.trapElement.focus(); - } - EventHandler.off(document, EVENT_KEY); // guard against infinite focus loop - EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event)); - EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)); - this._isActive = true; - } - deactivate() { - if (!this._isActive) { - return; - } - this._isActive = false; - EventHandler.off(document, EVENT_KEY); - } - - // Private - _handleFocusin(event) { - const { - trapElement - } = this._config; - if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { - return; - } - const elements = SelectorEngine.focusableChildren(trapElement); - if (elements.length === 0) { - trapElement.focus(); - } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { - elements[elements.length - 1].focus(); - } else { - elements[0].focus(); - } - } - _handleKeydown(event) { - if (event.key !== TAB_KEY) { - return; - } - this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD; - } - } - - return FocusTrap; - -})); diff --git a/assets/javascripts/bootstrap/util/index.js b/assets/javascripts/bootstrap/util/index.js index 0d31eed0..57bee61d 100644 --- a/assets/javascripts/bootstrap/util/index.js +++ b/assets/javascripts/bootstrap/util/index.js @@ -1,280 +1,226 @@ /*! - * Bootstrap index.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap index.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Index = {})); -})(this, (function (exports) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/index.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const MAX_UID = 1000000; - const MILLISECONDS_MULTIPLIER = 1000; - const TRANSITION_END = 'transitionend'; - - /** - * Properly escape IDs selectors to handle weird IDs - * @param {string} selector - * @returns {string} - */ - const parseSelector = selector => { - if (selector && window.CSS && window.CSS.escape) { - // document.querySelector needs escaping to handle IDs (html5+) containing for instance / - selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); - } - return selector; - }; - - // Shout-out Angus Croll (https://goo.gl/pxwQGp) - const toType = object => { - if (object === null || object === undefined) { - return `${object}`; - } - return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); - }; - - /** - * Public Util API - */ - - const getUID = prefix => { - do { - prefix += Math.floor(Math.random() * MAX_UID); - } while (document.getElementById(prefix)); - return prefix; - }; - const getTransitionDurationFromElement = element => { - if (!element) { - return 0; - } - - // Get transition-duration of the element - let { - transitionDuration, - transitionDelay - } = window.getComputedStyle(element); - const floatTransitionDuration = Number.parseFloat(transitionDuration); - const floatTransitionDelay = Number.parseFloat(transitionDelay); - - // Return 0 if element or transition duration is not found - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0; - } - - // If multiple durations are defined, take the first - transitionDuration = transitionDuration.split(',')[0]; - transitionDelay = transitionDelay.split(',')[0]; - return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; - }; - const triggerTransitionEnd = element => { - element.dispatchEvent(new Event(TRANSITION_END)); - }; - const isElement = object => { - if (!object || typeof object !== 'object') { +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { return false; } - if (typeof object.jquery !== 'undefined') { - object = object[0]; - } - return typeof object.nodeType !== 'undefined'; - }; - const getElement = object => { - // it's a jQuery object or a node element - if (isElement(object)) { - return object.jquery ? object[0] : object; - } - if (typeof object === 'string' && object.length > 0) { - return document.querySelector(parseSelector(object)); - } - return null; - }; - const isVisible = element => { - if (!isElement(element) || element.getClientRects().length === 0) { + if (summary === null) { return false; } - const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; - // Handle `details` element as its content may falsie appear visible when it is closed - const closedDetails = element.closest('details:not([open])'); - if (!closedDetails) { - return elementIsVisible; - } - if (closedDetails !== element) { - const summary = element.closest('summary'); - if (summary && summary.parentNode !== closedDetails) { - return false; - } - if (summary === null) { - return false; - } - } - return elementIsVisible; - }; - const isDisabled = element => { - if (!element || element.nodeType !== Node.ELEMENT_NODE) { - return true; - } - if (element.classList.contains('disabled')) { - return true; - } - if (typeof element.disabled !== 'undefined') { - return element.disabled; - } - return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; - }; - const findShadowRoot = element => { - if (!document.documentElement.attachShadow) { - return null; - } - - // Can find the shadow root otherwise it'll return the document - if (typeof element.getRootNode === 'function') { - const root = element.getRootNode(); - return root instanceof ShadowRoot ? root : null; - } - if (element instanceof ShadowRoot) { - return element; - } - - // when we don't find a shadow root - if (!element.parentNode) { - return null; - } - return findShadowRoot(element.parentNode); - }; - const noop = () => {}; - - /** - * Trick to restart an element's animation - * - * @param {HTMLElement} element - * @return void - * - * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation - */ - const reflow = element => { - element.offsetHeight; // eslint-disable-line no-unused-expressions - }; - const getjQuery = () => { - if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { - return window.jQuery; - } + } + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; + } + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { return null; - }; - const DOMContentLoadedCallbacks = []; - const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { - // add listener on the first call when the document is in loading state - if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { - for (const callback of DOMContentLoadedCallbacks) { - callback(); - } - }); - } - DOMContentLoadedCallbacks.push(callback); - } else { - callback(); - } - }; - const isRTL = () => document.documentElement.dir === 'rtl'; - const defineJQueryPlugin = plugin => { - onDOMContentLoaded(() => { - const $ = getjQuery(); - /* istanbul ignore if */ - if ($) { - const name = plugin.NAME; - const JQUERY_NO_CONFLICT = $.fn[name]; - $.fn[name] = plugin.jQueryInterface; - $.fn[name].Constructor = plugin; - $.fn[name].noConflict = () => { - $.fn[name] = JQUERY_NO_CONFLICT; - return plugin.jQueryInterface; - }; - } - }); - }; - const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { - return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; - }; - const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { - if (!waitForTransition) { - execute(callback); + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const DOMContentLoadedCallbacks = []; +const onDOMContentLoaded = callback => { + if (document.readyState === 'loading') { + // add listener on the first call when the document is in loading state + if (!DOMContentLoadedCallbacks.length) { + document.addEventListener('DOMContentLoaded', () => { + for (const callback of DOMContentLoadedCallbacks) { + callback(); + } + }); + } + DOMContentLoadedCallbacks.push(callback); + } else { + callback(); + } +}; +const isRTL = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { return; } - const durationPadding = 5; - const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; - let called = false; - const handler = ({ - target - }) => { - if (target !== transitionElement) { - return; - } - called = true; - transitionElement.removeEventListener(TRANSITION_END, handler); - execute(callback); - }; - transitionElement.addEventListener(TRANSITION_END, handler); - setTimeout(() => { - if (!called) { - triggerTransitionEnd(transitionElement); - } - }, emulatedDuration); - }; - - /** - * Return the previous/next element of a list. - * - * @param {array} list The list of elements - * @param activeElement The active element - * @param shouldGetNext Choose to get next or previous element - * @param isCycleAllowed - * @return {Element|elem} The proper element - */ - const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { - const listLength = list.length; - let index = list.indexOf(activeElement); - - // if the element does not exist in the list return an element - // depending on the direction and if cycle is allowed - if (index === -1) { - return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; - } - index += shouldGetNext ? 1 : -1; - if (isCycleAllowed) { - index = (index + listLength) % listLength; - } - return list[Math.max(0, Math.min(index, listLength - 1))]; - }; - - exports.defineJQueryPlugin = defineJQueryPlugin; - exports.execute = execute; - exports.executeAfterTransition = executeAfterTransition; - exports.findShadowRoot = findShadowRoot; - exports.getElement = getElement; - exports.getNextActiveElement = getNextActiveElement; - exports.getTransitionDurationFromElement = getTransitionDurationFromElement; - exports.getUID = getUID; - exports.getjQuery = getjQuery; - exports.isDisabled = isDisabled; - exports.isElement = isElement; - exports.isRTL = isRTL; - exports.isVisible = isVisible; - exports.noop = noop; - exports.onDOMContentLoaded = onDOMContentLoaded; - exports.parseSelector = parseSelector; - exports.reflow = reflow; - exports.toType = toType; - exports.triggerTransitionEnd = triggerTransitionEnd; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); + }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; + +export { execute, executeAfterTransition, findShadowRoot, getElement, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, onDOMContentLoaded, parseSelector, reflow, toType, triggerTransitionEnd }; diff --git a/assets/javascripts/bootstrap/util/sanitizer.js b/assets/javascripts/bootstrap/util/sanitizer.js index cf6a02d8..82320432 100644 --- a/assets/javascripts/bootstrap/util/sanitizer.js +++ b/assets/javascripts/bootstrap/util/sanitizer.js @@ -1,112 +1,109 @@ /*! - * Bootstrap sanitizer.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap sanitizer.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Sanitizer = {})); -})(this, (function (exports) { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap util/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list - // js-docs-start allow-list - const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; - const DefaultAllowlist = { - // Global attributes allowed on any supplied element below. - '*': ['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: [] - }; - // 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']); +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - /** - * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation - * contexts. - * - * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 - */ - const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - const allowedAttribute = (attribute, allowedAttributeList) => { - const attributeName = attribute.nodeName.toLowerCase(); - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)); - } - return true; +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); } + return true; + } - // Check if a regular expression validates the attribute. - return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); - }; - function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { - if (!unsafeHtml.length) { - return unsafeHtml; - } - if (sanitizeFunction && typeof sanitizeFunction === 'function') { - return sanitizeFunction(unsafeHtml); + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; } - const domParser = new window.DOMParser(); - const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); - const elements = [].concat(...createdDocument.body.querySelectorAll('*')); - for (const element of elements) { - const elementName = element.nodeName.toLowerCase(); - if (!Object.keys(allowList).includes(elementName)) { - element.remove(); - continue; - } - const attributeList = [].concat(...element.attributes); - const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []); - for (const attribute of attributeList) { - if (!allowedAttribute(attribute, allowedAttributes)) { - element.removeAttribute(attribute.nodeName); - } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); } } - return createdDocument.body.innerHTML; } + return createdDocument.body.innerHTML; +} - exports.DefaultAllowlist = DefaultAllowlist; - exports.sanitizeHtml = sanitizeHtml; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); +export { DefaultAllowlist, sanitizeHtml }; diff --git a/assets/javascripts/bootstrap/util/scrollbar.js b/assets/javascripts/bootstrap/util/scrollbar.js deleted file mode 100644 index f99e3dc7..00000000 --- a/assets/javascripts/bootstrap/util/scrollbar.js +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * Bootstrap scrollbar.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('../dom/selector-engine.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/manipulator', '../dom/selector-engine', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollbar = factory(global.Manipulator, global.SelectorEngine, global.Index)); -})(this, (function (Manipulator, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/scrollBar.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; - const SELECTOR_STICKY_CONTENT = '.sticky-top'; - const PROPERTY_PADDING = 'padding-right'; - const PROPERTY_MARGIN = 'margin-right'; - - /** - * Class definition - */ - - class ScrollBarHelper { - constructor() { - this._element = document.body; - } - - // Public - getWidth() { - // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes - const documentWidth = document.documentElement.clientWidth; - return Math.abs(window.innerWidth - documentWidth); - } - hide() { - const width = this.getWidth(); - this._disableOverFlow(); - // give padding to element to balance the hidden scrollbar width - this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth - this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width); - } - reset() { - this._resetElementAttributes(this._element, 'overflow'); - this._resetElementAttributes(this._element, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN); - } - isOverflowing() { - return this.getWidth() > 0; - } - - // Private - _disableOverFlow() { - this._saveInitialAttribute(this._element, 'overflow'); - this._element.style.overflow = 'hidden'; - } - _setElementAttributes(selector, styleProperty, callback) { - const scrollbarWidth = this.getWidth(); - const manipulationCallBack = element => { - if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { - return; - } - this._saveInitialAttribute(element, styleProperty); - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty); - element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`); - }; - this._applyManipulationCallback(selector, manipulationCallBack); - } - _saveInitialAttribute(element, styleProperty) { - const actualValue = element.style.getPropertyValue(styleProperty); - if (actualValue) { - Manipulator.setDataAttribute(element, styleProperty, actualValue); - } - } - _resetElementAttributes(selector, styleProperty) { - const manipulationCallBack = element => { - const value = Manipulator.getDataAttribute(element, styleProperty); - // We only want to remove the property if the value is `null`; the value can also be zero - if (value === null) { - element.style.removeProperty(styleProperty); - return; - } - Manipulator.removeDataAttribute(element, styleProperty); - element.style.setProperty(styleProperty, value); - }; - this._applyManipulationCallback(selector, manipulationCallBack); - } - _applyManipulationCallback(selector, callBack) { - if (index_js.isElement(selector)) { - callBack(selector); - return; - } - for (const sel of SelectorEngine.find(selector, this._element)) { - callBack(sel); - } - } - } - - return ScrollBarHelper; - -})); diff --git a/assets/javascripts/bootstrap/util/swipe.js b/assets/javascripts/bootstrap/util/swipe.js index 6c296425..f02c778f 100644 --- a/assets/javascripts/bootstrap/util/swipe.js +++ b/assets/javascripts/bootstrap/util/swipe.js @@ -1,134 +1,159 @@ /*! - * Bootstrap swipe.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap swipe.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Swipe = factory(global.EventHandler, global.Config, global.Index)); -})(this, (function (EventHandler, Config, index_js) { 'use strict'; +import EventHandler from '../dom/event-handler.js'; +import Config from './config.js'; +import { execute } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/swipe.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'swipe'; - const EVENT_KEY = '.bs.swipe'; - const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`; - const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`; - const EVENT_TOUCHEND = `touchend${EVENT_KEY}`; - const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; - const EVENT_POINTERUP = `pointerup${EVENT_KEY}`; - const POINTER_TYPE_TOUCH = 'touch'; - const POINTER_TYPE_PEN = 'pen'; - const CLASS_NAME_POINTER_EVENT = 'pointer-event'; - const SWIPE_THRESHOLD = 40; - const Default = { - endCallback: null, - leftCallback: null, - rightCallback: null - }; - const DefaultType = { - endCallback: '(function|null)', - leftCallback: '(function|null)', - rightCallback: '(function|null)' - }; +const NAME = 'swipe'; +const EVENT_KEY = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class Swipe extends Config { - constructor(element, config) { - super(); - this._element = element; - if (!element || !Swipe.isSupported()) { - return; - } - this._config = this._getConfig(config); - this._deltaX = 0; - this._supportPointerEvents = Boolean(window.PointerEvent); - this._initEvents(); +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Getters - static get Default() { - return Default; + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; } - static get DefaultType() { - return DefaultType; + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; } - static get NAME() { - return NAME; + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; } - - // Public - dispose() { - EventHandler.off(this._element, EVENT_KEY); + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; } + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); - // Private - _start(event) { - if (!this._supportPointerEvents) { - this._deltaX = event.touches[0].clientX; - return; - } - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX; - } - } - _end(event) { - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX - this._deltaX; - } - this._handleSwipe(); - index_js.execute(this._config.endCallback); - } - _move(event) { - this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX; + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; } - _handleSwipe() { - const absDeltaX = Math.abs(this._deltaX); - if (absDeltaX <= SWIPE_THRESHOLD) { - return; - } + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe const direction = absDeltaX / this._deltaX; this._deltaX = 0; + this._deltaY = 0; if (!direction) { return; } - index_js.execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; } - _initEvents() { - if (this._supportPointerEvents) { - EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); - EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); - this._element.classList.add(CLASS_NAME_POINTER_EVENT); - } else { - EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); - EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); - EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); - } - } - _eventIsPointerPenTouch(event) { - return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); - } - - // Static - static isSupported() { - return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); } } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } - return Swipe; + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} -})); +export { Swipe as default }; diff --git a/assets/javascripts/bootstrap/util/template-factory.js b/assets/javascripts/bootstrap/util/template-factory.js index b15e31d1..36ca1c04 100644 --- a/assets/javascripts/bootstrap/util/template-factory.js +++ b/assets/javascripts/bootstrap/util/template-factory.js @@ -1,150 +1,147 @@ /*! - * Bootstrap template-factory.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap template-factory.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/selector-engine.js'), require('./config.js'), require('./sanitizer.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/selector-engine', './config', './sanitizer', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TemplateFactory = factory(global.SelectorEngine, global.Config, global.Sanitizer, global.Index)); -})(this, (function (SelectorEngine, Config, sanitizer_js, index_js) { 'use strict'; +import SelectorEngine from '../dom/selector-engine.js'; +import Config from './config.js'; +import { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'; +import { isElement, getElement, execute } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/template-factory.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'TemplateFactory'; - const Default = { - allowList: sanitizer_js.DefaultAllowlist, - content: {}, - // { selector : text , selector2 : text2 , } - extraClass: '', - html: false, - sanitize: true, - sanitizeFn: null, - template: '' - }; - const DefaultType = { - allowList: 'object', - content: 'object', - extraClass: '(string|function)', - html: 'boolean', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - template: 'string' - }; - const DefaultContentType = { - entry: '(string|element|function|null)', - selector: '(string|element)' - }; +const NAME = 'TemplateFactory'; +const Default = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class TemplateFactory extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - } +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Public - getContent() { - return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); - } - hasContent() { - return this.getContent().length > 0; - } - changeContent(content) { - this._checkContent(content); - this._config.content = { - ...this._config.content, - ...content - }; - return this; + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); } - toHtml() { - const templateWrapper = document.createElement('div'); - templateWrapper.innerHTML = this._maybeSanitize(this._config.template); - for (const [selector, text] of Object.entries(this._config.content)) { - this._setContent(templateWrapper, text, selector); - } - const template = templateWrapper.children[0]; - const extraClass = this._resolvePossibleFunction(this._config.extraClass); - if (extraClass) { - template.classList.add(...extraClass.split(' ')); - } - return template; + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); } + return template; + } - // Private - _typeCheckConfig(config) { - super._typeCheckConfig(config); - this._checkContent(config.content); + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); } - _checkContent(arg) { - for (const [selector, content] of Object.entries(arg)) { - super._typeCheckConfig({ - selector, - entry: content - }, DefaultContentType); - } + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; } - _setContent(template, content, selector) { - const templateElement = SelectorEngine.findOne(selector, template); - if (!templateElement) { - return; - } - content = this._resolvePossibleFunction(content); - if (!content) { - templateElement.remove(); - return; - } - if (index_js.isElement(content)) { - this._putElementInTemplate(index_js.getElement(content), templateElement); - return; - } - if (this._config.html) { - templateElement.innerHTML = this._maybeSanitize(content); - return; - } - templateElement.textContent = content; + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; } - _maybeSanitize(arg) { - return this._config.sanitize ? sanitizer_js.sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + if (isElement(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; } - _resolvePossibleFunction(arg) { - return index_js.execute(arg, [undefined, this]); + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; } - _putElementInTemplate(element, templateElement) { - if (this._config.html) { - templateElement.innerHTML = ''; - templateElement.append(element); - return; - } - templateElement.textContent = element.textContent; + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; } + templateElement.textContent = element.textContent; } +} - return TemplateFactory; - -})); +export { TemplateFactory as default }; diff --git a/assets/javascripts/floating-ui.js b/assets/javascripts/floating-ui.js new file mode 100644 index 00000000..1310f438 --- /dev/null +++ b/assets/javascripts/floating-ui.js @@ -0,0 +1,3 @@ +/* esm.sh - @floating-ui/dom@1.7.6 */ +var yt=["top","right","bottom","left"],St=["start","end"],vt=yt.reduce((t,e)=>t.concat(e,e+"-"+St[0],e+"-"+St[1]),[]),W=Math.min,P=Math.max,st=Math.round,rt=Math.floor,X=t=>({x:t,y:t}),ce={left:"right",right:"left",bottom:"top",top:"bottom"};function ft(t,e,n){return P(t,W(e,n))}function $(t,e){return typeof t=="function"?t(e):t}function D(t){return t.split("-")[0]}function H(t){return t.split("-")[1]}function at(t){return t==="x"?"y":"x"}function ut(t){return t==="y"?"height":"width"}function V(t){let e=t[0];return e==="t"||e==="b"?"y":"x"}function dt(t){return at(V(t))}function bt(t,e,n){n===void 0&&(n=!1);let o=H(t),i=dt(t),s=ut(i),r=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[s]>e.floating[s]&&(r=it(r)),[r,it(r)]}function Lt(t){let e=it(t);return[ot(t),e,ot(e)]}function ot(t){return t.includes("start")?t.replace("start","end"):t.replace("end","start")}var Pt=["left","right"],Tt=["right","left"],le=["top","bottom"],fe=["bottom","top"];function ae(t,e,n){switch(t){case"top":case"bottom":return n?e?Tt:Pt:e?Pt:Tt;case"left":case"right":return e?le:fe;default:return[]}}function Et(t,e,n,o){let i=H(t),s=ae(D(t),n==="start",o);return i&&(s=s.map(r=>r+"-"+i),e&&(s=s.concat(s.map(ot)))),s}function it(t){let e=D(t);return ce[e]+t.slice(e.length)}function ue(t){return{top:0,right:0,bottom:0,left:0,...t}}function mt(t){return typeof t!="number"?ue(t):{top:t,right:t,bottom:t,left:t}}function q(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function Dt(t,e,n){let{reference:o,floating:i}=t,s=V(e),r=dt(e),c=ut(r),a=D(e),d=s==="y",u=o.x+o.width/2-i.width/2,l=o.y+o.height/2-i.height/2,m=o[c]/2-i[c]/2,f;switch(a){case"top":f={x:u,y:o.y-i.height};break;case"bottom":f={x:u,y:o.y+o.height};break;case"right":f={x:o.x+o.width,y:l};break;case"left":f={x:o.x-i.width,y:l};break;default:f={x:o.x,y:o.y}}switch(H(e)){case"start":f[r]-=m*(n&&d?-1:1);break;case"end":f[r]+=m*(n&&d?-1:1);break}return f}async function At(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:s,rects:r,elements:c,strategy:a}=t,{boundary:d="clippingAncestors",rootBoundary:u="viewport",elementContext:l="floating",altBoundary:m=!1,padding:f=0}=$(e,t),g=mt(f),p=c[m?l==="floating"?"reference":"floating":l],w=q(await s.getClippingRect({element:(n=await(s.isElement==null?void 0:s.isElement(p)))==null||n?p:p.contextElement||await(s.getDocumentElement==null?void 0:s.getDocumentElement(c.floating)),boundary:d,rootBoundary:u,strategy:a})),x=l==="floating"?{x:o,y:i,width:r.floating.width,height:r.floating.height}:r.reference,y=await(s.getOffsetParent==null?void 0:s.getOffsetParent(c.floating)),v=await(s.isElement==null?void 0:s.isElement(y))?await(s.getScale==null?void 0:s.getScale(y))||{x:1,y:1}:{x:1,y:1},A=q(s.convertOffsetParentRelativeRectToViewportRelativeRect?await s.convertOffsetParentRelativeRectToViewportRelativeRect({elements:c,rect:x,offsetParent:y,strategy:a}):x);return{top:(w.top-A.top+g.top)/v.y,bottom:(A.bottom-w.bottom+g.bottom)/v.y,left:(w.left-A.left+g.left)/v.x,right:(A.right-w.right+g.right)/v.x}}var de=50,Bt=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:s=[],platform:r}=n,c=r.detectOverflow?r:{...r,detectOverflow:At},a=await(r.isRTL==null?void 0:r.isRTL(e)),d=await r.getElementRects({reference:t,floating:e,strategy:i}),{x:u,y:l}=Dt(d,o,a),m=o,f=0,g={};for(let h=0;h({name:"arrow",options:t,async fn(e){let{x:n,y:o,placement:i,rects:s,platform:r,elements:c,middlewareData:a}=e,{element:d,padding:u=0}=$(t,e)||{};if(d==null)return{};let l=mt(u),m={x:n,y:o},f=dt(i),g=ut(f),h=await r.getDimensions(d),p=f==="y",w=p?"top":"left",x=p?"bottom":"right",y=p?"clientHeight":"clientWidth",v=s.reference[g]+s.reference[f]-m[f]-s.floating[g],A=m[f]-s.reference[f],O=await(r.getOffsetParent==null?void 0:r.getOffsetParent(d)),R=O?O[y]:0;(!R||!await(r.isElement==null?void 0:r.isElement(O)))&&(R=c.floating[y]||s.floating[g]);let k=v/2-A/2,T=R/2-h[g]/2-1,b=W(l[w],T),C=W(l[x],T),L=b,E=R-h[g]-C,S=R/2-h[g]/2+k,I=ft(L,S,E),N=!a.arrow&&H(i)!=null&&S!==I&&s.reference[g]/2-(SH(i)===t),...n.filter(i=>H(i)!==t)]:n.filter(i=>D(i)===i)).filter(i=>t?H(i)===t||(e?ot(i)!==i:!1):!0)}var kt=function(t){return t===void 0&&(t={}),{name:"autoPlacement",options:t,async fn(e){var n,o,i;let{rects:s,middlewareData:r,placement:c,platform:a,elements:d}=e,{crossAxis:u=!1,alignment:l,allowedPlacements:m=vt,autoAlignment:f=!0,...g}=$(t,e),h=l!==void 0||m===vt?me(l||null,f,m):m,p=await a.detectOverflow(e,g),w=((n=r.autoPlacement)==null?void 0:n.index)||0,x=h[w];if(x==null)return{};let y=bt(x,s,await(a.isRTL==null?void 0:a.isRTL(d.floating)));if(c!==x)return{reset:{placement:h[0]}};let v=[p[D(x)],p[y[0]],p[y[1]]],A=[...((o=r.autoPlacement)==null?void 0:o.overflows)||[],{placement:x,overflows:v}],O=h[w+1];if(O)return{data:{index:w+1,overflows:A},reset:{placement:O}};let R=A.map(b=>{let C=H(b.placement);return[b.placement,C&&u?b.overflows.slice(0,2).reduce((L,E)=>L+E,0):b.overflows[0],b.overflows]}).sort((b,C)=>b[1]-C[1]),T=((i=R.filter(b=>b[2].slice(0,H(b[0])?2:3).every(C=>C<=0))[0])==null?void 0:i[0])||R[0][0];return T!==c?{data:{index:w+1,overflows:A},reset:{placement:T}}:{}}}},Nt=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,o;let{placement:i,middlewareData:s,rects:r,initialPlacement:c,platform:a,elements:d}=e,{mainAxis:u=!0,crossAxis:l=!0,fallbackPlacements:m,fallbackStrategy:f="bestFit",fallbackAxisSideDirection:g="none",flipAlignment:h=!0,...p}=$(t,e);if((n=s.arrow)!=null&&n.alignmentOffset)return{};let w=D(i),x=V(c),y=D(c)===c,v=await(a.isRTL==null?void 0:a.isRTL(d.floating)),A=m||(y||!h?[it(c)]:Lt(c)),O=g!=="none";!m&&O&&A.push(...Et(c,h,g,v));let R=[c,...A],k=await a.detectOverflow(e,p),T=[],b=((o=s.flip)==null?void 0:o.overflows)||[];if(u&&T.push(k[w]),l){let S=bt(i,r,v);T.push(k[S[0]],k[S[1]])}if(b=[...b,{placement:i,overflows:T}],!T.every(S=>S<=0)){var C,L;let S=(((C=s.flip)==null?void 0:C.index)||0)+1,I=R[S];if(I&&(!(l==="alignment"?x!==V(I):!1)||b.every(B=>V(B.placement)===x?B.overflows[0]>0:!0)))return{data:{index:S,overflows:b},reset:{placement:I}};let N=(L=b.filter(F=>F.overflows[0]<=0).sort((F,B)=>F.overflows[1]-B.overflows[1])[0])==null?void 0:L.placement;if(!N)switch(f){case"bestFit":{var E;let F=(E=b.filter(B=>{if(O){let U=V(B.placement);return U===x||U==="y"}return!0}).map(B=>[B.placement,B.overflows.filter(U=>U>0).reduce((U,re)=>U+re,0)]).sort((B,U)=>B[1]-U[1])[0])==null?void 0:E[0];F&&(N=F);break}case"initialPlacement":N=c;break}if(i!==N)return{reset:{placement:N}}}return{}}}};function Mt(t,e){return{top:t.top-e.height,right:t.right-e.width,bottom:t.bottom-e.height,left:t.left-e.width}}function Ft(t){return yt.some(e=>t[e]>=0)}var $t=function(t){return t===void 0&&(t={}),{name:"hide",options:t,async fn(e){let{rects:n,platform:o}=e,{strategy:i="referenceHidden",...s}=$(t,e);switch(i){case"referenceHidden":{let r=await o.detectOverflow(e,{...s,elementContext:"reference"}),c=Mt(r,n.reference);return{data:{referenceHiddenOffsets:c,referenceHidden:Ft(c)}}}case"escaped":{let r=await o.detectOverflow(e,{...s,altBoundary:!0}),c=Mt(r,n.floating);return{data:{escapedOffsets:c,escaped:Ft(c)}}}default:return{}}}}};function Ht(t){let e=W(...t.map(s=>s.left)),n=W(...t.map(s=>s.top)),o=P(...t.map(s=>s.right)),i=P(...t.map(s=>s.bottom));return{x:e,y:n,width:o-e,height:i-n}}function ge(t){let e=t.slice().sort((i,s)=>i.y-s.y),n=[],o=null;for(let i=0;io.height/2?n.push([s]):n[n.length-1].push(s),o=s}return n.map(i=>q(Ht(i)))}var Vt=function(t){return t===void 0&&(t={}),{name:"inline",options:t,async fn(e){let{placement:n,elements:o,rects:i,platform:s,strategy:r}=e,{padding:c=2,x:a,y:d}=$(t,e),u=Array.from(await(s.getClientRects==null?void 0:s.getClientRects(o.reference))||[]),l=ge(u),m=q(Ht(u)),f=mt(c);function g(){if(l.length===2&&l[0].left>l[1].right&&a!=null&&d!=null)return l.find(p=>a>p.left-f.left&&ap.top-f.top&&d=2){if(V(n)==="y"){let b=l[0],C=l[l.length-1],L=D(n)==="top",E=b.top,S=C.bottom,I=L?b.left:C.left,N=L?b.right:C.right,F=N-I,B=S-E;return{top:E,bottom:S,left:I,right:N,width:F,height:B,x:I,y:E}}let p=D(n)==="left",w=P(...l.map(b=>b.right)),x=W(...l.map(b=>b.left)),y=l.filter(b=>p?b.left===x:b.right===w),v=y[0].top,A=y[y.length-1].bottom,O=x,R=w,k=R-O,T=A-v;return{top:v,bottom:A,left:O,right:R,width:k,height:T,x:O,y:v}}return m}let h=await s.getElementRects({reference:{getBoundingClientRect:g},floating:o.floating,strategy:r});return i.reference.x!==h.reference.x||i.reference.y!==h.reference.y||i.reference.width!==h.reference.width||i.reference.height!==h.reference.height?{reset:{rects:h}}:{}}}},_t=new Set(["left","top"]);async function he(t,e){let{placement:n,platform:o,elements:i}=t,s=await(o.isRTL==null?void 0:o.isRTL(i.floating)),r=D(n),c=H(n),a=V(n)==="y",d=_t.has(r)?-1:1,u=s&&a?-1:1,l=$(e,t),{mainAxis:m,crossAxis:f,alignmentAxis:g}=typeof l=="number"?{mainAxis:l,crossAxis:0,alignmentAxis:null}:{mainAxis:l.mainAxis||0,crossAxis:l.crossAxis||0,alignmentAxis:l.alignmentAxis};return c&&typeof g=="number"&&(f=c==="end"?g*-1:g),a?{x:f*u,y:m*d}:{x:m*d,y:f*u}}var zt=function(t){return t===void 0&&(t=0),{name:"offset",options:t,async fn(e){var n,o;let{x:i,y:s,placement:r,middlewareData:c}=e,a=await he(e,t);return r===((n=c.offset)==null?void 0:n.placement)&&(o=c.arrow)!=null&&o.alignmentOffset?{}:{x:i+a.x,y:s+a.y,data:{...a,placement:r}}}}},It=function(t){return t===void 0&&(t={}),{name:"shift",options:t,async fn(e){let{x:n,y:o,placement:i,platform:s}=e,{mainAxis:r=!0,crossAxis:c=!1,limiter:a={fn:w=>{let{x,y}=w;return{x,y}}},...d}=$(t,e),u={x:n,y:o},l=await s.detectOverflow(e,d),m=V(D(i)),f=at(m),g=u[f],h=u[m];if(r){let w=f==="y"?"top":"left",x=f==="y"?"bottom":"right",y=g+l[w],v=g-l[x];g=ft(y,g,v)}if(c){let w=m==="y"?"top":"left",x=m==="y"?"bottom":"right",y=h+l[w],v=h-l[x];h=ft(y,h,v)}let p=a.fn({...e,[f]:g,[m]:h});return{...p,data:{x:p.x-n,y:p.y-o,enabled:{[f]:r,[m]:c}}}}}},Xt=function(t){return t===void 0&&(t={}),{options:t,fn(e){let{x:n,y:o,placement:i,rects:s,middlewareData:r}=e,{offset:c=0,mainAxis:a=!0,crossAxis:d=!0}=$(t,e),u={x:n,y:o},l=V(i),m=at(l),f=u[m],g=u[l],h=$(c,e),p=typeof h=="number"?{mainAxis:h,crossAxis:0}:{mainAxis:0,crossAxis:0,...h};if(a){let y=m==="y"?"height":"width",v=s.reference[m]-s.floating[y]+p.mainAxis,A=s.reference[m]+s.reference[y]-p.mainAxis;fA&&(f=A)}if(d){var w,x;let y=m==="y"?"width":"height",v=_t.has(D(i)),A=s.reference[l]-s.floating[y]+(v&&((w=r.offset)==null?void 0:w[l])||0)+(v?0:p.crossAxis),O=s.reference[l]+s.reference[y]+(v?0:((x=r.offset)==null?void 0:x[l])||0)-(v?p.crossAxis:0);gO&&(g=O)}return{[m]:f,[l]:g}}}},jt=function(t){return t===void 0&&(t={}),{name:"size",options:t,async fn(e){var n,o;let{placement:i,rects:s,platform:r,elements:c}=e,{apply:a=()=>{},...d}=$(t,e),u=await r.detectOverflow(e,d),l=D(i),m=H(i),f=V(i)==="y",{width:g,height:h}=s.floating,p,w;l==="top"||l==="bottom"?(p=l,w=m===(await(r.isRTL==null?void 0:r.isRTL(c.floating))?"start":"end")?"left":"right"):(w=l,p=m==="end"?"top":"bottom");let x=h-u.top-u.bottom,y=g-u.left-u.right,v=W(h-u[p],x),A=W(g-u[w],y),O=!e.middlewareData.shift,R=v,k=A;if((n=e.middlewareData.shift)!=null&&n.enabled.x&&(k=y),(o=e.middlewareData.shift)!=null&&o.enabled.y&&(R=x),O&&!m){let b=P(u.left,0),C=P(u.right,0),L=P(u.top,0),E=P(u.bottom,0);f?k=g-2*(b!==0||C!==0?b+C:P(u.left,u.right)):R=h-2*(L!==0||E!==0?L+E:P(u.top,u.bottom))}await a({...e,availableWidth:k,availableHeight:R});let T=await r.getDimensions(c.floating);return g!==T.width||h!==T.height?{reset:{rects:!0}}:{}}}};function gt(){return typeof window<"u"}function Q(t){return qt(t)?(t.nodeName||"").toLowerCase():"#document"}function M(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function j(t){var e;return(e=(qt(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function qt(t){return gt()?t instanceof Node||t instanceof M(t).Node:!1}function _(t){return gt()?t instanceof Element||t instanceof M(t).Element:!1}function Y(t){return gt()?t instanceof HTMLElement||t instanceof M(t).HTMLElement:!1}function Yt(t){return!gt()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof M(t).ShadowRoot}function et(t){let{overflow:e,overflowX:n,overflowY:o,display:i}=z(t);return/auto|scroll|overlay|hidden|clip/.test(e+o+n)&&i!=="inline"&&i!=="contents"}function Kt(t){return/^(table|td|th)$/.test(Q(t))}function ct(t){try{if(t.matches(":popover-open"))return!0}catch{}try{return t.matches(":modal")}catch{return!1}}var pe=/transform|translate|scale|rotate|perspective|filter/,we=/paint|layout|strict|content/,G=t=>!!t&&t!=="none",Ot;function ht(t){let e=_(t)?z(t):t;return G(e.transform)||G(e.translate)||G(e.scale)||G(e.rotate)||G(e.perspective)||!pt()&&(G(e.backdropFilter)||G(e.filter))||pe.test(e.willChange||"")||we.test(e.contain||"")}function Ut(t){let e=K(t);for(;Y(e)&&!Z(e);){if(ht(e))return e;if(ct(e))return null;e=K(e)}return null}function pt(){return Ot==null&&(Ot=typeof CSS<"u"&&CSS.supports&&CSS.supports("-webkit-backdrop-filter","none")),Ot}function Z(t){return/^(html|body|#document)$/.test(Q(t))}function z(t){return M(t).getComputedStyle(t)}function lt(t){return _(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function K(t){if(Q(t)==="html")return t;let e=t.assignedSlot||t.parentNode||Yt(t)&&t.host||j(t);return Yt(e)?e.host:e}function Gt(t){let e=K(t);return Z(e)?t.ownerDocument?t.ownerDocument.body:t.body:Y(e)&&et(e)?e:Gt(e)}function J(t,e,n){var o;e===void 0&&(e=[]),n===void 0&&(n=!0);let i=Gt(t),s=i===((o=t.ownerDocument)==null?void 0:o.body),r=M(i);if(s){let c=wt(r);return e.concat(r,r.visualViewport||[],et(i)?i:[],c&&n?J(c):[])}else return e.concat(i,J(i,[],n))}function wt(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function te(t){let e=z(t),n=parseFloat(e.width)||0,o=parseFloat(e.height)||0,i=Y(t),s=i?t.offsetWidth:n,r=i?t.offsetHeight:o,c=st(n)!==s||st(o)!==r;return c&&(n=s,o=r),{width:n,height:o,$:c}}function Ct(t){return _(t)?t:t.contextElement}function nt(t){let e=Ct(t);if(!Y(e))return X(1);let n=e.getBoundingClientRect(),{width:o,height:i,$:s}=te(e),r=(s?st(n.width):n.width)/o,c=(s?st(n.height):n.height)/i;return(!r||!Number.isFinite(r))&&(r=1),(!c||!Number.isFinite(c))&&(c=1),{x:r,y:c}}var xe=X(0);function ee(t){let e=M(t);return!pt()||!e.visualViewport?xe:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function ye(t,e,n){return e===void 0&&(e=!1),!n||e&&n!==M(t)?!1:e}function tt(t,e,n,o){e===void 0&&(e=!1),n===void 0&&(n=!1);let i=t.getBoundingClientRect(),s=Ct(t),r=X(1);e&&(o?_(o)&&(r=nt(o)):r=nt(t));let c=ye(s,n,o)?ee(s):X(0),a=(i.left+c.x)/r.x,d=(i.top+c.y)/r.y,u=i.width/r.x,l=i.height/r.y;if(s){let m=M(s),f=o&&_(o)?M(o):o,g=m,h=wt(g);for(;h&&o&&f!==g;){let p=nt(h),w=h.getBoundingClientRect(),x=z(h),y=w.left+(h.clientLeft+parseFloat(x.paddingLeft))*p.x,v=w.top+(h.clientTop+parseFloat(x.paddingTop))*p.y;a*=p.x,d*=p.y,u*=p.x,l*=p.y,a+=y,d+=v,g=M(h),h=wt(g)}}return q({width:u,height:l,x:a,y:d})}function xt(t,e){let n=lt(t).scrollLeft;return e?e.left+n:tt(j(t)).left+n}function ne(t,e){let n=t.getBoundingClientRect(),o=n.left+e.scrollLeft-xt(t,n),i=n.top+e.scrollTop;return{x:o,y:i}}function ve(t){let{elements:e,rect:n,offsetParent:o,strategy:i}=t,s=i==="fixed",r=j(o),c=e?ct(e.floating):!1;if(o===r||c&&s)return n;let a={scrollLeft:0,scrollTop:0},d=X(1),u=X(0),l=Y(o);if((l||!l&&!s)&&((Q(o)!=="body"||et(r))&&(a=lt(o)),l)){let f=tt(o);d=nt(o),u.x=f.x+o.clientLeft,u.y=f.y+o.clientTop}let m=r&&!l&&!s?ne(r,a):X(0);return{width:n.width*d.x,height:n.height*d.y,x:n.x*d.x-a.scrollLeft*d.x+u.x+m.x,y:n.y*d.y-a.scrollTop*d.y+u.y+m.y}}function be(t){return Array.from(t.getClientRects())}function Ae(t){let e=j(t),n=lt(t),o=t.ownerDocument.body,i=P(e.scrollWidth,e.clientWidth,o.scrollWidth,o.clientWidth),s=P(e.scrollHeight,e.clientHeight,o.scrollHeight,o.clientHeight),r=-n.scrollLeft+xt(t),c=-n.scrollTop;return z(o).direction==="rtl"&&(r+=P(e.clientWidth,o.clientWidth)-i),{width:i,height:s,x:r,y:c}}var Jt=25;function Oe(t,e){let n=M(t),o=j(t),i=n.visualViewport,s=o.clientWidth,r=o.clientHeight,c=0,a=0;if(i){s=i.width,r=i.height;let u=pt();(!u||u&&e==="fixed")&&(c=i.offsetLeft,a=i.offsetTop)}let d=xt(o);if(d<=0){let u=o.ownerDocument,l=u.body,m=getComputedStyle(l),f=u.compatMode==="CSS1Compat"&&parseFloat(m.marginLeft)+parseFloat(m.marginRight)||0,g=Math.abs(o.clientWidth-l.clientWidth-f);g<=Jt&&(s-=g)}else d<=Jt&&(s+=d);return{width:s,height:r,x:c,y:a}}function Re(t,e){let n=tt(t,!0,e==="fixed"),o=n.top+t.clientTop,i=n.left+t.clientLeft,s=Y(t)?nt(t):X(1),r=t.clientWidth*s.x,c=t.clientHeight*s.y,a=i*s.x,d=o*s.y;return{width:r,height:c,x:a,y:d}}function Qt(t,e,n){let o;if(e==="viewport")o=Oe(t,n);else if(e==="document")o=Ae(j(t));else if(_(e))o=Re(e,n);else{let i=ee(t);o={x:e.x-i.x,y:e.y-i.y,width:e.width,height:e.height}}return q(o)}function oe(t,e){let n=K(t);return n===e||!_(n)||Z(n)?!1:z(n).position==="fixed"||oe(n,e)}function Ce(t,e){let n=e.get(t);if(n)return n;let o=J(t,[],!1).filter(c=>_(c)&&Q(c)!=="body"),i=null,s=z(t).position==="fixed",r=s?K(t):t;for(;_(r)&&!Z(r);){let c=z(r),a=ht(r);!a&&c.position==="fixed"&&(i=null),(s?!a&&!i:!a&&c.position==="static"&&!!i&&(i.position==="absolute"||i.position==="fixed")||et(r)&&!a&&oe(t,r))?o=o.filter(u=>u!==r):i=c,r=K(r)}return e.set(t,o),o}function Se(t){let{element:e,boundary:n,rootBoundary:o,strategy:i}=t,r=[...n==="clippingAncestors"?ct(e)?[]:Ce(e,this._c):[].concat(n),o],c=Qt(e,r[0],i),a=c.top,d=c.right,u=c.bottom,l=c.left;for(let m=1;m{r(!1,1e-7)},1e3)}R===1&&!se(d,t.getBoundingClientRect())&&r(),v=!1}try{n=new IntersectionObserver(A,{...y,root:i.ownerDocument})}catch{n=new IntersectionObserver(A,y)}n.observe(t)}return r(!0),s}function _e(t,e,n,o){o===void 0&&(o={});let{ancestorScroll:i=!0,ancestorResize:s=!0,elementResize:r=typeof ResizeObserver=="function",layoutShift:c=typeof IntersectionObserver=="function",animationFrame:a=!1}=o,d=Ct(t),u=i||s?[...d?J(d):[],...e?J(e):[]]:[];u.forEach(w=>{i&&w.addEventListener("scroll",n,{passive:!0}),s&&w.addEventListener("resize",n)});let l=d&&c?Me(d,n):null,m=-1,f=null;r&&(f=new ResizeObserver(w=>{let[x]=w;x&&x.target===d&&f&&e&&(f.unobserve(e),cancelAnimationFrame(m),m=requestAnimationFrame(()=>{var y;(y=f)==null||y.observe(e)})),n()}),d&&!a&&f.observe(d),e&&f.observe(e));let g,h=a?tt(t):null;a&&p();function p(){let w=tt(t);h&&!se(h,w)&&n(),h=w,g=requestAnimationFrame(p)}return n(),()=>{var w;u.forEach(x=>{i&&x.removeEventListener("scroll",n),s&&x.removeEventListener("resize",n)}),l?.(),(w=f)==null||w.disconnect(),f=null,a&&cancelAnimationFrame(g)}}var ze=At,Ie=zt,Xe=kt,je=It,Ye=Nt,qe=jt,Ke=$t,Ue=Wt,Ge=Vt,Je=Xt,Qe=(t,e,n)=>{let o=new Map,i={platform:De,...n},s={...i.platform,_c:o};return Bt(t,e,{...i,platform:s})};export{Ue as arrow,Xe as autoPlacement,_e as autoUpdate,Qe as computePosition,ze as detectOverflow,Ye as flip,J as getOverflowAncestors,Ke as hide,Ge as inline,Je as limitShift,Ie as offset,De as platform,je as shift,qe as size}; +//# sourceMappingURL=dom.bundle.mjs.map \ No newline at end of file diff --git a/assets/stylesheets/_bootstrap-grid.scss b/assets/stylesheets/_bootstrap-grid.scss deleted file mode 100644 index 5185c78f..00000000 --- a/assets/stylesheets/_bootstrap-grid.scss +++ /dev/null @@ -1,62 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Grid); - -$include-column-box-sizing: true !default; - -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; - -@import "bootstrap/mixins/breakpoints"; -@import "bootstrap/mixins/container"; -@import "bootstrap/mixins/grid"; -@import "bootstrap/mixins/utilities"; - -@import "bootstrap/vendor/rfs"; - -@import "bootstrap/containers"; -@import "bootstrap/grid"; - -@import "bootstrap/utilities"; -// Only use the utilities we need -// stylelint-disable-next-line scss/dollar-variable-default -$utilities: map-get-multiple( - $utilities, - ( - "bootstrap/display", - "bootstrap/order", - "bootstrap/flex", - "bootstrap/flex-direction", - "bootstrap/flex-grow", - "bootstrap/flex-shrink", - "bootstrap/flex-wrap", - "bootstrap/justify-content", - "bootstrap/align-items", - "bootstrap/align-content", - "bootstrap/align-self", - "bootstrap/margin", - "bootstrap/margin-x", - "bootstrap/margin-y", - "bootstrap/margin-top", - "bootstrap/margin-end", - "bootstrap/margin-bottom", - "bootstrap/margin-start", - "bootstrap/negative-margin", - "bootstrap/negative-margin-x", - "bootstrap/negative-margin-y", - "bootstrap/negative-margin-top", - "bootstrap/negative-margin-end", - "bootstrap/negative-margin-bottom", - "bootstrap/negative-margin-start", - "bootstrap/padding", - "bootstrap/padding-x", - "bootstrap/padding-y", - "bootstrap/padding-top", - "bootstrap/padding-end", - "bootstrap/padding-bottom", - "bootstrap/padding-start", - ) -); - -@import "bootstrap/utilities/api"; diff --git a/assets/stylesheets/_bootstrap-reboot.scss b/assets/stylesheets/_bootstrap-reboot.scss deleted file mode 100644 index 9d4266ed..00000000 --- a/assets/stylesheets/_bootstrap-reboot.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Reboot); - -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/root"; -@import "bootstrap/reboot"; diff --git a/assets/stylesheets/_bootstrap-utilities.scss b/assets/stylesheets/_bootstrap-utilities.scss deleted file mode 100644 index 475783b8..00000000 --- a/assets/stylesheets/_bootstrap-utilities.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Utilities); - -// Configuration -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/utilities"; - -// Layout & components -@import "bootstrap/root"; - -// Helpers -@import "bootstrap/helpers"; - -// Utilities -@import "bootstrap/utilities/api"; diff --git a/assets/stylesheets/_bootstrap.scss b/assets/stylesheets/_bootstrap.scss index c2dfdf3a..f21610bc 100644 --- a/assets/stylesheets/_bootstrap.scss +++ b/assets/stylesheets/_bootstrap.scss @@ -1,52 +1,47 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(""); - +@forward "bootstrap/banner"; // scss-docs-start import-stack -// Configuration -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/utilities"; +@forward "bootstrap/colors"; + +// Global CSS variables, layer definitions, and configuration +@forward "bootstrap/root"; + +// Subdir imports +@forward "bootstrap/content"; +@forward "bootstrap/layout"; +@forward "bootstrap/forms"; +@forward "bootstrap/buttons"; -// Layout & components -@import "bootstrap/root"; -@import "bootstrap/reboot"; -@import "bootstrap/type"; -@import "bootstrap/images"; -@import "bootstrap/containers"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; -@import "bootstrap/transitions"; -@import "bootstrap/dropdown"; -@import "bootstrap/button-group"; -@import "bootstrap/nav"; -@import "bootstrap/navbar"; -@import "bootstrap/card"; -@import "bootstrap/accordion"; -@import "bootstrap/breadcrumb"; -@import "bootstrap/pagination"; -@import "bootstrap/badge"; -@import "bootstrap/alert"; -@import "bootstrap/progress"; -@import "bootstrap/list-group"; -@import "bootstrap/close"; -@import "bootstrap/toasts"; -@import "bootstrap/modal"; -@import "bootstrap/tooltip"; -@import "bootstrap/popover"; -@import "bootstrap/carousel"; -@import "bootstrap/spinners"; -@import "bootstrap/offcanvas"; -@import "bootstrap/placeholders"; +// Components +@forward "bootstrap/accordion"; +@forward "bootstrap/alert"; +@forward "bootstrap/avatar"; +@forward "bootstrap/badge"; +@forward "bootstrap/breadcrumb"; +@forward "bootstrap/chip"; +@forward "bootstrap/card"; +@forward "bootstrap/carousel"; +@forward "bootstrap/datepicker"; +@forward "bootstrap/dialog"; +@forward "bootstrap/menu"; +@forward "bootstrap/list-group"; +@forward "bootstrap/nav"; +@forward "bootstrap/nav-overflow"; +@forward "bootstrap/navbar"; +@forward "bootstrap/drawer"; +@forward "bootstrap/pagination"; +@forward "bootstrap/placeholder"; +@forward "bootstrap/popover"; +@forward "bootstrap/progress"; +@forward "bootstrap/spinner"; +@forward "bootstrap/stepper"; +@forward "bootstrap/toasts"; +@forward "bootstrap/tooltip"; +@forward "bootstrap/transitions"; // Helpers -@import "bootstrap/helpers"; +@forward "bootstrap/helpers"; // Utilities -@import "bootstrap/utilities/api"; +@forward "bootstrap/utilities/api"; // scss-docs-end import-stack diff --git a/assets/stylesheets/bootstrap/_accordion.scss b/assets/stylesheets/bootstrap/_accordion.scss index e9f267fb..6a9d7d56 100644 --- a/assets/stylesheets/bootstrap/_accordion.scss +++ b/assets/stylesheets/bootstrap/_accordion.scss @@ -1,153 +1,171 @@ -// -// Base styles -// - -.accordion { - // scss-docs-start accordion-css-vars - --#{$prefix}accordion-color: #{$accordion-color}; - --#{$prefix}accordion-bg: #{$accordion-bg}; - --#{$prefix}accordion-transition: #{$accordion-transition}; - --#{$prefix}accordion-border-color: #{$accordion-border-color}; - --#{$prefix}accordion-border-width: #{$accordion-border-width}; - --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; - --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; - --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; - --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; - --#{$prefix}accordion-btn-color: #{$accordion-button-color}; - --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; - --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; - --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; - --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; - --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; - --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; - --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; - --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; - --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; - --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; - --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; - // scss-docs-end accordion-css-vars -} +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/focus-ring" as *; +@use "mixins/tokens" as *; + +$accordion-tokens: () !default; + +// scss-docs-start accordion-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$accordion-tokens: defaults( + ( + --accordion-padding-x: 1.25rem, + --accordion-padding-y: 1rem, + --accordion-color: var(--fg-body), + --accordion-bg: var(--bg-body), + --accordion-transition-property: "color, background-color, border-radius", + --accordion-transition-timing: ".15s ease-in-out", + --accordion-transition: var(--accordion-transition-property) var(--accordion-timing), + --accordion-border-color: var(--border-color), + --accordion-border-width: var(--border-width), + --accordion-border-radius: var(--accordion-radius, var(--radius-7)), + --accordion-btn-color: var(--fg-2), + --accordion-btn-bg: var(--bg-body), + --accordion-btn-icon-width: 1rem, + --accordion-btn-icon-transform: rotate(-180deg), + --accordion-btn-icon-transition: transform .2s ease-in-out, + --accordion-active-color: var(--fg), + --accordion-active-bg: var(--bg-2), + ), + $accordion-tokens +); +// scss-docs-end accordion-tokens + +@layer components { + .accordion { + @include tokens($accordion-tokens); + } -.accordion-button { - position: relative; - display: flex; - align-items: center; - width: 100%; - padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); - @include font-size($font-size-base); - color: var(--#{$prefix}accordion-btn-color); - text-align: left; // Reset button style - background-color: var(--#{$prefix}accordion-btn-bg); - border: 0; - @include border-radius(0); - overflow-anchor: none; - @include transition(var(--#{$prefix}accordion-transition)); - - &:not(.collapsed) { - color: var(--#{$prefix}accordion-active-color); - background-color: var(--#{$prefix}accordion-active-bg); - box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list - - &::after { - background-image: var(--#{$prefix}accordion-btn-active-icon); - transform: var(--#{$prefix}accordion-btn-icon-transform); + .accordion-header { + display: flex; + align-items: center; + width: 100%; + padding: var(--accordion-btn-padding-y, var(--accordion-padding-y)) var(--accordion-btn-padding-x, var(--accordion-padding-x)); + font-size: var(--accordion-font-size, var(--font-size-base)); + color: var(--accordion-btn-color); + text-align: start; + list-style: none; // Remove default marker + cursor: pointer; + background-color: var(--accordion-btn-bg); + @include transition(var(--accordion-transition)); + + &::-webkit-details-marker { + display: none; } - } - // Accordion icon - &::after { - flex-shrink: 0; - width: var(--#{$prefix}accordion-btn-icon-width); - height: var(--#{$prefix}accordion-btn-icon-width); - margin-left: auto; - content: ""; - background-image: var(--#{$prefix}accordion-btn-icon); - background-repeat: no-repeat; - background-size: var(--#{$prefix}accordion-btn-icon-width); - @include transition(var(--#{$prefix}accordion-btn-icon-transition)); - } + .accordion-icon { + flex-shrink: 0; + width: var(--accordion-btn-icon-width); + height: var(--accordion-btn-icon-width); + margin-inline-start: auto; + color: currentcolor; + @include transition(var(--accordion-btn-icon-transition)); + } - &:hover { - z-index: 2; - } + &:hover { + z-index: 2; + } - &:focus { - z-index: 3; - outline: 0; - box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); + &:focus-visible { + position: relative; + z-index: 3; + @include focus-ring(true); + outline-offset: -1px; + } } -} -.accordion-header { - margin-bottom: 0; -} + .accordion-item { + color: var(--accordion-color); + background-color: var(--accordion-bg); + border: var(--accordion-border-width) solid var(--accordion-border-color); -.accordion-item { - color: var(--#{$prefix}accordion-color); - background-color: var(--#{$prefix}accordion-bg); - border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); + @media (prefers-reduced-motion: no-preference) { + interpolate-size: allow-keywords; + } + + &::details-content { + block-size: 0; + overflow-y: clip; + @include transition(content-visibility .2s allow-discrete, block-size .2s); + } - &:first-of-type { - @include border-top-radius(var(--#{$prefix}accordion-border-radius)); + &:first-of-type { + @include border-top-radius(var(--accordion-border-radius)); - > .accordion-header .accordion-button { - @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); + > .accordion-header { + @include border-top-radius(calc(var(--accordion-border-radius) - var(--accordion-border-width))); + } } - } - &:not(:first-of-type) { - border-top: 0; - } + &:not(:first-of-type) { + border-block-start: 0; + } - // Only set a border-radius on the last item if the accordion is collapsed - &:last-of-type { - @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius(var(--accordion-border-radius)); + + > .accordion-header { + @include border-bottom-radius(calc(var(--accordion-border-radius) - var(--accordion-border-width))); + } - > .accordion-header .accordion-button { - &.collapsed { - @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); + > .accordion-body { + @include border-bottom-radius(var(--accordion-border-radius)); } } - > .accordion-collapse { - @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + // Open state - details[open] applies these styles + &[open] { + + border-color: var(--theme-border, var(--accordion-border-color)); + &::details-content { + block-size: auto; + } + + > .accordion-header { + color: var(--theme-fg, var(--accordion-active-color)); + background-color: var(--theme-bg-subtle, var(--accordion-active-bg)); + box-shadow: inset 0 calc(-1 * var(--accordion-border-width)) 0 var(--theme-border, var(--accordion-border-color)); + + .accordion-icon { + transform: var(--accordion-btn-icon-transform); + } + } + + // Remove bottom radius from header when open (even on last item) + &:last-of-type > .accordion-header { + @include border-bottom-radius(0); + } } } -} -.accordion-body { - padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); -} + .accordion-body { + padding: var(--accordion-body-padding-y, var(--accordion-padding-y)) var(--accordion-body-padding-x, var(--accordion-padding-x)); + } -// Flush accordion items -// -// Remove borders and border-radius to keep accordion items edge-to-edge. + // Flush accordion items + // + // Remove borders and border-radius to keep accordion items edge-to-edge. -.accordion-flush { - > .accordion-item { - border-right: 0; - border-left: 0; - @include border-radius(0); + .accordion-flush { + > .accordion-item { + border-inline: 0; + @include border-radius(0); - &:first-child { border-top: 0; } - &:last-child { border-bottom: 0; } + &:first-child { + border-block-start: 0; + } - // stylelint-disable selector-max-class - > .accordion-collapse, - > .accordion-header .accordion-button, - > .accordion-header .accordion-button.collapsed { - @include border-radius(0); - } - // stylelint-enable selector-max-class - } -} + &:last-child { + border-block-end: 0; + } -@if $enable-dark-mode { - @include color-mode(dark) { - .accordion-button::after { - --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; - --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; + > .accordion-header, + > .accordion-body { + @include border-radius(0); + } } } } diff --git a/assets/stylesheets/bootstrap/_alert.scss b/assets/stylesheets/bootstrap/_alert.scss index b8cff9b7..10077c8b 100644 --- a/assets/stylesheets/bootstrap/_alert.scss +++ b/assets/stylesheets/bootstrap/_alert.scss @@ -1,68 +1,55 @@ -// -// Base styles -// - -.alert { - // scss-docs-start alert-css-vars - --#{$prefix}alert-bg: transparent; - --#{$prefix}alert-padding-x: #{$alert-padding-x}; - --#{$prefix}alert-padding-y: #{$alert-padding-y}; - --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; - --#{$prefix}alert-color: inherit; - --#{$prefix}alert-border-color: transparent; - --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); - --#{$prefix}alert-border-radius: #{$alert-border-radius}; - --#{$prefix}alert-link-color: inherit; - // scss-docs-end alert-css-vars - - position: relative; - padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); - margin-bottom: var(--#{$prefix}alert-margin-bottom); - color: var(--#{$prefix}alert-color); - background-color: var(--#{$prefix}alert-bg); - border: var(--#{$prefix}alert-border); - @include border-radius(var(--#{$prefix}alert-border-radius)); -} - -// Headings for larger alerts -.alert-heading { - // Specified to prevent conflicts of changing $headings-color - color: inherit; -} - -// Provide class for links that match alerts -.alert-link { - font-weight: $alert-link-font-weight; - color: var(--#{$prefix}alert-link-color); -} - - -// Dismissible alerts -// -// Expand the right padding and account for the close button's positioning. - -.alert-dismissible { - padding-right: $alert-dismissible-padding-r; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$alert-tokens: () !default; + +// scss-docs-start alert-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$alert-tokens: defaults( + ( + --alert-gap: var(--spacer-3), + --alert-bg: var(--theme-bg-subtle, var(--bg-1)), + --alert-padding-x: var(--spacer), + --alert-padding-y: var(--spacer), + --alert-color: var(--theme-fg, inherit), + --alert-border-color: var(--theme-border, var(--border-color)), + --alert-border: var(--border-width) solid var(--alert-border-color), + --alert-border-radius: var(--radius-5), + --alert-link-color: inherit, + --hr-border-color: var(--theme-border, var(--border-color)), + ), + $alert-tokens +); +// scss-docs-end alert-tokens + +@layer components { + .alert { + @include tokens($alert-tokens); + + display: flex; + gap: var(--alert-gap); + align-items: start; + padding: var(--alert-padding-y) var(--alert-padding-x); + color: var(--alert-color); + background-color: var(--alert-bg); + border: var(--alert-border); + @include border-radius(var(--alert-border-radius)); + } - // Adjust close link position - .btn-close { - position: absolute; - top: 0; - right: 0; - z-index: $stretched-link-z-index + 1; - padding: $alert-padding-y * 1.25 $alert-padding-x; + .alert > p { + margin-bottom: 0; } -} + .alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; + } -// scss-docs-start alert-modifiers -// Generate contextual modifier classes for colorizing the alert -@each $state in map-keys($theme-colors) { - .alert-#{$state} { - --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); + // Provide class for links that match alerts + .alert-link { + font-weight: var(--font-weight-semibold); + color: var(--alert-link-color); } } -// scss-docs-end alert-modifiers diff --git a/assets/stylesheets/bootstrap/_avatar.scss b/assets/stylesheets/bootstrap/_avatar.scss new file mode 100644 index 00000000..14326ace --- /dev/null +++ b/assets/stylesheets/bootstrap/_avatar.scss @@ -0,0 +1,159 @@ +@use "sass:map"; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +$avatar-tokens: () !default; + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start avatar-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$avatar-tokens: defaults( + ( + --avatar-size: 2.5rem, + --avatar-border-radius: 50%, + --avatar-border-width: 2px, + --avatar-border-color: var(--bg-body), + --avatar-bg: var(--bg-2), + --avatar-color: var(--fg-body), + // --avatar-font-weight: var(--font-weight-medium), // Defaults to fallback + --avatar-status-size: .75rem, + --avatar-status-border-width: 2px, + --avatar-status-border-color: var(--bg-body), + --avatar-stack-spacing: -.3, + --avatar-stack-transition: "transform .2s ease-in-out", + ), + $avatar-tokens +); +// scss-docs-end avatar-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start avatar-sizes +$avatar-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$avatar-sizes: defaults( + ( + "xs": ( + size: 1.5rem, + status-size: .625rem, + ), + "sm": ( + size: 2rem, + ), + "lg": ( + size: 3rem, + status-size: 1rem, + border-width: 3px, + ), + "xl": ( + size: 4rem, + status-size: 1.25rem, + border-width: 4px, + ), + ), + $avatar-sizes +); +// scss-docs-end avatar-sizes + +@layer components { + .avatar { + @include tokens($avatar-tokens); + + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--avatar-size); + height: var(--avatar-size); + font-size: calc(var(--avatar-size) * .4); + font-weight: var(--avatar-font-weight, var(--font-weight-medium)); + line-height: 1; + color: var(--theme-contrast, var(--avatar-color)); + text-transform: uppercase; + vertical-align: middle; + background-color: var(--theme-bg, var(--avatar-bg)); + @include border-radius(var(--avatar-border-radius)); + + > .avatar-img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .avatar-subtle { + color: var(--theme-fg, var(--avatar-color)); + background-color: var(--theme-bg-subtle, var(--avatar-bg)); + } + + .avatar-img { + @include border-radius(var(--avatar-border-radius, 50%)); + } + + .avatar-status { + position: absolute; + right: calc(var(--avatar-status-border-width) * -1); + bottom: calc(var(--avatar-status-border-width) * -1); + width: var(--avatar-status-size); + height: var(--avatar-status-size); + background-color: var(--gray-400); + border: var(--avatar-status-border-width) solid var(--avatar-status-border-color); + @include border-radius(50%); + + &.status-online { + background-color: var(--green-500); + } + + &.status-offline { + background-color: var(--gray-400); + @include border-radius(20%); + } + + &.status-busy { + background-color: var(--red-500); + @include border-radius(20%); + } + + &.status-away { + background-color: var(--yellow-500); + } + } + + .avatar-stack { + display: inline-flex; + flex-direction: row-reverse; + + .avatar { + // Stack spacing is calculated as a percentage of avatar size + margin-left: calc(var(--avatar-size) * var(--avatar-stack-spacing)); + border: var(--avatar-border-width) solid var(--avatar-border-color); + mask-image: none; + @include transition(var(--avatar-stack-transition)); + + &:last-child { + margin-left: 0; + } + + &:hover { + z-index: 1; + transform: translateY(-2px); + } + } + } + + @each $size, $tokens in $avatar-sizes { + .avatar-#{$size}, + .avatar-stack-#{$size} > .avatar { + --avatar-size: #{map.get($tokens, size)}; + + @if map.has-key($tokens, status-size) { + --avatar-status-size: #{map.get($tokens, status-size)}; + } + + @if map.has-key($tokens, border-width) { + --avatar-border-width: #{map.get($tokens, border-width)}; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/_badge.scss b/assets/stylesheets/bootstrap/_badge.scss index cc3d2695..7fdb38d8 100644 --- a/assets/stylesheets/bootstrap/_badge.scss +++ b/assets/stylesheets/bootstrap/_badge.scss @@ -1,38 +1,90 @@ -// Base class -// -// Requires one of the contextual, color modifier classes for `color` and -// `background-color`. - -.badge { - // scss-docs-start badge-css-vars - --#{$prefix}badge-padding-x: #{$badge-padding-x}; - --#{$prefix}badge-padding-y: #{$badge-padding-y}; - @include rfs($badge-font-size, --#{$prefix}badge-font-size); - --#{$prefix}badge-font-weight: #{$badge-font-weight}; - --#{$prefix}badge-color: #{$badge-color}; - --#{$prefix}badge-border-radius: #{$badge-border-radius}; - // scss-docs-end badge-css-vars - - display: inline-block; - padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); - @include font-size(var(--#{$prefix}badge-font-size)); - font-weight: var(--#{$prefix}badge-font-weight); - line-height: 1; - color: var(--#{$prefix}badge-color); - text-align: center; - white-space: nowrap; - vertical-align: baseline; - @include border-radius(var(--#{$prefix}badge-border-radius)); - @include gradient-bg(); - - // Empty badges collapse automatically - &:empty { - display: none; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$badge-tokens: () !default; + +// scss-docs-start badge-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$badge-tokens: defaults( + ( + --badge-padding-x: .625em, + --badge-padding-y: .25em, + --badge-font-size: clamp(12px, .75em, .75em), + --badge-font-weight: var(--font-weight-semibold), + --badge-color: inherit, + --badge-bg: var(--bg-2), + --badge-border-width: var(--border-width), + --badge-border-color: transparent, + --badge-border-radius: var(--radius-7), + ), + $badge-tokens +); +// scss-docs-end badge-tokens + +// scss-docs-start badge-variants +$badge-variants: ( + "subtle": ( + "color": "fg", + "bg": "bg-subtle", + "border-color": "transparent" + ), + "outline": ( + "color": "fg", + "bg": "transparent", + "border-color": "border" + ) +) !default; +// scss-docs-end badge-variants + +@layer components { + .badge { + @include tokens($badge-tokens); + + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.375rem; + padding: var(--badge-padding-y) var(--badge-padding-x); + font-size: var(--badge-font-size); + font-weight: var(--badge-font-weight); + line-height: 1; + color: var(--theme-contrast, var(--badge-color)); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: var(--theme-bg, var(--badge-bg)); + border: var(--badge-border-width) solid var(--badge-border-color); + @include border-radius(var(--badge-border-radius)); + // @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } + } + + // Quick fix for badges in buttons + .btn .badge { + position: relative; + top: -1px; } -} -// Quick fix for badges in buttons -.btn .badge { - position: relative; - top: -1px; + // scss-docs-start badge-variant-loop + @each $variant, $properties in $badge-variants { + .badge-#{$variant} { + @each $property, $value in $properties { + @if $value == "transparent" { + --badge-#{$property}: transparent; + } @else { + --badge-#{$property}: var(--theme-#{$value}); + } + } + + color: var(--badge-color); + background-color: var(--badge-bg); + border-color: var(--badge-border-color); + } + } + // scss-docs-end badge-variant-loop } diff --git a/assets/stylesheets/bootstrap/_banner.scss b/assets/stylesheets/bootstrap/_banner.scss new file mode 100644 index 00000000..c96aa3ca --- /dev/null +++ b/assets/stylesheets/bootstrap/_banner.scss @@ -0,0 +1,7 @@ +$file: "" !default; + +/*! + * Bootstrap #{$file} v6.0.0-dev (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/assets/stylesheets/bootstrap/_breadcrumb.scss b/assets/stylesheets/bootstrap/_breadcrumb.scss index b8252ff2..430999d6 100644 --- a/assets/stylesheets/bootstrap/_breadcrumb.scss +++ b/assets/stylesheets/bootstrap/_breadcrumb.scss @@ -1,40 +1,90 @@ -.breadcrumb { - // scss-docs-start breadcrumb-css-vars - --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; - --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; - --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; - @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); - --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; - --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; - --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; - --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; - --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; - // scss-docs-end breadcrumb-css-vars - - display: flex; - flex-wrap: wrap; - padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); - margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); - @include font-size(var(--#{$prefix}breadcrumb-font-size)); - list-style: none; - background-color: var(--#{$prefix}breadcrumb-bg); - @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); -} +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/mask-icon" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +$breadcrumb-tokens: () !default; + +// scss-docs-start breadcrumb-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$breadcrumb-tokens: defaults( + ( + --breadcrumb-margin-bottom: 1rem, + --breadcrumb-font-size: inherit, + --breadcrumb-bg: transparent, + --breadcrumb-border-radius: var(--radius-5), + --breadcrumb-divider-color: var(--fg-4), + --breadcrumb-divider-icon: #{escape-svg(url("data:image/svg+xml,"))}, + --breadcrumb-divider-width: .375rem, + --breadcrumb-divider-height: .75rem, + --breadcrumb-link-padding-x: .75rem, + --breadcrumb-link-padding-y: .25rem, + --breadcrumb-link-color: var(--fg-3), + --breadcrumb-link-hover-color: var(--fg-2), + --breadcrumb-link-hover-bg: var(--bg-1), + --breadcrumb-link-active-color: var(--fg-1), + --breadcrumb-link-border-radius: var(--radius-7), + ), + $breadcrumb-tokens +); +// scss-docs-end breadcrumb-tokens + +@layer components { + .breadcrumb { + @include tokens($breadcrumb-tokens); + + display: flex; + flex-wrap: wrap; + align-items: center; + padding: var(--breadcrumb-padding-y, 0) var(--breadcrumb-padding-x, 0); + font-size: var(--breadcrumb-font-size); + list-style-type: ""; + background-color: var(--breadcrumb-bg); + @include border-radius(var(--breadcrumb-border-radius)); + } -.breadcrumb-item { - // The separator between breadcrumbs (by default, a forward-slash: "/") - + .breadcrumb-item { - padding-left: var(--#{$prefix}breadcrumb-item-padding-x); + .breadcrumb-item { + display: flex; + } + + .breadcrumb-divider { + margin-inline: calc(var(--breadcrumb-link-padding-x) / 4); + color: var(--breadcrumb-divider-color); - &::before { - float: left; // Suppress inline spacings and underlining of the separator - padding-right: var(--#{$prefix}breadcrumb-item-padding-x); - color: var(--#{$prefix}breadcrumb-divider-color); - content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; + // Render a default chevron, painted with `currentcolor` via a mask, when the + // divider has no explicit content. Any content (an inline SVG, a text + // character, etc.) added to the element overrides this default. + &:empty::before { + display: block; + width: var(--breadcrumb-divider-width); + height: var(--breadcrumb-divider-height); + content: ""; + background-color: currentcolor; + @include mask-icon(var(--breadcrumb-divider-icon)); } } - &.active { - color: var(--#{$prefix}breadcrumb-item-active-color); + .breadcrumb-link { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 2.25rem; + padding: var(--breadcrumb-link-padding-y) var(--breadcrumb-link-padding-x); + color: var(--breadcrumb-link-color); + text-decoration: none; + @include border-radius(var(--breadcrumb-link-border-radius)); + @include transition(.1s text-decoration-color ease-in-out); + + &:hover { + z-index: 2; + color: var(--breadcrumb-link-hover-color); + background-color: var(--breadcrumb-link-hover-bg); + } + + &.active { + color: var(--breadcrumb-link-active-color); + } } } diff --git a/assets/stylesheets/bootstrap/_button-group.scss b/assets/stylesheets/bootstrap/_button-group.scss deleted file mode 100644 index 78e12522..00000000 --- a/assets/stylesheets/bootstrap/_button-group.scss +++ /dev/null @@ -1,147 +0,0 @@ -// Make the div behave like a button -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-flex; - vertical-align: middle; // match .btn alignment given font-size hack above - - > .btn { - position: relative; - flex: 1 1 auto; - } - - // Bring the hover, focused, and "active" buttons to the front to overlay - // the borders properly - > .btn-check:checked + .btn, - > .btn-check:focus + .btn, - > .btn:hover, - > .btn:focus, - > .btn:active, - > .btn.active { - z-index: 1; - } -} - -// Optional: Group multiple button groups together for a toolbar -.btn-toolbar { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - - .input-group { - width: auto; - } -} - -.btn-group { - @include border-radius($btn-border-radius); - - // Prevent double borders when buttons are next to each other - > :not(.btn-check:first-child) + .btn, - > .btn-group:not(:first-child) { - margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list - } - - // Reset rounded corners - > .btn:not(:last-child):not(.dropdown-toggle), - > .btn.dropdown-toggle-split:first-child, - > .btn-group:not(:last-child) > .btn { - @include border-end-radius(0); - } - - // The left radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, - > .btn-group:not(:first-child) > .btn { - @include border-start-radius(0); - } -} - -// Sizing -// -// Remix the default button sizing classes into new ones for easier manipulation. - -.btn-group-sm > .btn { @extend .btn-sm; } -.btn-group-lg > .btn { @extend .btn-lg; } - - -// -// Split button dropdowns -// - -.dropdown-toggle-split { - padding-right: $btn-padding-x * .75; - padding-left: $btn-padding-x * .75; - - &::after, - .dropup &::after, - .dropend &::after { - margin-left: 0; - } - - .dropstart &::before { - margin-right: 0; - } -} - -.btn-sm + .dropdown-toggle-split { - padding-right: $btn-padding-x-sm * .75; - padding-left: $btn-padding-x-sm * .75; -} - -.btn-lg + .dropdown-toggle-split { - padding-right: $btn-padding-x-lg * .75; - padding-left: $btn-padding-x-lg * .75; -} - - -// The clickable button for toggling the menu -// Set the same inset shadow as the :active state -.btn-group.show .dropdown-toggle { - @include box-shadow($btn-active-box-shadow); - - // Show no shadow for `.btn-link` since it has no other button styles. - &.btn-link { - @include box-shadow(none); - } -} - - -// -// Vertical button groups -// - -.btn-group-vertical { - flex-direction: column; - align-items: flex-start; - justify-content: center; - - > .btn, - > .btn-group { - width: 100%; - } - - > .btn:not(:first-child), - > .btn-group:not(:first-child) { - margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list - } - - // Reset rounded corners - > .btn:not(:last-child):not(.dropdown-toggle), - > .btn-group:not(:last-child) > .btn { - @include border-bottom-radius(0); - } - - // The top radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, - > .btn-group:not(:first-child) > .btn { - @include border-top-radius(0); - } -} diff --git a/assets/stylesheets/bootstrap/_buttons.scss b/assets/stylesheets/bootstrap/_buttons.scss deleted file mode 100644 index caa4518a..00000000 --- a/assets/stylesheets/bootstrap/_buttons.scss +++ /dev/null @@ -1,216 +0,0 @@ -// -// Base styles -// - -.btn { - // scss-docs-start btn-css-vars - --#{$prefix}btn-padding-x: #{$btn-padding-x}; - --#{$prefix}btn-padding-y: #{$btn-padding-y}; - --#{$prefix}btn-font-family: #{$btn-font-family}; - @include rfs($btn-font-size, --#{$prefix}btn-font-size); - --#{$prefix}btn-font-weight: #{$btn-font-weight}; - --#{$prefix}btn-line-height: #{$btn-line-height}; - --#{$prefix}btn-color: #{$btn-color}; - --#{$prefix}btn-bg: transparent; - --#{$prefix}btn-border-width: #{$btn-border-width}; - --#{$prefix}btn-border-color: transparent; - --#{$prefix}btn-border-radius: #{$btn-border-radius}; - --#{$prefix}btn-hover-border-color: transparent; - --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; - --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; - --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); - // scss-docs-end btn-css-vars - - display: inline-block; - padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); - font-family: var(--#{$prefix}btn-font-family); - @include font-size(var(--#{$prefix}btn-font-size)); - font-weight: var(--#{$prefix}btn-font-weight); - line-height: var(--#{$prefix}btn-line-height); - color: var(--#{$prefix}btn-color); - text-align: center; - text-decoration: if($link-decoration == none, null, none); - white-space: $btn-white-space; - vertical-align: middle; - cursor: if($enable-button-pointers, pointer, null); - user-select: none; - border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); - @include border-radius(var(--#{$prefix}btn-border-radius)); - @include gradient-bg(var(--#{$prefix}btn-bg)); - @include box-shadow(var(--#{$prefix}btn-box-shadow)); - @include transition($btn-transition); - - &:hover { - color: var(--#{$prefix}btn-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - background-color: var(--#{$prefix}btn-hover-bg); - border-color: var(--#{$prefix}btn-hover-border-color); - } - - .btn-check + &:hover { - // override for the checkbox/radio buttons - color: var(--#{$prefix}btn-color); - background-color: var(--#{$prefix}btn-bg); - border-color: var(--#{$prefix}btn-border-color); - } - - &:focus-visible { - color: var(--#{$prefix}btn-hover-color); - @include gradient-bg(var(--#{$prefix}btn-hover-bg)); - border-color: var(--#{$prefix}btn-hover-border-color); - outline: 0; - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - .btn-check:focus-visible + & { - border-color: var(--#{$prefix}btn-hover-border-color); - outline: 0; - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - .btn-check:checked + &, - :not(.btn-check) + &:active, - &:first-child:active, - &.active, - &.show { - color: var(--#{$prefix}btn-active-color); - background-color: var(--#{$prefix}btn-active-bg); - // Remove CSS gradients if they're enabled - background-image: if($enable-gradients, none, null); - border-color: var(--#{$prefix}btn-active-border-color); - @include box-shadow(var(--#{$prefix}btn-active-shadow)); - - &:focus-visible { - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - } - - .btn-check:checked:focus-visible + & { - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - &:disabled, - &.disabled, - fieldset:disabled & { - color: var(--#{$prefix}btn-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}btn-disabled-bg); - background-image: if($enable-gradients, none, null); - border-color: var(--#{$prefix}btn-disabled-border-color); - opacity: var(--#{$prefix}btn-disabled-opacity); - @include box-shadow(none); - } -} - - -// -// Alternate buttons -// - -// scss-docs-start btn-variant-loops -@each $color, $value in $theme-colors { - .btn-#{$color} { - @if $color == "light" { - @include button-variant( - $value, - $value, - $hover-background: shade-color($value, $btn-hover-bg-shade-amount), - $hover-border: shade-color($value, $btn-hover-border-shade-amount), - $active-background: shade-color($value, $btn-active-bg-shade-amount), - $active-border: shade-color($value, $btn-active-border-shade-amount) - ); - } @else if $color == "dark" { - @include button-variant( - $value, - $value, - $hover-background: tint-color($value, $btn-hover-bg-tint-amount), - $hover-border: tint-color($value, $btn-hover-border-tint-amount), - $active-background: tint-color($value, $btn-active-bg-tint-amount), - $active-border: tint-color($value, $btn-active-border-tint-amount) - ); - } @else { - @include button-variant($value, $value); - } - } -} - -@each $color, $value in $theme-colors { - .btn-outline-#{$color} { - @include button-outline-variant($value); - } -} -// scss-docs-end btn-variant-loops - - -// -// Link buttons -// - -// Make a button look and behave like a link -.btn-link { - --#{$prefix}btn-font-weight: #{$font-weight-normal}; - --#{$prefix}btn-color: #{$btn-link-color}; - --#{$prefix}btn-bg: transparent; - --#{$prefix}btn-border-color: transparent; - --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; - --#{$prefix}btn-hover-border-color: transparent; - --#{$prefix}btn-active-color: #{$btn-link-hover-color}; - --#{$prefix}btn-active-border-color: transparent; - --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; - --#{$prefix}btn-disabled-border-color: transparent; - --#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows - --#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb}; - - text-decoration: $link-decoration; - @if $enable-gradients { - background-image: none; - } - - &:hover, - &:focus-visible { - text-decoration: $link-hover-decoration; - } - - &:focus-visible { - color: var(--#{$prefix}btn-color); - } - - &:hover { - color: var(--#{$prefix}btn-hover-color); - } - - // No need for an active state here -} - - -// -// Button Sizes -// - -.btn-lg { - @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); -} - -.btn-sm { - @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); -} diff --git a/assets/stylesheets/bootstrap/_card.scss b/assets/stylesheets/bootstrap/_card.scss index dcebe6ac..ee99de75 100644 --- a/assets/stylesheets/bootstrap/_card.scss +++ b/assets/stylesheets/bootstrap/_card.scss @@ -1,235 +1,306 @@ -// -// Base styles -// - -.card { - // scss-docs-start card-css-vars - --#{$prefix}card-spacer-y: #{$card-spacer-y}; - --#{$prefix}card-spacer-x: #{$card-spacer-x}; - --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; - --#{$prefix}card-title-color: #{$card-title-color}; - --#{$prefix}card-subtitle-color: #{$card-subtitle-color}; - --#{$prefix}card-border-width: #{$card-border-width}; - --#{$prefix}card-border-color: #{$card-border-color}; - --#{$prefix}card-border-radius: #{$card-border-radius}; - --#{$prefix}card-box-shadow: #{$card-box-shadow}; - --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; - --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; - --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; - --#{$prefix}card-cap-bg: #{$card-cap-bg}; - --#{$prefix}card-cap-color: #{$card-cap-color}; - --#{$prefix}card-height: #{$card-height}; - --#{$prefix}card-color: #{$card-color}; - --#{$prefix}card-bg: #{$card-bg}; - --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; - --#{$prefix}card-group-margin: #{$card-group-margin}; - // scss-docs-end card-css-vars - - position: relative; - display: flex; - flex-direction: column; - min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 - height: var(--#{$prefix}card-height); - color: var(--#{$prefix}body-color); - word-wrap: break-word; - background-color: var(--#{$prefix}card-bg); - background-clip: border-box; - border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); - @include border-radius(var(--#{$prefix}card-border-radius)); - @include box-shadow(var(--#{$prefix}card-box-shadow)); - - > hr { - margin-right: 0; - margin-left: 0; - } - - > .list-group { - border-top: inherit; - border-bottom: inherit; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; +@use "layout/breakpoints" as *; + +$card-tokens: () !default; + +// scss-docs-start card-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$card-tokens: defaults( + ( + --card-spacer-y: var(--spacer-5), + --card-spacer-x: var(--spacer-5), + --card-subtitle-color: inherit, + --card-border-width: var(--border-width), + --card-border-color: var(--border-color-translucent), + --card-border-radius: var(--radius-7), + --card-box-shadow: none, + --card-inner-border-radius: calc(var(--radius-7) - var(--border-width)), + --card-cap-padding-y: var(--spacer-3), + --card-cap-padding-x: var(--spacer), + --card-cap-bg: var(--bg-1), + --card-cap-color: inherit, + --card-height: auto, + --card-color: inherit, + --card-bg: var(--bg-body), + --card-img-overlay-padding: var(--card-spacer-y), + --card-group-margin: #{$grid-gutter-x * .5}, + --card-body-gap: calc(var(--card-spacer-y) * .5), + ), + $card-tokens +); +// scss-docs-end card-tokens + +@layer components { + .card { + @include tokens($card-tokens); + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: var(--card-height); + color: var(--fg-body); + word-wrap: break-word; + background-color: var(--card-bg); + // border: var(--card-border-width) solid var(--card-border-color); + @include border-radius(var(--card-border-radius)); + @include box-shadow(var(--card-box-shadow)); + + > hr { + margin-inline: 0; + } + } + + .card-body { + display: flex; + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + flex-direction: column; + gap: var(--card-body-gap); + align-items: flex-start; + padding: var(--card-spacer-y) var(--card-spacer-x); + color: var(--card-color); + border: solid var(--theme-bg, var(--card-border-color)); + border-width: 0 var(--card-border-width); + + > * { + margin-block: 0; + } + } + + .card-body, + .card-list { + border: solid var(--theme-bg, var(--card-border-color)); + border-width: 0 var(--card-border-width); &:first-child { - border-top-width: 0; - @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); + @include border-top-radius(var(--card-border-radius)); + border-top-width: var(--card-border-width); } - &:last-child { - border-bottom-width: 0; - @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); + &:last-child { + @include border-bottom-radius(var(--card-border-radius)); + border-bottom-width: var(--card-border-width); + } + + &:not(:first-child, :last-child) { + border-block-end-width: var(--card-border-width); + } + + // The footer draws a full border (including its top edge), so a body/list + // segment that precedes it must not also draw a bottom border or the seam + // doubles up. + &:has(+ .card-footer) { + border-block-end-width: 0; } } - // Due to specificity of the above selector (`.card > .list-group`), we must - // use a child selector here to prevent double borders. - > .card-header + .list-group, - > .list-group + .card-footer { - border-top: 0; + .card-title, + .card-subtitle, + .card-text { + align-self: stretch; } -} -.card-body { - // Enable `flex-grow: 1` for decks and groups so that card blocks take up - // as much space as possible, ensuring footers are aligned to the bottom. - flex: 1 1 auto; - padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); - color: var(--#{$prefix}card-color); -} + .card-subtitle { + margin-top: calc(var(--card-body-gap) * -.5); + } -.card-title { - margin-bottom: var(--#{$prefix}card-title-spacer-y); - color: var(--#{$prefix}card-title-color); -} + .card-header { + padding: var(--card-cap-padding-y) var(--card-cap-padding-x); + margin-bottom: 0; // Removes the default margin-bottom of + color: var(--theme-contrast, var(--card-cap-color)); + background-color: var(--theme-bg, var(--card-cap-bg)); + border: var(--card-border-width) solid var(--theme-bg, var(--card-border-color)); -.card-subtitle { - margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list - margin-bottom: 0; - color: var(--#{$prefix}card-subtitle-color); -} + &:first-child { + @include border-radius(var(--card-inner-border-radius) var(--card-inner-border-radius) 0 0); + } + } -.card-text:last-child { - margin-bottom: 0; -} + .card-footer { + padding: var(--card-cap-padding-y) var(--card-cap-padding-x); + color: var(--card-cap-color); + background-color: var(--theme-bg, var(--card-cap-bg)); + border: var(--card-border-width) solid var(--theme-bg, var(--card-border-color)); -.card-link { - &:hover { - text-decoration: if($link-hover-decoration == underline, none, null); + &:last-child { + @include border-radius(0 0 var(--card-inner-border-radius) var(--card-inner-border-radius)); + } } - + .card-link { - margin-left: var(--#{$prefix}card-spacer-x); + .card-translucent { + background-color: color-mix(in oklch, var(--card-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + + .card-header, + .card-footer { + background-color: color-mix(in oklch, var(--card-cap-bg) 60%, transparent); + } } -} -// -// Optional textual caps -// + .card-subtle { + border-color: var(--theme-border, var(--card-border-color)); -.card-header { - padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); - margin-bottom: 0; // Removes the default margin-bottom of - color: var(--#{$prefix}card-cap-color); - background-color: var(--#{$prefix}card-cap-bg); - border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + .card-header { + color: var(--theme-fg-emphasis, currentcolor); + background-color: var(--theme-bg-subtle, var(--card-cap-bg)); + border-color: var(--theme-border, var(--card-border-color)); + } - &:first-child { - @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); + .card-footer { + color: var(--theme-fg-emphasis, currentcolor); + background-color: var(--theme-bg-subtle, var(--card-cap-bg)); + border-color: var(--theme-border, var(--card-border-color)); + } + + .card-body, + .card-list { + border-color: var(--theme-border, var(--card-border-color)); + } } -} -.card-footer { - padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); - color: var(--#{$prefix}card-cap-color); - background-color: var(--#{$prefix}card-cap-bg); - border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + // + // Header navs + // - &:last-child { - @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); - } -} + // Combined selector because of specificity match with `.nav` base class + .nav.card-header-tabs { + margin-inline: calc(-.5 * var(--card-cap-padding-x)); + margin-bottom: calc(-1 * var(--card-cap-padding-y) - var(--nav-tabs-border-width)); + border-block-end: 0; + .nav-link.active { + background-color: var(--card-bg); + border-block-end-color: var(--card-bg); + } + } -// -// Header navs -// + // Card image + .card-img-overlay { + position: absolute; + inset: 0; + padding: var(--card-img-overlay-padding); + @include border-radius(var(--card-inner-border-radius)); + } -.card-header-tabs { - margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - border-bottom: 0; + .card-img, + .card-img-top, + .card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch + outline: var(--card-border-width) solid var(--card-border-color); + outline-offset: calc(var(--card-border-width) * -1); + } - .nav-link.active { - background-color: var(--#{$prefix}card-bg); - border-bottom-color: var(--#{$prefix}card-bg); + .card-img, + .card-img-top { + @include border-top-radius(var(--card-inner-border-radius)); } -} -.card-header-pills { - margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list -} + .card-img, + .card-img-bottom { + @include border-bottom-radius(var(--card-inner-border-radius)); + } -// Card image -.card-img-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: var(--#{$prefix}card-img-overlay-padding); - @include border-radius(var(--#{$prefix}card-inner-border-radius)); -} + .card-row { + flex-direction: row; -.card-img, -.card-img-top, -.card-img-bottom { - width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch -} + .card-body, + .card-list { + border-width: var(--card-border-width) 0; + @include border-radius(0); -.card-img, -.card-img-top { - @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); -} + &:first-child { + @include border-start-radius(var(--card-inner-border-radius)); + border-inline-start-width: var(--card-border-width); + } -.card-img, -.card-img-bottom { - @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); -} + &:last-child { + @include border-end-radius(var(--card-inner-border-radius)); + border-inline-end-width: var(--card-border-width); + } + &:not(:first-child, :last-child) { + border-inline-end-width: var(--card-border-width); + } + } + } -// -// Card groups -// + .card-img-start { + @include border-start-radius(var(--card-inner-border-radius)); + } -.card-group { - // The child selector allows nested `.card` within `.card-group` - // to display properly. - > .card { - margin-bottom: var(--#{$prefix}card-group-margin); + .card-img-end { + @include border-end-radius(var(--card-inner-border-radius)); } - @include media-breakpoint-up(sm) { - display: flex; - flex-flow: row wrap; + // + // Card groups + // + + // Card groups lay out their cards in a row using a container query, so wrap the + // group in a query container (e.g., the `.contains-inline` utility) for the row + // layout to take effect. Without a query container the cards remain stacked. + .card-group { // The child selector allows nested `.card` within `.card-group` // to display properly. > .card { - flex: 1 0 0; - margin-bottom: 0; - - + .card { - margin-left: 0; - border-left: 0; - } - - // Handle rounded corners - @if $enable-rounded { - &:not(:last-child) { - @include border-end-radius(0); + margin-bottom: var(--card-group-margin); + } - > .card-img-top, - > .card-header { - // stylelint-disable-next-line property-disallowed-list - border-top-right-radius: 0; - } - > .card-img-bottom, - > .card-footer { - // stylelint-disable-next-line property-disallowed-list - border-bottom-right-radius: 0; - } + @include container-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + flex: 1 0 0; + margin-bottom: 0; + + // Borders now live on the inner segments (header, body, list, footer) + // and the card images use outlines, so adjacent cards would otherwise + // render a doubled-up border at each seam. Pull subsequent cards back by + // one border width so their leading edges overlap the previous card's + // trailing edge, collapsing the seam into a single line. Gap can't be + // negative, so this relies on a negative margin. + + .card { + margin-inline-start: calc(-1 * var(--card-border-width)); } - &:not(:first-child) { - @include border-start-radius(0); - - > .card-img-top, - > .card-header { - // stylelint-disable-next-line property-disallowed-list - border-top-left-radius: 0; + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + > .card-img-top, + > .card-header, + > .card-body { + border-start-end-radius: 0; + } + > .card-img-bottom, + > .card-footer, + > .card-body { + border-end-end-radius: 0; + } } - > .card-img-bottom, - > .card-footer { - // stylelint-disable-next-line property-disallowed-list - border-bottom-left-radius: 0; + + &:not(:first-child) { + @include border-start-radius(0); + + > .card-img-top, + > .card-header, + > .card-body { + border-start-start-radius: 0; + } + > .card-img-bottom, + > .card-footer, + > .card-body { + border-end-start-radius: 0; + } } } } diff --git a/assets/stylesheets/bootstrap/_carousel.scss b/assets/stylesheets/bootstrap/_carousel.scss index 5ebf6b15..2cbeab7b 100644 --- a/assets/stylesheets/bootstrap/_carousel.scss +++ b/assets/stylesheets/bootstrap/_carousel.scss @@ -1,226 +1,263 @@ -// Notes on the classes: -// -// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) -// even when their scroll action started on a carousel, but for compatibility (with Firefox) -// we're preventing all actions instead -// 2. The .carousel-item-start and .carousel-item-end is used to indicate where -// the active slide is heading. -// 3. .active.carousel-item is the current slide. -// 4. .active.carousel-item-start and .active.carousel-item-end is the current -// slide in its in-transition state. Only one of these occurs at a time. -// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end -// is the upcoming slide in transition. - -.carousel { - position: relative; -} - -.carousel.pointer-event { - touch-action: pan-y; -} +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; - @include clearfix(); -} +$carousel-tokens: () !default; -.carousel-item { - position: relative; - display: none; - float: left; - width: 100%; - margin-right: -100%; - backface-visibility: hidden; - @include transition($carousel-transition); -} +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start carousel-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$carousel-tokens: defaults( + ( + --carousel-gap: .75rem, + --carousel-indicator-bg: var(--fg-3), + --carousel-indicator-width: .75rem, + --carousel-indicator-height: .75rem, + --carousel-indicator-spacer: .25rem, + --carousel-indicator-transition: "opacity .6s ease, width .3s ease", + --carousel-indicator-progress-bg: var(--carousel-indicator-bg), + --carousel-control-icon-width: 1rem, + --carousel-control-prev-icon: url("data:image/svg+xml,"), + --carousel-control-next-icon: url("data:image/svg+xml,"), + --carousel-control-pause-icon: url("data:image/svg+xml,"), + --carousel-control-play-icon: url("data:image/svg+xml,"), + // Scroll-snap engine. `gap` must carry a length unit: it feeds the + // `.carousel-item` flex-basis `calc()`, and subtracting a unitless `0` from a + // percentage is invalid CSS (it would drop the whole declaration and collapse + // every slide to its content width). `peek` only feeds `padding-inline`/ + // `scroll-padding-inline`, so a bare `0` would be valid there, but we keep it + // unit-bearing for consistency. + --carousel-items: 1, + --carousel-items-gap: 0px, + --carousel-items-peek: 0px, + --carousel-fade-duration: .6s, + ), + $carousel-tokens +); +// scss-docs-end carousel-tokens +// stylelint-enable custom-property-no-missing-var-function -.carousel-item.active, -.carousel-item-next, -.carousel-item-prev { - display: block; -} +@layer components { + .carousel { + @include tokens($carousel-tokens); -.carousel-item-next:not(.carousel-item-start), -.active.carousel-item-end { - transform: translateX(100%); -} + position: relative; + display: flex; + flex-direction: column; + gap: var(--carousel-gap); + } -.carousel-item-prev:not(.carousel-item-end), -.active.carousel-item-start { - transform: translateX(-100%); -} + // The scroll viewport + .carousel-inner { + display: flex; + gap: var(--carousel-items-gap); + width: 100%; + padding-inline: var(--carousel-items-peek); + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; + scroll-padding-inline: var(--carousel-items-peek); + scrollbar-width: none; // Hide the scrollbar without losing scrollability + &::-webkit-scrollbar { + display: none; + } + } -// -// Alternate transitions -// + // Smooth programmatic/keyboard scrolling, disabled under reduced-motion + @media (prefers-reduced-motion: no-preference) { + .carousel-inner { + scroll-behavior: smooth; + } + } -.carousel-fade { .carousel-item { - opacity: 0; - transition-property: opacity; - transform: none; + // `100%` here is `.carousel-inner`'s content box, which `padding-inline` + // has already inset by the peek on each side, so the peek must NOT be + // subtracted again — doing so makes every slide `2 * peek` too narrow and + // the peek lopsided. Only the inter-slide gaps need removing. + flex: 0 0 calc((100% - (var(--carousel-items) - 1) * var(--carousel-items-gap)) / var(--carousel-items)); + min-width: 0; + scroll-snap-align: start; + scroll-snap-stop: always; } - .carousel-item.active, - .carousel-item-next.carousel-item-start, - .carousel-item-prev.carousel-item-end { - z-index: 1; - opacity: 1; + // + // Layout variants + // + + // Center the active slide in the viewport (pairs well with `--carousel-items-peek`) + .carousel-center { + .carousel-item { + scroll-snap-align: center; + } } - .active.carousel-item-start, - .active.carousel-item-end { - z-index: 0; - opacity: 0; - @include transition(opacity 0s $carousel-transition-duration); + // Let each slide size itself; snap points still land on every item + .carousel-auto { + .carousel-item { + flex-basis: auto; + } } -} + // + // Alternate transitions + // + + // Fade can't ride scroll-snap (it stacks slides instead of scrolling), so it + // becomes a JavaScript-driven mode: every slide is stacked and the active one + // is faded in via a CSS opacity transition. + .carousel-fade { + .carousel-inner { + display: grid; + overflow: hidden; + scroll-snap-type: none; + } -// -// Left/right controls for nav -// - -.carousel-control-prev, -.carousel-control-next { - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - // Use flex for alignment (1-3) - display: flex; // 1. allow flex styles - align-items: center; // 2. vertically center contents - justify-content: center; // 3. horizontally center contents - width: $carousel-control-width; - padding: 0; - color: $carousel-control-color; - text-align: center; - background: none; - filter: var(--#{$prefix}carousel-control-icon-filter); - border: 0; - opacity: $carousel-control-opacity; - @include transition($carousel-control-transition); - - // Hover/focus state - &:hover, - &:focus { - color: $carousel-control-color; - text-decoration: none; - outline: 0; - opacity: $carousel-control-hover-opacity; + .carousel-item { + grid-area: 1 / 1; + width: 100%; + visibility: hidden; + opacity: 0; + @include transition(opacity var(--carousel-fade-duration) ease, visibility 0s linear var(--carousel-fade-duration)); + } + + .carousel-item.active { + visibility: visible; + opacity: 1; + @include transition(opacity var(--carousel-fade-duration) ease); + } } -} -.carousel-control-prev { - left: 0; - background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); -} -.carousel-control-next { - right: 0; - background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); -} -// Icons for within -.carousel-control-prev-icon, -.carousel-control-next-icon { - display: inline-block; - width: $carousel-control-icon-width; - height: $carousel-control-icon-width; - background-repeat: no-repeat; - background-position: 50%; - background-size: 100% 100%; -} + // Icons for within, rendered via CSS mask so they inherit the current text + // color (white on the overlay controls, the button color inside `.btn-*`). + .carousel-icon-prev, + .carousel-icon-next, + .carousel-icon-pause, + .carousel-icon-play { + display: inline-block; + width: var(--carousel-control-icon-width); + height: var(--carousel-control-icon-width); + background-color: currentcolor; + @include mask-icon($size: 100% 100%, $position: 50%); + } -.carousel-control-prev-icon { - background-image: escape-svg($carousel-control-prev-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-next-icon-bg) + "*/"}; -} -.carousel-control-next-icon { - background-image: escape-svg($carousel-control-next-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-prev-icon-bg) + "*/"}; -} + .carousel-icon-prev { + mask-image: var(--carousel-control-prev-icon); + } -// Optional indicator pips/controls -// -// Add a container (such as a list) with the following class and add an item (ideally a focusable control, -// like a button) with data-bs-target for each slide your carousel holds. - -.carousel-indicators { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - display: flex; - justify-content: center; - padding: 0; - // Use the .carousel-control's width as margin so we don't overlay those - margin-right: $carousel-control-width; - margin-bottom: 1rem; - margin-left: $carousel-control-width; - - [data-bs-target] { - box-sizing: content-box; - flex: 0 1 auto; - width: $carousel-indicator-width; - height: $carousel-indicator-height; - padding: 0; - margin-right: $carousel-indicator-spacer; - margin-left: $carousel-indicator-spacer; - text-indent: -999px; - cursor: pointer; - background-color: var(--#{$prefix}carousel-indicator-active-bg); - background-clip: padding-box; - border: 0; - // Use transparent borders to increase the hit area by 10px on top and bottom. - border-top: $carousel-indicator-hit-area-height solid transparent; - border-bottom: $carousel-indicator-hit-area-height solid transparent; - opacity: $carousel-indicator-opacity; - @include transition($carousel-indicator-transition); - } - - .active { - opacity: $carousel-indicator-active-opacity; + .carousel-icon-next { + mask-image: var(--carousel-control-next-icon); } -} + [dir="rtl"] .carousel-icon-prev, + [dir="rtl"] .carousel-icon-next { + transform: scaleX(-1); + } -// Optional captions -// -// + .carousel-icon-pause { + mask-image: var(--carousel-control-pause-icon); + } -.carousel-caption { - position: absolute; - right: (100% - $carousel-caption-width) * .5; - bottom: $carousel-caption-spacer; - left: (100% - $carousel-caption-width) * .5; - padding-top: $carousel-caption-padding-y; - padding-bottom: $carousel-caption-padding-y; - color: var(--#{$prefix}carousel-caption-color); - text-align: center; -} + .carousel-icon-play { + mask-image: var(--carousel-control-play-icon); + } -// Dark mode carousel + // Optional play/pause control + // + // A discoverable toggle so users can stop an autoplaying carousel, as required + // by WCAG 2.2.2 (Pause, Stop, Hide). `.carousel-control-play-pause` is only a + // behavior hook—JS toggles `.paused` on it and its appearance comes from the + // wrapping button (e.g. `.btn-icon`). The button holds both glyphs and we show + // whichever `.carousel-icon-*` matches the current state. + .carousel-control-play-pause .carousel-icon-play { + display: none; + } -@mixin carousel-dark() { - --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; - --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; - --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; -} + .carousel-control-play-pause.paused { + .carousel-icon-pause { + display: none; + } -.carousel-dark { - @include carousel-dark(); -} + .carousel-icon-play { + display: inline-block; + } + } -:root, -[data-bs-theme="light"] { - --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; - --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; - --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; -} + .carousel-indicators { + display: flex; + gap: var(--carousel-indicator-spacer); + justify-content: center; + + [data-bs-target] { + flex: 0 1 auto; + width: var(--carousel-indicator-width); + height: var(--carousel-indicator-height); + padding: 0; + cursor: pointer; + background-color: transparent; + border: 1px solid var(--carousel-indicator-bg); + @include border-radius(var(--carousel-indicator-width)); + @include transition(var(--carousel-indicator-transition)); + } + + .active { + width: calc(var(--carousel-indicator-width) * 2.5); + background-color: var(--carousel-indicator-bg); + border-color: var(--carousel-indicator-bg); + } + } + + // Autoplay progress: fill the active indicator like a progress bar over the + // current slide's interval. The JS adds `.carousel-playing` and sets + // `--carousel-interval` (shipped as `--bs-carousel-interval`) while autoplay is + // running. The fill restarts on its own each slide because `.active` moves to a + // fresh indicator, so its `::after` animation begins from scratch. + @if $enable-transitions { + @keyframes carousel-indicator-progress { + from { inline-size: 0; } + to { inline-size: 100%; } + } + + .carousel-playing .carousel-indicators .active { + @media (prefers-reduced-motion: no-preference) { + position: relative; + overflow: hidden; + // Empty the pill so it reads as a track that the fill grows across. + background-color: transparent; + + &::after { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + inline-size: 0; + content: ""; + background-color: var(--carousel-indicator-progress-bg); + animation: carousel-indicator-progress var(--carousel-interval, 5000ms) linear forwards; + } + } + } + } + + // Overlay layout + // + // Overlays the prev/next controls, play/pause button, and indicators on top of + // the slides (the classic carousel look) instead of stacking them in the flow. + + .carousel-overlay { + --carousel-indicator-bg: light-dark(var(--white), var(--black)); -@if $enable-dark-mode { - @include color-mode(dark, true) { - @include carousel-dark(); + .carousel-overlay-controls { + position: absolute; + inset-block-end: 1rem; + inset-inline: 1rem; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + } } } diff --git a/assets/stylesheets/bootstrap/_chip.scss b/assets/stylesheets/bootstrap/_chip.scss new file mode 100644 index 00000000..9f618ba1 --- /dev/null +++ b/assets/stylesheets/bootstrap/_chip.scss @@ -0,0 +1,148 @@ +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/tokens" as *; + +$chip-tokens: () !default; + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start chip-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$chip-tokens: defaults( + ( + --chip-height: 1.75rem, + --chip-padding-x: .625rem, + --chip-gap: .3125rem, + --chip-border-radius: var(--radius-pill), + --chip-img-size: 1.25rem, + --chip-icon-size: 1rem, + --chip-dismiss-size: 1rem, + --chip-dismiss-opacity: .65, + --chip-dismiss-hover-opacity: 1, + --chip-color: var(--theme-fg, var(--fg-body)), + --chip-bg: var(--theme-bg-subtle, var(--bg-2)), + --chip-border-color: transparent, + --chip-selected-color: var(--theme-contrast, var(--primary-contrast)), + --chip-selected-bg: var(--theme-bg, var(--primary-bg)), + --chip-selected-border-color: var(--theme-bg, var(--primary-bg)), + ), + $chip-tokens +); +// scss-docs-end chip-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + .chip { + @include tokens($chip-tokens); + + display: inline-flex; + gap: var(--chip-gap); + align-items: center; + height: var(--chip-height); + padding-inline: var(--chip-padding-x); + font-size: var(--chip-font-size, var(--font-size-sm)); + font-weight: var(--chip-font-weight, var(--font-weight-base)); + line-height: var(--chip-line-height, 1.25rem); + color: var(--chip-color); + text-decoration: none; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + background-color: var(--chip-bg); + border: var(--border-width) solid var(--chip-border-color); + @include border-radius(var(--chip-border-radius)); + + &:hover { + --chip-bg: var(--theme-bg-muted, var(--bg-3)); + } + + &:focus-visible { + outline: 0; + // @include focus-ring(); + } + + &.active { + --chip-color: var(--chip-selected-color); + --chip-bg: var(--chip-selected-bg); + --chip-border-color: var(--chip-selected-border-color); + + &:hover { + --chip-bg: var(--chip-selected-bg); + opacity: .9; + } + } + + &.disabled, + &:disabled { + pointer-events: none; + opacity: .65; + } + } + + .chip-img { + width: var(--chip-img-size); + height: var(--chip-img-size); + @include border-radius(50%); + + &:first-child { + margin-inline-start: -.375rem; + } + } + + // Chip icon (left side) + .chip-icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + margin-inline-start: calc(var(--chip-gap) * -.25); + + > svg { + display: block; // Prevents baseline alignment issues + width: var(--chip-icon-size); + height: var(--chip-icon-size); + } + + > img { + width: var(--chip-icon-size); + height: var(--chip-icon-size); + object-fit: cover; + @include border-radius(50%); + } + } + + // Dismiss button (right side) + .chip-dismiss { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: var(--chip-min-height); + height: var(--chip-min-height); + padding: 0; + // margin-inline-start: calc(var(--chip-padding-x) * -.5); + margin-inline-end: calc(var(--chip-padding-x) * -.25); + color: inherit; + cursor: pointer; + background: transparent; + border: 0; + opacity: var(--chip-dismiss-opacity); + // @include transition(opacity .15s ease-in-out); + + &:hover { + opacity: var(--chip-dismiss-hover-opacity); + } + + &:focus-visible { + outline: 0; + opacity: 1; + @include focus-ring(); + } + + > svg { + display: block; // Prevents baseline alignment issues + width: var(--chip-dismiss-size); + height: var(--chip-dismiss-size); + } + } +} diff --git a/assets/stylesheets/bootstrap/_close.scss b/assets/stylesheets/bootstrap/_close.scss deleted file mode 100644 index d53c96fb..00000000 --- a/assets/stylesheets/bootstrap/_close.scss +++ /dev/null @@ -1,66 +0,0 @@ -// Transparent background and border properties included for button version. -// iOS requires the button element instead of an anchor tag. -// If you want the anchor version, it requires `href="#"`. -// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - -.btn-close { - // scss-docs-start close-css-vars - --#{$prefix}btn-close-color: #{$btn-close-color}; - --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; - --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; - --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; - --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; - --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; - --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; - // scss-docs-end close-css-vars - - box-sizing: content-box; - width: $btn-close-width; - height: $btn-close-height; - padding: $btn-close-padding-y $btn-close-padding-x; - color: var(--#{$prefix}btn-close-color); - background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements - filter: var(--#{$prefix}btn-close-filter); - border: 0; // for button elements - @include border-radius(); - opacity: var(--#{$prefix}btn-close-opacity); - - // Override 's hover style - &:hover { - color: var(--#{$prefix}btn-close-color); - text-decoration: none; - opacity: var(--#{$prefix}btn-close-hover-opacity); - } - - &:focus { - outline: 0; - box-shadow: var(--#{$prefix}btn-close-focus-shadow); - opacity: var(--#{$prefix}btn-close-focus-opacity); - } - - &:disabled, - &.disabled { - pointer-events: none; - user-select: none; - opacity: var(--#{$prefix}btn-close-disabled-opacity); - } -} - -@mixin btn-close-white() { - --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; -} - -.btn-close-white { - @include btn-close-white(); -} - -:root, -[data-bs-theme="light"] { - --#{$prefix}btn-close-filter: #{$btn-close-filter}; -} - -@if $enable-dark-mode { - @include color-mode(dark, true) { - @include btn-close-white(); - } -} diff --git a/assets/stylesheets/bootstrap/_colors.scss b/assets/stylesheets/bootstrap/_colors.scss new file mode 100644 index 00000000..cba537ae --- /dev/null +++ b/assets/stylesheets/bootstrap/_colors.scss @@ -0,0 +1,102 @@ +// stylelint-disable hue-degree-notation, @stylistic/number-leading-zero + +@use "sass:map"; +@use "functions" as *; +@use "mixins/tokens" as *; + +// Easily convert colors to oklch() with https://oklch.com/ + +$white: #fff !default; +$black: #000 !default; + +// scss-docs-start colors-list +$blue: oklch(60% 0.24 240) !default; +$indigo: oklch(56% 0.26 288) !default; +$violet: oklch(56% 0.24 300) !default; +$purple: oklch(56% 0.24 320) !default; +$pink: oklch(60% 0.22 4) !default; +$red: oklch(60% 0.22 20) !default; +$orange: oklch(70% 0.22 52) !default; +$amber: oklch(79% 0.2 78) !default; +$yellow: oklch(88% 0.24 88) !default; +$lime: oklch(65% 0.24 135) !default; +$green: oklch(64% 0.22 160) !default; +$teal: oklch(68% 0.22 190) !default; +$cyan: oklch(69% 0.22 220) !default; +$brown: oklch(60% 0.12 54) !default; +$gray: oklch(60% 0.02 245) !default; +$pewter: oklch(65% 0.01 290) !default; +// scss-docs-end colors-list + +// scss-docs-start colors-map +$colors: () !default; + +// stylelint-disable-next-line scss/dollar-variable-default +$colors: defaults( + ( + "blue": $blue, + "indigo": $indigo, + "violet": $violet, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "amber": $amber, + "yellow": $yellow, + "lime": $lime, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "brown": $brown, + "gray": $gray, + "pewter": $pewter, + ), + $colors +); +// scss-docs-end colors-map + +// scss-docs-start color-mix-options +$color-mix-space: lab !default; +$tint-color: var(--white) !default; +$shade-color: var(--black) !default; + +$color-tints: ( + "025": 94%, + "050": 90%, + "100": 80%, + "200": 60%, + "300": 40%, + "400": 20%, +) !default; + +$color-shades: ( + "600": 16%, + "700": 32%, + "800": 48%, + "900": 64%, + "950": 76%, + "975": 88%, +) !default; +// scss-docs-end color-mix-options + +// scss-docs-start color-tokens +$color-tokens: () !default; + +$-color-defaults: () !default; +@each $color, $value in $colors { + @each $stop, $percent in $color-tints { + $-color-defaults: map.set($-color-defaults, --#{$color}-#{$stop}, color-mix(in #{$color-mix-space}, #{$tint-color} #{$percent}, #{$value})); + } + $-color-defaults: map.set($-color-defaults, --#{$color}-500, #{$value}); + @each $stop, $percent in $color-shades { + $-color-defaults: map.set($-color-defaults, --#{$color}-#{$stop}, color-mix(in #{$color-mix-space}, #{$shade-color} #{$percent}, #{$value})); + } +} + +// stylelint-disable-next-line scss/dollar-variable-default +$color-tokens: defaults($-color-defaults, $color-tokens); +// scss-docs-end color-tokens + +:root { + @include tokens($color-tokens); +} diff --git a/assets/stylesheets/bootstrap/_config.scss b/assets/stylesheets/bootstrap/_config.scss new file mode 100644 index 00000000..580f0cc1 --- /dev/null +++ b/assets/stylesheets/bootstrap/_config.scss @@ -0,0 +1,348 @@ +@use "sass:map"; +@use "sass:meta"; + +// Configuration +// +// Variables and settings not related to theme, components, and more go here. It does include layout. + +// Merge overrides on top of defaults, stripping null entries. +// Null values let users remove map keys via @use ... with(). +// Accepts a list as $defaults (converted to a map with `true` values). +@function defaults($defaults, $overrides) { + @if meta.type-of($defaults) == "list" { + $map: (); + @each $key in $defaults { + $map: map.merge($map, ($key: true)); + } + $defaults: $map; + } + $merged: map.merge($defaults, $overrides); + @each $key, $value in $merged { + @if $value == null { + $merged: map.remove($merged, $key); + } + } + @return $merged; +} + +$enable-caret: true !default; +$enable-rounded: true !default; +$enable-shadows: true !default; +$enable-gradients: true !default; +$enable-transitions: true !default; +$enable-reduced-motion: true !default; +$enable-smooth-scroll: false !default; +$enable-grid-classes: true !default; +$enable-container-classes: true !default; +$enable-cssgrid: true !default; +$enable-button-pointers: true !default; +// $enable-negative-margins: false !default; +$enable-deprecation-messages: true !default; + +$color-mode-type: "media-query" !default; +$color-contrast-dark: #000 !default; +$color-contrast-light: #fff !default; +$min-contrast-ratio: 4.5 !default; + +// scss-docs-start spacer-variables-maps +$spacer: 1rem !default; +$spacers: ( + 0: 0, + 1: $spacer * .25, + 2: $spacer * .5, + 3: $spacer * .75, + 4: $spacer, + 5: $spacer * 1.25, + 6: $spacer * 1.5, + 7: $spacer * 2, + 8: $spacer * 2.5, + 9: $spacer * 3, +) !default; + +$negative-spacers: ( + "-1": $spacer * -.25, + "-2": $spacer * -.5, +) !default; +// scss-docs-end spacer-variables-maps + +$sizes: ( + 1: $spacer, + 2: $spacer * 2, + 3: $spacer * 3, + 4: $spacer * 4, + 5: $spacer * 5, + 6: $spacer * 6, + 7: $spacer * 7, + 8: $spacer * 8, + 9: $spacer * 9, + 10: $spacer * 10, + 11: $spacer * 11, + 12: $spacer * 12, +) !default; + +$radius: .5rem !default; +$radii: ( + 0: 0, + 1: $radius * .25, + 2: $radius * .375, + 3: $radius * .5, + 4: $radius * .75, + 5: $radius, + 6: $radius * 1.25, + 7: $radius * 1.5, + 8: $radius * 2, + 9: $radius * 3, +) !default; + +// Breakpoints +// +// Define the minimum dimensions at which your layout will change, +// adapting to different screen sizes, for use in media queries. + +// scss-docs-start breakpoints +$breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 1024px, + xl: 1280px, + 2xl: 1536px +) !default; +// scss-docs-end breakpoints + +// @include _assert-ascending($breakpoints, "$breakpoints"); +// @include _assert-starts-at-zero($breakpoints, "$breakpoints"); + +// Grid columns +// +// Set the number of columns and specify the width of the gutters. + +$grid-columns: 12 !default; +$grid-gutter-x: 1.5rem !default; +$grid-gutter-y: 0 !default; +$grid-row-columns: 6 !default; + +$gutters: $spacers !default; + +// Grid containers +// +// Define the maximum width of `.container` for different screen sizes. + +// scss-docs-start container-max-widths +$container-max-widths: ( + sm: 540px, + md: 720px, + lg: 960px, + xl: 1200px, + 2xl: 1440px +) !default; +// scss-docs-end container-max-widths + +$container-padding-x: $grid-gutter-x !default; + +$utilities: () !default; + +// Characters which are escaped by the escape-svg function +$escaped-characters: ( + ("<", "%3c"), + (">", "%3e"), + ("#", "%23"), + ("(", "%28"), + (")", "%29"), +) !default; + +// Gradient +// +// The gradient which is added to components if `$enable-gradients` is `true` +// This gradient is also added to elements with `.bg-gradient` +// scss-docs-start variable-gradient +$gradient: linear-gradient(180deg, color-mix(var(--white) 15%, transparent), color-mix(var(--white) 0%, transparent)) !default; +// scss-docs-end variable-gradient + +// Position +// +// Define the edge positioning anchors of the position utilities. + +// scss-docs-start position-map +$position-values: ( + 0: 0, + 50: 50%, + 100: 100% +) !default; +// scss-docs-end position-map + +// Links +// +// Style anchor elements. + +$link-decoration: underline !default; +$link-underline-offset: .2em !default; + +$stretched-link-pseudo-element: after !default; +$stretched-link-z-index: 1 !default; + +// Icon links +// scss-docs-start icon-link-variables +$icon-link-gap: .375rem !default; +$icon-link-underline-offset: .25em !default; +$icon-link-icon-size: 1em !default; +$icon-link-icon-transition: .2s ease-in-out transform !default; +$icon-link-icon-transform: translate3d(.25em, 0, 0) !default; +// scss-docs-end icon-link-variables + +// Paragraphs +// +// Style p element. + +$paragraph-margin-bottom: 1rem !default; + +// Components +// +// Define common padding and border radius sizes and more. + +// scss-docs-start border-variables +$border-width: 1px !default; +$border-widths: ( + 1: 1px, + 2: 2px, + 3: 3px, + 4: 4px, + 5: 5px +) !default; +$border-style: solid !default; +$border-color: color-mix(in oklch, var(--gray-100), var(--gray-200)) !default; +// scss-docs-end border-variables + +$transition-base: all .2s ease-in-out !default; +$transition-fade: opacity .15s linear !default; + +// scss-docs-start collapse-transition +$transition-collapse: height .35s ease !default; +$transition-collapse-width: width .35s ease !default; +// scss-docs-end collapse-transition + +// scss-docs-start aspect-ratios +$aspect-ratios: ( + "auto": auto, + "1x1": #{"1 / 1"}, + "4x3": #{"4 / 3"}, + "16x9": #{"16 / 9"}, + "21x9": #{"21 / 9"} +) !default; +// scss-docs-end aspect-ratios + +// Typography +// +// Font, line-height, and color for body text, headings, and more. + +// scss-docs-start font-variables +$font-weight-lighter: lighter !default; +$font-weight-light: 300 !default; +$font-weight-normal: 400 !default; +$font-weight-medium: 500 !default; +$font-weight-semibold: 600 !default; +$font-weight-bold: 700 !default; +$font-weight-bolder: bolder !default; + +$font-weight-base: $font-weight-normal !default; + +$line-height-base: 1.5 !default; +$line-height-sm: 1.25 !default; +$line-height-lg: 2 !default; +// scss-docs-end font-variables + +// scss-docs-start font-sizes +$font-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$font-sizes: defaults( + ( + "xs": ( + "font-size": .75rem, + "line-height": 1.25 + ), + "sm": ( + "font-size": .875rem, + "line-height": 1.5 + ), + "md": ( + "font-size": 1rem, + "line-height": 1.5 + ), + "lg": ( + "font-size": clamp(1.25rem, 1rem + .625vw, 1.5rem), + "line-height": 1.5 + ), + "xl": ( + "font-size": clamp(1.5rem, 1.1rem + .75vw, 1.75rem), + "line-height": calc(2.5 / 1.75) + ), + "2xl": ( + "font-size": clamp(1.75rem, 1.3rem + 1vw, 2rem), + "line-height": calc(3 / 2.25) + ), + "3xl": ( + "font-size": clamp(2rem, 1.5rem + 1.875vw, 2.5rem), + "line-height": 1.2 + ), + "4xl": ( + "font-size": clamp(2.25rem, 1.75rem + 2.5vw, 3rem), + "line-height": 1.1 + ), + "5xl": ( + "font-size": clamp(3rem, 2rem + 5vw, 4rem), + "line-height": 1.1 + ), + "6xl": ( + "font-size": clamp(3.75rem, 2.5rem + 6.25vw, 5rem), + "line-height": 1 + ), + ), + $font-sizes +); +// scss-docs-end font-sizes + +// scss-docs-start headings-variables +$headings-margin-bottom: var(--spacer-2) !default; +$headings-font-family: null !default; +$headings-font-style: null !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.2 !default; +$headings-color: inherit !default; +// scss-docs-end headings-variables + +// scss-docs-start type-variables + +$legend-margin-bottom: .5rem !default; +$legend-font-size: 1.5rem !default; +$legend-font-weight: null !default; + +$dt-font-weight: $font-weight-bold !default; + +// scss-docs-end type-variables + +// Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. + +// scss-docs-start zindex-stack +$zindex-menu: 1000 !default; +$zindex-sticky: 1020 !default; +$zindex-fixed: 1030 !default; +// $zindex-drawer-backdrop: 1040 !default; +$zindex-drawer: 1045 !default; +$zindex-dialog: 1055 !default; +$zindex-popover: 1070 !default; +$zindex-tooltip: 1080 !default; +$zindex-toast: 1090 !default; +// scss-docs-end zindex-stack + +// scss-docs-start zindex-levels-map +$zindex-levels: ( + n1: -1, + 0: 0, + 1: 1, + 2: 2, + 3: 3 +) !default; +// scss-docs-end zindex-levels-map diff --git a/assets/stylesheets/bootstrap/_containers.scss b/assets/stylesheets/bootstrap/_containers.scss deleted file mode 100644 index 83b31381..00000000 --- a/assets/stylesheets/bootstrap/_containers.scss +++ /dev/null @@ -1,41 +0,0 @@ -// Container widths -// -// Set the container width, and override it for fixed navbars in media queries. - -@if $enable-container-classes { - // Single container class with breakpoint max-widths - .container, - // 100% wide container at all breakpoints - .container-fluid { - @include make-container(); - } - - // Responsive containers that are 100% wide until a breakpoint - @each $breakpoint, $container-max-width in $container-max-widths { - .container-#{$breakpoint} { - @extend .container-fluid; - } - - @include media-breakpoint-up($breakpoint, $grid-breakpoints) { - %responsive-container-#{$breakpoint} { - max-width: $container-max-width; - } - - // Extend each breakpoint which is smaller or equal to the current breakpoint - $extend-breakpoint: true; - - @each $name, $width in $grid-breakpoints { - @if ($extend-breakpoint) { - .container#{breakpoint-infix($name, $grid-breakpoints)} { - @extend %responsive-container-#{$breakpoint}; - } - - // Once the current breakpoint is reached, stop extending - @if ($breakpoint == $name) { - $extend-breakpoint: false; - } - } - } - } - } -} diff --git a/assets/stylesheets/bootstrap/_datepicker.scss b/assets/stylesheets/bootstrap/_datepicker.scss new file mode 100644 index 00000000..d45f1e39 --- /dev/null +++ b/assets/stylesheets/bootstrap/_datepicker.scss @@ -0,0 +1,415 @@ +// stylelint-disable selector-max-attribute, property-disallowed-list, selector-no-qualifying-type -- VCP uses extensive data attributes and requires direct border-radius properties for range selection + +@use "functions" as *; +@use "config" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; + +$datepicker-tokens: () !default; + +// scss-docs-start datepicker-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$datepicker-tokens: defaults( + ( + --datepicker-padding: 1rem, + --datepicker-bg: var(--bg-body), + --datepicker-color: var(--fg-body), + --datepicker-border-color: var(--border-color-translucent), + --datepicker-border-width: var(--border-width), + --datepicker-border-radius: var(--radius-7), + --datepicker-box-shadow: var(--box-shadow), + --datepicker-font-size: var(--font-size-sm), + --datepicker-min-width: 280px, + --datepicker-zindex: #{$zindex-menu}, + --datepicker-header-font-weight: 600, + --datepicker-weekday-color: var(--fg-3), + --datepicker-day-hover-bg: var(--bg-1), + --datepicker-day-selected-bg: var(--primary-bg), + --datepicker-day-selected-color: var(--primary-contrast), + --datepicker-day-today-bg: var(--bg-2), + --datepicker-day-today-color: var(--fg-1), + --datepicker-day-disabled-color: var(--fg-4), + ), + $datepicker-tokens +); +// scss-docs-end datepicker-tokens + +@layer components { + [data-vc="calendar"] { + @include tokens($datepicker-tokens); + + position: absolute; + z-index: var(--datepicker-zindex); + box-sizing: border-box; + display: flex; + flex-direction: column; + min-width: var(--datepicker-min-width); + padding: var(--datepicker-padding); + font-family: var(--font-sans-serif); + font-size: var(--datepicker-font-size); + color: var(--datepicker-color); + color-scheme: light dark; + background-color: var(--datepicker-bg); + border: var(--datepicker-border-width) solid var(--datepicker-border-color); + box-shadow: var(--datepicker-box-shadow); + opacity: 1; + @include border-radius(var(--datepicker-border-radius)); + + // Respond to Bootstrap's color mode system + &[data-bs-theme="light"] { + color-scheme: light; + } + + &[data-bs-theme="dark"] { + color-scheme: dark; + } + + // Catch-all for focus styles + button:focus-visible { + position: relative; + z-index: 1; + @include focus-ring(); + } + } + + [data-vc-calendar-hidden] { + pointer-events: none; + opacity: 0; + } + + // Inline calendars + // + // Remove popover styling for more neutral styling + [data-vc="calendar"]:not([data-vc-input]) { + position: relative; + width: fit-content; + padding: 0; + border: 0; + box-shadow: none; + } + + [data-vc-position="bottom"] { + margin-block-start: .25rem; + } + + [data-vc-position="top"] { + margin-block-end: -.25rem; + } + + [data-vc-arrow] { + position: relative; + display: block; + width: 2rem; + height: 2rem; + color: var(--datepicker-color); + pointer-events: auto; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &::before { + position: absolute; + inset: .25rem; + content: ""; + background-color: var(--datepicker-color); + @include mask-icon(url("data:image/svg+xml,"), $size: null); + } + + &:hover { + background-color: var(--datepicker-day-hover-bg); + } + } + + [data-vc-arrow="prev"]::before { + transform: rotate(90deg); + } + + [data-vc-arrow="next"]::before { + transform: rotate(-90deg); + } + + // Grid layout + [data-vc="controls"] { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + padding-right: 1rem; + padding-left: 1rem; + pointer-events: none; + } + + [data-vc="grid"] { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + gap: 1.75rem; + } + + [data-vc="column"] { + display: flex; + flex-grow: 1; + flex-direction: column; + min-width: 240px; + } + + // + // Header + // + + [data-vc="header"] { + position: relative; + display: flex; + align-items: center; + margin-bottom: .75rem; + } + + // Month and year + [data-vc-header="content"] { + display: inline-flex; + flex-grow: 1; + align-items: center; + justify-content: center; + white-space: pre-wrap; + } + + [data-vc="month"], + [data-vc="year"] { + padding: .25rem .5rem; + margin-inline: -.125rem; + font-size: 1rem; + font-weight: var(--datepicker-header-font-weight); + color: var(--datepicker-color); + // cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--datepicker-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--datepicker-day-hover-bg); + } + } + + [data-vc="content"] { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + // Month/Year grids + [data-vc="months"], + [data-vc="years"] { + display: grid; + flex-grow: 1; + grid-template-columns: repeat(var(--vc-columns, 4), minmax(0, 1fr)); + row-gap: 1rem; + column-gap: .25rem; + align-items: center; + } + + [data-vc="years"] { + --vc-columns: 5; + } + + [data-vc-months-month], + [data-vc-years-year] { + display: flex; + align-items: center; + justify-content: center; + height: 2.5rem; + padding: .25rem; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--datepicker-weekday-color); + text-align: center; + word-break: break-all; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--datepicker-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--datepicker-day-hover-bg); + } + + &[data-vc-months-month-selected], + &[data-vc-years-year-selected] { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + + &:hover { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + } + } + } + + // Week days header + [data-vc="week"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + justify-items: center; + margin-bottom: .5rem; + } + + [data-vc-week-day] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + padding: 0; + margin: 0; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--datepicker-weekday-color); + background-color: transparent; + border: 0; + } + + button[data-vc-week-day] { + cursor: pointer; + } + + // Dates grid + [data-vc="dates"] { + pointer-events: none; + } + + [data-vc-dates="row"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + justify-items: center; + width: 100%; + } + + [data-vc-date] { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding-top: .125rem; + padding-bottom: .125rem; + pointer-events: auto; + + &:not(:has([data-vc-date-btn])), + &[data-vc-date-disabled], + &[data-vc-date-disabled] [data-vc-date-btn] { + pointer-events: none; + } + } + + // Date button + [data-vc-date-btn] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + height: 100%; + min-height: 1.875rem; + padding: 0; + font-size: .75rem; + font-weight: 400; + line-height: 1rem; + color: var(--datepicker-color); + cursor: pointer; + background-color: transparent; + border: 0; + border-radius: var(--radius-5); + + &:hover { + background-color: var(--datepicker-day-hover-bg); + } + } + + // Today + [data-vc-date-today] [data-vc-date-btn] { + font-weight: 600; + color: var(--datepicker-day-today-color); + background-color: var(--datepicker-day-today-bg); + } + + // Outside month + [data-vc-date-month="next"] [data-vc-date-btn], + [data-vc-date-month="prev"] [data-vc-date-btn] { + opacity: .5; + } + + // Disabled + [data-vc-date-disabled] [data-vc-date-btn] { + color: var(--datepicker-day-disabled-color); + } + + // Range selection styles + [data-vc-date-hover] [data-vc-date-btn] { + background-color: var(--datepicker-day-hover-bg); + border-radius: 0; + } + + [data-vc-date-hover="first"] [data-vc-date-btn] { + border-start-start-radius: var(--radius-5); + border-end-start-radius: var(--radius-5); + } + + [data-vc-date-hover="last"] [data-vc-date-btn] { + border-start-end-radius: var(--radius-5); + border-end-end-radius: var(--radius-5); + } + + [data-vc-date-hover="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } + + [data-vc-date-selected="middle"] [data-vc-date-btn] { + border-radius: 0; + opacity: .8; + } + + // Selected + [data-vc-date-selected] [data-vc-date-btn] { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + + } + + [data-vc-date-selected="first"] [data-vc-date-btn] { + border-top-left-radius: var(--radius-5); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: var(--radius-5); + } + + [data-vc-date-selected="last"] [data-vc-date-btn] { + border-top-left-radius: 0; + border-top-right-radius: var(--radius-5); + border-bottom-right-radius: var(--radius-5); + border-bottom-left-radius: 0; + } + + [data-vc-date-selected="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } +} diff --git a/assets/stylesheets/bootstrap/_dialog.scss b/assets/stylesheets/bootstrap/_dialog.scss new file mode 100644 index 00000000..69bada97 --- /dev/null +++ b/assets/stylesheets/bootstrap/_dialog.scss @@ -0,0 +1,289 @@ +@use "sass:map"; +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/dialog-shared" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +// Native component +// Uses the browser's native dialog element with showModal()/show()/close() APIs +// Leverages native [open] attribute and ::backdrop pseudo-element + +// stylelint-disable custom-property-no-missing-var-function +$dialog-tokens: () !default; + +// scss-docs-start dialog-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$dialog-tokens: defaults( + ( + --dialog-padding: 1rem, + --dialog-width: 500px, + --dialog-margin: 1.75rem, + --dialog-color: var(--fg-body), + --dialog-bg: var(--bg-body), + --dialog-border-color: var(--border-color-translucent), + --dialog-border-width: var(--border-width), + --dialog-border-radius: var(--radius-7), + --dialog-box-shadow: var(--box-shadow-lg), + --dialog-transition-duration: .3s, + --dialog-transition-timing: cubic-bezier(.22, 1, .36, 1), + --dialog-backdrop-bg: light-dark(rgb(0 0 0 / 50%), rgb(0 0 0 / 65%)), + --dialog-backdrop-blur: 8px, + --dialog-header-padding: 1rem, + --dialog-header-border-color: var(--border-color-translucent), + --dialog-header-border-width: var(--border-width), + --dialog-footer-padding: 1rem, + --dialog-footer-border-color: var(--border-color-translucent), + --dialog-footer-border-width: var(--border-width), + --dialog-footer-gap: .5rem, + ), + $dialog-tokens +); +// scss-docs-end dialog-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start dialog-sizes +$dialog-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$dialog-sizes: defaults( + ( + sm: 280px, + lg: 800px, + xl: 1140px, + ), + $dialog-sizes +); +// scss-docs-end dialog-sizes + +@layer components { + // Prevent page scroll when a dialog is open. Applied to the root element so + // `overflow: hidden` sits on the same element as `scrollbar-gutter: stable` + // (see _root.scss): the gutter stays reserved while the scrollbar is hidden, + // so the page doesn't shift when a dialog opens. + :root.dialog-open { + overflow: hidden; + } + + .dialog { + @include tokens($dialog-tokens); + + // Override UA display:none so visibility controls the hidden state, + // enabling reliable cross-browser exit animations after close(). + display: flex; + flex-direction: column; + width: var(--dialog-width); + max-width: calc(100% - var(--dialog-margin) * 2); + max-height: calc(100% - var(--dialog-margin) * 2); + padding: 0; + margin: auto; + overflow: visible; + color: var(--dialog-color); + visibility: hidden; + background-color: var(--dialog-bg); + background-clip: padding-box; + border: var(--dialog-border-width) solid var(--dialog-border-color); + @include border-radius(var(--dialog-border-radius)); + @include box-shadow(var(--dialog-box-shadow)); + + // Animated variant (default) — transitions, opacity fade, slide transforms. + // Adding .dialog-instant skips all animations (instant show/hide). + &:not(.dialog-instant) { + // Exit state: faded out + opacity: 0; + + // Exit transition: opacity and transform animate out, then visibility + // flips hidden after the animation completes (via the delay). + @include transition( + opacity var(--dialog-transition-duration) var(--dialog-transition-timing), + transform var(--dialog-transition-duration) var(--dialog-transition-timing), + visibility 0s var(--dialog-transition-duration) + ); + + // Slide-down variant: enters from above sliding down, exits by reversing + // back up. Base value is the entry-from / exit-to position so the + // animation works on every open (not just the first, which is the only + // time @starting-style applies for a persistent element). + &.dialog-slide-down { + transform: translateY(-3rem); + } + + // Slide-up variant: enters from below sliding up, exits by reversing + // back down. See note above re: base value choice. + &.dialog-slide-up { + transform: translateY(3rem); + } + + // Open state: visible and faded in. + // Entry transition: visibility flips visible immediately (0s, no delay), + // then opacity and transform animate in. + // The :not(.hiding) qualifier lets the exit transition fall back to the + // base "exit" state above while [open] is still present (the JS keeps + // the dialog in the top layer during the exit so the ::backdrop and + // the browser's modal centering remain intact). + &[open]:not(.hiding) { + overflow: visible; + visibility: visible; + opacity: 1; + @include transition( + opacity var(--dialog-transition-duration) var(--dialog-transition-timing), + transform var(--dialog-transition-duration) var(--dialog-transition-timing), + visibility 0s + ); + transform: none; + } + + // Static backdrop "bounce" animation (modal dialogs only). Qualified + // with [open] (to outrank the open-state `transform: none` selector + // which now also includes `:not(.hiding)`) and `:not(.hiding)` (so + // a backdrop click while the dialog is mid-exit doesn't fight the + // slide-out transform). + &[open].dialog-static:not(.hiding) { + transform: scale(1.02); + } + + // Native backdrop styling with transitions + &::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + @include backdrop-transitions(var(--dialog-transition-duration), var(--dialog-transition-timing)); + } + + // Exit: fade the native backdrop out alongside the dialog. The dialog + // is kept in the top layer (and thus the ::backdrop is still rendered) + // for the duration of the exit transition. + &.hiding::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + } + + // Instant variant — no transitions, just snap visibility + &.dialog-instant { + &::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + } + } + + // Open state base (always applies, regardless of animation mode). + // Excluded while .hiding is present so the animated exit (above) can + // fall through to the base "exit" state — for instant dialogs, .hiding + // is removed synchronously after close() so this still applies normally. + &[open]:not(.hiding) { + overflow: visible; + visibility: visible; + opacity: 1; + transform: none; + } + + // Non-modal dialog positioning + // show() doesn't use the top layer, so we need explicit positioning and z-index + &.dialog-nonmodal { + position: fixed; + inset-block-start: 50%; + inset-inline-start: 50%; + z-index: $zindex-dialog; + margin-inline: 0; + transform: translate(-50%, -50%); + } + + // Scrollable dialog body (header/footer stay fixed) + &.dialog-scrollable[open] { + max-height: calc(100% - var(--dialog-margin) * 2); + + .dialog-body { + overflow-y: auto; + } + } + } + + // Entry animation for ::backdrop via @starting-style. The backdrop only + // exists while the dialog is in the top layer, so its starting state can't + // be expressed on the base selector. + // Default dialog (fade only) and the slide variants do NOT need + // @starting-style — the base opacity: 0 (and base transform for slides) + // serves as the entry-from state with the visibility trick. + @starting-style { + .dialog:not(.dialog-instant)::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + + // Swap entry: when this dialog is opened as the target of a swap, the + // outgoing dialog's ::backdrop is being removed synchronously in the same + // JS tick. To avoid any flicker (either a dip from a fade-in over nothing, + // or double-darkening from two stacked backdrops), start this backdrop + // already-opaque so it takes over from the outgoing one seamlessly. + .dialog.dialog-swap-in:not(.dialog-instant)::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + } + } + + // Dialog sizes + @each $size, $value in $dialog-sizes { + .dialog-#{$size} { --dialog-width: #{$value}; } + } + + // Fullscreen dialog + .dialog-fullscreen { + --dialog-width: 100vw; + --dialog-margin: 0; + --dialog-border-radius: 0; + + width: 100%; + max-width: none; + height: 100%; + max-height: none; + } + + // Responsive fullscreen dialogs + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + @if $prefix != "" { + @include media-breakpoint-down($breakpoint) { + .#{css-escape-ident($breakpoint)}-down\:dialog-fullscreen { + --dialog-width: 100vw; + --dialog-margin: 0; + --dialog-border-radius: 0; + + width: 100%; + max-width: none; + height: 100%; + max-height: none; + } + } + } + } + + // Dialog header + .dialog-header { + @include dialog-header(var(--dialog-header-padding)); + border-block-end: var(--dialog-header-border-width) solid var(--dialog-header-border-color); + + .btn-close { + margin-inline-start: auto; + } + } + + // Dialog title + .dialog-title { + @include dialog-title(); + font-size: var(--font-size-md); + } + + // Dialog body + .dialog-body { + position: relative; + @include dialog-body(var(--dialog-padding)); + } + + // Dialog footer + .dialog-footer { + @include dialog-footer(var(--dialog-footer-padding), var(--dialog-footer-gap), var(--dialog-footer-border-width), var(--dialog-footer-border-color)); + } +} diff --git a/assets/stylesheets/bootstrap/_drawer.scss b/assets/stylesheets/bootstrap/_drawer.scss new file mode 100644 index 00000000..fd5f7846 --- /dev/null +++ b/assets/stylesheets/bootstrap/_drawer.scss @@ -0,0 +1,302 @@ +@use "functions" as *; +@use "config" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/dialog-shared" as *; +@use "mixins/transition" as *; +@use "layout/breakpoints" as *; +@use "mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$drawer-tokens: () !default; + +// scss-docs-start drawer-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$drawer-tokens: defaults( + ( + --drawer-inset: var(--spacer), + --drawer-zindex: #{$zindex-drawer}, + --drawer-width: 400px, + --drawer-height: 30vh, + --drawer-padding-x: var(--spacer), + --drawer-padding-y: var(--spacer), + --drawer-color: var(--fg-body), + --drawer-bg: var(--bg-body), + --drawer-border-width: var(--border-width), + --drawer-border-color: var(--border-color-translucent), + --drawer-border-radius: var(--radius-7), + --drawer-box-shadow: var(--box-shadow-lg), + --drawer-transition-duration: .3s, + --drawer-transition-timing: cubic-bezier(.22, 1, .36, 1), + --drawer-title-line-height: 1.5, + --drawer-backdrop-bg: color-mix(in oklch, var(--bg-body) 25%, transparent), + --drawer-backdrop-blur: 8px, + ), + $drawer-tokens +); +// scss-docs-end drawer-tokens +// stylelint-enable custom-property-no-missing-var-function + +$drawer-backdrop-tokens: () !default; + +// scss-docs-start drawer-backdrop-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$drawer-backdrop-tokens: defaults( + ( + --drawer-backdrop-bg: var(--bg-body), + --drawer-backdrop-opacity: 25%, + --drawer-backdrop-blur: 8px, + ), + $drawer-backdrop-tokens +); +// scss-docs-end drawer-backdrop-tokens + +%drawer-css-vars { + @include tokens($drawer-tokens); +} + +@layer components { + // Apply CSS vars to all drawer responsive variants + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer { + @extend %drawer-css-vars; + } + } + + // Responsive drawer styles + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer { + @include media-breakpoint-down($next) { + // Reset native UA defaults (fit-content sizing, inset, margins) + // and override display:none so visibility controls the hidden state. + position: fixed; + inset: auto; + z-index: var(--drawer-zindex); + display: flex; + flex-direction: column; + width: auto; + max-width: calc(100% - var(--drawer-inset) * 2); + height: auto; + max-height: calc(100% - var(--drawer-inset) * 2); + padding: 0; + margin: 0; + color: var(--drawer-color); + visibility: hidden; + background-color: var(--drawer-bg); + background-clip: padding-box; + border: var(--drawer-border-width) solid var(--drawer-border-color); + outline: 0; + + @include border-radius(var(--drawer-border-radius)); + @include box-shadow(var(--drawer-box-shadow)); + + // Placement positioning and sizing — always applied regardless of animation mode. + &:where(.drawer-start) { + inset-block: var(--drawer-inset); + inset-inline-start: var(--drawer-inset); + width: var(--drawer-width); + } + + &:where(.drawer-end) { + inset-block: var(--drawer-inset); + inset-inline-end: var(--drawer-inset); + width: var(--drawer-width); + } + + &:where(.drawer-top) { + inset: var(--drawer-inset) var(--drawer-inset) auto; + height: var(--drawer-height); + } + + &:where(.drawer-bottom) { + inset: auto var(--drawer-inset) var(--drawer-inset); + height: var(--drawer-height); + } + + &:where(.drawer-fullscreen) { + inset: var(--drawer-inset); + width: auto; + max-width: none; + height: auto; + max-height: none; + } + + // Animated variant (default) — transitions + off-screen transforms. + // Adding .drawer-instant skips all animations. + &:not(.drawer-instant) { + @include transition(transform var(--drawer-transition-duration) var(--drawer-transition-timing), visibility 0s var(--drawer-transition-duration)); + + // Off-screen transforms per placement + &:where(.drawer-start) { + transform: translateX(calc(-100% - var(--drawer-inset))); + + :root:dir(rtl) & { + transform: translateX(calc(100% + var(--drawer-inset))); + } + } + + &:where(.drawer-end) { + transform: translateX(calc(100% + var(--drawer-inset))); + + :root:dir(rtl) & { + transform: translateX(calc(-100% - var(--drawer-inset))); + } + } + + &:where(.drawer-top) { + transform: translateY(calc(-100% - var(--drawer-inset))); + } + + &:where(.drawer-bottom) { + transform: translateY(calc(100% + var(--drawer-inset))); + } + + &:where(.drawer-fullscreen) { + transform: translateY(calc(100% + var(--drawer-inset))); + } + + // Open state: slide in with transition + &[open] { + visibility: visible; + @include transition(transform var(--drawer-transition-duration) var(--drawer-transition-timing), visibility 0s); + transform: none; + } + } + + // Open state base (always applies, regardless of animation mode) + &[open] { + visibility: visible; + transform: none; + } + } + + // Above breakpoint - show content inline (for responsive drawer) + // Above breakpoint - show content inline (for responsive drawer). + // Must fully reset all drawer styles so the element behaves as an + // inline flex container within its parent (e.g., a navbar). + @if not ($prefix == "") { + @include media-breakpoint-up($next) { + // stylelint-disable declaration-no-important + --drawer-height: auto; + --drawer-border-width: 0; + // Reset native UA styles + position: static !important; + inset: auto; + z-index: auto; + display: flex !important; + flex-grow: 1; + width: auto !important; + max-width: none; + height: auto !important; + max-height: none; + padding: 0; + margin: 0; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + @include transition(none !important); + // stylelint-enable declaration-no-important + + .drawer-header { + display: none; + } + + .drawer-body { + display: flex; + flex-grow: 0; + flex-direction: row; + width: 100%; + padding: 0; + overflow-y: visible; + // stylelint-disable-next-line declaration-no-important + background-color: transparent !important; + } + @include border-radius(0); + @include box-shadow(none); + } + } + } + } + + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer::backdrop { + background-color: var(--drawer-backdrop-bg); + backdrop-filter: blur(var(--drawer-backdrop-blur)); + @include backdrop-transitions(var(--drawer-transition-duration), var(--drawer-transition-timing)); + } + } + + // Backdrop entry animation — ::backdrop can safely use @starting-style + // since it only exists when the dialog is in the top layer (no responsive issue). + @starting-style { + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + } + } + + // Static backdrop transition ("bounce") + .drawer-static { + transform: scale(1.02); + } + + .drawer-translucent { + background-color: color-mix(in oklch, var(--drawer-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + } + + // Sheet variant: flush-to-edge panel with no inset, border-radius, or shadow. + // Overrides tokens so placement transforms (which use calc() with --drawer-inset) + // automatically position the drawer at the viewport edge. + .drawer-sheet { + right: 0; + bottom: 0; + left: 0; + width: 100vw; + margin-inline: auto; + margin-bottom: calc(-1 * var(--drawer-border-width)); + border-end-start-radius: 0; + border-end-end-radius: 0; + + @include media-breakpoint-up(lg) { + max-width: var(--drawer-sheet-width, 760px); + } + } + + // Header with close button + .drawer-header { + @include dialog-header(var(--drawer-padding-y) var(--drawer-padding-x)); + + .btn-close { + margin-block: calc(-.5 * var(--drawer-padding-y)); + margin-inline-start: auto; + } + } + + // Title + .drawer-title { + @include dialog-title(var(--drawer-title-line-height)); + } + + // Scrollable body + .drawer-body { + display: flex; + flex-direction: column; + gap: var(--drawer-padding-y); + @include dialog-body(var(--drawer-padding-y) var(--drawer-padding-x)); + overflow-y: auto; + } + + // Optional footer + .drawer-footer { + @include dialog-footer(var(--drawer-padding-y) var(--drawer-padding-x), .5rem, var(--drawer-border-width), var(--drawer-border-color)); + } + + .drawer-fit-content { + inset-block-end: auto; + } +} diff --git a/assets/stylesheets/bootstrap/_dropdown.scss b/assets/stylesheets/bootstrap/_dropdown.scss deleted file mode 100644 index 587ebb48..00000000 --- a/assets/stylesheets/bootstrap/_dropdown.scss +++ /dev/null @@ -1,250 +0,0 @@ -// The dropdown wrapper (``) -.dropup, -.dropend, -.dropdown, -.dropstart, -.dropup-center, -.dropdown-center { - position: relative; -} - -.dropdown-toggle { - white-space: nowrap; - - // Generate the caret automatically - @include caret(); -} - -// The dropdown menu -.dropdown-menu { - // scss-docs-start dropdown-css-vars - --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; - --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; - --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; - --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; - --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; - @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); - --#{$prefix}dropdown-color: #{$dropdown-color}; - --#{$prefix}dropdown-bg: #{$dropdown-bg}; - --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; - --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; - --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; - --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; - --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; - --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; - --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; - --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; - --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; - --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; - --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; - --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; - --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; - --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; - --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; - --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; - --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; - --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; - // scss-docs-end dropdown-css-vars - - position: absolute; - z-index: var(--#{$prefix}dropdown-zindex); - display: none; // none by default, but block on "open" of the menu - min-width: var(--#{$prefix}dropdown-min-width); - padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); - margin: 0; // Override default margin of ul - @include font-size(var(--#{$prefix}dropdown-font-size)); - color: var(--#{$prefix}dropdown-color); - text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) - list-style: none; - background-color: var(--#{$prefix}dropdown-bg); - background-clip: padding-box; - border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); - @include border-radius(var(--#{$prefix}dropdown-border-radius)); - @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); - - &[data-bs-popper] { - top: 100%; - left: 0; - margin-top: var(--#{$prefix}dropdown-spacer); - } - - @if $dropdown-padding-y == 0 { - > .dropdown-item:first-child, - > li:first-child .dropdown-item { - @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); - } - > .dropdown-item:last-child, - > li:last-child .dropdown-item { - @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); - } - - } -} - -// scss-docs-start responsive-breakpoints -// We deliberately hardcode the `bs-` prefix because we check -// this custom property in JS to determine Popper's positioning - -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - .dropdown-menu#{$infix}-start { - --bs-position: start; - - &[data-bs-popper] { - right: auto; - left: 0; - } - } - - .dropdown-menu#{$infix}-end { - --bs-position: end; - - &[data-bs-popper] { - right: 0; - left: auto; - } - } - } -} -// scss-docs-end responsive-breakpoints - -// Allow for dropdowns to go bottom up (aka, dropup-menu) -// Just add .dropup after the standard .dropdown class and you're set. -.dropup { - .dropdown-menu[data-bs-popper] { - top: auto; - bottom: 100%; - margin-top: 0; - margin-bottom: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(up); - } -} - -.dropend { - .dropdown-menu[data-bs-popper] { - top: 0; - right: auto; - left: 100%; - margin-top: 0; - margin-left: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(end); - &::after { - vertical-align: 0; - } - } -} - -.dropstart { - .dropdown-menu[data-bs-popper] { - top: 0; - right: 100%; - left: auto; - margin-top: 0; - margin-right: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(start); - &::before { - vertical-align: 0; - } - } -} - - -// Dividers (basically an ``) within the dropdown -.dropdown-divider { - height: 0; - margin: var(--#{$prefix}dropdown-divider-margin-y) 0; - overflow: hidden; - border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); - opacity: 1; // Revisit in v6 to de-dupe styles that conflict with element -} - -// Links, buttons, and more within the dropdown menu -// -// ``-specific styles are denoted with `// For s` -.dropdown-item { - display: block; - width: 100%; // For ``s - padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x); - clear: both; - font-weight: $font-weight-normal; - color: var(--#{$prefix}dropdown-link-color); - text-align: inherit; // For ``s - text-decoration: if($link-decoration == none, null, none); - white-space: nowrap; // prevent links from randomly breaking onto new lines - background-color: transparent; // For ``s - border: 0; // For ``s - @include border-radius(var(--#{$prefix}dropdown-item-border-radius, 0)); - - &:hover, - &:focus { - color: var(--#{$prefix}dropdown-link-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - @include gradient-bg(var(--#{$prefix}dropdown-link-hover-bg)); - } - - &.active, - &:active { - color: var(--#{$prefix}dropdown-link-active-color); - text-decoration: none; - @include gradient-bg(var(--#{$prefix}dropdown-link-active-bg)); - } - - &.disabled, - &:disabled { - color: var(--#{$prefix}dropdown-link-disabled-color); - pointer-events: none; - background-color: transparent; - // Remove CSS gradients if they're enabled - background-image: if($enable-gradients, none, null); - } -} - -.dropdown-menu.show { - display: block; -} - -// Dropdown section headers -.dropdown-header { - display: block; - padding: var(--#{$prefix}dropdown-header-padding-y) var(--#{$prefix}dropdown-header-padding-x); - margin-bottom: 0; // for use with heading elements - @include font-size($font-size-sm); - color: var(--#{$prefix}dropdown-header-color); - white-space: nowrap; // as with > li > a -} - -// Dropdown text -.dropdown-item-text { - display: block; - padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x); - color: var(--#{$prefix}dropdown-link-color); -} - -// Dark dropdowns -.dropdown-menu-dark { - // scss-docs-start dropdown-dark-css-vars - --#{$prefix}dropdown-color: #{$dropdown-dark-color}; - --#{$prefix}dropdown-bg: #{$dropdown-dark-bg}; - --#{$prefix}dropdown-border-color: #{$dropdown-dark-border-color}; - --#{$prefix}dropdown-box-shadow: #{$dropdown-dark-box-shadow}; - --#{$prefix}dropdown-link-color: #{$dropdown-dark-link-color}; - --#{$prefix}dropdown-link-hover-color: #{$dropdown-dark-link-hover-color}; - --#{$prefix}dropdown-divider-bg: #{$dropdown-dark-divider-bg}; - --#{$prefix}dropdown-link-hover-bg: #{$dropdown-dark-link-hover-bg}; - --#{$prefix}dropdown-link-active-color: #{$dropdown-dark-link-active-color}; - --#{$prefix}dropdown-link-active-bg: #{$dropdown-dark-link-active-bg}; - --#{$prefix}dropdown-link-disabled-color: #{$dropdown-dark-link-disabled-color}; - --#{$prefix}dropdown-header-color: #{$dropdown-dark-header-color}; - // scss-docs-end dropdown-dark-css-vars -} diff --git a/assets/stylesheets/bootstrap/_forms.scss b/assets/stylesheets/bootstrap/_forms.scss deleted file mode 100644 index 7b17d849..00000000 --- a/assets/stylesheets/bootstrap/_forms.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import "forms/labels"; -@import "forms/form-text"; -@import "forms/form-control"; -@import "forms/form-select"; -@import "forms/form-check"; -@import "forms/form-range"; -@import "forms/floating-labels"; -@import "forms/input-group"; -@import "forms/validation"; diff --git a/assets/stylesheets/bootstrap/_functions.scss b/assets/stylesheets/bootstrap/_functions.scss index 59d431a1..9c9b1ac7 100644 --- a/assets/stylesheets/bootstrap/_functions.scss +++ b/assets/stylesheets/bootstrap/_functions.scss @@ -1,3 +1,12 @@ +@use "sass:color"; +@use "sass:list"; +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +@use "sass:string"; +@forward "config" show defaults; +@use "config" as *; + // Bootstrap functions // // Utility mixins and functions for evaluating source code across our variables, maps, and mixins. @@ -8,9 +17,9 @@ $prev-key: null; $prev-num: null; @each $key, $num in $map { - @if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" { + @if $prev-num == null or math.unit($num) == "%" or math.unit($prev-num) == "%" { // Do nothing - } @else if not comparable($prev-num, $num) { + } @else if not math.compatible($prev-num, $num) { @warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !"; } @else if $prev-num >= $num { @warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !"; @@ -22,64 +31,23 @@ // Starts at zero // Used to ensure the min-width of the lowest breakpoint starts at 0. -@mixin _assert-starts-at-zero($map, $map-name: "$grid-breakpoints") { - @if length($map) > 0 { - $values: map-values($map); - $first-value: nth($values, 1); +@mixin _assert-starts-at-zero($map, $map-name: "$breakpoints") { + @if list.length($map) > 0 { + $values: map.values($map); + $first-value: list.nth($values, 1); @if $first-value != 0 { @warn "First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}."; } } } -// Colors -@function to-rgb($value) { - @return red($value), green($value), blue($value); -} - -// stylelint-disable scss/dollar-variable-pattern -@function rgba-css-var($identifier, $target) { - @if $identifier == "body" and $target == "bg" { - @return rgba(var(--#{$prefix}#{$identifier}-bg-rgb), var(--#{$prefix}#{$target}-opacity)); - } @if $identifier == "body" and $target == "text" { - @return rgba(var(--#{$prefix}#{$identifier}-color-rgb), var(--#{$prefix}#{$target}-opacity)); - } @else { - @return rgba(var(--#{$prefix}#{$identifier}-rgb), var(--#{$prefix}#{$target}-opacity)); - } -} - -@function map-loop($map, $func, $args...) { - $_map: (); - - @each $key, $value in $map { - // allow to pass the $key and $value of the map as an function argument - $_args: (); - @each $arg in $args { - $_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg))); - } - - $_map: map-merge($_map, ($key: call(get-function($func), $_args...))); - } - - @return $_map; -} -// stylelint-enable scss/dollar-variable-pattern - -@function varify($list) { - $result: null; - @each $entry in $list { - $result: append($result, var(--#{$prefix}#{$entry}), space); - } - @return $result; -} - // Internal Bootstrap function to turn maps into its negative variant. // It prefixes the keys with `n` and makes the value negative. @function negativify-map($map) { $result: (); @each $key, $value in $map { @if $key != 0 { - $result: map-merge($result, ("n" + $key: (-$value))); + $result: map.merge($result, ("n" + $key: (-$value))); } } @return $result; @@ -89,8 +57,25 @@ @function map-get-multiple($map, $values) { $result: (); @each $key, $value in $map { - @if (index($values, $key) != null) { - $result: map-merge($result, ($key: $value)); + @if (list.index($values, $key) != null) { + $result: map.merge($result, ($key: $value)); + } + } + @return $result; +} + +// Extract a specific nested property from all items in a map +// Useful for extracting a single property from nested map structures +// Example: map-get-nested($font-sizes, "font-size") +// Returns: ("xs": clamp(...), "sm": clamp(...), ...) +@function map-get-nested($map, $nested-key) { + $result: (); + @each $key, $value in $map { + @if meta.type-of($value) == "map" { + $nested-value: map.get($value, $nested-key); + @if $nested-value != null { + $result: map.merge($result, ($key: $nested-value)); + } } } @return $result; @@ -101,7 +86,7 @@ $merged-maps: (); @each $map in $maps { - $merged-maps: map-merge($merged-maps, $map); + $merged-maps: map.merge($merged-maps, $map); } @return $merged-maps; } @@ -115,10 +100,10 @@ // @param {String} $replace ('') - New value // @return {String} - Updated string @function str-replace($string, $search, $replace: "") { - $index: str-index($string, $search); + $index: string.index($string, $search); @if $index { - @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); + @return string.slice($string, 1, $index - 1) + $replace + str-replace(string.slice($string, $index + string.length($search)), $search, $replace); } @return $string; @@ -129,11 +114,11 @@ // Requires the use of quotes around data URIs. @function escape-svg($string) { - @if str-index($string, "data:image/svg+xml") { + @if string.index($string, "data:image/svg+xml") { @each $char, $encoded in $escaped-characters { // Do not escape the url brackets - @if str-index($string, "url(") == 1 { - $string: url("#{str-replace(str-slice($string, 6, -3), $char, $encoded)}"); + @if string.index($string, "url(") == 1 { + $string: url("#{str-replace(string.slice($string, 6, -3), $char, $encoded)}"); } @else { $string: str-replace($string, $char, $encoded); } @@ -151,7 +136,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 .0033 .0037 .004 .0044 .0048 .0052 .0056 .006 .0065 .007 .0075 .008 .0086 .0091 .0097 .0103 .011 .0116 .0123 .013 .0137 .0144 .0152 .016 .0168 .0176 .0185 .0194 .0203 .0212 .0222 .0232 .0242 .0252 .0262 .0273 .0284 .0296 .0307 .0319 .0331 .0343 .0356 .0369 .0382 .0395 .0409 .0423 .0437 .0452 .0467 .0482 .0497 .0513 .0529 .0545 .0561 .0578 .0595 .0612 .063 .0648 .0666 .0685 .0704 .0723 .0742 .0762 .0782 .0802 .0823 .0844 .0865 .0887 .0908 .0931 .0953 .0976 .0999 .1022 .1046 .107 .1095 .1119 .1144 .117 .1195 .1221 .1248 .1274 .1301 .1329 .1356 .1384 .1413 .1441 .147 .15 .1529 .1559 .159 .162 .1651 .1683 .1714 .1746 .1779 .1812 .1845 .1878 .1912 .1946 .1981 .2016 .2051 .2086 .2122 .2159 .2195 .2232 .227 .2307 .2346 .2384 .2423 .2462 .2502 .2542 .2582 .2623 .2664 .2705 .2747 .2789 .2831 .2874 .2918 .2961 .3005 .305 .3095 .314 .3185 .3231 .3278 .3325 .3372 .3419 .3467 .3515 .3564 .3613 .3663 .3712 .3763 .3813 .3864 .3916 .3968 .402 .4072 .4125 .4179 .4233 .4287 .4342 .4397 .4452 .4508 .4564 .4621 .4678 .4735 .4793 .4851 .491 .4969 .5029 .5089 .5149 .521 .5271 .5333 .5395 .5457 .552 .5583 .5647 .5711 .5776 .5841 .5906 .5972 .6038 .6105 .6172 .624 .6308 .6376 .6445 .6514 .6584 .6654 .6724 .6795 .6867 .6939 .7011 .7084 .7157 .7231 .7305 .7379 .7454 .7529 .7605 .7682 .7758 .7835 .7913 .7991 .807 .8148 .8228 .8308 .8388 .8469 .855 .8632 .8714 .8796 .8879 .8963 .9047 .9131 .9216 .9301 .9387 .9473 .956 .9647 .9734 .9823 .9911 1; @function color-contrast($background, $color-contrast-dark: $color-contrast-dark, $color-contrast-light: $color-contrast-light, $min-contrast-ratio: $min-contrast-ratio) { - $foregrounds: $color-contrast-light, $color-contrast-dark, $white, $black; + $foregrounds: $color-contrast-light, $color-contrast-dark, #fff, #000; $max-ratio: 0; $max-ratio-color: null; @@ -174,7 +159,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 $l1: luminance($background); $l2: luminance(opaque($background, $foreground)); - @return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05)); + @return if(sass($l1 > $l2): math.div($l1 + .05, $l2 + .05); else: math.div($l2 + .05, $l1 + .05)); } // Return WCAG2.2 relative luminance @@ -182,121 +167,22 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 // See https://www.w3.org/TR/WCAG/#dfn-contrast-ratio @function luminance($color) { $rgb: ( - "r": red($color), - "g": green($color), - "b": blue($color) + "r": color.channel($color, "red"), + "g": color.channel($color, "green"), + "b": color.channel($color, "blue") ); @each $name, $value in $rgb { - $value: if(divide($value, 255) < .04045, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1)); - $rgb: map-merge($rgb, ($name: $value)); + // stylelint-disable-next-line scss/at-function-named-arguments, @stylistic/function-whitespace-after + $value: if(sass(math.div($value, 255) < .04045): math.div(math.div($value, 255), 12.92); else: list.nth($_luminance-list, math.round($value + 1))); + $rgb: map.merge($rgb, ($name: $value)); } - @return (map-get($rgb, "r") * .2126) + (map-get($rgb, "g") * .7152) + (map-get($rgb, "b") * .0722); + @return (map.get($rgb, "r") * .2126) + (map.get($rgb, "g") * .7152) + (map.get($rgb, "b") * .0722); } // Return opaque color -// opaque(#fff, rgba(0, 0, 0, .5)) => #808080 +// opaque(#fff, rgb(0 0 0 / .5)) => #808080 @function opaque($background, $foreground) { - @return mix(rgba($foreground, 1), $background, opacity($foreground) * 100%); -} - -// scss-docs-start color-functions -// Tint a color: mix a color with white -@function tint-color($color, $weight) { - @return mix(white, $color, $weight); -} - -// Shade a color: mix a color with black -@function shade-color($color, $weight) { - @return mix(black, $color, $weight); -} - -// Shade the color if the weight is positive, else tint it -@function shift-color($color, $weight) { - @return if($weight > 0, shade-color($color, $weight), tint-color($color, -$weight)); -} -// scss-docs-end color-functions - -// Return valid calc -@function add($value1, $value2, $return-calc: true) { - @if $value1 == null { - @return $value2; - } - - @if $value2 == null { - @return $value1; - } - - @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) { - @return $value1 + $value2; - } - - @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2); -} - -@function subtract($value1, $value2, $return-calc: true) { - @if $value1 == null and $value2 == null { - @return null; - } - - @if $value1 == null { - @return -$value2; - } - - @if $value2 == null { - @return $value1; - } - - @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) { - @return $value1 - $value2; - } - - @if type-of($value2) != number { - $value2: unquote("(") + $value2 + unquote(")"); - } - - @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(" - ") + $value2); -} - -@function divide($dividend, $divisor, $precision: 10) { - $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1); - $dividend: abs($dividend); - $divisor: abs($divisor); - @if $dividend == 0 { - @return 0; - } - @if $divisor == 0 { - @error "Cannot divide by 0"; - } - $remainder: $dividend; - $result: 0; - $factor: 10; - @while ($remainder > 0 and $precision >= 0) { - $quotient: 0; - @while ($remainder >= $divisor) { - $remainder: $remainder - $divisor; - $quotient: $quotient + 1; - } - $result: $result * 10 + $quotient; - $factor: $factor * .1; - $remainder: $remainder * 10; - $precision: $precision - 1; - @if ($precision < 0 and $remainder >= $divisor * 5) { - $result: $result + 1; - } - } - $result: $result * $factor * $sign; - $dividend-unit: unit($dividend); - $divisor-unit: unit($divisor); - $unit-map: ( - "px": 1px, - "rem": 1rem, - "em": 1em, - "%": 1% - ); - @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) { - $result: $result * map-get($unit-map, $dividend-unit); - } - @return $result; + @return color-mix(in srgb, rgba($foreground, 1), $background, color.opacity($foreground) * 100%); } diff --git a/assets/stylesheets/bootstrap/_grid.scss b/assets/stylesheets/bootstrap/_grid.scss deleted file mode 100644 index 048f8009..00000000 --- a/assets/stylesheets/bootstrap/_grid.scss +++ /dev/null @@ -1,39 +0,0 @@ -// Row -// -// Rows contain your columns. - -:root { - @each $name, $value in $grid-breakpoints { - --#{$prefix}breakpoint-#{$name}: #{$value}; - } -} - -@if $enable-grid-classes { - .row { - @include make-row(); - - > * { - @include make-col-ready(); - } - } -} - -@if $enable-cssgrid { - .grid { - display: grid; - grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr); - grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr); - gap: var(--#{$prefix}gap, #{$grid-gutter-width}); - - @include make-cssgrid(); - } -} - - -// Columns -// -// Common styles for small and large grid columns - -@if $enable-grid-classes { - @include make-grid-columns(); -} diff --git a/assets/stylesheets/bootstrap/_helpers.scss b/assets/stylesheets/bootstrap/_helpers.scss deleted file mode 100644 index 13f2752c..00000000 --- a/assets/stylesheets/bootstrap/_helpers.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import "helpers/clearfix"; -@import "helpers/color-bg"; -@import "helpers/colored-links"; -@import "helpers/focus-ring"; -@import "helpers/icon-link"; -@import "helpers/ratio"; -@import "helpers/position"; -@import "helpers/stacks"; -@import "helpers/visually-hidden"; -@import "helpers/stretched-link"; -@import "helpers/text-truncation"; -@import "helpers/vr"; diff --git a/assets/stylesheets/bootstrap/_images.scss b/assets/stylesheets/bootstrap/_images.scss deleted file mode 100644 index 3d6a1014..00000000 --- a/assets/stylesheets/bootstrap/_images.scss +++ /dev/null @@ -1,42 +0,0 @@ -// Responsive images (ensure images don't scale beyond their parents) -// -// This is purposefully opt-in via an explicit class rather than being the default for all ``s. -// We previously tried the "images are responsive by default" approach in Bootstrap v2, -// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps) -// which weren't expecting the images within themselves to be involuntarily resized. -// See also https://github.com/twbs/bootstrap/issues/18178 -.img-fluid { - @include img-fluid(); -} - - -// Image thumbnails -.img-thumbnail { - padding: $thumbnail-padding; - background-color: $thumbnail-bg; - border: $thumbnail-border-width solid $thumbnail-border-color; - @include border-radius($thumbnail-border-radius); - @include box-shadow($thumbnail-box-shadow); - - // Keep them at most 100% wide - @include img-fluid(); -} - -// -// Figures -// - -.figure { - // Ensures the caption's text aligns with the image. - display: inline-block; -} - -.figure-img { - margin-bottom: $spacer * .5; - line-height: 1; -} - -.figure-caption { - @include font-size($figure-caption-font-size); - color: $figure-caption-color; -} diff --git a/assets/stylesheets/bootstrap/_list-group.scss b/assets/stylesheets/bootstrap/_list-group.scss index 3bdff679..6860abd7 100644 --- a/assets/stylesheets/bootstrap/_list-group.scss +++ b/assets/stylesheets/bootstrap/_list-group.scss @@ -1,199 +1,191 @@ -// Base class -// -// Easily usable on , , or . - -.list-group { - // scss-docs-start list-group-css-vars - --#{$prefix}list-group-color: #{$list-group-color}; - --#{$prefix}list-group-bg: #{$list-group-bg}; - --#{$prefix}list-group-border-color: #{$list-group-border-color}; - --#{$prefix}list-group-border-width: #{$list-group-border-width}; - --#{$prefix}list-group-border-radius: #{$list-group-border-radius}; - --#{$prefix}list-group-item-padding-x: #{$list-group-item-padding-x}; - --#{$prefix}list-group-item-padding-y: #{$list-group-item-padding-y}; - --#{$prefix}list-group-action-color: #{$list-group-action-color}; - --#{$prefix}list-group-action-hover-color: #{$list-group-action-hover-color}; - --#{$prefix}list-group-action-hover-bg: #{$list-group-hover-bg}; - --#{$prefix}list-group-action-active-color: #{$list-group-action-active-color}; - --#{$prefix}list-group-action-active-bg: #{$list-group-action-active-bg}; - --#{$prefix}list-group-disabled-color: #{$list-group-disabled-color}; - --#{$prefix}list-group-disabled-bg: #{$list-group-disabled-bg}; - --#{$prefix}list-group-active-color: #{$list-group-active-color}; - --#{$prefix}list-group-active-bg: #{$list-group-active-bg}; - --#{$prefix}list-group-active-border-color: #{$list-group-active-border-color}; - // scss-docs-end list-group-css-vars - - display: flex; - flex-direction: column; - - // No need to set list-style: none; since .list-group-item is block level - padding-left: 0; // reset padding because ul and ol - margin-bottom: 0; - @include border-radius(var(--#{$prefix}list-group-border-radius)); -} - -.list-group-numbered { - list-style-type: none; - counter-reset: section; - - > .list-group-item::before { - // Increments only this instance of the section counter - content: counters(section, ".") ". "; - counter-increment: section; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "layout/breakpoints" as *; +@use "mixins/tokens" as *; + +$list-group-tokens: () !default; + +// scss-docs-start list-group-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$list-group-tokens: defaults( + ( + --list-group-color: var(--fg-body), + --list-group-bg: var(--bg-body), + --list-group-border-color: var(--border-color), + --list-group-border-width: var(--border-width), + --list-group-border-radius: var(--radius-5), + --list-group-item-padding-x: var(--spacer), + --list-group-item-padding-y: var(--spacer-2), + --list-group-action-color: var(--fg-2), + --list-group-action-hover-color: var(--fg-1), + --list-group-action-hover-bg: var(--bg-1), + --list-group-action-active-color: var(--fg-body), + --list-group-action-active-bg: var(--bg-2), + --list-group-disabled-color: var(--fg-3), + --list-group-disabled-bg: var(--bg-body), + --list-group-active-color: var(--primary-contrast), + --list-group-active-bg: var(--primary-bg), + --list-group-active-border-color: var(--primary-bg), + ), + $list-group-tokens +); +// scss-docs-end list-group-tokens + +@layer components { + .list-group { + @include tokens($list-group-tokens); + + display: flex; + flex-direction: column; + + // No need to set list-style-type: ""; since .list-group-item is block level + padding-inline-start: 0; // reset padding because ul and ol + margin-bottom: 0; + @include border-radius(var(--list-group-border-radius)); } -} -// Individual list items -// -// Use on `li`s or `div`s within the `.list-group` parent. - -.list-group-item { - position: relative; - display: block; - padding: var(--#{$prefix}list-group-item-padding-y) var(--#{$prefix}list-group-item-padding-x); - color: var(--#{$prefix}list-group-color); - text-decoration: if($link-decoration == none, null, none); - background-color: var(--#{$prefix}list-group-bg); - border: var(--#{$prefix}list-group-border-width) solid var(--#{$prefix}list-group-border-color); - - &:first-child { - @include border-top-radius(inherit); - } + .list-group-numbered { + list-style-type: none; + counter-reset: section; - &:last-child { - @include border-bottom-radius(inherit); + > .list-group-item::before { + // Increments only this instance of the section counter + content: counters(section, ".") ". "; + counter-increment: section; + } } - &.disabled, - &:disabled { - color: var(--#{$prefix}list-group-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}list-group-disabled-bg); - } + // Individual list items + // + // Use on `li`s or `div`s within the `.list-group` parent. + + .list-group-item { + position: relative; + display: block; + padding: var(--list-group-item-padding-y) var(--list-group-item-padding-x); + color: var(--theme-fg, var(--list-group-color)); + // stylelint-disable-next-line scss/at-function-named-arguments + text-decoration: if(sass($link-decoration == none): null); + background-color: var(--theme-bg-subtle, var(--list-group-bg)); + border: var(--list-group-border-width) solid var(--theme-border, var(--list-group-border-color)); + + &:first-child { + @include border-top-radius(inherit); + } - // Include both here for ``s and ``s - &.active { - z-index: 2; // Place active items above their siblings for proper border styling - color: var(--#{$prefix}list-group-active-color); - background-color: var(--#{$prefix}list-group-active-bg); - border-color: var(--#{$prefix}list-group-active-border-color); - } + &:last-child { + @include border-bottom-radius(inherit); + } - // stylelint-disable-next-line scss/selector-no-redundant-nesting-selector - & + .list-group-item { - border-top-width: 0; + &.disabled, + &:disabled { + color: var(--list-group-disabled-color); + pointer-events: none; + background-color: var(--list-group-disabled-bg); + } + // Include both here for ``s and ``s &.active { - margin-top: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list - border-top-width: var(--#{$prefix}list-group-border-width); + z-index: 2; // Place active items above their siblings for proper border styling + color: var(--list-group-active-color); + background-color: var(--list-group-active-bg); + border-color: var(--list-group-active-border-color); } - } -} -// Interactive list items -// -// Use anchor or button elements instead of `li`s or `div`s to create interactive -// list items. Includes an extra `.active` modifier class for selected items. - -.list-group-item-action { - width: 100%; // For ``s (anchors become 100% by default though) - color: var(--#{$prefix}list-group-action-color); - text-align: inherit; // For ``s (anchors inherit) - - &:not(.active) { - // Hover state - &:hover, - &:focus { - z-index: 1; // Place hover/focus items above their siblings for proper border styling - color: var(--#{$prefix}list-group-action-hover-color); - text-decoration: none; - background-color: var(--#{$prefix}list-group-action-hover-bg); - } + // stylelint-disable-next-line scss/selector-no-redundant-nesting-selector + & + .list-group-item { + border-block-start-width: 0; - &:active { - color: var(--#{$prefix}list-group-action-active-color); - background-color: var(--#{$prefix}list-group-action-active-bg); + &.active { + margin-top: calc(-1 * var(--list-group-border-width)); + border-block-start-width: var(--list-group-border-width); + } } } -} -// Horizontal -// -// Change the layout of list group items from vertical (default) to horizontal. - -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + // Interactive list items + // + // Use anchor or button elements instead of `li`s or `div`s to create interactive + // list items. Includes an extra `.active` modifier class for selected items. + + .list-group-item-action { + width: 100%; // For ``s (anchors become 100% by default though) + color: var(--theme-fg, var(--list-group-action-color)); + text-align: inherit; // For ``s (anchors inherit) + text-decoration: none; + + &:not(.active) { + // Hover state + &:hover, + &:focus { + z-index: 1; // Place hover/focus items above their siblings for proper border styling + color: var(--theme-fg-emphasis, var(--list-group-action-hover-color)); + text-decoration: none; + background-color: var(--theme-bg-muted, var(--list-group-action-hover-bg)); + } - .list-group-horizontal#{$infix} { - flex-direction: row; + &:active { + color: var(--theme-fg-emphasis, var(--list-group-action-active-color)); + background-color: var(--theme-bg-muted, var(--list-group-action-active-bg)); + } + } + } - > .list-group-item { - &:first-child:not(:last-child) { - @include border-bottom-start-radius(var(--#{$prefix}list-group-border-radius)); - @include border-top-end-radius(0); - } + // Horizontal + // + // Change the layout of list group items from vertical (default) to horizontal. + // The responsive variants use container queries, so wrap the list group in a + // query container (e.g., the `.contains-inline` utility) for them to take effect. + + @include loop-breakpoints-up() using ($breakpoint, $prefix) { + .#{$prefix}list-group-horizontal { + @include container-breakpoint-up($breakpoint) { + flex-direction: row; + + > .list-group-item { + &:first-child:not(:last-child) { + @include border-bottom-start-radius(var(--list-group-border-radius)); + @include border-top-end-radius(0); + } - &:last-child:not(:first-child) { - @include border-top-end-radius(var(--#{$prefix}list-group-border-radius)); - @include border-bottom-start-radius(0); - } + &:last-child:not(:first-child) { + @include border-top-end-radius(var(--list-group-border-radius)); + @include border-bottom-start-radius(0); + } - &.active { - margin-top: 0; - } + &.active { + margin-top: 0; + } - + .list-group-item { - border-top-width: var(--#{$prefix}list-group-border-width); - border-left-width: 0; + + .list-group-item { + border-block-start-width: var(--list-group-border-width); + border-inline-start-width: 0; - &.active { - margin-left: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list - border-left-width: var(--#{$prefix}list-group-border-width); + &.active { + margin-inline-start: calc(-1 * var(--list-group-border-width)); + border-inline-start-width: var(--list-group-border-width); + } } } } } } -} - -// Flush list items -// -// Remove borders and border-radius to keep list group items edge-to-edge. Most -// useful within other components (e.g., cards). + // Flush list items + // + // Remove borders and border-radius to keep list group items edge-to-edge. Most + // useful within other components (e.g., cards). -.list-group-flush { - @include border-radius(0); + .list-group-flush { + @include border-radius(0); - > .list-group-item { - border-width: 0 0 var(--#{$prefix}list-group-border-width); + > .list-group-item { + border-width: 0 0 var(--list-group-border-width); - &:last-child { - border-bottom-width: 0; + &:last-child { + border-block-end-width: 0; + } } } } - - -// scss-docs-start list-group-modifiers -// List group contextual variants -// -// Add modifier classes to change text and background color on individual items. -// Organizationally, this must come after the `:hover` states. - -@each $state in map-keys($theme-colors) { - .list-group-item-#{$state} { - --#{$prefix}list-group-color: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}list-group-bg: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}list-group-border-color: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-action-hover-color: var(--#{$prefix}emphasis-color); - --#{$prefix}list-group-action-hover-bg: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-action-active-color: var(--#{$prefix}emphasis-color); - --#{$prefix}list-group-action-active-bg: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-active-color: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}list-group-active-bg: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}list-group-active-border-color: var(--#{$prefix}#{$state}-text-emphasis); - } -} -// scss-docs-end list-group-modifiers diff --git a/assets/stylesheets/bootstrap/_maps.scss b/assets/stylesheets/bootstrap/_maps.scss deleted file mode 100644 index 68ee421c..00000000 --- a/assets/stylesheets/bootstrap/_maps.scss +++ /dev/null @@ -1,174 +0,0 @@ -// Re-assigned maps -// -// Placed here so that others can override the default Sass maps and see automatic updates to utilities and more. - -// scss-docs-start theme-colors-rgb -$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value") !default; -// scss-docs-end theme-colors-rgb - -// scss-docs-start theme-text-map -$theme-colors-text: ( - "primary": $primary-text-emphasis, - "secondary": $secondary-text-emphasis, - "success": $success-text-emphasis, - "info": $info-text-emphasis, - "warning": $warning-text-emphasis, - "danger": $danger-text-emphasis, - "light": $light-text-emphasis, - "dark": $dark-text-emphasis, -) !default; -// scss-docs-end theme-text-map - -// scss-docs-start theme-bg-subtle-map -$theme-colors-bg-subtle: ( - "primary": $primary-bg-subtle, - "secondary": $secondary-bg-subtle, - "success": $success-bg-subtle, - "info": $info-bg-subtle, - "warning": $warning-bg-subtle, - "danger": $danger-bg-subtle, - "light": $light-bg-subtle, - "dark": $dark-bg-subtle, -) !default; -// scss-docs-end theme-bg-subtle-map - -// scss-docs-start theme-border-subtle-map -$theme-colors-border-subtle: ( - "primary": $primary-border-subtle, - "secondary": $secondary-border-subtle, - "success": $success-border-subtle, - "info": $info-border-subtle, - "warning": $warning-border-subtle, - "danger": $danger-border-subtle, - "light": $light-border-subtle, - "dark": $dark-border-subtle, -) !default; -// scss-docs-end theme-border-subtle-map - -$theme-colors-text-dark: null !default; -$theme-colors-bg-subtle-dark: null !default; -$theme-colors-border-subtle-dark: null !default; - -@if $enable-dark-mode { - // scss-docs-start theme-text-dark-map - $theme-colors-text-dark: ( - "primary": $primary-text-emphasis-dark, - "secondary": $secondary-text-emphasis-dark, - "success": $success-text-emphasis-dark, - "info": $info-text-emphasis-dark, - "warning": $warning-text-emphasis-dark, - "danger": $danger-text-emphasis-dark, - "light": $light-text-emphasis-dark, - "dark": $dark-text-emphasis-dark, - ) !default; - // scss-docs-end theme-text-dark-map - - // scss-docs-start theme-bg-subtle-dark-map - $theme-colors-bg-subtle-dark: ( - "primary": $primary-bg-subtle-dark, - "secondary": $secondary-bg-subtle-dark, - "success": $success-bg-subtle-dark, - "info": $info-bg-subtle-dark, - "warning": $warning-bg-subtle-dark, - "danger": $danger-bg-subtle-dark, - "light": $light-bg-subtle-dark, - "dark": $dark-bg-subtle-dark, - ) !default; - // scss-docs-end theme-bg-subtle-dark-map - - // scss-docs-start theme-border-subtle-dark-map - $theme-colors-border-subtle-dark: ( - "primary": $primary-border-subtle-dark, - "secondary": $secondary-border-subtle-dark, - "success": $success-border-subtle-dark, - "info": $info-border-subtle-dark, - "warning": $warning-border-subtle-dark, - "danger": $danger-border-subtle-dark, - "light": $light-border-subtle-dark, - "dark": $dark-border-subtle-dark, - ) !default; - // scss-docs-end theme-border-subtle-dark-map -} - -// Utilities maps -// -// Extends the default `$theme-colors` maps to help create our utilities. - -// Come v6, we'll de-dupe these variables. Until then, for backward compatibility, we keep them to reassign. -// scss-docs-start utilities-colors -$utilities-colors: $theme-colors-rgb !default; -// scss-docs-end utilities-colors - -// scss-docs-start utilities-text-colors -$utilities-text: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white), - "body": to-rgb($body-color) - ) -) !default; -$utilities-text-colors: map-loop($utilities-text, rgba-css-var, "$key", "text") !default; - -$utilities-text-emphasis-colors: ( - "primary-emphasis": var(--#{$prefix}primary-text-emphasis), - "secondary-emphasis": var(--#{$prefix}secondary-text-emphasis), - "success-emphasis": var(--#{$prefix}success-text-emphasis), - "info-emphasis": var(--#{$prefix}info-text-emphasis), - "warning-emphasis": var(--#{$prefix}warning-text-emphasis), - "danger-emphasis": var(--#{$prefix}danger-text-emphasis), - "light-emphasis": var(--#{$prefix}light-text-emphasis), - "dark-emphasis": var(--#{$prefix}dark-text-emphasis) -) !default; -// scss-docs-end utilities-text-colors - -// scss-docs-start utilities-bg-colors -$utilities-bg: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white), - "body": to-rgb($body-bg) - ) -) !default; -$utilities-bg-colors: map-loop($utilities-bg, rgba-css-var, "$key", "bg") !default; - -$utilities-bg-subtle: ( - "primary-subtle": var(--#{$prefix}primary-bg-subtle), - "secondary-subtle": var(--#{$prefix}secondary-bg-subtle), - "success-subtle": var(--#{$prefix}success-bg-subtle), - "info-subtle": var(--#{$prefix}info-bg-subtle), - "warning-subtle": var(--#{$prefix}warning-bg-subtle), - "danger-subtle": var(--#{$prefix}danger-bg-subtle), - "light-subtle": var(--#{$prefix}light-bg-subtle), - "dark-subtle": var(--#{$prefix}dark-bg-subtle) -) !default; -// scss-docs-end utilities-bg-colors - -// scss-docs-start utilities-border-colors -$utilities-border: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white) - ) -) !default; -$utilities-border-colors: map-loop($utilities-border, rgba-css-var, "$key", "border") !default; - -$utilities-border-subtle: ( - "primary-subtle": var(--#{$prefix}primary-border-subtle), - "secondary-subtle": var(--#{$prefix}secondary-border-subtle), - "success-subtle": var(--#{$prefix}success-border-subtle), - "info-subtle": var(--#{$prefix}info-border-subtle), - "warning-subtle": var(--#{$prefix}warning-border-subtle), - "danger-subtle": var(--#{$prefix}danger-border-subtle), - "light-subtle": var(--#{$prefix}light-border-subtle), - "dark-subtle": var(--#{$prefix}dark-border-subtle) -) !default; -// scss-docs-end utilities-border-colors - -$utilities-links-underline: map-loop($utilities-colors, rgba-css-var, "$key", "link-underline") !default; - -$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default; - -$gutters: $spacers !default; diff --git a/assets/stylesheets/bootstrap/_menu.scss b/assets/stylesheets/bootstrap/_menu.scss new file mode 100644 index 00000000..cddc5ab7 --- /dev/null +++ b/assets/stylesheets/bootstrap/_menu.scss @@ -0,0 +1,289 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +// stylelint-disable scss/dollar-variable-default, custom-property-no-missing-var-function +$menu-tokens: () !default; + +// scss-docs-start menu-tokens +$menu-tokens: defaults( + ( + --menu-zindex: #{$zindex-menu}, + --menu-gap: .125rem, + --menu-min-width: 10rem, + --menu-padding-x: .25rem, + --menu-padding-y: .25rem, + --menu-spacer: .125rem, + --menu-font-size: var(--font-size-sm), + --menu-color: var(--fg-body), + --menu-bg: var(--bg-body), + // --menu-border-color: var(--border-color-translucent), + // --menu-border-radius: var(--radius-7), + // --menu-border-width: var(--border-width), + --menu-box-shadow: var(--box-shadow), + // --menu-max-height: none, + --menu-divider-bg: var(--border-color-translucent), + --menu-divider-margin-y: .125rem, + --menu-divider-margin-x: .25rem, + --menu-item-color: var(--menu-color, var(--fg-body)), + --menu-item-hover-color: var(--menu-color, var(--fg-body)), + --menu-item-hover-bg: var(--bg-1), + --menu-item-active-color: var(--primary-contrast), + --menu-item-active-bg: var(--primary-bg), + --menu-item-disabled-color: var(--fg-3), + --menu-item-gap: .5rem, + --menu-item-padding-x: .75rem, + --menu-item-padding-y: .25rem, + --menu-item-border-radius: var(--radius-5), + --menu-icon-size: 1rem, + --menu-description-font-size: var(--font-size-xs), + --menu-check-color: currentcolor, + --menu-header-color: var(--fg-3), + --menu-header-padding-x: .75rem, + --menu-header-padding-y: .25rem, + --menu-transition-duration: .15s, + --menu-transition-timing: cubic-bezier(.22, 1, .36, 1), + ), + $menu-tokens +); +// scss-docs-end menu-tokens + +// stylelint-enable custom-property-no-missing-var-function, scss/dollar-variable-default + +@layer components { + .menu { + @include tokens($menu-tokens); + + position: absolute; + z-index: var(--menu-zindex); + display: none; + flex-direction: column; + gap: var(--menu-gap); + min-width: var(--menu-min-width); + max-height: var(--menu-max-height, none); + padding: var(--menu-padding-y) var(--menu-padding-x); + margin: 0; + overflow-y: var(--menu-overflow-y, initial); + overscroll-behavior: contain; + font-size: var(--menu-font-size); + color: var(--menu-color); + text-align: start; + list-style-type: ""; + background-color: var(--menu-bg); + background-clip: padding-box; + border: var(--menu-border-width, var(--border-width)) solid var(--menu-border-color, var(--border-color-translucent)); + @include border-radius(var(--menu-border-radius, var(--radius-7))); + @include box-shadow(var(--menu-box-shadow)); + opacity: 0; + transform: scale(.95); + transform-origin: top start; + + &[data-bs-placement^="top"] { + transform-origin: bottom start; + } + + &[data-bs-placement="bottom-end"] { + transform-origin: top end; + } + + &[data-bs-placement="top-end"] { + transform-origin: bottom end; + } + + &[data-bs-placement^="left"] { + transform-origin: top end; + } + + @include transition( + opacity var(--menu-transition-duration) var(--menu-transition-timing), + transform var(--menu-transition-duration) var(--menu-transition-timing), + display var(--menu-transition-duration) allow-discrete + ); + + &.show { + display: flex; + opacity: 1; + transform: none; + } + } + + @starting-style { + .menu.show { + opacity: 0; + transform: scale(.95); + } + } + + .menu-scrollable { + --menu-max-height: 80dvh; + --menu-overflow-y: auto; + } + + .menu-translucent { + --menu-item-hover-bg-light: color-mix(in oklch, var(--bg-1) 90%, transparent); + --menu-item-hover-bg-dark: color-mix(in oklch, var(--bg-1) 80%, transparent); + + --menu-item-active-bg-light: color-mix(in oklch, var(--primary-bg) 80%, transparent); + --menu-item-active-bg-dark: color-mix(in oklch, var(--primary-bg) 70%, transparent); + + --menu-item-active-bg: light-dark(var(--menu-item-active-bg-light), var(--menu-item-active-bg-dark)); + --menu-item-hover-bg: light-dark(var(--menu-item-hover-bg-light), var(--menu-item-hover-bg-dark)); + + background-color: color-mix(in oklch, var(--menu-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + } + + .menu-divider { + height: 0; + margin: var(--menu-divider-margin-y) var(--menu-divider-margin-x); + overflow: hidden; + border-block-start: 1px solid var(--menu-divider-bg); + opacity: 1; + } + + .menu-item { + display: flex; + gap: var(--menu-item-gap); + align-items: center; + width: 100%; + padding: var(--menu-item-padding-y) var(--menu-item-padding-x); + font-weight: var(--menu-item-font-weight, var(--font-weight-normal)); + color: var(--theme-fg, var(--menu-item-color)); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + background-color: transparent; + border: 0; + outline: 0; + @include border-radius(var(--menu-item-border-radius, 0)); + + &:hover, + &:focus { + color: var(--theme-fg-emphasis, var(--menu-item-hover-color)); + background-color: var(--theme-bg-subtle, var(--menu-item-hover-bg)); + // @include gradient-bg(var(--theme-bg-subtle, var(--menu-item-hover-bg))); + } + + &.active, + &:active { + color: var(--theme-contrast, var(--menu-item-active-color)); + background-color: var(--theme-bg, var(--menu-item-active-bg)); + // @include gradient-bg(var(--theme-bg, var(--menu-item-active-bg))); + + .menu-item-icon { + color: inherit !important; // stylelint-disable-line declaration-no-important + } + } + + &.selected { + font-weight: $font-weight-semibold; + } + + &.disabled, + &:disabled { + color: var(--menu-item-disabled-color); + pointer-events: none; + background-color: transparent; + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + } + } + + .menu-item-icon { + flex-shrink: 0; + align-self: flex-start; + width: var(--menu-icon-size); + height: auto; + margin-top: .125rem; + } + + .menu-item-content { + display: flex; + flex: 1; + flex-direction: column; + min-width: fit-content; + } + + .menu-item-description { + font-size: var(--menu-description-font-size); + font-weight: var(--font-weight-normal); + color: color-mix(in oklch, currentcolor 65%, transparent); + } + + .menu-item-check { + flex-shrink: 0; + align-self: flex-start; + margin-block-start: .125rem; + margin-inline-start: auto; + color: var(--menu-check-color); + visibility: hidden; + + .selected > & { + visibility: visible; + } + } + + .menu-header { + display: block; + padding: var(--menu-header-padding-y) var(--menu-header-padding-x); + margin-bottom: 0; + font-size: var(--font-size-sm); + color: var(--menu-header-color); + white-space: nowrap; + } + + .menu-text { + display: block; + padding: var(--menu-item-padding-y) var(--menu-item-padding-x); + color: var(--fg-2); + } + + // scss-docs-start submenu + .submenu { + position: relative; + + > .menu-item { + display: flex; + align-items: center; + justify-content: space-between; + } + + > .menu-item::after { + display: inline-block; + flex-shrink: 0; + width: .375em; + height: .375em; + margin-inline-start: auto; + content: ""; + border-color: currentcolor; + border-style: solid; + border-width: 0 .125em .125em 0; + transform: rotate(-45deg); + + [dir="rtl"] & { + transform: rotate(135deg); + } + } + + > .menu { + top: 0; + margin-top: calc(-1 * var(--menu-padding-y)); + } + + &:hover > .menu-item, + &:focus-within > .menu-item { + color: var(--menu-item-hover-color); + background-color: var(--menu-item-hover-bg); + } + + &.show > .menu-item { + color: var(--menu-item-hover-color); + background-color: var(--menu-item-hover-bg); + } + } + // scss-docs-end submenu +} diff --git a/assets/stylesheets/bootstrap/_mixins.scss b/assets/stylesheets/bootstrap/_mixins.scss deleted file mode 100644 index e1e130b1..00000000 --- a/assets/stylesheets/bootstrap/_mixins.scss +++ /dev/null @@ -1,42 +0,0 @@ -// Toggles -// -// Used in conjunction with global variables to enable certain theme features. - -// Vendor -@import "vendor/rfs"; - -// Deprecate -@import "mixins/deprecate"; - -// Helpers -@import "mixins/breakpoints"; -@import "mixins/color-mode"; -@import "mixins/color-scheme"; -@import "mixins/image"; -@import "mixins/resize"; -@import "mixins/visually-hidden"; -@import "mixins/reset-text"; -@import "mixins/text-truncate"; - -// Utilities -@import "mixins/utilities"; - -// Components -@import "mixins/backdrop"; -@import "mixins/buttons"; -@import "mixins/caret"; -@import "mixins/pagination"; -@import "mixins/lists"; -@import "mixins/forms"; -@import "mixins/table-variants"; - -// Skins -@import "mixins/border-radius"; -@import "mixins/box-shadow"; -@import "mixins/gradients"; -@import "mixins/transition"; - -// Layout -@import "mixins/clearfix"; -@import "mixins/container"; -@import "mixins/grid"; diff --git a/assets/stylesheets/bootstrap/_modal.scss b/assets/stylesheets/bootstrap/_modal.scss deleted file mode 100644 index a3492c17..00000000 --- a/assets/stylesheets/bootstrap/_modal.scss +++ /dev/null @@ -1,240 +0,0 @@ -// stylelint-disable function-disallowed-list - -// .modal-open - body class for killing the scroll -// .modal - container to scroll within -// .modal-dialog - positioning shell for the actual modal -// .modal-content - actual modal w/ bg and corners and stuff - - -// Container that the modal scrolls within -.modal { - // scss-docs-start modal-css-vars - --#{$prefix}modal-zindex: #{$zindex-modal}; - --#{$prefix}modal-width: #{$modal-md}; - --#{$prefix}modal-padding: #{$modal-inner-padding}; - --#{$prefix}modal-margin: #{$modal-dialog-margin}; - --#{$prefix}modal-color: #{$modal-content-color}; - --#{$prefix}modal-bg: #{$modal-content-bg}; - --#{$prefix}modal-border-color: #{$modal-content-border-color}; - --#{$prefix}modal-border-width: #{$modal-content-border-width}; - --#{$prefix}modal-border-radius: #{$modal-content-border-radius}; - --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-xs}; - --#{$prefix}modal-inner-border-radius: #{$modal-content-inner-border-radius}; - --#{$prefix}modal-header-padding-x: #{$modal-header-padding-x}; - --#{$prefix}modal-header-padding-y: #{$modal-header-padding-y}; - --#{$prefix}modal-header-padding: #{$modal-header-padding}; // Todo in v6: Split this padding into x and y - --#{$prefix}modal-header-border-color: #{$modal-header-border-color}; - --#{$prefix}modal-header-border-width: #{$modal-header-border-width}; - --#{$prefix}modal-title-line-height: #{$modal-title-line-height}; - --#{$prefix}modal-footer-gap: #{$modal-footer-margin-between}; - --#{$prefix}modal-footer-bg: #{$modal-footer-bg}; - --#{$prefix}modal-footer-border-color: #{$modal-footer-border-color}; - --#{$prefix}modal-footer-border-width: #{$modal-footer-border-width}; - // scss-docs-end modal-css-vars - - position: fixed; - top: 0; - left: 0; - z-index: var(--#{$prefix}modal-zindex); - display: none; - width: 100%; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - // Prevent Chrome on Windows from adding a focus outline. For details, see - // https://github.com/twbs/bootstrap/pull/10951. - outline: 0; - // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a - // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342 - // See also https://github.com/twbs/bootstrap/issues/17695 -} - -// Shell div to position the modal with bottom padding -.modal-dialog { - position: relative; - width: auto; - margin: var(--#{$prefix}modal-margin); - // allow clicks to pass through for custom click handling to close modal - pointer-events: none; - - // When fading in the modal, animate it to slide down - .modal.fade & { - transform: $modal-fade-transform; - @include transition($modal-transition); - } - .modal.show & { - transform: $modal-show-transform; - } - - // When trying to close, animate focus to scale - .modal.modal-static & { - transform: $modal-scale-transform; - } -} - -.modal-dialog-scrollable { - height: calc(100% - var(--#{$prefix}modal-margin) * 2); - - .modal-content { - max-height: 100%; - overflow: hidden; - } - - .modal-body { - overflow-y: auto; - } -} - -.modal-dialog-centered { - display: flex; - align-items: center; - min-height: calc(100% - var(--#{$prefix}modal-margin) * 2); -} - -// Actual modal -.modal-content { - position: relative; - display: flex; - flex-direction: column; - width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog` - // counteract the pointer-events: none; in the .modal-dialog - color: var(--#{$prefix}modal-color); - pointer-events: auto; - background-color: var(--#{$prefix}modal-bg); - background-clip: padding-box; - border: var(--#{$prefix}modal-border-width) solid var(--#{$prefix}modal-border-color); - @include border-radius(var(--#{$prefix}modal-border-radius)); - @include box-shadow(var(--#{$prefix}modal-box-shadow)); - // Remove focus outline from opened modal - outline: 0; -} - -// Modal background -.modal-backdrop { - // scss-docs-start modal-backdrop-css-vars - --#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop}; - --#{$prefix}backdrop-bg: #{$modal-backdrop-bg}; - --#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity}; - // scss-docs-end modal-backdrop-css-vars - - @include overlay-backdrop(var(--#{$prefix}backdrop-zindex), var(--#{$prefix}backdrop-bg), var(--#{$prefix}backdrop-opacity)); -} - -// Modal header -// Top section of the modal w/ title and dismiss -.modal-header { - display: flex; - flex-shrink: 0; - align-items: center; - padding: var(--#{$prefix}modal-header-padding); - border-bottom: var(--#{$prefix}modal-header-border-width) solid var(--#{$prefix}modal-header-border-color); - @include border-top-radius(var(--#{$prefix}modal-inner-border-radius)); - - .btn-close { - padding: calc(var(--#{$prefix}modal-header-padding-y) * .5) calc(var(--#{$prefix}modal-header-padding-x) * .5); - // Split properties to avoid invalid calc() function if value is 0 - margin-top: calc(-.5 * var(--#{$prefix}modal-header-padding-y)); - margin-right: calc(-.5 * var(--#{$prefix}modal-header-padding-x)); - margin-bottom: calc(-.5 * var(--#{$prefix}modal-header-padding-y)); - margin-left: auto; - } -} - -// Title text within header -.modal-title { - margin-bottom: 0; - line-height: var(--#{$prefix}modal-title-line-height); -} - -// Modal body -// Where all modal content resides (sibling of .modal-header and .modal-footer) -.modal-body { - position: relative; - // Enable `flex-grow: 1` so that the body take up as much space as possible - // when there should be a fixed height on `.modal-dialog`. - flex: 1 1 auto; - padding: var(--#{$prefix}modal-padding); -} - -// Footer (for actions) -.modal-footer { - display: flex; - flex-shrink: 0; - flex-wrap: wrap; - align-items: center; // vertically center - justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items - padding: calc(var(--#{$prefix}modal-padding) - var(--#{$prefix}modal-footer-gap) * .5); - background-color: var(--#{$prefix}modal-footer-bg); - border-top: var(--#{$prefix}modal-footer-border-width) solid var(--#{$prefix}modal-footer-border-color); - @include border-bottom-radius(var(--#{$prefix}modal-inner-border-radius)); - - // Place margin between footer elements - // This solution is far from ideal because of the universal selector usage, - // but is needed to fix https://github.com/twbs/bootstrap/issues/24800 - > * { - margin: calc(var(--#{$prefix}modal-footer-gap) * .5); // Todo in v6: replace with gap on parent class - } -} - -// Scale up the modal -@include media-breakpoint-up(sm) { - .modal { - --#{$prefix}modal-margin: #{$modal-dialog-margin-y-sm-up}; - --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-sm-up}; - } - - // Automatically set modal's width for larger viewports - .modal-dialog { - max-width: var(--#{$prefix}modal-width); - margin-right: auto; - margin-left: auto; - } - - .modal-sm { - --#{$prefix}modal-width: #{$modal-sm}; - } -} - -@include media-breakpoint-up(lg) { - .modal-lg, - .modal-xl { - --#{$prefix}modal-width: #{$modal-lg}; - } -} - -@include media-breakpoint-up(xl) { - .modal-xl { - --#{$prefix}modal-width: #{$modal-xl}; - } -} - -// scss-docs-start modal-fullscreen-loop -@each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - $postfix: if($infix != "", $infix + "-down", ""); - - @include media-breakpoint-down($breakpoint) { - .modal-fullscreen#{$postfix} { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - - .modal-content { - height: 100%; - border: 0; - @include border-radius(0); - } - - .modal-header, - .modal-footer { - @include border-radius(0); - } - - .modal-body { - overflow-y: auto; - } - } - } -} -// scss-docs-end modal-fullscreen-loop diff --git a/assets/stylesheets/bootstrap/_nav-overflow.scss b/assets/stylesheets/bootstrap/_nav-overflow.scss new file mode 100644 index 00000000..0b810a35 --- /dev/null +++ b/assets/stylesheets/bootstrap/_nav-overflow.scss @@ -0,0 +1,39 @@ +// Nav Overflow (Priority+ Pattern) +// +// A responsive navigation pattern that automatically moves items +// to an overflow menu when space is limited. + +@layer components { + .nav-overflow { + flex-wrap: nowrap; + min-width: 0; // Allow flex child to shrink below content width + } + + // Pills use inline-flex by default; override so the nav fills its container + // and the ResizeObserver can detect width changes. + .nav-pills.nav-overflow { + display: flex; + } + + // Inside a navbar the nav is a flex child that sizes to content by default; + // grow it so it fills remaining space and shrinks with the container. + .navbar-nav.nav-overflow { + flex: 1 1 0; + } + + // Container item for overflow + .nav-overflow-item { + flex-shrink: 0; + margin-inline-start: auto; + } + + // Hide items that have been moved to overflow + .nav-overflow [data-bs-nav-overflow="true"] { + display: none; + } + + // Preserve items that should never overflow + .nav-overflow-keep { + flex-shrink: 0; + } +} diff --git a/assets/stylesheets/bootstrap/_nav.scss b/assets/stylesheets/bootstrap/_nav.scss index 96fa5289..90ac39d0 100644 --- a/assets/stylesheets/bootstrap/_nav.scss +++ b/assets/stylesheets/bootstrap/_nav.scss @@ -1,197 +1,289 @@ +@use "functions" as *; +@use "config" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/gradients" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +$nav-tokens: () !default; + +// scss-docs-start nav-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-tokens: defaults( + ( + --nav-gap: .125rem, + --nav-link-gap: .5rem, + --nav-link-align: center, + --nav-link-justify: center, + --nav-link-padding-x: .75rem, + --nav-link-padding-y: .375rem, + --nav-link-color: var(--fg-2), + --nav-link-hover-color: var(--fg-1), + --nav-link-hover-bg: var(--bg-1), + --nav-link-active-color: var(--fg-body), + --nav-link-active-bg: var(--bg-2), + --nav-link-disabled-color: var(--fg-4), + --nav-link-border-width: var(--border-width), + --nav-link-transition-property: "color, background-color, border-color", + --nav-link-transition-timing: .15s ease-in-out, + --nav-link-transition: var(--nav-link-transition-property) var(--nav-link-transition-timing), + ), + $nav-tokens +); +// scss-docs-end nav-tokens + +$nav-tabs-tokens: () !default; + +// scss-docs-start nav-tabs-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-tabs-tokens: defaults( + ( + --nav-tabs-border-width: var(--border-width), + --nav-tabs-border-color: var(--border-color), + --nav-tabs-border-radius: var(--radius-5), + --nav-tabs-link-hover-border-color: var(--border-subtle), + --nav-tabs-link-active-color: var(--fg-color), + --nav-tabs-link-active-bg: var(--bg-body), + --nav-tabs-link-active-border-color: var(--border-color) var(--border-color) var(--bg-body), + ), + $nav-tabs-tokens +); +// scss-docs-end nav-tabs-tokens + +$nav-pills-tokens: () !default; + +// scss-docs-start nav-pills-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-pills-tokens: defaults( + ( + --nav-pills-bg: var(--bg-1), + --nav-pills-padding: .25rem, + --nav-pills-border-radius: var(--radius-9), + --nav-pills-link-active-color: var(--primary-contrast), + --nav-pills-link-active-bg: var(--primary-bg), + --nav-pills-link-border-radius: var(--radius-9), + ), + $nav-pills-tokens +); +// scss-docs-end nav-pills-tokens + +$nav-underline-tokens: () !default; + +// scss-docs-start nav-underline-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-underline-tokens: defaults( + ( + --nav-gap: 1rem, + --nav-link-active-bg: transparent, + --nav-underline-border-width: .125rem, + --nav-underline-link-active-color: var(--fg-color), + ), + $nav-underline-tokens +); +// scss-docs-end nav-underline-tokens + // Base class // // Kickstart any navigation component with a set of style resets. Works with // ``s, ``s or ``s. -.nav { - // scss-docs-start nav-css-vars - --#{$prefix}nav-link-padding-x: #{$nav-link-padding-x}; - --#{$prefix}nav-link-padding-y: #{$nav-link-padding-y}; - @include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size); - --#{$prefix}nav-link-font-weight: #{$nav-link-font-weight}; - --#{$prefix}nav-link-color: #{$nav-link-color}; - --#{$prefix}nav-link-hover-color: #{$nav-link-hover-color}; - --#{$prefix}nav-link-disabled-color: #{$nav-link-disabled-color}; - // scss-docs-end nav-css-vars - - display: flex; - flex-wrap: wrap; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} - -.nav-link { - display: block; - padding: var(--#{$prefix}nav-link-padding-y) var(--#{$prefix}nav-link-padding-x); - @include font-size(var(--#{$prefix}nav-link-font-size)); - font-weight: var(--#{$prefix}nav-link-font-weight); - color: var(--#{$prefix}nav-link-color); - text-decoration: if($link-decoration == none, null, none); - background: none; - border: 0; - @include transition($nav-link-transition); - - &:hover, - &:focus { - color: var(--#{$prefix}nav-link-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - } +@layer components { + .nav { + @include tokens($nav-tokens); - &:focus-visible { - outline: 0; - box-shadow: $nav-link-focus-box-shadow; + display: flex; + flex-wrap: wrap; + gap: var(--nav-gap); + padding-inline-start: 0; + margin-bottom: 0; + list-style-type: ""; } - // Disabled state lightens text - &.disabled, - &:disabled { - color: var(--#{$prefix}nav-link-disabled-color); - pointer-events: none; - cursor: default; + .nav-item { + display: flex; } -} - -// -// Tabs -// - -.nav-tabs { - // scss-docs-start nav-tabs-css-vars - --#{$prefix}nav-tabs-border-width: #{$nav-tabs-border-width}; - --#{$prefix}nav-tabs-border-color: #{$nav-tabs-border-color}; - --#{$prefix}nav-tabs-border-radius: #{$nav-tabs-border-radius}; - --#{$prefix}nav-tabs-link-hover-border-color: #{$nav-tabs-link-hover-border-color}; - --#{$prefix}nav-tabs-link-active-color: #{$nav-tabs-link-active-color}; - --#{$prefix}nav-tabs-link-active-bg: #{$nav-tabs-link-active-bg}; - --#{$prefix}nav-tabs-link-active-border-color: #{$nav-tabs-link-active-border-color}; - // scss-docs-end nav-tabs-css-vars - - border-bottom: var(--#{$prefix}nav-tabs-border-width) solid var(--#{$prefix}nav-tabs-border-color); .nav-link { - margin-bottom: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list - border: var(--#{$prefix}nav-tabs-border-width) solid transparent; - @include border-top-radius(var(--#{$prefix}nav-tabs-border-radius)); + display: flex; + gap: var(--nav-link-gap); + align-items: var(--nav-link-align); + justify-content: var(--nav-link-justify); + padding: var(--nav-link-padding-y) var(--nav-link-padding-x); + font-weight: var(--nav-link-font-weight); + color: var(--nav-link-color); + text-decoration: none; + white-space: nowrap; + background: none; + border: var(--nav-link-border-width) solid transparent; + @include border-radius(var(--radius-5)); + @include transition(var(--nav-link-transition)); &:hover, &:focus { - // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link - isolation: isolate; - border-color: var(--#{$prefix}nav-tabs-link-hover-border-color); + color: var(--nav-link-hover-color); + background-color: var(--nav-link-hover-bg); } - } - .nav-link.active, - .nav-item.show .nav-link { - color: var(--#{$prefix}nav-tabs-link-active-color); - background-color: var(--#{$prefix}nav-tabs-link-active-bg); - border-color: var(--#{$prefix}nav-tabs-link-active-border-color); - } + &:focus-visible { + --focus-ring-offset: 1px; + color: var(--nav-link-hover-color); + @include focus-ring(true); + } - .dropdown-menu { - // Make dropdown border overlap tab border - margin-top: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list - // Remove the top rounded corners here since there is a hard edge above the menu - @include border-top-radius(0); + &.active, + &:active { + color: var(--nav-link-active-color); + background-color: var(--nav-link-active-bg); + } + + // Disabled state lightens text + &.disabled, + &:disabled { + color: var(--nav-link-disabled-color); + pointer-events: none; + cursor: default; + } } -} + // + // Tabs + // -// -// Pills -// + .nav-tabs { + // scss-docs-start nav-tabs-css-vars + @include tokens($nav-tabs-tokens); + // scss-docs-end nav-tabs-css-vars -.nav-pills { - // scss-docs-start nav-pills-css-vars - --#{$prefix}nav-pills-border-radius: #{$nav-pills-border-radius}; - --#{$prefix}nav-pills-link-active-color: #{$nav-pills-link-active-color}; - --#{$prefix}nav-pills-link-active-bg: #{$nav-pills-link-active-bg}; - // scss-docs-end nav-pills-css-vars + box-shadow: inset 0 calc(-1 * var(--nav-tabs-border-width)) 0 var(--nav-tabs-border-color); - .nav-link { - @include border-radius(var(--#{$prefix}nav-pills-border-radius)); - } + .nav-link { + border: var(--nav-tabs-border-width) solid transparent; + border-bottom-color: var(--nav-tabs-border-color); + @include border-bottom-radius(0); - .nav-link.active, - .show > .nav-link { - color: var(--#{$prefix}nav-pills-link-active-color); - @include gradient-bg(var(--#{$prefix}nav-pills-link-active-bg)); - } -} + &:hover { + // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link + isolation: isolate; + border-color: var(--nav-tabs-link-hover-border-color); + border-bottom-color: var(--nav-tabs-border-color); + } + } + .nav-link.active, + .nav-item.show .nav-link { + color: var(--nav-tabs-link-active-color); + background-color: var(--nav-tabs-link-active-bg); + border-color: var(--nav-tabs-link-active-border-color); + border-bottom-color: var(--nav-tabs-link-active-bg); + } -// -// Underline -// + .menu { + margin-top: calc(-1 * var(--nav-tabs-border-width)); + @include border-top-radius(0); + } + } -.nav-underline { - // scss-docs-start nav-underline-css-vars - --#{$prefix}nav-underline-gap: #{$nav-underline-gap}; - --#{$prefix}nav-underline-border-width: #{$nav-underline-border-width}; - --#{$prefix}nav-underline-link-active-color: #{$nav-underline-link-active-color}; - // scss-docs-end nav-underline-css-vars + // + // Pills + // - gap: var(--#{$prefix}nav-underline-gap); + .nav-pills { + @include tokens($nav-pills-tokens); - .nav-link { - padding-right: 0; - padding-left: 0; - border-bottom: var(--#{$prefix}nav-underline-border-width) solid transparent; + display: inline-flex; + padding: var(--nav-pills-padding); + background-color: var(--nav-pills-bg); + @include border-radius(var(--nav-pills-border-radius)); - &:hover, - &:focus { - border-bottom-color: currentcolor; + .nav-link { + @include border-radius(var(--nav-pills-link-border-radius)); } - } - .nav-link.active, - .show > .nav-link { - font-weight: $font-weight-bold; - color: var(--#{$prefix}nav-underline-link-active-color); - border-bottom-color: currentcolor; + .nav-link.active, + .show > .nav-link { + color: var(--nav-pills-link-active-color); + @include gradient-bg(var(--nav-pills-link-active-bg)); + } } -} + .nav-pills-vertical { + flex-direction: column; + align-items: stretch; -// -// Justified variants -// + .nav-item, + .nav-link { + width: 100%; + } + } -.nav-fill { - > .nav-link, - .nav-item { - flex: 1 1 auto; - text-align: center; + // + // Underline + // + + .nav-underline { + // scss-docs-start nav-underline-css-vars + @include tokens($nav-underline-tokens); + // scss-docs-end nav-underline-css-vars + + .nav-link { + padding-inline: 0; + border: 0; + border-block-end: var(--nav-underline-border-width) solid transparent; + @include border-radius(0); + + &:hover, + &:focus { + border-block-end-color: currentcolor; + } + } + + .nav-link.active, + .show > .nav-link { + font-weight: $font-weight-bold; + color: var(--nav-underline-link-active-color); + border-block-end-color: currentcolor; + } } -} -.nav-justified { - > .nav-link, - .nav-item { - flex-grow: 1; - flex-basis: 0; - text-align: center; + // + // Justified variants + // + + .nav-fill { + > .nav-link, + .nav-item { + flex: 1 1 auto; + text-align: center; + } } -} -.nav-fill, -.nav-justified { - .nav-item .nav-link { - width: 100%; // Make sure button will grow + .nav-justified { + > .nav-link, + .nav-item { + flex-grow: 1; + flex-basis: 0; + text-align: center; + } } -} + .nav-fill, + .nav-justified { + .nav-item .nav-link { + width: 100%; // Make sure button will grow + } + } -// Tabbable tabs -// -// Hide tabbable panes to start, show them when `.active` + // Tabbable tabs + // + // Hide tabbable panes to start, show them when `.active` -.tab-content { - > .tab-pane { - display: none; - } - > .active { - display: block; + .tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } } } diff --git a/assets/stylesheets/bootstrap/_navbar.scss b/assets/stylesheets/bootstrap/_navbar.scss index 86aa441e..3dc4bebf 100644 --- a/assets/stylesheets/bootstrap/_navbar.scss +++ b/assets/stylesheets/bootstrap/_navbar.scss @@ -1,289 +1,312 @@ -// Navbar -// -// Provide a static navbar from which we expand to create full-width, fixed, and -// other navbar variations. - -.navbar { - // scss-docs-start navbar-css-vars - --#{$prefix}navbar-padding-x: #{if($navbar-padding-x == null, 0, $navbar-padding-x)}; - --#{$prefix}navbar-padding-y: #{$navbar-padding-y}; - --#{$prefix}navbar-color: #{$navbar-light-color}; - --#{$prefix}navbar-hover-color: #{$navbar-light-hover-color}; - --#{$prefix}navbar-disabled-color: #{$navbar-light-disabled-color}; - --#{$prefix}navbar-active-color: #{$navbar-light-active-color}; - --#{$prefix}navbar-brand-padding-y: #{$navbar-brand-padding-y}; - --#{$prefix}navbar-brand-margin-end: #{$navbar-brand-margin-end}; - --#{$prefix}navbar-brand-font-size: #{$navbar-brand-font-size}; - --#{$prefix}navbar-brand-color: #{$navbar-light-brand-color}; - --#{$prefix}navbar-brand-hover-color: #{$navbar-light-brand-hover-color}; - --#{$prefix}navbar-nav-link-padding-x: #{$navbar-nav-link-padding-x}; - --#{$prefix}navbar-toggler-padding-y: #{$navbar-toggler-padding-y}; - --#{$prefix}navbar-toggler-padding-x: #{$navbar-toggler-padding-x}; - --#{$prefix}navbar-toggler-font-size: #{$navbar-toggler-font-size}; - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-light-toggler-icon-bg)}; - --#{$prefix}navbar-toggler-border-color: #{$navbar-light-toggler-border-color}; - --#{$prefix}navbar-toggler-border-radius: #{$navbar-toggler-border-radius}; - --#{$prefix}navbar-toggler-focus-width: #{$navbar-toggler-focus-width}; - --#{$prefix}navbar-toggler-transition: #{$navbar-toggler-transition}; - // scss-docs-end navbar-css-vars - - position: relative; - display: flex; - flex-wrap: wrap; // allow us to do the line break for collapsing content - align-items: center; - justify-content: space-between; // space out brand from logo - padding: var(--#{$prefix}navbar-padding-y) var(--#{$prefix}navbar-padding-x); - @include gradient-bg(); - - // Because flex properties aren't inherited, we need to redeclare these first - // few properties so that content nested within behave properly. - // The `flex-wrap` property is inherited to simplify the expanded navbars - %container-flex-properties { +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/box-shadow" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +// mdo-do: fix nav-link-height and navbar-brand-height, which we previously calculated with font-size, line-height, and block padding + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start navbar-breakpoints +$navbar-breakpoints: $breakpoints !default; +// scss-docs-end navbar-breakpoints + +$navbar-tokens: () !default; +$navbar-dark-tokens: () !default; +$navbar-nav-tokens: () !default; + +// scss-docs-start navbar-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-tokens: defaults( + ( + --navbar-padding-x: 0, + --navbar-padding-y: .5rem, + --navbar-color: var(--fg-2), + --navbar-hover-color: var(--fg-1), + --navbar-disabled-color: var(--fg-3), + --navbar-active-color: var(--fg-body), + --navbar-brand-padding-y: .75rem, + --navbar-brand-margin-end: 1rem, + --navbar-brand-font-size: var(--font-size-md), + --navbar-brand-font-weight: var(--font-weight-medium), + --navbar-brand-color: var(--fg-body), + --navbar-brand-hover-color: var(--fg-body), + --navbar-nav-link-padding-x: .75rem, + --navbar-toggler-width: 2rem, + --navbar-toggler-padding-y: .25rem, + --navbar-toggler-padding-x: .75rem, + --navbar-toggler-font-size: var(--font-size-lg), + --navbar-toggler-border-color: color-mix(in oklch, var(--fg-body) 15%, transparent), + --navbar-toggler-border-radius: var(--radius-5), + --navbar-toggler-transition: box-shadow .15s ease-in-out, + --navbar-toggler-icon-size: 1.25rem, + --navbar-toggler-icon: #{escape-svg(url("data:image/svg+xml,"))}, + ), + $navbar-tokens +); +// scss-docs-end navbar-tokens + +// scss-docs-start navbar-dark-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-dark-tokens: defaults( + ( + --navbar-color: color-mix(in oklch, var(--white) .55, transparent), + --navbar-hover-color: color-mix(in oklch, var(--white) .75, transparent), + --navbar-disabled-color: color-mix(in oklch, var(--white) .25, transparent), + --navbar-active-color: var(--white), + --navbar-brand-color: var(--white), + --navbar-brand-hover-color: var(--white), + --navbar-toggler-border-color: color-mix(in oklch, var(--white) .1, transparent), + ), + $navbar-dark-tokens +); +// scss-docs-end navbar-dark-tokens + +// scss-docs-start navbar-nav-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-nav-tokens: defaults( + ( + --nav-gap: .25rem, + --nav-link-gap: .5rem, + --nav-link-padding-x: .5rem, + --nav-link-padding-y: .375rem, + --nav-link-color: var(--navbar-color), + --nav-link-border-width: var(--border-width), + //--nav-link-border-color: var(--border-color), + --nav-link-hover-color: var(--navbar-hover-color), + --nav-link-hover-bg: transparent, + --nav-link-active-color: var(--navbar-active-color), + --nav-link-active-bg: transparent, + --nav-link-disabled-color: var(--navbar-disabled-color), + ), + $navbar-nav-tokens +); +// scss-docs-end navbar-nav-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + // Base navbar + .navbar { + @include tokens($navbar-tokens); + + position: relative; display: flex; - flex-wrap: inherit; + flex-wrap: wrap; align-items: center; justify-content: space-between; - } - - > .container, - > .container-fluid { - @extend %container-flex-properties; - } + padding: var(--navbar-padding-y) var(--navbar-padding-x); + @include set-container(); + color: var(--navbar-color, var(--fg-body)); + background-color: var(--navbar-bg, var(--bg-body)); + // @include gradient-bg(var(--navbar-bg, var(--bg-body))); + + // Container properties for nested containers + %container-flex-properties { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; + } - @each $breakpoint, $container-max-width in $container-max-widths { - > .container#{breakpoint-infix($breakpoint, $container-max-widths)} { + > .container, + > .container-fluid { @extend %container-flex-properties; } - } -} - -// Navbar brand -// -// Used for brand, project, or site names. + @each $breakpoint, $container-max-width in $container-max-widths { + > .#{breakpoint-prefix($breakpoint, $container-max-widths)}container { + @extend %container-flex-properties; + } + } + } -.navbar-brand { - padding-top: var(--#{$prefix}navbar-brand-padding-y); - padding-bottom: var(--#{$prefix}navbar-brand-padding-y); - margin-right: var(--#{$prefix}navbar-brand-margin-end); - @include font-size(var(--#{$prefix}navbar-brand-font-size)); - color: var(--#{$prefix}navbar-brand-color); - text-decoration: if($link-decoration == none, null, none); - white-space: nowrap; + // Navbar brand + // + // Used for brand, project, or site names. + .navbar-brand { + padding-top: var(--navbar-brand-padding-y); + padding-bottom: var(--navbar-brand-padding-y); + margin-inline-end: var(--navbar-brand-margin-end); + font-size: var(--navbar-brand-font-size); + font-weight: var(--navbar-brand-font-weight); + color: var(--navbar-brand-color); + text-decoration: none; + white-space: nowrap; - &:hover, - &:focus { - color: var(--#{$prefix}navbar-brand-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); + &:hover, + &:focus { + color: var(--navbar-brand-hover-color); + } } -} + // Navigation within navbars. Sets all nav-link CSS variables needed for + // proper styling. + // + // Relies on `.nav` base class. + .navbar-nav { + @include tokens($navbar-nav-tokens); -// Navbar nav -// -// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`). - -.navbar-nav { - // scss-docs-start navbar-nav-css-vars - --#{$prefix}nav-link-padding-x: 0; - --#{$prefix}nav-link-padding-y: #{$nav-link-padding-y}; - @include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size); - --#{$prefix}nav-link-font-weight: #{$nav-link-font-weight}; - --#{$prefix}nav-link-color: var(--#{$prefix}navbar-color); - --#{$prefix}nav-link-hover-color: var(--#{$prefix}navbar-hover-color); - --#{$prefix}nav-link-disabled-color: var(--#{$prefix}navbar-disabled-color); - // scss-docs-end navbar-nav-css-vars - - display: flex; - flex-direction: column; // cannot use `inherit` to get the `.navbar`s value - padding-left: 0; - margin-bottom: 0; - list-style: none; - - .nav-link { - &.active, - &.show { - color: var(--#{$prefix}navbar-active-color); + display: flex; + flex-direction: column; + gap: var(--nav-gap); + padding-inline-start: 0; + margin-bottom: 0; + list-style-type: ""; + + .nav-link { + &.active, + &.show { + color: var(--navbar-active-color); + border: var(--nav-link-border-width) solid var(--nav-link-border-color, transparent); + } } } - .dropdown-menu { - position: static; + // Navbar text + // + // For adding text or inline elements to the navbar + .navbar-text { + padding-top: var(--navbar-brand-padding-y); + padding-bottom: var(--navbar-brand-padding-y); + color: var(--navbar-color); + + a, + a:hover, + a:focus { + color: var(--navbar-active-color); + } } -} - - -// Navbar text -// -// - -.navbar-text { - padding-top: $nav-link-padding-y; - padding-bottom: $nav-link-padding-y; - color: var(--#{$prefix}navbar-color); - a, - a:hover, - a:focus { - color: var(--#{$prefix}navbar-active-color); + // Button for toggling the navbar when in its collapsed state + .navbar-toggler { + --btn-bg: transparent; + --btn-hover-bg: var(--bg-2); } -} - - -// Responsive navbar -// -// Custom styles for responsive collapsing and toggling of navbar contents. -// Powered by the collapse Bootstrap JavaScript plugin. - -// When collapsed, prevent the toggleable navbar contents from appearing in -// the default flexbox row orientation. Requires the use of `flex-wrap: wrap` -// on the `.navbar` parent. -.navbar-collapse { - flex-grow: 1; - flex-basis: 100%; - // For always expanded or extra full navbars, ensure content aligns itself - // properly vertically. Can be easily overridden with flex utilities. - align-items: center; -} -// Button for toggling the navbar when in its collapsed state -.navbar-toggler { - padding: var(--#{$prefix}navbar-toggler-padding-y) var(--#{$prefix}navbar-toggler-padding-x); - @include font-size(var(--#{$prefix}navbar-toggler-font-size)); - line-height: 1; - color: var(--#{$prefix}navbar-color); - background-color: transparent; // remove default button style - border: var(--#{$prefix}border-width) solid var(--#{$prefix}navbar-toggler-border-color); // remove default button style - @include border-radius(var(--#{$prefix}navbar-toggler-border-radius)); - @include transition(var(--#{$prefix}navbar-toggler-transition)); - - &:hover { - text-decoration: none; + // Hamburger icon, rendered via CSS mask so it inherits the navbar color + .navbar-toggler-icon { + display: inline-block; + width: var(--navbar-toggler-icon-size); + height: var(--navbar-toggler-icon-size); + background-color: currentcolor; + @include mask-icon(var(--navbar-toggler-icon)); } - &:focus { - text-decoration: none; - outline: 0; - box-shadow: 0 0 0 var(--#{$prefix}navbar-toggler-focus-width); - } -} + // scss-docs-start navbar-expand-loop + // Generate series of responsive `.navbar-expand` classes for configuring + // where your navbar collapses and expands. Uses container queries so the + // navbar responds to its own width, not the viewport width. + + // Mixin for expanded state styles (applied to descendants) + @mixin navbar-expanded { + // Style the inner container since we can't style .navbar itself with container queries + > .container, + > .container-fluid, + %navbar-expand-container { + flex-wrap: nowrap; + justify-content: flex-start; + } -// Keep as a separate element so folks can easily override it with another icon -// or image file as needed. -.navbar-toggler-icon { - display: inline-block; - width: 1.5em; - height: 1.5em; - vertical-align: middle; - background-image: var(--#{$prefix}navbar-toggler-icon-bg); - background-repeat: no-repeat; - background-position: center; - background-size: 100%; -} + .navbar-nav { + --nav-link-padding-x: var(--navbar-nav-link-padding-x); + flex-direction: row; + } -.navbar-nav-scroll { - max-height: var(--#{$prefix}scroll-height, 75vh); - overflow-y: auto; -} + .navbar-toggler { + display: none !important; // stylelint-disable-line declaration-no-important + } -// scss-docs-start navbar-expand-loop -// Generate series of `.navbar-expand-*` responsive classes for configuring -// where your navbar collapses. -.navbar-expand { - @each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - // stylelint-disable-next-line scss/selector-no-union-class-name - {$infix} { - @include media-breakpoint-up($next) { - flex-wrap: nowrap; - justify-content: flex-start; - - .navbar-nav { - flex-direction: row; - - .dropdown-menu { - position: absolute; - } - - .nav-link { - padding-right: var(--#{$prefix}navbar-nav-link-padding-x); - padding-left: var(--#{$prefix}navbar-nav-link-padding-x); - } - } + [class*="drawer"] { + // stylelint-disable declaration-no-important + // Reset native UA styles and below-breakpoint drawer styles. + // Must use !important to override both UA defaults and the + // responsive drawer styles from media-breakpoint-down(). + position: static !important; + inset: auto !important; + z-index: auto; + display: flex !important; + flex-grow: 1; + width: auto !important; + max-width: none !important; + height: auto !important; + max-height: none !important; + padding: 0; + margin: 0; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + @include box-shadow(none); + @include transition(none); + // stylelint-enable declaration-no-important + + .drawer-header { + display: none !important; // stylelint-disable-line declaration-no-important + } - .navbar-nav-scroll { - overflow: visible; - } + .drawer-body { + display: flex; + flex-grow: 1; + flex-direction: row; + align-items: center; + padding: 0; + overflow-y: visible; + } + } + } - .navbar-collapse { - display: flex !important; // stylelint-disable-line declaration-no-important - flex-basis: auto; - } + // Always expanded (no responsive behavior) + .navbar-expand { + @include navbar-expanded(); - .navbar-toggler { - display: none; - } + // Also set on navbar itself for non-responsive case + flex-wrap: nowrap; + justify-content: flex-start; + } - .offcanvas { - // stylelint-disable declaration-no-important - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - @include box-shadow(none); - @include transition(none); - // stylelint-enable declaration-no-important - - .offcanvas-header { - display: none; - } - - .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } + // Responsive navbar expand classes using container queries + @include loop-breakpoints-down($navbar-breakpoints) using ($breakpoint, $next, $prefix) { + @if $next { + .#{$prefix}navbar-expand { + @include container-breakpoint-up($next) { + @include navbar-expanded(); } } } } -} -// scss-docs-end navbar-expand-loop - -// Navbar themes -// -// Styles for switching between navbars with light or dark background. - -.navbar-light { - @include deprecate("`.navbar-light`", "v5.2.0", "v6.0.0", true); -} - -.navbar-dark, -.navbar[data-bs-theme="dark"] { - // scss-docs-start navbar-dark-css-vars - --#{$prefix}navbar-color: #{$navbar-dark-color}; - --#{$prefix}navbar-hover-color: #{$navbar-dark-hover-color}; - --#{$prefix}navbar-disabled-color: #{$navbar-dark-disabled-color}; - --#{$prefix}navbar-active-color: #{$navbar-dark-active-color}; - --#{$prefix}navbar-brand-color: #{$navbar-dark-brand-color}; - --#{$prefix}navbar-brand-hover-color: #{$navbar-dark-brand-hover-color}; - --#{$prefix}navbar-toggler-border-color: #{$navbar-dark-toggler-border-color}; - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; - // scss-docs-end navbar-dark-css-vars -} + // scss-docs-end navbar-expand-loop + + // Prevent drawer flash on breakpoint crossing. + // When the navbar crosses from expanded (inline) to collapsed (drawer), + // the drawer transitions from visibility:visible to visibility:hidden. + // Without this override, the slide transition plays — briefly showing the + // panel sliding away. Disabling transitions when not [open] ensures only + // intentional show/hide actions animate. + // stylelint-disable-next-line no-duplicate-selectors + .navbar { + [class*="drawer"]:not([open], .hiding) { + @include transition(none !important); + } + } -@if $enable-dark-mode { - @include color-mode(dark) { - .navbar-toggler-icon { - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; + .navbar-translucent { + position: relative; + background-color: transparent; + + &::before { + position: absolute; + inset: 0; + z-index: -1; + content: ""; + background-color: color-mix(in oklch, var(--navbar-bg, var(--bg-body)) 80%, transparent); + background-image: none; + backdrop-filter: blur(5px) saturate(180%); } } + + .navbar[data-bs-theme="dark"] { + @include tokens($navbar-dark-tokens); + } } diff --git a/assets/stylesheets/bootstrap/_offcanvas.scss b/assets/stylesheets/bootstrap/_offcanvas.scss deleted file mode 100644 index b40b2cd9..00000000 --- a/assets/stylesheets/bootstrap/_offcanvas.scss +++ /dev/null @@ -1,147 +0,0 @@ -// stylelint-disable function-disallowed-list - -%offcanvas-css-vars { - // scss-docs-start offcanvas-css-vars - --#{$prefix}offcanvas-zindex: #{$zindex-offcanvas}; - --#{$prefix}offcanvas-width: #{$offcanvas-horizontal-width}; - --#{$prefix}offcanvas-height: #{$offcanvas-vertical-height}; - --#{$prefix}offcanvas-padding-x: #{$offcanvas-padding-x}; - --#{$prefix}offcanvas-padding-y: #{$offcanvas-padding-y}; - --#{$prefix}offcanvas-color: #{$offcanvas-color}; - --#{$prefix}offcanvas-bg: #{$offcanvas-bg-color}; - --#{$prefix}offcanvas-border-width: #{$offcanvas-border-width}; - --#{$prefix}offcanvas-border-color: #{$offcanvas-border-color}; - --#{$prefix}offcanvas-box-shadow: #{$offcanvas-box-shadow}; - --#{$prefix}offcanvas-transition: #{transform $offcanvas-transition-duration ease-in-out}; - --#{$prefix}offcanvas-title-line-height: #{$offcanvas-title-line-height}; - // scss-docs-end offcanvas-css-vars -} - -@each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - .offcanvas#{$infix} { - @extend %offcanvas-css-vars; - } -} - -@each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - .offcanvas#{$infix} { - @include media-breakpoint-down($next) { - position: fixed; - bottom: 0; - z-index: var(--#{$prefix}offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--#{$prefix}offcanvas-color); - visibility: hidden; - background-color: var(--#{$prefix}offcanvas-bg); - background-clip: padding-box; - outline: 0; - @include box-shadow(var(--#{$prefix}offcanvas-box-shadow)); - @include transition(var(--#{$prefix}offcanvas-transition)); - - &.offcanvas-start { - top: 0; - left: 0; - width: var(--#{$prefix}offcanvas-width); - border-right: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateX(-100%); - } - - &.offcanvas-end { - top: 0; - right: 0; - width: var(--#{$prefix}offcanvas-width); - border-left: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateX(100%); - } - - &.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--#{$prefix}offcanvas-height); - max-height: 100%; - border-bottom: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateY(-100%); - } - - &.offcanvas-bottom { - right: 0; - left: 0; - height: var(--#{$prefix}offcanvas-height); - max-height: 100%; - border-top: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateY(100%); - } - - &.showing, - &.show:not(.hiding) { - transform: none; - } - - &.showing, - &.hiding, - &.show { - visibility: visible; - } - } - - @if not ($infix == "") { - @include media-breakpoint-up($next) { - --#{$prefix}offcanvas-height: auto; - --#{$prefix}offcanvas-border-width: 0; - background-color: transparent !important; // stylelint-disable-line declaration-no-important - - .offcanvas-header { - display: none; - } - - .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - // Reset `background-color` in case `.bg-*` classes are used in offcanvas - background-color: transparent !important; // stylelint-disable-line declaration-no-important - } - } - } - } -} - -.offcanvas-backdrop { - @include overlay-backdrop($zindex-offcanvas-backdrop, $offcanvas-backdrop-bg, $offcanvas-backdrop-opacity); -} - -.offcanvas-header { - display: flex; - align-items: center; - padding: var(--#{$prefix}offcanvas-padding-y) var(--#{$prefix}offcanvas-padding-x); - - .btn-close { - padding: calc(var(--#{$prefix}offcanvas-padding-y) * .5) calc(var(--#{$prefix}offcanvas-padding-x) * .5); - // Split properties to avoid invalid calc() function if value is 0 - margin-top: calc(-.5 * var(--#{$prefix}offcanvas-padding-y)); - margin-right: calc(-.5 * var(--#{$prefix}offcanvas-padding-x)); - margin-bottom: calc(-.5 * var(--#{$prefix}offcanvas-padding-y)); - margin-left: auto; - } -} - -.offcanvas-title { - margin-bottom: 0; - line-height: var(--#{$prefix}offcanvas-title-line-height); -} - -.offcanvas-body { - flex-grow: 1; - padding: var(--#{$prefix}offcanvas-padding-y) var(--#{$prefix}offcanvas-padding-x); - overflow-y: auto; -} diff --git a/assets/stylesheets/bootstrap/_pagination.scss b/assets/stylesheets/bootstrap/_pagination.scss index 9f09694c..9ddeed2a 100644 --- a/assets/stylesheets/bootstrap/_pagination.scss +++ b/assets/stylesheets/bootstrap/_pagination.scss @@ -1,109 +1,139 @@ -.pagination { - // scss-docs-start pagination-css-vars - --#{$prefix}pagination-padding-x: #{$pagination-padding-x}; - --#{$prefix}pagination-padding-y: #{$pagination-padding-y}; - @include rfs($pagination-font-size, --#{$prefix}pagination-font-size); - --#{$prefix}pagination-color: #{$pagination-color}; - --#{$prefix}pagination-bg: #{$pagination-bg}; - --#{$prefix}pagination-border-width: #{$pagination-border-width}; - --#{$prefix}pagination-border-color: #{$pagination-border-color}; - --#{$prefix}pagination-border-radius: #{$pagination-border-radius}; - --#{$prefix}pagination-hover-color: #{$pagination-hover-color}; - --#{$prefix}pagination-hover-bg: #{$pagination-hover-bg}; - --#{$prefix}pagination-hover-border-color: #{$pagination-hover-border-color}; - --#{$prefix}pagination-focus-color: #{$pagination-focus-color}; - --#{$prefix}pagination-focus-bg: #{$pagination-focus-bg}; - --#{$prefix}pagination-focus-box-shadow: #{$pagination-focus-box-shadow}; - --#{$prefix}pagination-active-color: #{$pagination-active-color}; - --#{$prefix}pagination-active-bg: #{$pagination-active-bg}; - --#{$prefix}pagination-active-border-color: #{$pagination-active-border-color}; - --#{$prefix}pagination-disabled-color: #{$pagination-disabled-color}; - --#{$prefix}pagination-disabled-bg: #{$pagination-disabled-bg}; - --#{$prefix}pagination-disabled-border-color: #{$pagination-disabled-border-color}; - // scss-docs-end pagination-css-vars - - display: flex; - @include list-unstyled(); -} +@use "functions" as *; +@use "mixins/lists" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/gradients" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; -.page-link { - position: relative; - display: block; - padding: var(--#{$prefix}pagination-padding-y) var(--#{$prefix}pagination-padding-x); - @include font-size(var(--#{$prefix}pagination-font-size)); - color: var(--#{$prefix}pagination-color); - text-decoration: if($link-decoration == none, null, none); - background-color: var(--#{$prefix}pagination-bg); - border: var(--#{$prefix}pagination-border-width) solid var(--#{$prefix}pagination-border-color); - @include transition($pagination-transition); - - &:hover { - z-index: 2; - color: var(--#{$prefix}pagination-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - background-color: var(--#{$prefix}pagination-hover-bg); - border-color: var(--#{$prefix}pagination-hover-border-color); - } +// mdo-do: Update pagination to support variant themes - &:focus { - z-index: 3; - color: var(--#{$prefix}pagination-focus-color); - background-color: var(--#{$prefix}pagination-focus-bg); - outline: $pagination-focus-outline; - box-shadow: var(--#{$prefix}pagination-focus-box-shadow); - } +// stylelint-disable custom-property-no-missing-var-function +$pagination-tokens: () !default; - &.active, - .active > & { - z-index: 3; - color: var(--#{$prefix}pagination-active-color); - @include gradient-bg(var(--#{$prefix}pagination-active-bg)); - border-color: var(--#{$prefix}pagination-active-border-color); - } +// scss-docs-start pagination-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$pagination-tokens: defaults( + ( + --pagination-min-height: var(--btn-input-min-height), + --pagination-padding-x: var(--btn-input-padding-x), + --pagination-padding-y: var(--btn-input-padding-y), + --pagination-font-size: var(--btn-input-font-size), + --pagination-color: var(--link-color), + --pagination-bg: var(--bg-body), + --pagination-border-width: var(--border-width), + --pagination-border-color: var(--border-color), + --pagination-border-radius: var(--btn-input-border-radius), + --pagination-hover-color: var(--link-hover-color), + --pagination-hover-bg: var(--bg-1), + --pagination-hover-border-color: var(--border-color), + --pagination-focus-color: var(--link-hover-color), + --pagination-focus-bg: var(--bg-2), + --pagination-active-color: var(--primary-contrast), + --pagination-active-bg: var(--primary-bg), + --pagination-active-border-color: var(--primary-bg), + --pagination-disabled-color: var(--fg-3), + --pagination-disabled-bg: var(--bg-2), + --pagination-disabled-border-color: var(--border-color), + ), + $pagination-tokens +); +// scss-docs-end pagination-tokens + +// scss-docs-start pagination-sizes +$pagination-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$pagination-sizes: defaults( + ("sm", "lg"), + $pagination-sizes +); +// scss-docs-end pagination-sizes +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + .pagination { + @include tokens($pagination-tokens); - &.disabled, - .disabled > & { - color: var(--#{$prefix}pagination-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}pagination-disabled-bg); - border-color: var(--#{$prefix}pagination-disabled-border-color); + display: flex; + @include list-unstyled(); } -} -.page-item { - &:not(:first-child) .page-link { - margin-left: $pagination-margin-start; + .page-link { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: var(--pagination-min-height); + padding: var(--pagination-padding-y) var(--pagination-padding-x); + font-size: var(--pagination-font-size); + color: var(--pagination-color); + text-decoration: none; + background-color: var(--pagination-bg); + border: var(--pagination-border-width) solid var(--pagination-border-color); + @include transition(color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out); + + &:hover { + z-index: 2; + color: var(--pagination-hover-color); + background-color: var(--pagination-hover-bg); + border-color: var(--pagination-hover-border-color); + } + + &:focus-visible { + z-index: 3; + color: var(--pagination-focus-color); + background-color: var(--pagination-focus-bg); + @include focus-ring(true); + } + + &.active, + .active > & { + z-index: 3; + color: var(--pagination-active-color); + @include gradient-bg(var(--pagination-active-bg)); + border-color: var(--pagination-active-border-color); + } + + &.disabled, + .disabled > & { + color: var(--pagination-disabled-color); + pointer-events: none; + background-color: var(--pagination-disabled-bg); + border-color: var(--pagination-disabled-border-color); + } } - @if $pagination-margin-start == calc(-1 * #{$pagination-border-width}) { + .page-item { + &:not(:first-child) .page-link { + margin-inline-start: calc(-1 * var(--pagination-border-width)); + } + &:first-child { .page-link { - @include border-start-radius(var(--#{$prefix}pagination-border-radius)); + @include border-start-radius(var(--pagination-border-radius)); } } &:last-child { .page-link { - @include border-end-radius(var(--#{$prefix}pagination-border-radius)); + @include border-end-radius(var(--pagination-border-radius)); } } - } @else { - // Add border-radius to all pageLinks in case they have left margin - .page-link { - @include border-radius(var(--#{$prefix}pagination-border-radius)); - } } -} - - -// -// Sizing -// -.pagination-lg { - @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $pagination-border-radius-lg); -} + // + // Sizing + // -.pagination-sm { - @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $pagination-border-radius-sm); + // scss-docs-start pagination-sizes-loop + @each $size, $_ in $pagination-sizes { + .pagination-#{$size} { + --pagination-min-height: var(--bs-btn-input-#{$size}-min-height); + --pagination-padding-y: var(--btn-input-#{$size}-padding-y); + --pagination-padding-x: var(--btn-input-#{$size}-padding-x); + --pagination-font-size: var(--btn-input-#{$size}-font-size); + --pagination-border-radius: var(--btn-input-#{$size}-border-radius); + } + } + // scss-docs-end pagination-sizes-loop } diff --git a/assets/stylesheets/bootstrap/_placeholder.scss b/assets/stylesheets/bootstrap/_placeholder.scss new file mode 100644 index 00000000..7242dcb0 --- /dev/null +++ b/assets/stylesheets/bootstrap/_placeholder.scss @@ -0,0 +1,72 @@ +@use "colors" as *; +@use "functions" as *; +@use "mixins/tokens" as *; + +$placeholder-tokens: () !default; + +// scss-docs-start placeholder-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$placeholder-tokens: defaults( + ( + --placeholder-opacity-max: .5, + --placeholder-opacity-min: .2, + ), + $placeholder-tokens +); +// scss-docs-end placeholder-tokens + +@layer components { + .placeholder { + @include tokens($placeholder-tokens); + + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentcolor; + opacity: var(--placeholder-opacity-max); + + &.btn::before { + display: inline-block; + content: ""; + } + } + + // Sizing + .placeholder-xs { + min-height: .6em; + } + + .placeholder-sm { + min-height: .8em; + } + + .placeholder-lg { + min-height: 1.2em; + } + + // Animation + .placeholder-glow { + .placeholder { + animation: placeholder-glow 2s ease-in-out infinite; + } + } + + @keyframes placeholder-glow { + 50% { + opacity: var(--placeholder-opacity-min); + } + } + + .placeholder-wave { + mask-image: linear-gradient(130deg, $black 55%, rgb(0 0 0 / calc(1 - var(--placeholder-opacity-min))) 75%, $black 95%); + mask-size: 200% 100%; + animation: placeholder-wave 2s linear infinite; + } + + @keyframes placeholder-wave { + 100% { + mask-position: -200% 0%; + } + } +} diff --git a/assets/stylesheets/bootstrap/_placeholders.scss b/assets/stylesheets/bootstrap/_placeholders.scss deleted file mode 100644 index 6e32e1cd..00000000 --- a/assets/stylesheets/bootstrap/_placeholders.scss +++ /dev/null @@ -1,51 +0,0 @@ -.placeholder { - display: inline-block; - min-height: 1em; - vertical-align: middle; - cursor: wait; - background-color: currentcolor; - opacity: $placeholder-opacity-max; - - &.btn::before { - display: inline-block; - content: ""; - } -} - -// Sizing -.placeholder-xs { - min-height: .6em; -} - -.placeholder-sm { - min-height: .8em; -} - -.placeholder-lg { - min-height: 1.2em; -} - -// Animation -.placeholder-glow { - .placeholder { - animation: placeholder-glow 2s ease-in-out infinite; - } -} - -@keyframes placeholder-glow { - 50% { - opacity: $placeholder-opacity-min; - } -} - -.placeholder-wave { - mask-image: linear-gradient(130deg, $black 55%, rgba(0, 0, 0, (1 - $placeholder-opacity-min)) 75%, $black 95%); - mask-size: 200% 100%; - animation: placeholder-wave 2s linear infinite; -} - -@keyframes placeholder-wave { - 100% { - mask-position: -200% 0%; - } -} diff --git a/assets/stylesheets/bootstrap/_popover.scss b/assets/stylesheets/bootstrap/_popover.scss index 7b69f623..0060ff3c 100644 --- a/assets/stylesheets/bootstrap/_popover.scss +++ b/assets/stylesheets/bootstrap/_popover.scss @@ -1,196 +1,217 @@ -.popover { - // scss-docs-start popover-css-vars - --#{$prefix}popover-zindex: #{$zindex-popover}; - --#{$prefix}popover-max-width: #{$popover-max-width}; - @include rfs($popover-font-size, --#{$prefix}popover-font-size); - --#{$prefix}popover-bg: #{$popover-bg}; - --#{$prefix}popover-border-width: #{$popover-border-width}; - --#{$prefix}popover-border-color: #{$popover-border-color}; - --#{$prefix}popover-border-radius: #{$popover-border-radius}; - --#{$prefix}popover-inner-border-radius: #{$popover-inner-border-radius}; - --#{$prefix}popover-box-shadow: #{$popover-box-shadow}; - --#{$prefix}popover-header-padding-x: #{$popover-header-padding-x}; - --#{$prefix}popover-header-padding-y: #{$popover-header-padding-y}; - @include rfs($popover-header-font-size, --#{$prefix}popover-header-font-size); - --#{$prefix}popover-header-color: #{$popover-header-color}; - --#{$prefix}popover-header-bg: #{$popover-header-bg}; - --#{$prefix}popover-body-padding-x: #{$popover-body-padding-x}; - --#{$prefix}popover-body-padding-y: #{$popover-body-padding-y}; - --#{$prefix}popover-body-color: #{$popover-body-color}; - --#{$prefix}popover-arrow-width: #{$popover-arrow-width}; - --#{$prefix}popover-arrow-height: #{$popover-arrow-height}; - --#{$prefix}popover-arrow-border: var(--#{$prefix}popover-border-color); - // scss-docs-end popover-css-vars - - z-index: var(--#{$prefix}popover-zindex); - display: block; - max-width: var(--#{$prefix}popover-max-width); - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - @include reset-text(); - @include font-size(var(--#{$prefix}popover-font-size)); - // Allow breaking very long words so they don't overflow the popover's bounds - word-wrap: break-word; - background-color: var(--#{$prefix}popover-bg); - background-clip: padding-box; - border: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-border-color); - @include border-radius(var(--#{$prefix}popover-border-radius)); - @include box-shadow(var(--#{$prefix}popover-box-shadow)); - - .popover-arrow { +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/reset-text" as *; +@use "mixins/tokens" as *; + +$popover-tokens: () !default; + +// scss-docs-start popover-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$popover-tokens: defaults( + ( + --popover-zindex: #{$zindex-popover}, + --popover-max-width: 280px, + --popover-font-size: var(--font-size-sm), + --popover-bg: var(--bg-body), + --popover-border-width: var(--border-width), + --popover-border-color: var(--border-color-translucent), + --popover-border-radius: var(--radius-7), + --popover-inner-border-radius: calc(var(--radius-7) - var(--border-width)), + --popover-box-shadow: var(--box-shadow), + --popover-header-padding-x: var(--spacer), + --popover-header-padding-y: var(--spacer-3), + --popover-header-font-size: var(--font-size-sm), + --popover-header-color: #{$headings-color}, + --popover-header-bg: var(--bg-1), + --popover-body-padding-x: var(--spacer), + --popover-body-padding-y: var(--spacer-3), + --popover-body-color: var(--fg-body), + --popover-arrow-width: 1rem, + --popover-arrow-height: .5rem, + --popover-arrow-border: var(--popover-border-color), + ), + $popover-tokens +); +// scss-docs-end popover-tokens + +@layer components { + .popover { + // scss-docs-start popover-css-vars + @include tokens($popover-tokens); + // scss-docs-end popover-css-vars + + z-index: var(--popover-zindex); display: block; - width: var(--#{$prefix}popover-arrow-width); - height: var(--#{$prefix}popover-arrow-height); - - &::before, - &::after { - position: absolute; + max-width: var(--popover-max-width); + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + @include reset-text(); + font-size: var(--popover-font-size); + // Allow breaking very long words so they don't overflow the popover's bounds + word-wrap: break-word; + background-color: var(--popover-bg); + background-clip: padding-box; + border: var(--popover-border-width) solid var(--popover-border-color); + @include border-radius(var(--popover-border-radius)); + @include box-shadow(var(--popover-box-shadow)); + + .popover-arrow { display: block; - content: ""; - border-color: transparent; - border-style: solid; - border-width: 0; + width: var(--popover-arrow-width); + height: var(--popover-arrow-height); + + &::before, + &::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; + border-width: 0; + } } } -} -.bs-popover-top { - > .popover-arrow { - bottom: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list + .bs-popover-top { + > .popover-arrow { + bottom: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); - &::before, - &::after { - border-width: var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - } + &::before, + &::after { + border-width: var(--popover-arrow-height) calc(var(--popover-arrow-width) * .5) 0; + } - &::before { - bottom: 0; - border-top-color: var(--#{$prefix}popover-arrow-border); - } + &::before { + bottom: 0; + border-block-start-color: var(--popover-arrow-border); + } - &::after { - bottom: var(--#{$prefix}popover-border-width); - border-top-color: var(--#{$prefix}popover-bg); + &::after { + bottom: var(--popover-border-width); + border-block-start-color: var(--popover-bg); + } } } -} - -/* rtl:begin:ignore */ -.bs-popover-end { - > .popover-arrow { - left: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}popover-arrow-height); - height: var(--#{$prefix}popover-arrow-width); - - &::before, - &::after { - border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - } - - &::before { - left: 0; - border-right-color: var(--#{$prefix}popover-arrow-border); - } - &::after { - left: var(--#{$prefix}popover-border-width); - border-right-color: var(--#{$prefix}popover-bg); + .bs-popover-end { + > .popover-arrow { + left: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + width: var(--popover-arrow-height); + height: var(--popover-arrow-width); + + &::before, + &::after { + border-width: calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height) calc(var(--popover-arrow-width) * .5) 0; + } + + &::before { + left: 0; + border-inline-end-color: var(--popover-arrow-border); + } + + &::after { + left: var(--popover-border-width); + border-inline-end-color: var(--popover-bg); + } } } -} - -/* rtl:end:ignore */ -.bs-popover-bottom { - > .popover-arrow { - top: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - - &::before, - &::after { - border-width: 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height); // stylelint-disable-line function-disallowed-list + .bs-popover-bottom { + > .popover-arrow { + top: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + + &::before, + &::after { + border-width: 0 calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height); + } + + &::before { + top: 0; + border-block-end-color: var(--popover-arrow-border); + } + + &::after { + top: var(--popover-border-width); + border-block-end-color: var(--popover-bg); + } + + // When the popover has a header, the bottom arrow points into the header, + // so its fill should match the header background, not the body background. + &:has(+ .popover-header)::after { + border-block-end-color: var(--popover-header-bg); + } } - &::before { + // This will remove the popover-header's border just below the arrow + .popover-header::before { + position: absolute; top: 0; - border-bottom-color: var(--#{$prefix}popover-arrow-border); - } - - &::after { - top: var(--#{$prefix}popover-border-width); - border-bottom-color: var(--#{$prefix}popover-bg); + left: 50%; + display: block; + width: var(--popover-arrow-width); + margin-inline-start: calc(-.5 * var(--popover-arrow-width)); + content: ""; + border-block-end: var(--popover-border-width) solid var(--popover-header-bg); } } - // This will remove the popover-header's border just below the arrow - .popover-header::before { - position: absolute; - top: 0; - left: 50%; - display: block; - width: var(--#{$prefix}popover-arrow-width); - margin-left: calc(-.5 * var(--#{$prefix}popover-arrow-width)); // stylelint-disable-line function-disallowed-list - content: ""; - border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-header-bg); + .bs-popover-start { + > .popover-arrow { + right: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + width: var(--popover-arrow-height); + height: var(--popover-arrow-width); + + &::before, + &::after { + border-width: calc(var(--popover-arrow-width) * .5) 0 calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height); + } + + &::before { + right: 0; + border-inline-start-color: var(--popover-arrow-border); + } + + &::after { + right: var(--popover-border-width); + border-inline-start-color: var(--popover-bg); + } + } } -} -/* rtl:begin:ignore */ -.bs-popover-start { - > .popover-arrow { - right: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}popover-arrow-height); - height: var(--#{$prefix}popover-arrow-width); - - &::before, - &::after { - border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height); // stylelint-disable-line function-disallowed-list + .bs-popover-auto { + &[data-bs-placement^="top"] { + @extend .bs-popover-top; } - - &::before { - right: 0; - border-left-color: var(--#{$prefix}popover-arrow-border); + &[data-bs-placement^="right"] { + @extend .bs-popover-end; } - - &::after { - right: var(--#{$prefix}popover-border-width); - border-left-color: var(--#{$prefix}popover-bg); + &[data-bs-placement^="bottom"] { + @extend .bs-popover-bottom; + } + &[data-bs-placement^="left"] { + @extend .bs-popover-start; } } -} - -/* rtl:end:ignore */ -.bs-popover-auto { - &[data-popper-placement^="top"] { - @extend .bs-popover-top; - } - &[data-popper-placement^="right"] { - @extend .bs-popover-end; - } - &[data-popper-placement^="bottom"] { - @extend .bs-popover-bottom; - } - &[data-popper-placement^="left"] { - @extend .bs-popover-start; + // Offset the popover to account for the popover arrow + .popover-header { + padding: var(--popover-header-padding-y) var(--popover-header-padding-x); + margin-bottom: 0; // Reset the default from Reboot + font-size: var(--popover-header-font-size); + color: var(--popover-header-color); + background-color: var(--popover-header-bg); + border-block-end: var(--popover-border-width) solid var(--popover-border-color); + @include border-top-radius(var(--popover-inner-border-radius)); + + &:empty { + display: none; + } } -} -// Offset the popover to account for the popover arrow -.popover-header { - padding: var(--#{$prefix}popover-header-padding-y) var(--#{$prefix}popover-header-padding-x); - margin-bottom: 0; // Reset the default from Reboot - @include font-size(var(--#{$prefix}popover-header-font-size)); - color: var(--#{$prefix}popover-header-color); - background-color: var(--#{$prefix}popover-header-bg); - border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-border-color); - @include border-top-radius(var(--#{$prefix}popover-inner-border-radius)); - - &:empty { - display: none; + .popover-body { + padding: var(--popover-body-padding-y) var(--popover-body-padding-x); + color: var(--popover-body-color); } } - -.popover-body { - padding: var(--#{$prefix}popover-body-padding-y) var(--#{$prefix}popover-body-padding-x); - color: var(--#{$prefix}popover-body-color); -} diff --git a/assets/stylesheets/bootstrap/_progress.scss b/assets/stylesheets/bootstrap/_progress.scss index 732365c5..4d042e23 100644 --- a/assets/stylesheets/bootstrap/_progress.scss +++ b/assets/stylesheets/bootstrap/_progress.scss @@ -1,67 +1,88 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/transition" as *; +@use "mixins/gradients" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; + +$progress-tokens: () !default; + +// scss-docs-start progress-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$progress-tokens: defaults( + ( + --progress-height: 1rem, + --progress-font-size: var(--font-size-sm), + --progress-bg: var(--bg-2), + --progress-border-radius: var(--radius-5), + --progress-box-shadow: var(--box-shadow-inset), + --progress-bar-color: var(--white), + --progress-bar-bg: var(--primary-bg), + --progress-bar-transition: width .6s ease, + --progress-bar-animation: progress-bar-stripes 1s linear infinite, + ), + $progress-tokens +); +// scss-docs-end progress-tokens + // Disable animation if transitions are disabled -// scss-docs-start progress-keyframes -@if $enable-transitions { - @keyframes progress-bar-stripes { - 0% { background-position-x: var(--#{$prefix}progress-height); } +@layer components { + // scss-docs-start progress-keyframes + @if $enable-transitions { + @keyframes progress-bar-stripes { + 0% { background-position-x: var(--progress-height); } + } } -} -// scss-docs-end progress-keyframes + // scss-docs-end progress-keyframes -.progress, -.progress-stacked { - // scss-docs-start progress-css-vars - --#{$prefix}progress-height: #{$progress-height}; - @include rfs($progress-font-size, --#{$prefix}progress-font-size); - --#{$prefix}progress-bg: #{$progress-bg}; - --#{$prefix}progress-border-radius: #{$progress-border-radius}; - --#{$prefix}progress-box-shadow: #{$progress-box-shadow}; - --#{$prefix}progress-bar-color: #{$progress-bar-color}; - --#{$prefix}progress-bar-bg: #{$progress-bar-bg}; - --#{$prefix}progress-bar-transition: #{$progress-bar-transition}; - // scss-docs-end progress-css-vars + .progress, + .progress-stacked { + @include tokens($progress-tokens); - display: flex; - height: var(--#{$prefix}progress-height); - overflow: hidden; // force rounded corners by cropping it - @include font-size(var(--#{$prefix}progress-font-size)); - background-color: var(--#{$prefix}progress-bg); - @include border-radius(var(--#{$prefix}progress-border-radius)); - @include box-shadow(var(--#{$prefix}progress-box-shadow)); -} + display: flex; + height: var(--progress-height); + overflow: hidden; + font-size: var(--progress-font-size); + background-color: var(--progress-bg); + @include border-radius(var(--progress-border-radius)); + @include box-shadow(var(--progress-box-shadow)); + } -.progress-bar { - display: flex; - flex-direction: column; - justify-content: center; - overflow: hidden; - color: var(--#{$prefix}progress-bar-color); - text-align: center; - white-space: nowrap; - background-color: var(--#{$prefix}progress-bar-bg); - @include transition(var(--#{$prefix}progress-bar-transition)); -} + .progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: var(--theme-contrast, var(--progress-bar-color)); + text-align: center; + white-space: nowrap; + background-color: var(--theme-bg, var(--progress-bar-bg)); + @include transition(var(--progress-bar-transition)); + } -.progress-bar-striped { - @include gradient-striped(); - background-size: var(--#{$prefix}progress-height) var(--#{$prefix}progress-height); -} + .progress-bar-striped { + @include gradient-striped(); + background-size: var(--progress-height) var(--progress-height); + } -.progress-stacked > .progress { - overflow: visible; -} + .progress-stacked > .progress { + overflow: visible; + } -.progress-stacked > .progress > .progress-bar { - width: 100%; -} + .progress-stacked > .progress > .progress-bar { + width: 100%; + } -@if $enable-transitions { - .progress-bar-animated { - animation: $progress-bar-animation-timing progress-bar-stripes; + @if $enable-transitions { + .progress-bar-animated { + animation: var(--progress-bar-animation); - @if $enable-reduced-motion { - @media (prefers-reduced-motion: reduce) { - animation: none; + @if $enable-reduced-motion { + @media (prefers-reduced-motion: reduce) { + animation: none; + } } } } diff --git a/assets/stylesheets/bootstrap/_reboot.scss b/assets/stylesheets/bootstrap/_reboot.scss deleted file mode 100644 index 524645fb..00000000 --- a/assets/stylesheets/bootstrap/_reboot.scss +++ /dev/null @@ -1,617 +0,0 @@ -// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix - - -// Reboot -// -// Normalization of HTML elements, manually forked from Normalize.css to remove -// styles targeting irrelevant browsers while applying new styles. -// -// Normalize is licensed MIT. https://github.com/necolas/normalize.css - - -// Document -// -// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. - -*, -*::before, -*::after { - box-sizing: border-box; -} - - -// Root -// -// Ability to the value of the root font sizes, affecting the value of `rem`. -// null by default, thus nothing is generated. - -:root { - @if $font-size-root != null { - @include font-size(var(--#{$prefix}root-font-size)); - } - - @if $enable-smooth-scroll { - @media (prefers-reduced-motion: no-preference) { - scroll-behavior: smooth; - } - } -} - - -// Body -// -// 1. Remove the margin in all browsers. -// 2. As a best practice, apply a default `background-color`. -// 3. Prevent adjustments of font size after orientation changes in iOS. -// 4. Change the default tap highlight to be completely transparent in iOS. - -// scss-docs-start reboot-body-rules -body { - margin: 0; // 1 - font-family: var(--#{$prefix}body-font-family); - @include font-size(var(--#{$prefix}body-font-size)); - font-weight: var(--#{$prefix}body-font-weight); - line-height: var(--#{$prefix}body-line-height); - color: var(--#{$prefix}body-color); - text-align: var(--#{$prefix}body-text-align); - background-color: var(--#{$prefix}body-bg); // 2 - -webkit-text-size-adjust: 100%; // 3 - -webkit-tap-highlight-color: rgba($black, 0); // 4 -} -// scss-docs-end reboot-body-rules - - -// Content grouping -// -// 1. Reset Firefox's gray color - -hr { - margin: $hr-margin-y 0; - color: $hr-color; // 1 - border: 0; - border-top: $hr-border-width solid $hr-border-color; - opacity: $hr-opacity; -} - - -// Typography -// -// 1. Remove top margins from headings -// By default, ``-`` all receive top and bottom margins. We nuke the top -// margin for easier control within type scales as it avoids margin collapsing. - -%heading { - margin-top: 0; // 1 - margin-bottom: $headings-margin-bottom; - font-family: $headings-font-family; - font-style: $headings-font-style; - font-weight: $headings-font-weight; - line-height: $headings-line-height; - color: var(--#{$prefix}heading-color); -} - -h1 { - @extend %heading; - @include font-size($h1-font-size); -} - -h2 { - @extend %heading; - @include font-size($h2-font-size); -} - -h3 { - @extend %heading; - @include font-size($h3-font-size); -} - -h4 { - @extend %heading; - @include font-size($h4-font-size); -} - -h5 { - @extend %heading; - @include font-size($h5-font-size); -} - -h6 { - @extend %heading; - @include font-size($h6-font-size); -} - - -// Reset margins on paragraphs -// -// Similarly, the top margin on ``s get reset. However, we also reset the -// bottom margin to use `rem` units instead of `em`. - -p { - margin-top: 0; - margin-bottom: $paragraph-margin-bottom; -} - - -// Abbreviations -// -// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. -// 2. Add explicit cursor to indicate changed behavior. -// 3. Prevent the text-decoration to be skipped. - -abbr[title] { - text-decoration: underline dotted; // 1 - cursor: help; // 2 - text-decoration-skip-ink: none; // 3 -} - - -// Address - -address { - margin-bottom: 1rem; - font-style: normal; - line-height: inherit; -} - - -// Lists - -ol, -ul { - padding-left: 2rem; -} - -ol, -ul, -dl { - margin-top: 0; - margin-bottom: 1rem; -} - -ol ol, -ul ul, -ol ul, -ul ol { - margin-bottom: 0; -} - -dt { - font-weight: $dt-font-weight; -} - -// 1. Undo browser default - -dd { - margin-bottom: .5rem; - margin-left: 0; // 1 -} - - -// Blockquote - -blockquote { - margin: 0 0 1rem; -} - - -// Strong -// -// Add the correct font weight in Chrome, Edge, and Safari - -b, -strong { - font-weight: $font-weight-bolder; -} - - -// Small -// -// Add the correct font size in all browsers - -small { - @include font-size($small-font-size); -} - - -// Mark - -mark { - padding: $mark-padding; - color: var(--#{$prefix}highlight-color); - background-color: var(--#{$prefix}highlight-bg); -} - - -// Sub and Sup -// -// Prevent `sub` and `sup` elements from affecting the line height in -// all browsers. - -sub, -sup { - position: relative; - @include font-size($sub-sup-font-size); - line-height: 0; - vertical-align: baseline; -} - -sub { bottom: -.25em; } -sup { top: -.5em; } - - -// Links - -a { - color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1)); - text-decoration: $link-decoration; - - &:hover { - --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb); - text-decoration: $link-hover-decoration; - } -} - -// And undo these styles for placeholder links/named anchors (without href). -// It would be more straightforward to just use a[href] in previous block, but that -// causes specificity issues in many other styles that are too complex to fix. -// See https://github.com/twbs/bootstrap/issues/19402 - -a:not([href]):not([class]) { - &, - &:hover { - color: inherit; - text-decoration: none; - } -} - - -// Code - -pre, -code, -kbd, -samp { - font-family: $font-family-code; - @include font-size(1em); // Correct the odd `em` font sizing in all browsers. -} - -// 1. Remove browser default top margin -// 2. Reset browser default of `1em` to use `rem`s -// 3. Don't allow content to break outside - -pre { - display: block; - margin-top: 0; // 1 - margin-bottom: 1rem; // 2 - overflow: auto; // 3 - @include font-size($code-font-size); - color: $pre-color; - - // Account for some code outputs that place code tags in pre tags - code { - @include font-size(inherit); - color: inherit; - word-break: normal; - } -} - -code { - @include font-size($code-font-size); - color: var(--#{$prefix}code-color); - word-wrap: break-word; - - // Streamline the style when inside anchors to avoid broken underline and more - a > & { - color: inherit; - } -} - -kbd { - padding: $kbd-padding-y $kbd-padding-x; - @include font-size($kbd-font-size); - color: $kbd-color; - background-color: $kbd-bg; - @include border-radius($border-radius-sm); - - kbd { - padding: 0; - @include font-size(1em); - font-weight: $nested-kbd-font-weight; - } -} - - -// Figures -// -// Apply a consistent margin strategy (matches our type styles). - -figure { - margin: 0 0 1rem; -} - - -// Images and content - -img, -svg { - vertical-align: middle; -} - - -// Tables -// -// Prevent double borders - -table { - caption-side: bottom; - border-collapse: collapse; -} - -caption { - padding-top: $table-cell-padding-y; - padding-bottom: $table-cell-padding-y; - color: $table-caption-color; - text-align: left; -} - -// 1. Removes font-weight bold by inheriting -// 2. Matches default `` alignment by inheriting `text-align`. -// 3. Fix alignment for Safari - -th { - font-weight: $table-th-font-weight; // 1 - text-align: inherit; // 2 - text-align: -webkit-match-parent; // 3 -} - -thead, -tbody, -tfoot, -tr, -td, -th { - border-color: inherit; - border-style: solid; - border-width: 0; -} - - -// Forms -// -// 1. Allow labels to use `margin` for spacing. - -label { - display: inline-block; // 1 -} - -// Remove the default `border-radius` that macOS Chrome adds. -// See https://github.com/twbs/bootstrap/issues/24093 - -button { - // stylelint-disable-next-line property-disallowed-list - border-radius: 0; -} - -// Explicitly remove focus outline in Chromium when it shouldn't be -// visible (e.g. as result of mouse click or touch tap). It already -// should be doing this automatically, but seems to currently be -// confused and applies its very visible two-tone outline anyway. - -button:focus:not(:focus-visible) { - outline: 0; -} - -// 1. Remove the margin in Firefox and Safari - -input, -button, -select, -optgroup, -textarea { - margin: 0; // 1 - font-family: inherit; - @include font-size(inherit); - line-height: inherit; -} - -// Remove the inheritance of text transform in Firefox -button, -select { - text-transform: none; -} -// Set the cursor for non-`` buttons -// -// Details at https://github.com/twbs/bootstrap/pull/30562 -[role="button"] { - cursor: pointer; -} - -select { - // Remove the inheritance of word-wrap in Safari. - // See https://github.com/twbs/bootstrap/issues/24990 - word-wrap: normal; - - // Undo the opacity change from Chrome - &:disabled { - opacity: 1; - } -} - -// Remove the dropdown arrow only from text type inputs built with datalists in Chrome. -// See https://stackoverflow.com/a/54997118 - -[list]:not([type="date"]):not([type="datetime-local"]):not([type="month"]):not([type="week"]):not([type="time"])::-webkit-calendar-picker-indicator { - display: none !important; -} - -// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` -// controls in Android 4. -// 2. Correct the inability to style clickable types in iOS and Safari. -// 3. Opinionated: add "hand" cursor to non-disabled button elements. - -button, -[type="button"], // 1 -[type="reset"], -[type="submit"] { - -webkit-appearance: button; // 2 - - @if $enable-button-pointers { - &:not(:disabled) { - cursor: pointer; // 3 - } - } -} - -// Remove inner border and padding from Firefox, but don't restore the outline like Normalize. - -::-moz-focus-inner { - padding: 0; - border-style: none; -} - -// 1. Textareas should really only resize vertically so they don't break their (horizontal) containers. - -textarea { - resize: vertical; // 1 -} - -// 1. Browsers set a default `min-width: min-content;` on fieldsets, -// unlike e.g. ``s, which have `min-width: 0;` by default. -// So we reset that to ensure fieldsets behave more like a standard block element. -// See https://github.com/twbs/bootstrap/issues/12359 -// and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements -// 2. Reset the default outline behavior of fieldsets so they don't affect page layout. - -fieldset { - min-width: 0; // 1 - padding: 0; // 2 - margin: 0; // 2 - border: 0; // 2 -} - -// 1. By using `float: left`, the legend will behave like a block element. -// This way the border of a fieldset wraps around the legend if present. -// 2. Fix wrapping bug. -// See https://github.com/twbs/bootstrap/issues/29712 - -legend { - float: left; // 1 - width: 100%; - padding: 0; - margin-bottom: $legend-margin-bottom; - font-weight: $legend-font-weight; - line-height: inherit; - @include font-size($legend-font-size); - - + * { - clear: left; // 2 - } -} - -// Fix height of inputs with a type of datetime-local, date, month, week, or time -// See https://github.com/twbs/bootstrap/issues/18842 - -::-webkit-datetime-edit-fields-wrapper, -::-webkit-datetime-edit-text, -::-webkit-datetime-edit-minute, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-year-field { - padding: 0; -} - -::-webkit-inner-spin-button { - height: auto; -} - -// 1. This overrides the extra rounded corners on search inputs in iOS so that our -// `.form-control` class can properly style them. Note that this cannot simply -// be added to `.form-control` as it's not specific enough. For details, see -// https://github.com/twbs/bootstrap/issues/11586. -// 2. Correct the outline style in Safari. - -[type="search"] { - -webkit-appearance: textfield; // 1 - outline-offset: -2px; // 2 - - // 3. Better affordance and consistent appearance for search cancel button - &::-webkit-search-cancel-button { - cursor: pointer; - filter: grayscale(1); - } -} - -// 1. A few input types should stay LTR -// See https://rtlstyling.com/posts/rtl-styling#form-inputs -// 2. RTL only output -// See https://rtlcss.com/learn/usage-guide/control-directives/#raw - -/* rtl:raw: -[type="tel"], -[type="url"], -[type="email"], -[type="number"] { - direction: ltr; -} -*/ - -// Remove the inner padding in Chrome and Safari on macOS. - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -// Remove padding around color pickers in webkit browsers - -::-webkit-color-swatch-wrapper { - padding: 0; -} - - -// 1. Inherit font family and line height for file input buttons -// 2. Correct the inability to style clickable types in iOS and Safari. - -::file-selector-button { - font: inherit; // 1 - -webkit-appearance: button; // 2 -} - -// Correct element displays - -output { - display: inline-block; -} - -// Remove border from iframe - -iframe { - border: 0; -} - -// Summary -// -// 1. Add the correct display in all browsers - -summary { - display: list-item; // 1 - cursor: pointer; -} - - -// Progress -// -// Add the correct vertical alignment in Chrome, Firefox, and Opera. - -progress { - vertical-align: baseline; -} - - -// Hidden attribute -// -// Always hide an element with the `hidden` HTML attribute. - -[hidden] { - display: none !important; -} diff --git a/assets/stylesheets/bootstrap/_root.scss b/assets/stylesheets/bootstrap/_root.scss index becddf14..b35cb1c5 100644 --- a/assets/stylesheets/bootstrap/_root.scss +++ b/assets/stylesheets/bootstrap/_root.scss @@ -1,187 +1,187 @@ -:root, -[data-bs-theme="light"] { - // Note: Custom variable values only support SassScript inside `#{}`. - - // Colors - // - // Generate palettes for full colors, grays, and theme colors. - - @each $color, $value in $colors { - --#{$prefix}#{$color}: #{$value}; - } - - @each $color, $value in $grays { - --#{$prefix}gray-#{$color}: #{$value}; - } - - @each $color, $value in $theme-colors { - --#{$prefix}#{$color}: #{$value}; - } - - @each $color, $value in $theme-colors-rgb { - --#{$prefix}#{$color}-rgb: #{$value}; - } - - @each $color, $value in $theme-colors-text { - --#{$prefix}#{$color}-text-emphasis: #{$value}; - } - - @each $color, $value in $theme-colors-bg-subtle { - --#{$prefix}#{$color}-bg-subtle: #{$value}; - } - - @each $color, $value in $theme-colors-border-subtle { - --#{$prefix}#{$color}-border-subtle: #{$value}; - } - - --#{$prefix}white-rgb: #{to-rgb($white)}; - --#{$prefix}black-rgb: #{to-rgb($black)}; - - // Fonts - - // Note: Use `inspect` for lists so that quoted items keep the quotes. - // See https://github.com/sass/sass/issues/2383#issuecomment-336349172 - --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)}; - --#{$prefix}font-monospace: #{inspect($font-family-monospace)}; - --#{$prefix}gradient: #{$gradient}; - - // Root and body - // scss-docs-start root-body-variables - @if $font-size-root != null { - --#{$prefix}root-font-size: #{$font-size-root}; - } - --#{$prefix}body-font-family: #{inspect($font-family-base)}; - @include rfs($font-size-base, --#{$prefix}body-font-size); - --#{$prefix}body-font-weight: #{$font-weight-base}; - --#{$prefix}body-line-height: #{$line-height-base}; - @if $body-text-align != null { - --#{$prefix}body-text-align: #{$body-text-align}; - } - - --#{$prefix}body-color: #{$body-color}; - --#{$prefix}body-color-rgb: #{to-rgb($body-color)}; - --#{$prefix}body-bg: #{$body-bg}; - --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)}; - - --#{$prefix}emphasis-color: #{$body-emphasis-color}; - --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)}; - - --#{$prefix}secondary-color: #{$body-secondary-color}; - --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)}; - --#{$prefix}secondary-bg: #{$body-secondary-bg}; - --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)}; - - --#{$prefix}tertiary-color: #{$body-tertiary-color}; - --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)}; - --#{$prefix}tertiary-bg: #{$body-tertiary-bg}; - --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)}; - // scss-docs-end root-body-variables - - --#{$prefix}heading-color: #{$headings-color}; - - --#{$prefix}link-color: #{$link-color}; - --#{$prefix}link-color-rgb: #{to-rgb($link-color)}; - --#{$prefix}link-decoration: #{$link-decoration}; - - --#{$prefix}link-hover-color: #{$link-hover-color}; - --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)}; +@use "sass:map"; +@use "colors" as *; +@use "config" as *; +@use "functions" as *; +@use "theme" as *; +@use "mixins/tokens" as *; +// mdo-do: do we need theme? +@layer colors, theme, config, root, reboot, layout, content, forms, components, custom, helpers, utilities; + +$root-tokens: () !default; + +// scss-docs-start root-tokens +// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-disable-next-line scss/dollar-variable-default +$root-tokens: defaults( + ( + --black: #{$black}, + --white: #{$white}, + + --gradient: #{$gradient}, + + // scss-docs-start root-font-weight-variables + --font-weight-lighter: lighter, + --font-weight-light: 300, + --font-weight-normal: 400, + --font-weight-medium: 500, + --font-weight-semibold: 600, + --font-weight-bold: 700, + --font-weight-bolder: bolder, + // scss-docs-end root-font-weight-variables + + // scss-docs-start root-body-variables + --body-font-family: system-ui, + --body-font-size: var(--font-size-base), + --body-font-weight: #{$font-weight-base}, + --body-line-height: #{$line-height-base}, + + --heading-color: #{$headings-color}, + + --hr-border-color: var(--border-color), + + --link-color: light-dark(var(--primary-base), var(--primary-fg)), + --link-decoration: #{$link-decoration}, + --link-hover-color: color-mix(in oklch, var(--link-color) 90%, #000), + + --font-mono: "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, 'Cascadia Mono', Consolas, 'Liberation Mono', monospace;", + --code-font-size: 95%, + --code-color: var(--fg-2), + + // scss-docs-start root-border-var + --border-width: #{$border-width}, + --border-style: #{$border-style}, + --border-color: light-dark(var(--gray-200), var(--gray-700)), + --border-color-translucent: color-mix(in oklch, var(--fg-body) 15%, transparent), + // scss-docs-end root-border-var + + // scss-docs-start root-box-shadow-variables + --box-shadow-xs: 0 .0625rem .1875rem rgb(0 0 0 / 7.5%), + --box-shadow-sm: 0 .125rem .25rem rgb(0 0 0 / 7.5%), + --box-shadow: 0 .5rem 1rem rgb(0 0 0 / 15%), + --box-shadow-lg: 0 1rem 3rem rgb(0 0 0 / 17.5%), + --box-shadow-inset: inset 0 1px 2px rgb(0 0 0 / 7.5%), + // scss-docs-end root-box-shadow-variables + + --spacer: 1rem, + + // scss-docs-start root-focus-variables + --focus-ring-width: 3px, + --focus-ring-offset: 1px, + --focus-ring-color: var(--primary-focus-ring), + --focus-ring: var(--focus-ring-width) solid var(--focus-ring-color), + // scss-docs-end root-focus-variables + + // scss-docs-start root-form-variables + --control-checked-bg: var(--primary-base), + --control-checked-border-color: var(--control-checked-bg), + --control-active-bg: var(--primary-base), + --control-active-border-color: var(--control-active-bg), + --control-disabled-bg: var(--bg-3), + --control-disabled-opacity: .65, + + --btn-input-fg: var(--fg-body), + --btn-input-bg: var(--bg-body), + + --btn-input-min-height: 2.375rem, + --btn-input-padding-y: .375rem, + --btn-input-padding-x: .75rem, + --btn-input-font-size: var(--font-size-base), + --btn-input-line-height: var(--line-height-base), + --btn-input-border-radius: var(--radius-5), + + --btn-input-xs-min-height: 1.5rem, + --btn-input-xs-padding-y: .125rem, + --btn-input-xs-padding-x: .5rem, + --btn-input-xs-font-size: var(--font-size-xs), + --btn-input-xs-line-height: 1.125, + --btn-input-xs-border-radius: var(--radius-5), + + --btn-input-sm-min-height: 2rem, + --btn-input-sm-padding-y: .25rem, + --btn-input-sm-padding-x: .625rem, + --btn-input-sm-font-size: var(--font-size-sm), + --btn-input-sm-line-height: var(--line-height-sm), + --btn-input-sm-border-radius: var(--radius-5), + + --btn-input-lg-min-height: 2.75rem, + --btn-input-lg-padding-y: .5rem, + --btn-input-lg-padding-x: 1rem, + --btn-input-lg-font-size: var(--font-size-md), + --btn-input-lg-line-height: var(--line-height-md), + --btn-input-lg-border-radius: var(--radius-7), + // scss-docs-end root-form-variables + ), + $root-tokens +); +// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// scss-docs-end root-tokens + +// scss-docs-start root-font-size-loop +// Generate font-size and line-height tokens +@each $name, $props in $font-sizes { + $root-tokens: map.set($root-tokens, --font-size-#{$name}, map.get($props, "font-size")); + $root-tokens: map.set($root-tokens, --line-height-#{$name}, map.get($props, "line-height")); +} +// scss-docs-end root-font-size-loop - @if $link-hover-decoration != null { - --#{$prefix}link-hover-decoration: #{$link-hover-decoration}; +// scss-docs-start root-theme-tokens +// Generate semantic theme colors +@each $color-name, $color-map in $theme-colors { + @each $key, $value in $color-map { + $root-tokens: map.set($root-tokens, --#{$color-name}-#{$key}, $value); } - - --#{$prefix}code-color: #{$code-color}; - --#{$prefix}highlight-color: #{$mark-color}; - --#{$prefix}highlight-bg: #{$mark-bg}; - - // scss-docs-start root-border-var - --#{$prefix}border-width: #{$border-width}; - --#{$prefix}border-style: #{$border-style}; - --#{$prefix}border-color: #{$border-color}; - --#{$prefix}border-color-translucent: #{$border-color-translucent}; - - --#{$prefix}border-radius: #{$border-radius}; - --#{$prefix}border-radius-sm: #{$border-radius-sm}; - --#{$prefix}border-radius-lg: #{$border-radius-lg}; - --#{$prefix}border-radius-xl: #{$border-radius-xl}; - --#{$prefix}border-radius-xxl: #{$border-radius-xxl}; - --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency - --#{$prefix}border-radius-pill: #{$border-radius-pill}; - // scss-docs-end root-border-var - - --#{$prefix}box-shadow: #{$box-shadow}; - --#{$prefix}box-shadow-sm: #{$box-shadow-sm}; - --#{$prefix}box-shadow-lg: #{$box-shadow-lg}; - --#{$prefix}box-shadow-inset: #{$box-shadow-inset}; - - // Focus styles - // scss-docs-start root-focus-variables - --#{$prefix}focus-ring-width: #{$focus-ring-width}; - --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity}; - --#{$prefix}focus-ring-color: #{$focus-ring-color}; - // scss-docs-end root-focus-variables - - // scss-docs-start root-form-validation-variables - --#{$prefix}form-valid-color: #{$form-valid-color}; - --#{$prefix}form-valid-border-color: #{$form-valid-border-color}; - --#{$prefix}form-invalid-color: #{$form-invalid-color}; - --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color}; - // scss-docs-end root-form-validation-variables } -@if $enable-dark-mode { - @include color-mode(dark, true) { - color-scheme: dark; - - // scss-docs-start root-dark-mode-vars - --#{$prefix}body-color: #{$body-color-dark}; - --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)}; - --#{$prefix}body-bg: #{$body-bg-dark}; - --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)}; - - --#{$prefix}emphasis-color: #{$body-emphasis-color-dark}; - --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)}; - - --#{$prefix}secondary-color: #{$body-secondary-color-dark}; - --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)}; - --#{$prefix}secondary-bg: #{$body-secondary-bg-dark}; - --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)}; +// Generate background tokens +@each $key, $value in $theme-bgs { + $root-tokens: map.set($root-tokens, --bg-#{$key}, $value); +} - --#{$prefix}tertiary-color: #{$body-tertiary-color-dark}; - --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)}; - --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark}; - --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)}; +// Generate foreground tokens +@each $key, $value in $theme-fgs { + $root-tokens: map.set($root-tokens, --fg-#{$key}, $value); +} - @each $color, $value in $theme-colors-text-dark { - --#{$prefix}#{$color}-text-emphasis: #{$value}; - } +// Generate border tokens +@each $key, $value in $theme-borders { + $root-tokens: map.set($root-tokens, --border-#{$key}, $value); +} +// scss-docs-end root-theme-tokens - @each $color, $value in $theme-colors-bg-subtle-dark { - --#{$prefix}#{$color}-bg-subtle: #{$value}; - } +// Generate breakpoint tokens +@each $name, $value in $breakpoints { + $root-tokens: map.set($root-tokens, --breakpoint-#{$name}, $value); +} - @each $color, $value in $theme-colors-border-subtle-dark { - --#{$prefix}#{$color}-border-subtle: #{$value}; - } +// Generate spacer tokens +// scss-docs-start root-spacer-loop +@each $key, $value in $spacers { + $root-tokens: map.set($root-tokens, --spacer-#{$key}, $value); +} +// scss-docs-end root-spacer-loop - --#{$prefix}heading-color: #{$headings-color-dark}; +// Generate radius tokens +// scss-docs-start root-radius-loop +@each $key, $value in $radii { + $root-tokens: map.set($root-tokens, --radius-#{$key}, $value); +} +// stylelint-disable-next-line scss/dollar-variable-default +$root-tokens: map.set($root-tokens, --radius-pill, 50rem); +// scss-docs-end root-radius-loop - --#{$prefix}link-color: #{$link-color-dark}; - --#{$prefix}link-hover-color: #{$link-hover-color-dark}; - --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)}; - --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)}; +:root { + @include tokens($root-tokens); - --#{$prefix}code-color: #{$code-color-dark}; - --#{$prefix}highlight-color: #{$mark-color-dark}; - --#{$prefix}highlight-bg: #{$mark-bg-dark}; + color-scheme: light dark; + // Always reserve the viewport scrollbar gutter so layout doesn't shift + // when overflow: hidden is applied (e.g. when a dialog opens on Windows). + scrollbar-gutter: stable; +} - --#{$prefix}border-color: #{$border-color-dark}; - --#{$prefix}border-color-translucent: #{$border-color-translucent-dark}; +[data-bs-theme="dark"] { + color-scheme: dark; +} - --#{$prefix}form-valid-color: #{$form-valid-color-dark}; - --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark}; - --#{$prefix}form-invalid-color: #{$form-invalid-color-dark}; - --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark}; - // scss-docs-end root-dark-mode-vars - } +[data-bs-theme="light"] { + color-scheme: light; } diff --git a/assets/stylesheets/bootstrap/_spinner.scss b/assets/stylesheets/bootstrap/_spinner.scss new file mode 100644 index 00000000..5520c469 --- /dev/null +++ b/assets/stylesheets/bootstrap/_spinner.scss @@ -0,0 +1,118 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$spinner-border-tokens: () !default; + +// scss-docs-start spinner-border-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$spinner-border-tokens: defaults( + ( + --spinner-width: 2rem, + --spinner-height: 2rem, + --spinner-vertical-align: -.125em, + --spinner-border-width: .25em, + --spinner-animation-speed: .75s, + --spinner-animation-name: spinner-border, + ), + $spinner-border-tokens +); +// scss-docs-end spinner-border-tokens + +$spinner-grow-tokens: () !default; + +// scss-docs-start spinner-grow-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$spinner-grow-tokens: defaults( + ( + --spinner-width: 2rem, + --spinner-height: 2rem, + --spinner-vertical-align: -.125em, + --spinner-animation-speed: .75s, + --spinner-animation-name: spinner-grow, + ), + $spinner-grow-tokens +); +// scss-docs-end spinner-grow-tokens + +// stylelint-enable custom-property-no-missing-var-function + +// +// Rotating border +// + +@layer components { + // mdo-do: Refactor this to assume flex parent and remove `vertical-align` + .spinner-grow, + .spinner-border { + display: inline-block; + flex-shrink: 0; + width: var(--spinner-width); + height: var(--spinner-height); + vertical-align: var(--spinner-vertical-align); + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + animation: var(--spinner-animation-speed) linear infinite var(--spinner-animation-name); + } + + // scss-docs-start spinner-border-keyframes + @keyframes spinner-border { + to { transform: rotate(360deg); } + } + // scss-docs-end spinner-border-keyframes + + .spinner-border { + @include tokens($spinner-border-tokens); + + border: var(--spinner-border-width) solid currentcolor; + border-inline-end-color: transparent; + } + + .spinner-border-sm { + // scss-docs-start spinner-border-sm-css-vars + --spinner-width: 1rem; + --spinner-height: 1rem; + --spinner-border-width: .2em; + // scss-docs-end spinner-border-sm-css-vars + } + + // + // Growing circle + // + + // scss-docs-start spinner-grow-keyframes + @keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } + } + // scss-docs-end spinner-grow-keyframes + + .spinner-grow { + @include tokens($spinner-grow-tokens); + + background-color: currentcolor; + opacity: 0; + } + + .spinner-grow-sm { + // scss-docs-start spinner-grow-sm-css-vars + --spinner-width: 1rem; + --spinner-height: 1rem; + // scss-docs-end spinner-grow-sm-css-vars + } + + @if $enable-reduced-motion { + @media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + --spinner-animation-speed: 1.5s; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/_spinners.scss b/assets/stylesheets/bootstrap/_spinners.scss deleted file mode 100644 index 9dff2892..00000000 --- a/assets/stylesheets/bootstrap/_spinners.scss +++ /dev/null @@ -1,86 +0,0 @@ -// -// Rotating border -// - -.spinner-grow, -.spinner-border { - display: inline-block; - flex-shrink: 0; - width: var(--#{$prefix}spinner-width); - height: var(--#{$prefix}spinner-height); - vertical-align: var(--#{$prefix}spinner-vertical-align); - // stylelint-disable-next-line property-disallowed-list - border-radius: 50%; - animation: var(--#{$prefix}spinner-animation-speed) linear infinite var(--#{$prefix}spinner-animation-name); -} - -// scss-docs-start spinner-border-keyframes -@keyframes spinner-border { - to { transform: rotate(360deg) #{"/* rtl:ignore */"}; } -} -// scss-docs-end spinner-border-keyframes - -.spinner-border { - // scss-docs-start spinner-border-css-vars - --#{$prefix}spinner-width: #{$spinner-width}; - --#{$prefix}spinner-height: #{$spinner-height}; - --#{$prefix}spinner-vertical-align: #{$spinner-vertical-align}; - --#{$prefix}spinner-border-width: #{$spinner-border-width}; - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed}; - --#{$prefix}spinner-animation-name: spinner-border; - // scss-docs-end spinner-border-css-vars - - border: var(--#{$prefix}spinner-border-width) solid currentcolor; - border-right-color: transparent; -} - -.spinner-border-sm { - // scss-docs-start spinner-border-sm-css-vars - --#{$prefix}spinner-width: #{$spinner-width-sm}; - --#{$prefix}spinner-height: #{$spinner-height-sm}; - --#{$prefix}spinner-border-width: #{$spinner-border-width-sm}; - // scss-docs-end spinner-border-sm-css-vars -} - -// -// Growing circle -// - -// scss-docs-start spinner-grow-keyframes -@keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} -// scss-docs-end spinner-grow-keyframes - -.spinner-grow { - // scss-docs-start spinner-grow-css-vars - --#{$prefix}spinner-width: #{$spinner-width}; - --#{$prefix}spinner-height: #{$spinner-height}; - --#{$prefix}spinner-vertical-align: #{$spinner-vertical-align}; - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed}; - --#{$prefix}spinner-animation-name: spinner-grow; - // scss-docs-end spinner-grow-css-vars - - background-color: currentcolor; - opacity: 0; -} - -.spinner-grow-sm { - --#{$prefix}spinner-width: #{$spinner-width-sm}; - --#{$prefix}spinner-height: #{$spinner-height-sm}; -} - -@if $enable-reduced-motion { - @media (prefers-reduced-motion: reduce) { - .spinner-border, - .spinner-grow { - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed * 2}; - } - } -} diff --git a/assets/stylesheets/bootstrap/_stepper.scss b/assets/stylesheets/bootstrap/_stepper.scss new file mode 100644 index 00000000..26f32294 --- /dev/null +++ b/assets/stylesheets/bootstrap/_stepper.scss @@ -0,0 +1,156 @@ +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$stepper-tokens: () !default; + +// scss-docs-start stepper-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$stepper-tokens: defaults( + ( + --stepper-size: 2rem, + --stepper-gap: 1rem, + --stepper-font-size: var(--font-size-sm), + --stepper-text-gap: .5rem, + --stepper-track-size: .125rem, + --stepper-bg: var(--bg-2), + --stepper-active-color: var(--primary-contrast), + --stepper-active-bg: var(--primary-bg), + ), + $stepper-tokens +); +// scss-docs-end stepper-tokens + +// scss-docs-start stepper-horizontal-mixin +@mixin stepper-horizontal() { + display: inline-grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + + .stepper-item { + grid-template-rows: var(--stepper-size) auto; + grid-template-columns: auto; + align-items: start; + justify-items: center; + text-align: center; + + &::after { + inset-block-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + inset-block-end: auto; + inset-inline-start: 50%; + inset-inline-end: 100%; + width: calc(100% + var(--stepper-gap)); + height: var(--stepper-track-size); + } + + &:last-child::after { + right: 100%; + } + } +} +// scss-docs-end stepper-horizontal-mixin + +@layer components { + .stepper { + @include tokens($stepper-tokens); + + display: grid; + grid-auto-rows: 1fr; + grid-auto-flow: row; + gap: var(--stepper-gap); + padding-inline-start: 0; + list-style-type: ""; + counter-reset: stepper; + } + + .stepper-item { + position: relative; + display: grid; + grid-template-rows: auto; + grid-template-columns: var(--stepper-size) auto; + gap: var(--stepper-text-gap); + align-items: var(--stepper-align-items, center); + text-decoration: none; + + // The counter + &::before { + position: relative; + z-index: 1; + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: var(--stepper-size); + height: var(--stepper-size); + padding: .5rem; + font-size: var(--stepper-font-size); + font-weight: 600; + line-height: 1; + text-align: center; + content: counter(stepper); + counter-increment: stepper; + background-color: var(--stepper-bg); + @include border-radius(50%); + } + + // Connecting lines + &::after { + position: absolute; + inset-block-start: 50%; + inset-block-end: 100%; + inset-inline-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + width: var(--stepper-track-size); + height: calc(100% + var(--stepper-gap)); + content: ""; + background-color: var(--stepper-bg); + } + + // Avoid sibling selector for easier CSS overrides + &:last-child::after { + display: none; + } + + &.active { + &::before, + &::after { + color: var(--theme-contrast, var(--stepper-active-color)); + background-color: var(--theme-bg, var(--stepper-active-bg)); + } + } + } + + // Targets the last .active element from a sequence of active elements + .stepper-item.active:not(:has(+ .stepper-item.active))::after { + background-color: var(--stepper-bg); + } + + .stepper-horizontal { + @include stepper-horizontal(); + } + + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + @if $next { + .#{$prefix}stepper-horizontal { + @include container-breakpoint-up($next) { + @include stepper-horizontal(); + } + } + } + } + + // scss-docs-start stepper-overflow + .stepper-overflow { + container-type: inline-size; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + + > .stepper { + width: max-content; + min-width: 100%; + } + } + // scss-docs-end stepper-overflow +} diff --git a/assets/stylesheets/bootstrap/_tables.scss b/assets/stylesheets/bootstrap/_tables.scss deleted file mode 100644 index 276521a3..00000000 --- a/assets/stylesheets/bootstrap/_tables.scss +++ /dev/null @@ -1,171 +0,0 @@ -// -// Basic Bootstrap table -// - -.table { - // Reset needed for nesting tables - --#{$prefix}table-color-type: initial; - --#{$prefix}table-bg-type: initial; - --#{$prefix}table-color-state: initial; - --#{$prefix}table-bg-state: initial; - // End of reset - --#{$prefix}table-color: #{$table-color}; - --#{$prefix}table-bg: #{$table-bg}; - --#{$prefix}table-border-color: #{$table-border-color}; - --#{$prefix}table-accent-bg: #{$table-accent-bg}; - --#{$prefix}table-striped-color: #{$table-striped-color}; - --#{$prefix}table-striped-bg: #{$table-striped-bg}; - --#{$prefix}table-active-color: #{$table-active-color}; - --#{$prefix}table-active-bg: #{$table-active-bg}; - --#{$prefix}table-hover-color: #{$table-hover-color}; - --#{$prefix}table-hover-bg: #{$table-hover-bg}; - - width: 100%; - margin-bottom: $spacer; - vertical-align: $table-cell-vertical-align; - border-color: var(--#{$prefix}table-border-color); - - // Target th & td - // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class. - // We use the universal selectors here to simplify the selector (else we would need 6 different selectors). - // Another advantage is that this generates less code and makes the selector less specific making it easier to override. - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - padding: $table-cell-padding-y $table-cell-padding-x; - // Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb - color: var(--#{$prefix}table-color-state, var(--#{$prefix}table-color-type, var(--#{$prefix}table-color))); - background-color: var(--#{$prefix}table-bg); - border-bottom-width: $table-border-width; - box-shadow: inset 0 0 0 9999px var(--#{$prefix}table-bg-state, var(--#{$prefix}table-bg-type, var(--#{$prefix}table-accent-bg))); - } - - > tbody { - vertical-align: inherit; - } - - > thead { - vertical-align: bottom; - } -} - -.table-group-divider { - border-top: calc(#{$table-border-width} * 2) solid $table-group-separator-color; // stylelint-disable-line function-disallowed-list -} - -// -// Change placement of captions with a class -// - -.caption-top { - caption-side: top; -} - - -// -// Condensed table w/ half padding -// - -.table-sm { - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - padding: $table-cell-padding-y-sm $table-cell-padding-x-sm; - } -} - - -// Border versions -// -// Add or remove borders all around the table and between all the columns. -// -// When borders are added on all sides of the cells, the corners can render odd when -// these borders do not have the same color or if they are semi-transparent. -// Therefore we add top and border bottoms to the `tr`s and left and right borders -// to the `td`s or `th`s - -.table-bordered { - > :not(caption) > * { - border-width: $table-border-width 0; - - // stylelint-disable-next-line selector-max-universal - > * { - border-width: 0 $table-border-width; - } - } -} - -.table-borderless { - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - border-bottom-width: 0; - } - - > :not(:first-child) { - border-top-width: 0; - } -} - -// Zebra-striping -// -// Default zebra-stripe styles (alternating gray and transparent backgrounds) - -// For rows -.table-striped { - > tbody > tr:nth-of-type(#{$table-striped-order}) > * { - --#{$prefix}table-color-type: var(--#{$prefix}table-striped-color); - --#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg); - } -} - -// For columns -.table-striped-columns { - > :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) { - --#{$prefix}table-color-type: var(--#{$prefix}table-striped-color); - --#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg); - } -} - -// Active table -// -// The `.table-active` class can be added to highlight rows or cells - -.table-active { - --#{$prefix}table-color-state: var(--#{$prefix}table-active-color); - --#{$prefix}table-bg-state: var(--#{$prefix}table-active-bg); -} - -// Hover effect -// -// Placed here since it has to come after the potential zebra striping - -.table-hover { - > tbody > tr:hover > * { - --#{$prefix}table-color-state: var(--#{$prefix}table-hover-color); - --#{$prefix}table-bg-state: var(--#{$prefix}table-hover-bg); - } -} - - -// Table variants -// -// Table variants set the table cell backgrounds, border colors -// and the colors of the striped, hovered & active tables - -@each $color, $value in $table-variants { - @include table-variant($color, $value); -} - -// Responsive tables -// -// Generate series of `.table-responsive-*` classes for configuring the screen -// size of where your table will overflow. - -@each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - @include media-breakpoint-down($breakpoint) { - .table-responsive#{$infix} { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } - } -} diff --git a/assets/stylesheets/bootstrap/_theme.scss b/assets/stylesheets/bootstrap/_theme.scss new file mode 100644 index 00000000..0327ccfe --- /dev/null +++ b/assets/stylesheets/bootstrap/_theme.scss @@ -0,0 +1,217 @@ +@use "sass:map"; + +@function theme-color-values($key) { + $result: (); + + @each $color-name, $color-map in $theme-colors { + @if map.has-key($color-map, $key) { + $result: map.merge($result, ($color-name: map.get($color-map, $key))); + } + } + + @return $result; +} + +// Themes map sub-keys +// +// Return var() references to root tokens instead of raw values. +// Ex: theme-color-refs("bg") => (primary: var(--primary-bg), accent: var(--accent-bg), ...) +@function theme-color-refs($key) { + $result: (); + + @each $color-name, $color-map in $theme-colors { + @if map.has-key($color-map, $key) { + $result: map.merge($result, ($color-name: var(--#{$color-name}-#{$key}))); + } + } + + @return $result; +} + +// Theme token to root tokens +// +// Returns the global :root token reference for a given a given token map, prefix, and key. +// Ex: theme-token-refs($theme-bgs, "bg") => (body: var(--bg-body), 1: var(--bg-1), ...) +// Skips `inherit` since it's a CSS-wide keyword that can't be stored in a custom property. +@function theme-token-refs($map, $prefix) { + $result: (); + + @each $key, $value in $map { + @if $value != inherit { + $result: map.merge($result, ($key: var(--#{$prefix}-#{$key}))); + } + } + + @return $result; +} + +// Generate opacity values using color-mix() +@function theme-opacity-values($color-var, $opacities: $util-opacity) { + $result: (); + + @each $key, $value in $opacities { + @if $key == 100 { + // For 100%, use direct variable reference (more efficient) + $result: map.merge($result, ($key: var($color-var))); + } @else { + // For other values, use color-mix() + $percentage: $key * 1%; + $result: map.merge($result, ($key: color-mix(in oklch, var($color-var) $percentage, transparent))); + } + } + + @return $result; +} + +// Generate theme classes dynamically based on the keys in each theme color map +@mixin generate-theme-classes() { + @each $color-name, $color-map in $theme-colors { + .theme-#{$color-name} { + @each $key, $value in $color-map { + --theme-#{$key}: var(--#{$color-name}-#{$key}); + } + } + } +} + +// scss-docs-start theme-colors +$theme-colors: ( + "primary": ( + "base": var(--blue-500), + "fg": light-dark(var(--blue-600), var(--blue-400)), + "fg-emphasis": light-dark(var(--blue-800), var(--blue-200)), + "bg": var(--blue-500), + "bg-subtle": light-dark(var(--blue-100), var(--blue-900)), + "bg-muted": light-dark(var(--blue-200), var(--blue-800)), + "border": light-dark(var(--blue-300), var(--blue-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--blue-500) 50%, var(--bg-body)), color-mix(in oklch, var(--blue-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "accent": ( + "base": var(--indigo-500), + "fg": light-dark(var(--indigo-600), color-mix(in oklch, var(--indigo-400), var(--indigo-300))), + "fg-emphasis": light-dark(var(--indigo-800), var(--indigo-300)), + "bg": var(--indigo-500), + "bg-subtle": light-dark(var(--indigo-100), var(--indigo-900)), + "bg-muted": light-dark(var(--indigo-200), var(--indigo-800)), + "border": light-dark(var(--indigo-300), var(--indigo-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--indigo-500) 50%, var(--bg-body)), color-mix(in oklch, var(--indigo-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "success": ( + "base": var(--green-500), + "fg": light-dark(var(--green-600), var(--green-400)), + "fg-emphasis": light-dark(var(--green-800), var(--green-300)), + "bg": var(--green-500), + "bg-subtle": light-dark(var(--green-100), var(--green-900)), + "bg-muted": light-dark(var(--green-200), var(--green-800)), + "border": light-dark(var(--green-300), var(--green-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--green-500) 50%, var(--bg-body)), color-mix(in oklch, var(--green-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "danger": ( + "base": var(--red-500), + "fg": light-dark(var(--red-600), var(--red-400)), + "fg-emphasis": light-dark(var(--red-800), var(--red-300)), + "bg": var(--red-500), + "bg-subtle": light-dark(var(--red-100), var(--red-900)), + "bg-muted": light-dark(var(--red-200), var(--red-800)), + "border": light-dark(var(--red-300), var(--red-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--red-500) 50%, var(--bg-body)), color-mix(in oklch, var(--red-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "warning": ( + "base": var(--yellow-500), + "fg": light-dark(var(--yellow-700), var(--yellow-400)), + "fg-emphasis": light-dark(var(--yellow-800), var(--yellow-300)), + "bg": var(--yellow-500), + "bg-subtle": light-dark(var(--yellow-100), var(--yellow-900)), + "bg-muted": light-dark(var(--yellow-200), var(--yellow-800)), + "border": light-dark(var(--yellow-300), var(--yellow-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--yellow-500) 50%, var(--bg-body)), color-mix(in oklch, var(--yellow-400) 85%, var(--bg-body))), + "contrast": var(--gray-900) + ), + "info": ( + "base": var(--cyan-500), + "fg": light-dark(var(--cyan-600), var(--cyan-400)), + "fg-emphasis": light-dark(var(--cyan-800), var(--cyan-300)), + "bg": var(--cyan-500), + "bg-subtle": light-dark(var(--cyan-100), var(--cyan-900)), + "bg-muted": light-dark(var(--cyan-200), var(--cyan-800)), + "border": light-dark(var(--cyan-300), var(--cyan-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--cyan-500) 50%, var(--bg-body)), color-mix(in oklch, var(--cyan-500) 75%, var(--bg-body))), + "contrast": var(--gray-900) + ), + "inverse": ( + "base": var(--gray-900), + "fg": light-dark(var(--gray-900), var(--gray-200)), + "fg-emphasis": light-dark(var(--gray-975), var(--white)), + "bg": light-dark(var(--gray-900), var(--gray-025)), + "bg-subtle": light-dark(var(--gray-100), var(--gray-900)), + "bg-muted": light-dark(var(--gray-200), var(--gray-300)), + "border": light-dark(var(--gray-400), var(--gray-100)), + "focus-ring": color-mix(in oklch, light-dark(var(--gray-900), var(--gray-100)) 50%, var(--bg-body)), + "contrast": light-dark(var(--white), var(--gray-900)) + ), + "secondary": ( + "base": var(--gray-200), + "fg": light-dark(var(--gray-600), var(--gray-400)), + "fg-emphasis": light-dark(var(--gray-800), var(--gray-200)), + "bg": light-dark(var(--gray-100), var(--gray-600)), + "bg-subtle": light-dark(var(--gray-050), var(--gray-800)), + "bg-muted": light-dark(var(--gray-100), var(--gray-700)), + "border": light-dark(var(--gray-300), var(--gray-600)), + "focus-ring": color-mix(in oklch, light-dark(var(--gray-500), var(--gray-300)) 50%, var(--bg-body)), + "contrast": light-dark(var(--gray-900), var(--white)) + ) +) !default; +// scss-docs-end theme-colors + +// mdo-do: consider using muted, subtle, ghost or something instead of linear scale? +$theme-bgs: ( + "body": light-dark(var(--white), var(--gray-975)), + "1": light-dark(var(--gray-025), var(--gray-950)), + "2": light-dark(var(--gray-050), var(--gray-900)), + "3": light-dark(var(--gray-100), var(--gray-800)), + "4": light-dark(var(--gray-200), var(--gray-700)), + "fg": var(--fg-body), + "white": var(--white), + "black": var(--black), + "transparent": transparent, + "inherit": inherit, +) !default; + +$theme-fgs: ( + "body": light-dark(var(--gray-900), var(--gray-050)), + "1": light-dark(var(--gray-800), var(--gray-200)), + "2": light-dark(var(--gray-700), var(--gray-300)), + "3": light-dark(var(--gray-600), var(--gray-500)), + "4": light-dark(var(--gray-500), var(--gray-600)), + "bg": var(--bg-body), + "white": var(--white), + "black": var(--black), + "inherit": inherit, +) !default; + +$theme-borders: ( + "bg": var(--bg-body), + "body": light-dark(var(--gray-300), var(--gray-800)), + "muted": light-dark(var(--gray-200), var(--gray-800)), + "subtle": light-dark(color-mix(in oklch, var(--gray-100), var(--gray-200)), var(--gray-900)), + "emphasized": light-dark(var(--gray-400), var(--gray-600)), + "white": var(--white), + "black": var(--black), +) !default; + +$util-opacity: ( + 10: .1, + 20: .2, + 30: .3, + 40: .4, + 50: .5, + 60: .6, + 70: .7, + 80: .8, + 90: .9, + 100: 1 +) !default; diff --git a/assets/stylesheets/bootstrap/_toasts.scss b/assets/stylesheets/bootstrap/_toasts.scss index 2ce378d5..6b6359ea 100644 --- a/assets/stylesheets/bootstrap/_toasts.scss +++ b/assets/stylesheets/bootstrap/_toasts.scss @@ -1,73 +1,99 @@ -.toast { - // scss-docs-start toast-css-vars - --#{$prefix}toast-zindex: #{$zindex-toast}; - --#{$prefix}toast-padding-x: #{$toast-padding-x}; - --#{$prefix}toast-padding-y: #{$toast-padding-y}; - --#{$prefix}toast-spacing: #{$toast-spacing}; - --#{$prefix}toast-max-width: #{$toast-max-width}; - @include rfs($toast-font-size, --#{$prefix}toast-font-size); - --#{$prefix}toast-color: #{$toast-color}; - --#{$prefix}toast-bg: #{$toast-background-color}; - --#{$prefix}toast-border-width: #{$toast-border-width}; - --#{$prefix}toast-border-color: #{$toast-border-color}; - --#{$prefix}toast-border-radius: #{$toast-border-radius}; - --#{$prefix}toast-box-shadow: #{$toast-box-shadow}; - --#{$prefix}toast-header-color: #{$toast-header-color}; - --#{$prefix}toast-header-bg: #{$toast-header-background-color}; - --#{$prefix}toast-header-border-color: #{$toast-header-border-color}; - // scss-docs-end toast-css-vars +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; - width: var(--#{$prefix}toast-max-width); - max-width: 100%; - @include font-size(var(--#{$prefix}toast-font-size)); - color: var(--#{$prefix}toast-color); - pointer-events: auto; - background-color: var(--#{$prefix}toast-bg); - background-clip: padding-box; - border: var(--#{$prefix}toast-border-width) solid var(--#{$prefix}toast-border-color); - box-shadow: var(--#{$prefix}toast-box-shadow); - @include border-radius(var(--#{$prefix}toast-border-radius)); +$toast-tokens: () !default; - &.showing { - opacity: 0; - } +// scss-docs-start toast-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$toast-tokens: defaults( + ( + --toast-zindex: #{$zindex-toast}, + --toast-padding-x: 1rem, + --toast-padding-y: .75rem, + --toast-spacing: #{$container-padding-x}, + --toast-max-width: 350px, + --toast-font-size: var(--font-size-sm), + --toast-color: null, + --toast-bg: var(--bg-body), + --toast-border-width: var(--border-width), + --toast-border-color: var(--border-color-translucent), + --toast-border-radius: null, + --toast-box-shadow: var(--box-shadow), + --toast-header-color: var(--fg-3), + --toast-header-bg: var(--bg-1), + --toast-header-border-color: var(--border-color-translucent), + ), + $toast-tokens +); +// stylelint-enable custom-property-no-missing-var-function +// scss-docs-end toast-tokens + +@layer components { + .toast { + @include tokens($toast-tokens); + + display: flex; + flex-direction: column; + width: var(--toast-max-width); + max-width: 100%; + overflow: hidden; + font-size: var(--toast-font-size); + color: var(--toast-color, var(--fg-body)); + pointer-events: auto; + background-color: var(--toast-bg); + background-clip: padding-box; + border: var(--toast-border-width) solid var(--theme-border, var(--toast-border-color)); + box-shadow: var(--toast-box-shadow); + @include border-radius(var(--toast-border-radius, var(--radius-7))); - &:not(.show) { - display: none; + &.showing { + opacity: 0; + } + + &:not(.show) { + display: none; + } } -} -.toast-container { - --#{$prefix}toast-zindex: #{$zindex-toast}; + .toast-container { + --toast-zindex: #{$zindex-toast}; - position: absolute; - z-index: var(--#{$prefix}toast-zindex); - width: max-content; - max-width: 100%; - pointer-events: none; + position: absolute; + z-index: var(--toast-zindex); + width: max-content; + max-width: 100%; + pointer-events: none; - > :not(:last-child) { - margin-bottom: var(--#{$prefix}toast-spacing); + > :not(:last-child) { + margin-bottom: var(--toast-spacing); + } } -} -.toast-header { - display: flex; - align-items: center; - padding: var(--#{$prefix}toast-padding-y) var(--#{$prefix}toast-padding-x); - color: var(--#{$prefix}toast-header-color); - background-color: var(--#{$prefix}toast-header-bg); - background-clip: padding-box; - border-bottom: var(--#{$prefix}toast-border-width) solid var(--#{$prefix}toast-header-border-color); - @include border-top-radius(calc(var(--#{$prefix}toast-border-radius) - var(--#{$prefix}toast-border-width))); + .toast-header { + display: flex; + align-items: center; + padding: var(--toast-padding-y) var(--toast-padding-x); + color: var(--theme-fg-emphasis, var(--toast-header-color)); + background-color: var(--theme-bg-subtle, var(--toast-header-bg)); + // background-clip: padding-box; + border-block-end: var(--toast-border-width, var(--border-width)) solid var(--theme-border, var(--toast-header-border-color, var(--border-color-translucent))); - .btn-close { - margin-right: calc(-.5 * var(--#{$prefix}toast-padding-x)); // stylelint-disable-line function-disallowed-list - margin-left: var(--#{$prefix}toast-padding-x); + .btn-close { + margin-inline-start: calc(.5 * var(--toast-padding-x)); + margin-inline-end: calc(-.25 * var(--toast-padding-x)); + color: inherit; + } } -} -.toast-body { - padding: var(--#{$prefix}toast-padding-x); - word-wrap: break-word; + .toast-translucent { + backdrop-filter: blur(5px) saturate(180%); + } + + .toast-body { + padding: var(--toast-padding-x); + word-wrap: break-word; + } } diff --git a/assets/stylesheets/bootstrap/_tooltip.scss b/assets/stylesheets/bootstrap/_tooltip.scss index 85de90f5..ccbb6bb0 100644 --- a/assets/stylesheets/bootstrap/_tooltip.scss +++ b/assets/stylesheets/bootstrap/_tooltip.scss @@ -1,119 +1,127 @@ -// Base class -.tooltip { - // scss-docs-start tooltip-css-vars - --#{$prefix}tooltip-zindex: #{$zindex-tooltip}; - --#{$prefix}tooltip-max-width: #{$tooltip-max-width}; - --#{$prefix}tooltip-padding-x: #{$tooltip-padding-x}; - --#{$prefix}tooltip-padding-y: #{$tooltip-padding-y}; - --#{$prefix}tooltip-margin: #{$tooltip-margin}; - @include rfs($tooltip-font-size, --#{$prefix}tooltip-font-size); - --#{$prefix}tooltip-color: #{$tooltip-color}; - --#{$prefix}tooltip-bg: #{$tooltip-bg}; - --#{$prefix}tooltip-border-radius: #{$tooltip-border-radius}; - --#{$prefix}tooltip-opacity: #{$tooltip-opacity}; - --#{$prefix}tooltip-arrow-width: #{$tooltip-arrow-width}; - --#{$prefix}tooltip-arrow-height: #{$tooltip-arrow-height}; - // scss-docs-end tooltip-css-vars - - z-index: var(--#{$prefix}tooltip-zindex); - display: block; - margin: var(--#{$prefix}tooltip-margin); - @include deprecate("`$tooltip-margin`", "v5", "v5.x", true); - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - @include reset-text(); - @include font-size(var(--#{$prefix}tooltip-font-size)); - // Allow breaking very long words so they don't overflow the tooltip's bounds - word-wrap: break-word; - opacity: 0; - - &.show { opacity: var(--#{$prefix}tooltip-opacity); } - - .tooltip-arrow { +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/reset-text" as *; +@use "mixins/tokens" as *; + +$tooltip-tokens: () !default; + +// scss-docs-start tooltip-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$tooltip-tokens: defaults( + ( + --tooltip-zindex: #{$zindex-tooltip}, + --tooltip-max-width: 200px, + --tooltip-padding-x: var(--spacer-3), + --tooltip-padding-y: calc(var(--spacer) * .375), + --tooltip-font-size: var(--font-size-sm), + --tooltip-color: var(--bg-body), + --tooltip-bg: var(--fg-body), + --tooltip-border-radius: var(--radius-5), + --tooltip-opacity: .95, + --tooltip-arrow-width: .8rem, + --tooltip-arrow-height: .4rem, + ), + $tooltip-tokens +); +// scss-docs-end tooltip-tokens + +@layer components { + .tooltip { + @include tokens($tooltip-tokens); + + z-index: var(--tooltip-zindex); display: block; - width: var(--#{$prefix}tooltip-arrow-width); - height: var(--#{$prefix}tooltip-arrow-height); - - &::before { - position: absolute; - content: ""; - border-color: transparent; - border-style: solid; + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + @include reset-text(); + font-size: var(--tooltip-font-size); + // Allow breaking very long words so they don't overflow the tooltip's bounds + word-wrap: break-word; + opacity: 0; + + &.show { opacity: var(--tooltip-opacity); } + + .tooltip-arrow { + display: block; + width: var(--tooltip-arrow-width); + height: var(--tooltip-arrow-height); + + &::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; + } } } -} -.bs-tooltip-top .tooltip-arrow { - bottom: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list + .bs-tooltip-top .tooltip-arrow { + bottom: calc(-1 * var(--tooltip-arrow-height)); - &::before { - top: -1px; - border-width: var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - border-top-color: var(--#{$prefix}tooltip-bg); + &::before { + top: -1px; + border-width: var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) * .5) 0; + border-block-start-color: var(--tooltip-bg); + } } -} -/* rtl:begin:ignore */ -.bs-tooltip-end .tooltip-arrow { - left: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}tooltip-arrow-height); - height: var(--#{$prefix}tooltip-arrow-width); + .bs-tooltip-end .tooltip-arrow { + left: calc(-1 * var(--tooltip-arrow-height)); + width: var(--tooltip-arrow-height); + height: var(--tooltip-arrow-width); - &::before { - right: -1px; - border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - border-right-color: var(--#{$prefix}tooltip-bg); + &::before { + right: -1px; + border-width: calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) * .5) 0; + border-inline-end-color: var(--tooltip-bg); + } } -} - -/* rtl:end:ignore */ -.bs-tooltip-bottom .tooltip-arrow { - top: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list + .bs-tooltip-bottom .tooltip-arrow { + top: calc(-1 * var(--tooltip-arrow-height)); - &::before { - bottom: -1px; - border-width: 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list - border-bottom-color: var(--#{$prefix}tooltip-bg); + &::before { + bottom: -1px; + border-width: 0 calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height); + border-block-end-color: var(--tooltip-bg); + } } -} -/* rtl:begin:ignore */ -.bs-tooltip-start .tooltip-arrow { - right: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}tooltip-arrow-height); - height: var(--#{$prefix}tooltip-arrow-width); + .bs-tooltip-start .tooltip-arrow { + right: calc(-1 * var(--tooltip-arrow-height)); + width: var(--tooltip-arrow-height); + height: var(--tooltip-arrow-width); - &::before { - left: -1px; - border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list - border-left-color: var(--#{$prefix}tooltip-bg); + &::before { + left: -1px; + border-width: calc(var(--tooltip-arrow-width) * .5) 0 calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height); + border-inline-start-color: var(--tooltip-bg); + } } -} - -/* rtl:end:ignore */ -.bs-tooltip-auto { - &[data-popper-placement^="top"] { - @extend .bs-tooltip-top; - } - &[data-popper-placement^="right"] { - @extend .bs-tooltip-end; - } - &[data-popper-placement^="bottom"] { - @extend .bs-tooltip-bottom; - } - &[data-popper-placement^="left"] { - @extend .bs-tooltip-start; + .bs-tooltip-auto { + &[data-bs-placement^="top"] { + @extend .bs-tooltip-top; + } + &[data-bs-placement^="right"] { + @extend .bs-tooltip-end; + } + &[data-bs-placement^="bottom"] { + @extend .bs-tooltip-bottom; + } + &[data-bs-placement^="left"] { + @extend .bs-tooltip-start; + } } -} -// Wrapper for the tooltip content -.tooltip-inner { - max-width: var(--#{$prefix}tooltip-max-width); - padding: var(--#{$prefix}tooltip-padding-y) var(--#{$prefix}tooltip-padding-x); - color: var(--#{$prefix}tooltip-color); - text-align: center; - background-color: var(--#{$prefix}tooltip-bg); - @include border-radius(var(--#{$prefix}tooltip-border-radius)); + // Wrapper for the tooltip content + .tooltip-inner { + max-width: var(--tooltip-max-width); + padding: var(--tooltip-padding-y) var(--tooltip-padding-x); + color: var(--tooltip-color); + text-align: center; + background-color: var(--tooltip-bg); + @include border-radius(var(--tooltip-border-radius)); + } } diff --git a/assets/stylesheets/bootstrap/_transitions.scss b/assets/stylesheets/bootstrap/_transitions.scss index bfb26aa8..3a7f936b 100644 --- a/assets/stylesheets/bootstrap/_transitions.scss +++ b/assets/stylesheets/bootstrap/_transitions.scss @@ -1,3 +1,6 @@ +@use "config" as *; +@use "mixins/transition" as *; + .fade { @include transition($transition-fade); diff --git a/assets/stylesheets/bootstrap/_type.scss b/assets/stylesheets/bootstrap/_type.scss deleted file mode 100644 index 6961390f..00000000 --- a/assets/stylesheets/bootstrap/_type.scss +++ /dev/null @@ -1,106 +0,0 @@ -// -// Headings -// -.h1 { - @extend h1; -} - -.h2 { - @extend h2; -} - -.h3 { - @extend h3; -} - -.h4 { - @extend h4; -} - -.h5 { - @extend h5; -} - -.h6 { - @extend h6; -} - - -.lead { - @include font-size($lead-font-size); - font-weight: $lead-font-weight; -} - -// Type display classes -@each $display, $font-size in $display-font-sizes { - .display-#{$display} { - font-family: $display-font-family; - font-style: $display-font-style; - font-weight: $display-font-weight; - line-height: $display-line-height; - @include font-size($font-size); - } -} - -// -// Emphasis -// -.small { - @extend small; -} - -.mark { - @extend mark; -} - -// -// Lists -// - -.list-unstyled { - @include list-unstyled(); -} - -// Inline turns list items into inline-block -.list-inline { - @include list-unstyled(); -} -.list-inline-item { - display: inline-block; - - &:not(:last-child) { - margin-right: $list-inline-padding; - } -} - - -// -// Misc -// - -// Builds on `abbr` -.initialism { - @include font-size($initialism-font-size); - text-transform: uppercase; -} - -// Blockquotes -.blockquote { - margin-bottom: $blockquote-margin-y; - @include font-size($blockquote-font-size); - - > :last-child { - margin-bottom: 0; - } -} - -.blockquote-footer { - margin-top: -$blockquote-margin-y; - margin-bottom: $blockquote-margin-y; - @include font-size($blockquote-footer-font-size); - color: $blockquote-footer-color; - - &::before { - content: "\2014\00A0"; // em dash, nbsp - } -} diff --git a/assets/stylesheets/bootstrap/_utilities.scss b/assets/stylesheets/bootstrap/_utilities.scss index 696f906e..3bd09620 100644 --- a/assets/stylesheets/bootstrap/_utilities.scss +++ b/assets/stylesheets/bootstrap/_utilities.scss @@ -1,8 +1,17 @@ -// Utilities +@use "sass:map"; +@use "config" as *; +@use "functions" as *; +@use "theme" as *; + +// add: +// - double check css grid helpers +// +// update: +// - focus-ring if needed $utilities: () !default; // stylelint-disable-next-line scss/dollar-variable-default -$utilities: map-merge( +$utilities: map.merge( ( // scss-docs-start utils-vertical-align "align": ( @@ -11,13 +20,27 @@ $utilities: map-merge( values: baseline top middle bottom text-bottom text-top ), // scss-docs-end utils-vertical-align + // scss-docs-start utils-aspect-ratio + "aspect-ratio-attr": ( + selector: "attr-includes", + class: "ratio-", + property: aspect-ratio, + values: var(--ratio), + ), + "aspect-ratio": ( + // property: aspect-ratio, + property: --ratio, + class: ratio, + values: $aspect-ratios + ), + // scss-docs-end utils-aspect-ratio // scss-docs-start utils-float "float": ( - responsive: true, property: float, + responsive: true, values: ( - start: left, - end: right, + start: inline-start, + end: inline-end, none: none, ) ), @@ -63,13 +86,20 @@ $utilities: map-merge( values: auto hidden visible scroll, ), // scss-docs-end utils-overflow + "container": ( + property: container-type, + class: contains, + values: ( + "inline": inline-size, + "size": size, + ) + ), // scss-docs-start utils-display "display": ( responsive: true, - print: true, property: display, class: d, - values: inline inline-block block grid inline-grid table table-row table-cell flex inline-flex none + values: inline inline-block block grid inline-grid table table-row table-cell flex inline-flex contents flow-root none ), // scss-docs-end utils-display // scss-docs-start utils-shadow @@ -77,21 +107,14 @@ $utilities: map-merge( property: box-shadow, class: shadow, values: ( - null: var(--#{$prefix}box-shadow), - sm: var(--#{$prefix}box-shadow-sm), - lg: var(--#{$prefix}box-shadow-lg), + null: var(--box-shadow), + xs: var(--box-shadow-xs), + sm: var(--box-shadow-sm), + lg: var(--box-shadow-lg), none: none, ) ), // scss-docs-end utils-shadow - // scss-docs-start utils-focus-ring - "focus-ring": ( - css-var: true, - css-variable-name: focus-ring-color, - class: focus-ring, - values: map-loop($theme-colors-rgb, rgba-css-var, "$key", "focus-ring") - ), - // scss-docs-end utils-focus-ring // scss-docs-start utils-position "position": ( property: position, @@ -106,12 +129,12 @@ $utilities: map-merge( values: $position-values ), "start": ( - property: left, + property: inset-inline-start, class: start, values: $position-values ), "end": ( - property: right, + property: inset-inline-end, class: end, values: $position-values ), @@ -129,52 +152,75 @@ $utilities: map-merge( "border": ( property: border, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-top": ( - property: border-top, + class: border-top, + property: border-block-start, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-end": ( - property: border-right, + property: border-inline-end, class: border-end, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-bottom": ( - property: border-bottom, + property: border-block-end, + class: border-bottom, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-start": ( - property: border-left, + property: border-inline-start, class: border-start, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "border-y": ( + class: border-y, + property: border-block, + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "border-x": ( + class: border-x, + property: border-inline, + values: ( + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), + // scss-docs-end utils-borders + // scss-docs-start utils-border-color "border-color": ( - property: border-color, - class: border, - local-vars: ( - "border-opacity": 1 + property: ( + "--border-color": null, + "border-color": var(--border-color) ), - values: $utilities-border-colors - ), - "subtle-border-color": ( - property: border-color, class: border, - values: $utilities-border-subtle + values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-borders, "border")), + ), + "border-color-subtle": ( + property: ( + "--border-color": null, + "border-color": var(--border-color) + ), + class: border-subtle, + values: theme-color-refs("border"), ), "border-width": ( property: border-width, @@ -182,35 +228,43 @@ $utilities: map-merge( values: $border-widths ), "border-opacity": ( - css-var: true, - class: border-opacity, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + class: border, + property: border-color, + values: theme-opacity-values(--border-color) ), - // scss-docs-end utils-borders + // scss-docs-end utils-border-color // Sizing utilities - // scss-docs-start utils-sizing + // scss-docs-start utils-width "width": ( property: width, class: w, - values: ( - 25: 25%, - 50: 50%, - 75: 75%, - 100: 100%, - auto: auto + values: map.merge( + $sizes, + ( + 25: 25%, + 50: 50%, + 75: 75%, + 100: 100%, + auto: auto, + min: min-content, + max: max-content, + fit: fit-content, + ) ) ), "max-width": ( property: max-width, - class: mw, + class: max-w, values: (100: 100%) ), + "min-width": ( + property: min-width, + class: min-w, + values: ( + 0: 0, + 100: 100% + ) + ), "viewport-width": ( property: width, class: vw, @@ -221,6 +275,8 @@ $utilities: map-merge( class: min-vw, values: (100: 100vw) ), + // scss-docs-end utils-width + // scss-docs-start utils-height "height": ( property: height, class: h, @@ -229,14 +285,25 @@ $utilities: map-merge( 50: 50%, 75: 75%, 100: 100%, - auto: auto + auto: auto, + min: min-content, + max: max-content, + fit: fit-content, ) ), "max-height": ( property: max-height, - class: mh, + class: max-h, values: (100: 100%) ), + "min-height": ( + property: min-height, + class: min-h, + values: ( + 0: 0, + 100: 100%, + ), + ), "viewport-height": ( property: height, class: vh, @@ -247,7 +314,7 @@ $utilities: map-merge( class: min-vh, values: (100: 100vh) ), - // scss-docs-end utils-sizing + // scss-docs-end utils-height // Flex utilities // scss-docs-start utils-flex "flex": ( @@ -297,6 +364,25 @@ $utilities: map-merge( evenly: space-evenly, ) ), + "justify-items": ( + responsive: true, + property: justify-items, + values: ( + start: start, + end: end, + center: center, + stretch: stretch, + ) + ), + "justify-self": ( + responsive: true, + property: justify-self, + values: ( + start: start, + end: end, + center: center, + ) + ), "align-items": ( responsive: true, property: align-items, @@ -332,6 +418,43 @@ $utilities: map-merge( stretch: stretch, ) ), + "place-items": ( + responsive: true, + property: place-items, + values: ( + start: start, + end: end, + center: center, + stretch: stretch, + ) + ), + "grid-column-counts": ( + responsive: true, + // property: --columns, + property: grid-template-columns, + class: grid-cols, + values: ( + "1": 1fr, + "2": repeat(2, 1fr), + "3": repeat(3, 1fr), + "4": repeat(4, 1fr), + "6": repeat(6, 1fr), + ) + ), + "grid-columns": ( + responsive: true, + property: grid-column, + class: grid-cols, + values: ( + fill: #{"1 / -1"}, + ) + ), + "grid-auto-flow": ( + responsive: true, + property: grid-auto-flow, + class: grid-auto-flow, + values: row column dense + ), "order": ( responsive: true, property: order, @@ -349,92 +472,52 @@ $utilities: map-merge( // scss-docs-end utils-flex // Margin utilities // scss-docs-start utils-spacing + // scss-docs-start utils-margin "margin": ( responsive: true, property: margin, class: m, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-x": ( responsive: true, - property: margin-right margin-left, + property: margin-inline, class: mx, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-y": ( responsive: true, - property: margin-top margin-bottom, + property: margin-block, class: my, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-top": ( responsive: true, - property: margin-top, + property: margin-block-start, class: mt, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-end": ( responsive: true, - property: margin-right, + property: margin-inline-end, class: me, - values: map-merge($spacers, (auto: auto)) + values: map-merge-multiple($spacers, $negative-spacers, (auto: auto)) ), "margin-bottom": ( responsive: true, - property: margin-bottom, + property: margin-block-end, class: mb, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-start": ( responsive: true, - property: margin-left, + property: margin-inline-start, class: ms, - values: map-merge($spacers, (auto: auto)) - ), - // Negative margin utilities - "negative-margin": ( - responsive: true, - property: margin, - class: m, - values: $negative-spacers - ), - "negative-margin-x": ( - responsive: true, - property: margin-right margin-left, - class: mx, - values: $negative-spacers - ), - "negative-margin-y": ( - responsive: true, - property: margin-top margin-bottom, - class: my, - values: $negative-spacers - ), - "negative-margin-top": ( - responsive: true, - property: margin-top, - class: mt, - values: $negative-spacers - ), - "negative-margin-end": ( - responsive: true, - property: margin-right, - class: me, - values: $negative-spacers - ), - "negative-margin-bottom": ( - responsive: true, - property: margin-bottom, - class: mb, - values: $negative-spacers - ), - "negative-margin-start": ( - responsive: true, - property: margin-left, - class: ms, - values: $negative-spacers + values: map-merge-multiple($spacers, $negative-spacers, (auto: auto)) ), + // scss-docs-end utils-margin // Padding utilities + // scss-docs-start utils-padding "padding": ( responsive: true, property: padding, @@ -443,41 +526,43 @@ $utilities: map-merge( ), "padding-x": ( responsive: true, - property: padding-right padding-left, + property: padding-inline, class: px, values: $spacers ), "padding-y": ( responsive: true, - property: padding-top padding-bottom, + property: padding-block, class: py, values: $spacers ), "padding-top": ( responsive: true, - property: padding-top, + property: padding-block-start, class: pt, values: $spacers ), "padding-end": ( responsive: true, - property: padding-right, + property: padding-inline-end, class: pe, values: $spacers ), "padding-bottom": ( responsive: true, - property: padding-bottom, + property: padding-block-end, class: pb, values: $spacers ), "padding-start": ( responsive: true, - property: padding-left, + property: padding-inline-start, class: ps, values: $spacers ), + // scss-docs-end utils-padding // Gap utility + // scss-docs-start utils-gap "gap": ( responsive: true, property: gap, @@ -496,20 +581,72 @@ $utilities: map-merge( class: column-gap, values: $spacers ), + // scss-docs-end utils-gap // scss-docs-end utils-spacing + // scss-docs-start utils-space + "space-x": ( + responsive: true, + property: margin-inline-end, + class: space-x, + child-selector: "> :not(:last-child)", + values: $spacers + ), + "space-y": ( + responsive: true, + property: margin-block-end, + class: space-y, + child-selector: "> :not(:last-child)", + values: $spacers + ), + // scss-docs-end utils-space + // scss-docs-start utils-divide + "divide-x": ( + responsive: true, + property: border-inline-start, + class: divide-x, + child-selector: "> :not(:first-child)", + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "divide-y": ( + responsive: true, + property: border-block-start, + class: divide-y, + child-selector: "> :not(:first-child)", + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + // scss-docs-end utils-divide // Text - // scss-docs-start utils-text + // scss-docs-start utils-font-family "font-family": ( property: font-family, class: font, - values: (monospace: var(--#{$prefix}font-monospace)) + values: ( + "monospace": var(--font-mono), + "body": var(--body-font-family), + ) ), + // scss-docs-end utils-font-family + // scss-docs-start utils-font-size "font-size": ( - rfs: true, property: font-size, class: fs, + values: map-get-nested($font-sizes, "font-size") + ), + "text-size": ( + property: ( + "font-size": 1rem, + "line-height": 1.5 + ), + class: text, values: $font-sizes ), + // scss-docs-end utils-font-size "font-style": ( property: font-style, class: fst, @@ -543,8 +680,8 @@ $utilities: map-merge( property: text-align, class: text, values: ( - start: left, - end: right, + start: start, + end: end, center: center, ) ), @@ -552,78 +689,75 @@ $utilities: map-merge( property: text-decoration, values: none underline line-through ), + // scss-docs-start utils-text-transform "text-transform": ( property: text-transform, class: text, values: lowercase uppercase capitalize ), - "white-space": ( - property: white-space, + // scss-docs-end utils-text-transform + // scss-docs-start utils-text-wrap + "text-wrap": ( + property: text-wrap, class: text, - values: ( - wrap: normal, - nowrap: nowrap, - ) + values: wrap nowrap balance pretty, ), + // scss-docs-end utils-text-wrap + // scss-docs-start utils-text-break "word-wrap": ( property: word-wrap word-break, class: text, values: (break: break-word), - rtl: false ), + // scss-docs-end utils-text-break // scss-docs-end utils-text // scss-docs-start utils-color - "color": ( - property: color, - class: text, - local-vars: ( - "text-opacity": 1 + "fg": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg, + values: map.merge( + map.merge(theme-color-refs("fg"), theme-token-refs($theme-fgs, "fg")), + (reset: inherit) ), - values: map-merge( - $utilities-text-colors, - ( - "muted": var(--#{$prefix}secondary-color), // deprecated - "black-50": rgba($black, .5), // deprecated - "white-50": rgba($white, .5), // deprecated - "body-secondary": var(--#{$prefix}secondary-color), - "body-tertiary": var(--#{$prefix}tertiary-color), - "body-emphasis": var(--#{$prefix}emphasis-color), - "reset": inherit, - ) - ) ), - "text-opacity": ( - css-var: true, - class: text-opacity, - values: ( - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + "fg-emphasis": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg-emphasis, + values: theme-color-refs("fg-emphasis"), ), - "text-color": ( + "fg-contrast": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg-contrast, + values: theme-color-refs("contrast"), + ), + "fg-opacity": ( + class: fg, property: color, - class: text, - values: $utilities-text-emphasis-colors + values: theme-opacity-values(--fg) ), // scss-docs-end utils-color // scss-docs-start utils-links "link-opacity": ( - css-var: true, - class: link-opacity, + property: color, + // css-var: true, + class: link, state: hover, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + values: theme-opacity-values(--link-color) ), - "link-offset": ( + // scss-docs-end utils-links + // scss-docs-start utils-underline + "underline-offset": ( property: text-underline-offset, - class: link-offset, + class: underline-offset, state: hover, values: ( 1: .125em, @@ -631,75 +765,102 @@ $utilities: map-merge( 3: .375em, ) ), - "link-underline": ( + "underline-color": ( property: text-decoration-color, - class: link-underline, - local-vars: ( - "link-underline-opacity": 1 - ), - values: map-merge( - $utilities-links-underline, - ( - null: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-underline-opacity, 1)), - ) - ) + class: underline, + values: theme-color-values("fg"), + ), + "underline-opacity": ( + property: text-decoration-color, + class: underline, + state: hover, + values: theme-opacity-values(--link-color) ), - "link-underline-opacity": ( - css-var: true, - class: link-underline-opacity, + "underline-thickness": ( + property: text-decoration-thickness, + class: underline-thickness, state: hover, values: ( - 0: 0, - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ), + 1: 1px, + 2: 2px, + 3: 3px, + 4: 4px, + 5: 5px, + ) ), - // scss-docs-end utils-links + // scss-docs-end utils-underline // scss-docs-start utils-bg-color - "background-color": ( - property: background-color, + "bg-color": ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), class: bg, - local-vars: ( - "bg-opacity": 1 + values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-bgs, "bg")), + ), + "bg-color-subtle": ( + property: ( + "--bg": null, + "background-color": var(--bg) ), - values: map-merge( - $utilities-bg-colors, - ( - "transparent": transparent, - "body-secondary": rgba(var(--#{$prefix}secondary-bg-rgb), var(--#{$prefix}bg-opacity)), - "body-tertiary": rgba(var(--#{$prefix}tertiary-bg-rgb), var(--#{$prefix}bg-opacity)), - ) - ) + class: bg-subtle, + values: theme-color-refs("bg-subtle"), ), - "bg-opacity": ( - css-var: true, - class: bg-opacity, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + "bg-color-muted": ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), + class: bg-muted, + values: theme-color-refs("bg-muted"), ), - "subtle-background-color": ( - property: background-color, + "bg-opacity": ( class: bg, - values: $utilities-bg-subtle + property: background-color, + values: theme-opacity-values(--bg) ), // scss-docs-end utils-bg-color + // scss-docs-start utils-theme + // Theme style utilities - pair with .theme-{color} to apply coordinated bg + text colors + "theme-contrast": ( + property: ( + "background-color": var(--theme-bg), + "color": var(--theme-contrast) + ), + class: theme-contrast, + values: (null: null) + ), + "theme-subtle": ( + property: ( + "background-color": var(--theme-bg-subtle), + "color": var(--theme-fg) + ), + class: theme-subtle, + values: (null: null) + ), + "theme-muted": ( + property: ( + "background-color": var(--theme-bg-muted), + "color": var(--theme-fg-emphasis) + ), + class: theme-muted, + values: (null: null) + ), + "theme-border": ( + property: border, + class: theme-border, + values: (null: var(--border-width) solid var(--theme-border)) + ), + // scss-docs-end utils-theme "gradient": ( property: background-image, class: bg, - values: (gradient: var(--#{$prefix}gradient)) + values: (gradient: var(--gradient)) ), // scss-docs-start utils-interaction "user-select": ( property: user-select, - values: all auto none + values: all auto text none ), "pointer-events": ( property: pointer-events, @@ -708,79 +869,79 @@ $utilities: map-merge( ), // scss-docs-end utils-interaction // scss-docs-start utils-border-radius - "rounded": ( - property: border-radius, + "border-radius": ( + property: ( + "--rounded-size": null, + "border-radius": var(--rounded-size) + ), class: rounded, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: map.get($radii, 5), + circle: 50%, + pill: var(--radius-pill) + ) + ) + ), + "rounded-size": ( + property: --rounded-size, + class: rounded-size, + values: map.merge( + $radii, + ( + null: map.get($radii, 5), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-top": ( - property: border-top-left-radius border-top-right-radius, + property: border-start-start-radius border-start-end-radius, class: rounded-top, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-end": ( - property: border-top-right-radius border-bottom-right-radius, + property: border-start-end-radius border-end-end-radius, class: rounded-end, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-bottom": ( - property: border-bottom-right-radius border-bottom-left-radius, + property: border-end-start-radius border-end-end-radius, class: rounded-bottom, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-start": ( - property: border-bottom-left-radius border-top-left-radius, + property: border-start-start-radius border-end-start-radius, class: rounded-start, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), // scss-docs-end utils-border-radius @@ -799,7 +960,7 @@ $utilities: map-merge( property: z-index, class: z, values: $zindex-levels, - ) + ), // scss-docs-end utils-zindex ), $utilities diff --git a/assets/stylesheets/bootstrap/_variables-dark.scss b/assets/stylesheets/bootstrap/_variables-dark.scss deleted file mode 100644 index 260f6dcc..00000000 --- a/assets/stylesheets/bootstrap/_variables-dark.scss +++ /dev/null @@ -1,102 +0,0 @@ -// Dark color mode variables -// -// Custom variables for the `[data-bs-theme="dark"]` theme. Use this as a starting point for your own custom color modes by creating a new theme-specific file like `_variables-dark.scss` and adding the variables you need. - -// -// Global colors -// - -// scss-docs-start sass-dark-mode-vars -// scss-docs-start theme-text-dark-variables -$primary-text-emphasis-dark: tint-color($primary, 40%) !default; -$secondary-text-emphasis-dark: tint-color($secondary, 40%) !default; -$success-text-emphasis-dark: tint-color($success, 40%) !default; -$info-text-emphasis-dark: tint-color($info, 40%) !default; -$warning-text-emphasis-dark: tint-color($warning, 40%) !default; -$danger-text-emphasis-dark: tint-color($danger, 40%) !default; -$light-text-emphasis-dark: $gray-100 !default; -$dark-text-emphasis-dark: $gray-300 !default; -// scss-docs-end theme-text-dark-variables - -// scss-docs-start theme-bg-subtle-dark-variables -$primary-bg-subtle-dark: shade-color($primary, 80%) !default; -$secondary-bg-subtle-dark: shade-color($secondary, 80%) !default; -$success-bg-subtle-dark: shade-color($success, 80%) !default; -$info-bg-subtle-dark: shade-color($info, 80%) !default; -$warning-bg-subtle-dark: shade-color($warning, 80%) !default; -$danger-bg-subtle-dark: shade-color($danger, 80%) !default; -$light-bg-subtle-dark: $gray-800 !default; -$dark-bg-subtle-dark: mix($gray-800, $black) !default; -// scss-docs-end theme-bg-subtle-dark-variables - -// scss-docs-start theme-border-subtle-dark-variables -$primary-border-subtle-dark: shade-color($primary, 40%) !default; -$secondary-border-subtle-dark: shade-color($secondary, 40%) !default; -$success-border-subtle-dark: shade-color($success, 40%) !default; -$info-border-subtle-dark: shade-color($info, 40%) !default; -$warning-border-subtle-dark: shade-color($warning, 40%) !default; -$danger-border-subtle-dark: shade-color($danger, 40%) !default; -$light-border-subtle-dark: $gray-700 !default; -$dark-border-subtle-dark: $gray-800 !default; -// scss-docs-end theme-border-subtle-dark-variables - -$body-color-dark: $gray-300 !default; -$body-bg-dark: $gray-900 !default; -$body-secondary-color-dark: rgba($body-color-dark, .75) !default; -$body-secondary-bg-dark: $gray-800 !default; -$body-tertiary-color-dark: rgba($body-color-dark, .5) !default; -$body-tertiary-bg-dark: mix($gray-800, $gray-900, 50%) !default; -$body-emphasis-color-dark: $white !default; -$border-color-dark: $gray-700 !default; -$border-color-translucent-dark: rgba($white, .15) !default; -$headings-color-dark: inherit !default; -$link-color-dark: tint-color($primary, 40%) !default; -$link-hover-color-dark: shift-color($link-color-dark, -$link-shade-percentage) !default; -$code-color-dark: tint-color($code-color, 40%) !default; -$mark-color-dark: $body-color-dark !default; -$mark-bg-dark: $yellow-800 !default; - - -// -// Forms -// - -$form-select-indicator-color-dark: $body-color-dark !default; -$form-select-indicator-dark: url("data:image/svg+xml,") !default; - -$form-switch-color-dark: rgba($white, .25) !default; -$form-switch-bg-image-dark: url("data:image/svg+xml,") !default; - -// scss-docs-start form-validation-colors-dark -$form-valid-color-dark: $green-300 !default; -$form-valid-border-color-dark: $green-300 !default; -$form-invalid-color-dark: $red-300 !default; -$form-invalid-border-color-dark: $red-300 !default; -// scss-docs-end form-validation-colors-dark - - -// -// Accordion -// - -$accordion-icon-color-dark: $primary-text-emphasis-dark !default; -$accordion-icon-active-color-dark: $primary-text-emphasis-dark !default; - -$accordion-button-icon-dark: url("data:image/svg+xml,") !default; -$accordion-button-active-icon-dark: url("data:image/svg+xml,") !default; -// scss-docs-end sass-dark-mode-vars - - -// -// Carousel -// - -$carousel-indicator-active-bg-dark: $carousel-dark-indicator-active-bg !default; -$carousel-caption-color-dark: $carousel-dark-caption-color !default; -$carousel-control-icon-filter-dark: $carousel-dark-control-icon-filter !default; - -// -// Close button -// - -$btn-close-filter-dark: $btn-close-white-filter !default; diff --git a/assets/stylesheets/bootstrap/_variables.scss b/assets/stylesheets/bootstrap/_variables.scss deleted file mode 100644 index 1ffa7e74..00000000 --- a/assets/stylesheets/bootstrap/_variables.scss +++ /dev/null @@ -1,1753 +0,0 @@ -// Variables -// -// Variables should follow the `$component-state-property-size` formula for -// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. - -// Color system - -// scss-docs-start gray-color-variables -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #6c757d !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; -// scss-docs-end gray-color-variables - -// fusv-disable -// scss-docs-start gray-colors-map -$grays: ( - "100": $gray-100, - "200": $gray-200, - "300": $gray-300, - "400": $gray-400, - "500": $gray-500, - "600": $gray-600, - "700": $gray-700, - "800": $gray-800, - "900": $gray-900 -) !default; -// scss-docs-end gray-colors-map -// fusv-enable - -// scss-docs-start color-variables -$blue: #0d6efd !default; -$indigo: #6610f2 !default; -$purple: #6f42c1 !default; -$pink: #d63384 !default; -$red: #dc3545 !default; -$orange: #fd7e14 !default; -$yellow: #ffc107 !default; -$green: #198754 !default; -$teal: #20c997 !default; -$cyan: #0dcaf0 !default; -// scss-docs-end color-variables - -// scss-docs-start colors-map -$colors: ( - "blue": $blue, - "indigo": $indigo, - "purple": $purple, - "pink": $pink, - "red": $red, - "orange": $orange, - "yellow": $yellow, - "green": $green, - "teal": $teal, - "cyan": $cyan, - "black": $black, - "white": $white, - "gray": $gray-600, - "gray-dark": $gray-800 -) !default; -// scss-docs-end colors-map - -// The contrast ratio to reach against white, to determine if color changes from "light" to "dark". Acceptable values for WCAG 2.2 are 3, 4.5 and 7. -// See https://www.w3.org/TR/WCAG/#contrast-minimum -$min-contrast-ratio: 4.5 !default; - -// Customize the light and dark text colors for use in our color contrast function. -$color-contrast-dark: $black !default; -$color-contrast-light: $white !default; - -// fusv-disable -$blue-100: tint-color($blue, 80%) !default; -$blue-200: tint-color($blue, 60%) !default; -$blue-300: tint-color($blue, 40%) !default; -$blue-400: tint-color($blue, 20%) !default; -$blue-500: $blue !default; -$blue-600: shade-color($blue, 20%) !default; -$blue-700: shade-color($blue, 40%) !default; -$blue-800: shade-color($blue, 60%) !default; -$blue-900: shade-color($blue, 80%) !default; - -$indigo-100: tint-color($indigo, 80%) !default; -$indigo-200: tint-color($indigo, 60%) !default; -$indigo-300: tint-color($indigo, 40%) !default; -$indigo-400: tint-color($indigo, 20%) !default; -$indigo-500: $indigo !default; -$indigo-600: shade-color($indigo, 20%) !default; -$indigo-700: shade-color($indigo, 40%) !default; -$indigo-800: shade-color($indigo, 60%) !default; -$indigo-900: shade-color($indigo, 80%) !default; - -$purple-100: tint-color($purple, 80%) !default; -$purple-200: tint-color($purple, 60%) !default; -$purple-300: tint-color($purple, 40%) !default; -$purple-400: tint-color($purple, 20%) !default; -$purple-500: $purple !default; -$purple-600: shade-color($purple, 20%) !default; -$purple-700: shade-color($purple, 40%) !default; -$purple-800: shade-color($purple, 60%) !default; -$purple-900: shade-color($purple, 80%) !default; - -$pink-100: tint-color($pink, 80%) !default; -$pink-200: tint-color($pink, 60%) !default; -$pink-300: tint-color($pink, 40%) !default; -$pink-400: tint-color($pink, 20%) !default; -$pink-500: $pink !default; -$pink-600: shade-color($pink, 20%) !default; -$pink-700: shade-color($pink, 40%) !default; -$pink-800: shade-color($pink, 60%) !default; -$pink-900: shade-color($pink, 80%) !default; - -$red-100: tint-color($red, 80%) !default; -$red-200: tint-color($red, 60%) !default; -$red-300: tint-color($red, 40%) !default; -$red-400: tint-color($red, 20%) !default; -$red-500: $red !default; -$red-600: shade-color($red, 20%) !default; -$red-700: shade-color($red, 40%) !default; -$red-800: shade-color($red, 60%) !default; -$red-900: shade-color($red, 80%) !default; - -$orange-100: tint-color($orange, 80%) !default; -$orange-200: tint-color($orange, 60%) !default; -$orange-300: tint-color($orange, 40%) !default; -$orange-400: tint-color($orange, 20%) !default; -$orange-500: $orange !default; -$orange-600: shade-color($orange, 20%) !default; -$orange-700: shade-color($orange, 40%) !default; -$orange-800: shade-color($orange, 60%) !default; -$orange-900: shade-color($orange, 80%) !default; - -$yellow-100: tint-color($yellow, 80%) !default; -$yellow-200: tint-color($yellow, 60%) !default; -$yellow-300: tint-color($yellow, 40%) !default; -$yellow-400: tint-color($yellow, 20%) !default; -$yellow-500: $yellow !default; -$yellow-600: shade-color($yellow, 20%) !default; -$yellow-700: shade-color($yellow, 40%) !default; -$yellow-800: shade-color($yellow, 60%) !default; -$yellow-900: shade-color($yellow, 80%) !default; - -$green-100: tint-color($green, 80%) !default; -$green-200: tint-color($green, 60%) !default; -$green-300: tint-color($green, 40%) !default; -$green-400: tint-color($green, 20%) !default; -$green-500: $green !default; -$green-600: shade-color($green, 20%) !default; -$green-700: shade-color($green, 40%) !default; -$green-800: shade-color($green, 60%) !default; -$green-900: shade-color($green, 80%) !default; - -$teal-100: tint-color($teal, 80%) !default; -$teal-200: tint-color($teal, 60%) !default; -$teal-300: tint-color($teal, 40%) !default; -$teal-400: tint-color($teal, 20%) !default; -$teal-500: $teal !default; -$teal-600: shade-color($teal, 20%) !default; -$teal-700: shade-color($teal, 40%) !default; -$teal-800: shade-color($teal, 60%) !default; -$teal-900: shade-color($teal, 80%) !default; - -$cyan-100: tint-color($cyan, 80%) !default; -$cyan-200: tint-color($cyan, 60%) !default; -$cyan-300: tint-color($cyan, 40%) !default; -$cyan-400: tint-color($cyan, 20%) !default; -$cyan-500: $cyan !default; -$cyan-600: shade-color($cyan, 20%) !default; -$cyan-700: shade-color($cyan, 40%) !default; -$cyan-800: shade-color($cyan, 60%) !default; -$cyan-900: shade-color($cyan, 80%) !default; - -$blues: ( - "blue-100": $blue-100, - "blue-200": $blue-200, - "blue-300": $blue-300, - "blue-400": $blue-400, - "blue-500": $blue-500, - "blue-600": $blue-600, - "blue-700": $blue-700, - "blue-800": $blue-800, - "blue-900": $blue-900 -) !default; - -$indigos: ( - "indigo-100": $indigo-100, - "indigo-200": $indigo-200, - "indigo-300": $indigo-300, - "indigo-400": $indigo-400, - "indigo-500": $indigo-500, - "indigo-600": $indigo-600, - "indigo-700": $indigo-700, - "indigo-800": $indigo-800, - "indigo-900": $indigo-900 -) !default; - -$purples: ( - "purple-100": $purple-100, - "purple-200": $purple-200, - "purple-300": $purple-300, - "purple-400": $purple-400, - "purple-500": $purple-500, - "purple-600": $purple-600, - "purple-700": $purple-700, - "purple-800": $purple-800, - "purple-900": $purple-900 -) !default; - -$pinks: ( - "pink-100": $pink-100, - "pink-200": $pink-200, - "pink-300": $pink-300, - "pink-400": $pink-400, - "pink-500": $pink-500, - "pink-600": $pink-600, - "pink-700": $pink-700, - "pink-800": $pink-800, - "pink-900": $pink-900 -) !default; - -$reds: ( - "red-100": $red-100, - "red-200": $red-200, - "red-300": $red-300, - "red-400": $red-400, - "red-500": $red-500, - "red-600": $red-600, - "red-700": $red-700, - "red-800": $red-800, - "red-900": $red-900 -) !default; - -$oranges: ( - "orange-100": $orange-100, - "orange-200": $orange-200, - "orange-300": $orange-300, - "orange-400": $orange-400, - "orange-500": $orange-500, - "orange-600": $orange-600, - "orange-700": $orange-700, - "orange-800": $orange-800, - "orange-900": $orange-900 -) !default; - -$yellows: ( - "yellow-100": $yellow-100, - "yellow-200": $yellow-200, - "yellow-300": $yellow-300, - "yellow-400": $yellow-400, - "yellow-500": $yellow-500, - "yellow-600": $yellow-600, - "yellow-700": $yellow-700, - "yellow-800": $yellow-800, - "yellow-900": $yellow-900 -) !default; - -$greens: ( - "green-100": $green-100, - "green-200": $green-200, - "green-300": $green-300, - "green-400": $green-400, - "green-500": $green-500, - "green-600": $green-600, - "green-700": $green-700, - "green-800": $green-800, - "green-900": $green-900 -) !default; - -$teals: ( - "teal-100": $teal-100, - "teal-200": $teal-200, - "teal-300": $teal-300, - "teal-400": $teal-400, - "teal-500": $teal-500, - "teal-600": $teal-600, - "teal-700": $teal-700, - "teal-800": $teal-800, - "teal-900": $teal-900 -) !default; - -$cyans: ( - "cyan-100": $cyan-100, - "cyan-200": $cyan-200, - "cyan-300": $cyan-300, - "cyan-400": $cyan-400, - "cyan-500": $cyan-500, - "cyan-600": $cyan-600, - "cyan-700": $cyan-700, - "cyan-800": $cyan-800, - "cyan-900": $cyan-900 -) !default; -// fusv-enable - -// scss-docs-start theme-color-variables -$primary: $blue !default; -$secondary: $gray-600 !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-100 !default; -$dark: $gray-900 !default; -// scss-docs-end theme-color-variables - -// scss-docs-start theme-colors-map -$theme-colors: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark -) !default; -// scss-docs-end theme-colors-map - -// scss-docs-start theme-text-variables -$primary-text-emphasis: shade-color($primary, 60%) !default; -$secondary-text-emphasis: shade-color($secondary, 60%) !default; -$success-text-emphasis: shade-color($success, 60%) !default; -$info-text-emphasis: shade-color($info, 60%) !default; -$warning-text-emphasis: shade-color($warning, 60%) !default; -$danger-text-emphasis: shade-color($danger, 60%) !default; -$light-text-emphasis: $gray-700 !default; -$dark-text-emphasis: $gray-700 !default; -// scss-docs-end theme-text-variables - -// scss-docs-start theme-bg-subtle-variables -$primary-bg-subtle: tint-color($primary, 80%) !default; -$secondary-bg-subtle: tint-color($secondary, 80%) !default; -$success-bg-subtle: tint-color($success, 80%) !default; -$info-bg-subtle: tint-color($info, 80%) !default; -$warning-bg-subtle: tint-color($warning, 80%) !default; -$danger-bg-subtle: tint-color($danger, 80%) !default; -$light-bg-subtle: mix($gray-100, $white) !default; -$dark-bg-subtle: $gray-400 !default; -// scss-docs-end theme-bg-subtle-variables - -// scss-docs-start theme-border-subtle-variables -$primary-border-subtle: tint-color($primary, 60%) !default; -$secondary-border-subtle: tint-color($secondary, 60%) !default; -$success-border-subtle: tint-color($success, 60%) !default; -$info-border-subtle: tint-color($info, 60%) !default; -$warning-border-subtle: tint-color($warning, 60%) !default; -$danger-border-subtle: tint-color($danger, 60%) !default; -$light-border-subtle: $gray-200 !default; -$dark-border-subtle: $gray-500 !default; -// scss-docs-end theme-border-subtle-variables - -// Characters which are escaped by the escape-svg function -$escaped-characters: ( - ("<", "%3c"), - (">", "%3e"), - ("#", "%23"), - ("(", "%28"), - (")", "%29"), -) !default; - -// Options -// -// Quickly modify global styling by enabling or disabling optional features. - -$enable-caret: true !default; -$enable-rounded: true !default; -$enable-shadows: false !default; -$enable-gradients: false !default; -$enable-transitions: true !default; -$enable-reduced-motion: true !default; -$enable-smooth-scroll: true !default; -$enable-grid-classes: true !default; -$enable-container-classes: true !default; -$enable-cssgrid: false !default; -$enable-button-pointers: true !default; -$enable-rfs: true !default; -$enable-validation-icons: true !default; -$enable-negative-margins: false !default; -$enable-deprecation-messages: true !default; -$enable-important-utilities: true !default; - -$enable-dark-mode: true !default; -$color-mode-type: data !default; // `data` or `media-query` - -// Prefix for :root CSS variables - -$variable-prefix: bs- !default; // Deprecated in v5.2.0 for the shorter `$prefix` -$prefix: $variable-prefix !default; - -// Gradient -// -// The gradient which is added to components if `$enable-gradients` is `true` -// This gradient is also added to elements with `.bg-gradient` -// scss-docs-start variable-gradient -$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default; -// scss-docs-end variable-gradient - -// Spacing -// -// Control the default styling of most Bootstrap elements by modifying these -// variables. Mostly focused on spacing. -// You can add more entries to the $spacers map, should you need more variation. - -// scss-docs-start spacer-variables-maps -$spacer: 1rem !default; -$spacers: ( - 0: 0, - 1: $spacer * .25, - 2: $spacer * .5, - 3: $spacer, - 4: $spacer * 1.5, - 5: $spacer * 3, -) !default; -// scss-docs-end spacer-variables-maps - -// Position -// -// Define the edge positioning anchors of the position utilities. - -// scss-docs-start position-map -$position-values: ( - 0: 0, - 50: 50%, - 100: 100% -) !default; -// scss-docs-end position-map - -// Body -// -// Settings for the `` element. - -$body-text-align: null !default; -$body-color: $gray-900 !default; -$body-bg: $white !default; - -$body-secondary-color: rgba($body-color, .75) !default; -$body-secondary-bg: $gray-200 !default; - -$body-tertiary-color: rgba($body-color, .5) !default; -$body-tertiary-bg: $gray-100 !default; - -$body-emphasis-color: $black !default; - -// Links -// -// Style anchor elements. - -$link-color: $primary !default; -$link-decoration: underline !default; -$link-shade-percentage: 20% !default; -$link-hover-color: shift-color($link-color, $link-shade-percentage) !default; -$link-hover-decoration: null !default; - -$stretched-link-pseudo-element: after !default; -$stretched-link-z-index: 1 !default; - -// Icon links -// scss-docs-start icon-link-variables -$icon-link-gap: .375rem !default; -$icon-link-underline-offset: .25em !default; -$icon-link-icon-size: 1em !default; -$icon-link-icon-transition: .2s ease-in-out transform !default; -$icon-link-icon-transform: translate3d(.25em, 0, 0) !default; -// scss-docs-end icon-link-variables - -// Paragraphs -// -// Style p element. - -$paragraph-margin-bottom: 1rem !default; - - -// Grid breakpoints -// -// Define the minimum dimensions at which your layout will change, -// adapting to different screen sizes, for use in media queries. - -// scss-docs-start grid-breakpoints -$grid-breakpoints: ( - xs: 0, - sm: 576px, - md: 768px, - lg: 992px, - xl: 1200px, - xxl: 1400px -) !default; -// scss-docs-end grid-breakpoints - -@include _assert-ascending($grid-breakpoints, "$grid-breakpoints"); -@include _assert-starts-at-zero($grid-breakpoints, "$grid-breakpoints"); - - -// Grid containers -// -// Define the maximum width of `.container` for different screen sizes. - -// scss-docs-start container-max-widths -$container-max-widths: ( - sm: 540px, - md: 720px, - lg: 960px, - xl: 1140px, - xxl: 1320px -) !default; -// scss-docs-end container-max-widths - -@include _assert-ascending($container-max-widths, "$container-max-widths"); - - -// Grid columns -// -// Set the number of columns and specify the width of the gutters. - -$grid-columns: 12 !default; -$grid-gutter-width: 1.5rem !default; -$grid-row-columns: 6 !default; - -// Container padding - -$container-padding-x: $grid-gutter-width !default; - - -// Components -// -// Define common padding and border radius sizes and more. - -// scss-docs-start border-variables -$border-width: 1px !default; -$border-widths: ( - 1: 1px, - 2: 2px, - 3: 3px, - 4: 4px, - 5: 5px -) !default; -$border-style: solid !default; -$border-color: $gray-300 !default; -$border-color-translucent: rgba($black, .175) !default; -// scss-docs-end border-variables - -// scss-docs-start border-radius-variables -$border-radius: .375rem !default; -$border-radius-sm: .25rem !default; -$border-radius-lg: .5rem !default; -$border-radius-xl: 1rem !default; -$border-radius-xxl: 2rem !default; -$border-radius-pill: 50rem !default; -// scss-docs-end border-radius-variables -// fusv-disable -$border-radius-2xl: $border-radius-xxl !default; // Deprecated in v5.3.0 -// fusv-enable - -// scss-docs-start box-shadow-variables -$box-shadow: 0 .5rem 1rem rgba($black, .15) !default; -$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default; -$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default; -$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default; -// scss-docs-end box-shadow-variables - -$component-active-color: $white !default; -$component-active-bg: $primary !default; - -// scss-docs-start focus-ring-variables -$focus-ring-width: .25rem !default; -$focus-ring-opacity: .25 !default; -$focus-ring-color: rgba($primary, $focus-ring-opacity) !default; -$focus-ring-blur: 0 !default; -$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default; -// scss-docs-end focus-ring-variables - -// scss-docs-start caret-variables -$caret-width: .3em !default; -$caret-vertical-align: $caret-width * .85 !default; -$caret-spacing: $caret-width * .85 !default; -// scss-docs-end caret-variables - -$transition-base: all .2s ease-in-out !default; -$transition-fade: opacity .15s linear !default; -// scss-docs-start collapse-transition -$transition-collapse: height .35s ease !default; -$transition-collapse-width: width .35s ease !default; -// scss-docs-end collapse-transition - -// stylelint-disable function-disallowed-list -// scss-docs-start aspect-ratios -$aspect-ratios: ( - "1x1": 100%, - "4x3": calc(3 / 4 * 100%), - "16x9": calc(9 / 16 * 100%), - "21x9": calc(9 / 21 * 100%) -) !default; -// scss-docs-end aspect-ratios -// stylelint-enable function-disallowed-list - -// Typography -// -// Font, line-height, and color for body text, headings, and more. - -// scss-docs-start font-variables -// stylelint-disable value-keyword-case -$font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; -$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default; -// stylelint-enable value-keyword-case -$font-family-base: var(--#{$prefix}font-sans-serif) !default; -$font-family-code: var(--#{$prefix}font-monospace) !default; - -// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins -// $font-size-base affects the font size of the body text -$font-size-root: null !default; -$font-size-base: 1rem !default; // Assumes the browser default, typically `16px` -$font-size-sm: $font-size-base * .875 !default; -$font-size-lg: $font-size-base * 1.25 !default; - -$font-weight-lighter: lighter !default; -$font-weight-light: 300 !default; -$font-weight-normal: 400 !default; -$font-weight-medium: 500 !default; -$font-weight-semibold: 600 !default; -$font-weight-bold: 700 !default; -$font-weight-bolder: bolder !default; - -$font-weight-base: $font-weight-normal !default; - -$line-height-base: 1.5 !default; -$line-height-sm: 1.25 !default; -$line-height-lg: 2 !default; - -$h1-font-size: $font-size-base * 2.5 !default; -$h2-font-size: $font-size-base * 2 !default; -$h3-font-size: $font-size-base * 1.75 !default; -$h4-font-size: $font-size-base * 1.5 !default; -$h5-font-size: $font-size-base * 1.25 !default; -$h6-font-size: $font-size-base !default; -// scss-docs-end font-variables - -// scss-docs-start font-sizes -$font-sizes: ( - 1: $h1-font-size, - 2: $h2-font-size, - 3: $h3-font-size, - 4: $h4-font-size, - 5: $h5-font-size, - 6: $h6-font-size -) !default; -// scss-docs-end font-sizes - -// scss-docs-start headings-variables -$headings-margin-bottom: $spacer * .5 !default; -$headings-font-family: null !default; -$headings-font-style: null !default; -$headings-font-weight: 500 !default; -$headings-line-height: 1.2 !default; -$headings-color: inherit !default; -// scss-docs-end headings-variables - -// scss-docs-start display-headings -$display-font-sizes: ( - 1: 5rem, - 2: 4.5rem, - 3: 4rem, - 4: 3.5rem, - 5: 3rem, - 6: 2.5rem -) !default; - -$display-font-family: null !default; -$display-font-style: null !default; -$display-font-weight: 300 !default; -$display-line-height: $headings-line-height !default; -// scss-docs-end display-headings - -// scss-docs-start type-variables -$lead-font-size: $font-size-base * 1.25 !default; -$lead-font-weight: 300 !default; - -$small-font-size: .875em !default; - -$sub-sup-font-size: .75em !default; - -// fusv-disable -$text-muted: var(--#{$prefix}secondary-color) !default; // Deprecated in 5.3.0 -// fusv-enable - -$initialism-font-size: $small-font-size !default; - -$blockquote-margin-y: $spacer !default; -$blockquote-font-size: $font-size-base * 1.25 !default; -$blockquote-footer-color: $gray-600 !default; -$blockquote-footer-font-size: $small-font-size !default; - -$hr-margin-y: $spacer !default; -$hr-color: inherit !default; - -// fusv-disable -$hr-bg-color: null !default; // Deprecated in v5.2.0 -$hr-height: null !default; // Deprecated in v5.2.0 -// fusv-enable - -$hr-border-color: null !default; // Allows for inherited colors -$hr-border-width: var(--#{$prefix}border-width) !default; -$hr-opacity: .25 !default; - -// scss-docs-start vr-variables -$vr-border-width: var(--#{$prefix}border-width) !default; -// scss-docs-end vr-variables - -$legend-margin-bottom: .5rem !default; -$legend-font-size: 1.5rem !default; -$legend-font-weight: null !default; - -$dt-font-weight: $font-weight-bold !default; - -$list-inline-padding: .5rem !default; - -$mark-padding: .1875em !default; -$mark-color: $body-color !default; -$mark-bg: $yellow-100 !default; -// scss-docs-end type-variables - - -// Tables -// -// Customizes the `.table` component with basic values, each used across all table variations. - -// scss-docs-start table-variables -$table-cell-padding-y: .5rem !default; -$table-cell-padding-x: .5rem !default; -$table-cell-padding-y-sm: .25rem !default; -$table-cell-padding-x-sm: .25rem !default; - -$table-cell-vertical-align: top !default; - -$table-color: var(--#{$prefix}emphasis-color) !default; -$table-bg: var(--#{$prefix}body-bg) !default; -$table-accent-bg: transparent !default; - -$table-th-font-weight: null !default; - -$table-striped-color: $table-color !default; -$table-striped-bg-factor: .05 !default; -$table-striped-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-striped-bg-factor) !default; - -$table-active-color: $table-color !default; -$table-active-bg-factor: .1 !default; -$table-active-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-active-bg-factor) !default; - -$table-hover-color: $table-color !default; -$table-hover-bg-factor: .075 !default; -$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default; - -$table-border-factor: .2 !default; -$table-border-width: var(--#{$prefix}border-width) !default; -$table-border-color: var(--#{$prefix}border-color) !default; - -$table-striped-order: odd !default; -$table-striped-columns-order: even !default; - -$table-group-separator-color: currentcolor !default; - -$table-caption-color: var(--#{$prefix}secondary-color) !default; - -$table-bg-scale: -80% !default; -// scss-docs-end table-variables - -// scss-docs-start table-loop -$table-variants: ( - "primary": shift-color($primary, $table-bg-scale), - "secondary": shift-color($secondary, $table-bg-scale), - "success": shift-color($success, $table-bg-scale), - "info": shift-color($info, $table-bg-scale), - "warning": shift-color($warning, $table-bg-scale), - "danger": shift-color($danger, $table-bg-scale), - "light": $light, - "dark": $dark, -) !default; -// scss-docs-end table-loop - - -// Buttons + Forms -// -// Shared variables that are reassigned to `$input-` and `$btn-` specific variables. - -// scss-docs-start input-btn-variables -$input-btn-padding-y: .375rem !default; -$input-btn-padding-x: .75rem !default; -$input-btn-font-family: null !default; -$input-btn-font-size: $font-size-base !default; -$input-btn-line-height: $line-height-base !default; - -$input-btn-focus-width: $focus-ring-width !default; -$input-btn-focus-color-opacity: $focus-ring-opacity !default; -$input-btn-focus-color: $focus-ring-color !default; -$input-btn-focus-blur: $focus-ring-blur !default; -$input-btn-focus-box-shadow: $focus-ring-box-shadow !default; - -$input-btn-padding-y-sm: .25rem !default; -$input-btn-padding-x-sm: .5rem !default; -$input-btn-font-size-sm: $font-size-sm !default; - -$input-btn-padding-y-lg: .5rem !default; -$input-btn-padding-x-lg: 1rem !default; -$input-btn-font-size-lg: $font-size-lg !default; - -$input-btn-border-width: var(--#{$prefix}border-width) !default; -// scss-docs-end input-btn-variables - - -// Buttons -// -// For each of Bootstrap's buttons, define text, background, and border color. - -// scss-docs-start btn-variables -$btn-color: var(--#{$prefix}body-color) !default; -$btn-padding-y: $input-btn-padding-y !default; -$btn-padding-x: $input-btn-padding-x !default; -$btn-font-family: $input-btn-font-family !default; -$btn-font-size: $input-btn-font-size !default; -$btn-line-height: $input-btn-line-height !default; -$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping - -$btn-padding-y-sm: $input-btn-padding-y-sm !default; -$btn-padding-x-sm: $input-btn-padding-x-sm !default; -$btn-font-size-sm: $input-btn-font-size-sm !default; - -$btn-padding-y-lg: $input-btn-padding-y-lg !default; -$btn-padding-x-lg: $input-btn-padding-x-lg !default; -$btn-font-size-lg: $input-btn-font-size-lg !default; - -$btn-border-width: $input-btn-border-width !default; - -$btn-font-weight: $font-weight-normal !default; -$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; -$btn-focus-width: $input-btn-focus-width !default; -$btn-focus-box-shadow: $input-btn-focus-box-shadow !default; -$btn-disabled-opacity: .65 !default; -$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default; - -$btn-link-color: var(--#{$prefix}link-color) !default; -$btn-link-hover-color: var(--#{$prefix}link-hover-color) !default; -$btn-link-disabled-color: $gray-600 !default; -$btn-link-focus-shadow-rgb: to-rgb(mix(color-contrast($link-color), $link-color, 15%)) !default; - -// Allows for customizing button radius independently from global border radius -$btn-border-radius: var(--#{$prefix}border-radius) !default; -$btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; - -$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$btn-hover-bg-shade-amount: 15% !default; -$btn-hover-bg-tint-amount: 15% !default; -$btn-hover-border-shade-amount: 20% !default; -$btn-hover-border-tint-amount: 10% !default; -$btn-active-bg-shade-amount: 20% !default; -$btn-active-bg-tint-amount: 20% !default; -$btn-active-border-shade-amount: 25% !default; -$btn-active-border-tint-amount: 10% !default; -// scss-docs-end btn-variables - - -// Forms - -// scss-docs-start form-text-variables -$form-text-margin-top: .25rem !default; -$form-text-font-size: $small-font-size !default; -$form-text-font-style: null !default; -$form-text-font-weight: null !default; -$form-text-color: var(--#{$prefix}secondary-color) !default; -// scss-docs-end form-text-variables - -// scss-docs-start form-label-variables -$form-label-margin-bottom: .5rem !default; -$form-label-font-size: null !default; -$form-label-font-style: null !default; -$form-label-font-weight: null !default; -$form-label-color: null !default; -// scss-docs-end form-label-variables - -// scss-docs-start form-input-variables -$input-padding-y: $input-btn-padding-y !default; -$input-padding-x: $input-btn-padding-x !default; -$input-font-family: $input-btn-font-family !default; -$input-font-size: $input-btn-font-size !default; -$input-font-weight: $font-weight-base !default; -$input-line-height: $input-btn-line-height !default; - -$input-padding-y-sm: $input-btn-padding-y-sm !default; -$input-padding-x-sm: $input-btn-padding-x-sm !default; -$input-font-size-sm: $input-btn-font-size-sm !default; - -$input-padding-y-lg: $input-btn-padding-y-lg !default; -$input-padding-x-lg: $input-btn-padding-x-lg !default; -$input-font-size-lg: $input-btn-font-size-lg !default; - -$input-bg: var(--#{$prefix}body-bg) !default; -$input-disabled-color: null !default; -$input-disabled-bg: var(--#{$prefix}secondary-bg) !default; -$input-disabled-border-color: null !default; - -$input-color: var(--#{$prefix}body-color) !default; -$input-border-color: var(--#{$prefix}border-color) !default; -$input-border-width: $input-btn-border-width !default; -$input-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$input-border-radius: var(--#{$prefix}border-radius) !default; -$input-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$input-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; - -$input-focus-bg: $input-bg !default; -$input-focus-border-color: tint-color($component-active-bg, 50%) !default; -$input-focus-color: $input-color !default; -$input-focus-width: $input-btn-focus-width !default; -$input-focus-box-shadow: $input-btn-focus-box-shadow !default; - -$input-placeholder-color: var(--#{$prefix}secondary-color) !default; -$input-plaintext-color: var(--#{$prefix}body-color) !default; - -$input-height-border: calc(#{$input-border-width} * 2) !default; // stylelint-disable-line function-disallowed-list - -$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default; -$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default; -$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default; - -$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default; -$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default; -$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default; - -$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$form-color-width: 3rem !default; -// scss-docs-end form-input-variables - -// scss-docs-start form-check-variables -$form-check-input-width: 1em !default; -$form-check-min-height: $font-size-base * $line-height-base !default; -$form-check-padding-start: $form-check-input-width + .5em !default; -$form-check-margin-bottom: .125rem !default; -$form-check-label-color: null !default; -$form-check-label-cursor: null !default; -$form-check-transition: null !default; - -$form-check-input-active-filter: brightness(90%) !default; - -$form-check-input-bg: $input-bg !default; -$form-check-input-border: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color) !default; -$form-check-input-border-radius: .25em !default; -$form-check-radio-border-radius: 50% !default; -$form-check-input-focus-border: $input-focus-border-color !default; -$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default; - -$form-check-input-checked-color: $component-active-color !default; -$form-check-input-checked-bg-color: $component-active-bg !default; -$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default; -$form-check-input-checked-bg-image: url("data:image/svg+xml,") !default; -$form-check-radio-checked-bg-image: url("data:image/svg+xml,") !default; - -$form-check-input-indeterminate-color: $component-active-color !default; -$form-check-input-indeterminate-bg-color: $component-active-bg !default; -$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default; -$form-check-input-indeterminate-bg-image: url("data:image/svg+xml,") !default; - -$form-check-input-disabled-opacity: .5 !default; -$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default; -$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default; - -$form-check-inline-margin-end: 1rem !default; -// scss-docs-end form-check-variables - -// scss-docs-start form-switch-variables -$form-switch-color: rgba($black, .25) !default; -$form-switch-width: 2em !default; -$form-switch-padding-start: $form-switch-width + .5em !default; -$form-switch-bg-image: url("data:image/svg+xml,") !default; -$form-switch-border-radius: $form-switch-width !default; -$form-switch-transition: background-position .15s ease-in-out !default; - -$form-switch-focus-color: $input-focus-border-color !default; -$form-switch-focus-bg-image: url("data:image/svg+xml,") !default; - -$form-switch-checked-color: $component-active-color !default; -$form-switch-checked-bg-image: url("data:image/svg+xml,") !default; -$form-switch-checked-bg-position: right center !default; -// scss-docs-end form-switch-variables - -// scss-docs-start input-group-variables -$input-group-addon-padding-y: $input-padding-y !default; -$input-group-addon-padding-x: $input-padding-x !default; -$input-group-addon-font-weight: $input-font-weight !default; -$input-group-addon-color: $input-color !default; -$input-group-addon-bg: var(--#{$prefix}tertiary-bg) !default; -$input-group-addon-border-color: $input-border-color !default; -// scss-docs-end input-group-variables - -// scss-docs-start form-select-variables -$form-select-padding-y: $input-padding-y !default; -$form-select-padding-x: $input-padding-x !default; -$form-select-font-family: $input-font-family !default; -$form-select-font-size: $input-font-size !default; -$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image -$form-select-font-weight: $input-font-weight !default; -$form-select-line-height: $input-line-height !default; -$form-select-color: $input-color !default; -$form-select-bg: $input-bg !default; -$form-select-disabled-color: null !default; -$form-select-disabled-bg: $input-disabled-bg !default; -$form-select-disabled-border-color: $input-disabled-border-color !default; -$form-select-bg-position: right $form-select-padding-x center !default; -$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions -$form-select-indicator-color: $gray-800 !default; -$form-select-indicator: url("data:image/svg+xml,") !default; - -$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default; -$form-select-feedback-icon-position: center right $form-select-indicator-padding !default; -$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default; - -$form-select-border-width: $input-border-width !default; -$form-select-border-color: $input-border-color !default; -$form-select-border-radius: $input-border-radius !default; -$form-select-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$form-select-focus-border-color: $input-focus-border-color !default; -$form-select-focus-width: $input-focus-width !default; -$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default; - -$form-select-padding-y-sm: $input-padding-y-sm !default; -$form-select-padding-x-sm: $input-padding-x-sm !default; -$form-select-font-size-sm: $input-font-size-sm !default; -$form-select-border-radius-sm: $input-border-radius-sm !default; - -$form-select-padding-y-lg: $input-padding-y-lg !default; -$form-select-padding-x-lg: $input-padding-x-lg !default; -$form-select-font-size-lg: $input-font-size-lg !default; -$form-select-border-radius-lg: $input-border-radius-lg !default; - -$form-select-transition: $input-transition !default; -// scss-docs-end form-select-variables - -// scss-docs-start form-range-variables -$form-range-track-width: 100% !default; -$form-range-track-height: .5rem !default; -$form-range-track-cursor: pointer !default; -$form-range-track-bg: var(--#{$prefix}secondary-bg) !default; -$form-range-track-border-radius: 1rem !default; -$form-range-track-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$form-range-thumb-width: 1rem !default; -$form-range-thumb-height: $form-range-thumb-width !default; -$form-range-thumb-bg: $component-active-bg !default; -$form-range-thumb-border: 0 !default; -$form-range-thumb-border-radius: 1rem !default; -$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default; -$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default; -$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge -$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default; -$form-range-thumb-disabled-bg: var(--#{$prefix}secondary-color) !default; -$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; -// scss-docs-end form-range-variables - -// scss-docs-start form-file-variables -$form-file-button-color: $input-color !default; -$form-file-button-bg: var(--#{$prefix}tertiary-bg) !default; -$form-file-button-hover-bg: var(--#{$prefix}secondary-bg) !default; -// scss-docs-end form-file-variables - -// scss-docs-start form-floating-variables -$form-floating-height: add(3.5rem, $input-height-border) !default; -$form-floating-line-height: 1.25 !default; -$form-floating-padding-x: $input-padding-x !default; -$form-floating-padding-y: 1rem !default; -$form-floating-input-padding-t: 1.625rem !default; -$form-floating-input-padding-b: .625rem !default; -$form-floating-label-height: 1.5em !default; -$form-floating-label-opacity: .65 !default; -$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default; -$form-floating-label-disabled-color: $gray-600 !default; -$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default; -// scss-docs-end form-floating-variables - -// Form validation - -// scss-docs-start form-feedback-variables -$form-feedback-margin-top: $form-text-margin-top !default; -$form-feedback-font-size: $form-text-font-size !default; -$form-feedback-font-style: $form-text-font-style !default; -$form-feedback-valid-color: $success !default; -$form-feedback-invalid-color: $danger !default; - -$form-feedback-icon-valid-color: $form-feedback-valid-color !default; -$form-feedback-icon-valid: url("data:image/svg+xml,") !default; -$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default; -$form-feedback-icon-invalid: url("data:image/svg+xml,") !default; -// scss-docs-end form-feedback-variables - -// scss-docs-start form-validation-colors -$form-valid-color: $form-feedback-valid-color !default; -$form-valid-border-color: $form-feedback-valid-color !default; -$form-invalid-color: $form-feedback-invalid-color !default; -$form-invalid-border-color: $form-feedback-invalid-color !default; -// scss-docs-end form-validation-colors - -// scss-docs-start form-validation-states -$form-validation-states: ( - "valid": ( - "color": var(--#{$prefix}form-valid-color), - "icon": $form-feedback-icon-valid, - "tooltip-color": #fff, - "tooltip-bg-color": var(--#{$prefix}success), - "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity), - "border-color": var(--#{$prefix}form-valid-border-color), - ), - "invalid": ( - "color": var(--#{$prefix}form-invalid-color), - "icon": $form-feedback-icon-invalid, - "tooltip-color": #fff, - "tooltip-bg-color": var(--#{$prefix}danger), - "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity), - "border-color": var(--#{$prefix}form-invalid-border-color), - ) -) !default; -// scss-docs-end form-validation-states - -// Z-index master list -// -// Warning: Avoid customizing these values. They're used for a bird's eye view -// of components dependent on the z-axis and are designed to all work together. - -// scss-docs-start zindex-stack -$zindex-dropdown: 1000 !default; -$zindex-sticky: 1020 !default; -$zindex-fixed: 1030 !default; -$zindex-offcanvas-backdrop: 1040 !default; -$zindex-offcanvas: 1045 !default; -$zindex-modal-backdrop: 1050 !default; -$zindex-modal: 1055 !default; -$zindex-popover: 1070 !default; -$zindex-tooltip: 1080 !default; -$zindex-toast: 1090 !default; -// scss-docs-end zindex-stack - -// scss-docs-start zindex-levels-map -$zindex-levels: ( - n1: -1, - 0: 0, - 1: 1, - 2: 2, - 3: 3 -) !default; -// scss-docs-end zindex-levels-map - - -// Navs - -// scss-docs-start nav-variables -$nav-link-padding-y: .5rem !default; -$nav-link-padding-x: 1rem !default; -$nav-link-font-size: null !default; -$nav-link-font-weight: null !default; -$nav-link-color: var(--#{$prefix}link-color) !default; -$nav-link-hover-color: var(--#{$prefix}link-hover-color) !default; -$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default; -$nav-link-disabled-color: var(--#{$prefix}secondary-color) !default; -$nav-link-focus-box-shadow: $focus-ring-box-shadow !default; - -$nav-tabs-border-color: var(--#{$prefix}border-color) !default; -$nav-tabs-border-width: var(--#{$prefix}border-width) !default; -$nav-tabs-border-radius: var(--#{$prefix}border-radius) !default; -$nav-tabs-link-hover-border-color: var(--#{$prefix}secondary-bg) var(--#{$prefix}secondary-bg) $nav-tabs-border-color !default; -$nav-tabs-link-active-color: var(--#{$prefix}emphasis-color) !default; -$nav-tabs-link-active-bg: var(--#{$prefix}body-bg) !default; -$nav-tabs-link-active-border-color: var(--#{$prefix}border-color) var(--#{$prefix}border-color) $nav-tabs-link-active-bg !default; - -$nav-pills-border-radius: var(--#{$prefix}border-radius) !default; -$nav-pills-link-active-color: $component-active-color !default; -$nav-pills-link-active-bg: $component-active-bg !default; - -$nav-underline-gap: 1rem !default; -$nav-underline-border-width: .125rem !default; -$nav-underline-link-active-color: var(--#{$prefix}emphasis-color) !default; -// scss-docs-end nav-variables - - -// Navbar - -// scss-docs-start navbar-variables -$navbar-padding-y: $spacer * .5 !default; -$navbar-padding-x: null !default; - -$navbar-nav-link-padding-x: .5rem !default; - -$navbar-brand-font-size: $font-size-lg !default; -// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link -$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default; -$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default; -$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default; -$navbar-brand-margin-end: 1rem !default; - -$navbar-toggler-padding-y: .25rem !default; -$navbar-toggler-padding-x: .75rem !default; -$navbar-toggler-font-size: $font-size-lg !default; -$navbar-toggler-border-radius: $btn-border-radius !default; -$navbar-toggler-focus-width: $btn-focus-width !default; -$navbar-toggler-transition: box-shadow .15s ease-in-out !default; - -$navbar-light-color: rgba(var(--#{$prefix}emphasis-color-rgb), .65) !default; -$navbar-light-hover-color: rgba(var(--#{$prefix}emphasis-color-rgb), .8) !default; -$navbar-light-active-color: rgba(var(--#{$prefix}emphasis-color-rgb), 1) !default; -$navbar-light-disabled-color: rgba(var(--#{$prefix}emphasis-color-rgb), .3) !default; -$navbar-light-icon-color: rgba($body-color, .75) !default; -$navbar-light-toggler-icon-bg: url("data:image/svg+xml,") !default; -$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), .15) !default; -$navbar-light-brand-color: $navbar-light-active-color !default; -$navbar-light-brand-hover-color: $navbar-light-active-color !default; -// scss-docs-end navbar-variables - -// scss-docs-start navbar-dark-variables -$navbar-dark-color: rgba($white, .55) !default; -$navbar-dark-hover-color: rgba($white, .75) !default; -$navbar-dark-active-color: $white !default; -$navbar-dark-disabled-color: rgba($white, .25) !default; -$navbar-dark-icon-color: $navbar-dark-color !default; -$navbar-dark-toggler-icon-bg: url("data:image/svg+xml,") !default; -$navbar-dark-toggler-border-color: rgba($white, .1) !default; -$navbar-dark-brand-color: $navbar-dark-active-color !default; -$navbar-dark-brand-hover-color: $navbar-dark-active-color !default; -// scss-docs-end navbar-dark-variables - - -// Dropdowns -// -// Dropdown menu container and contents. - -// scss-docs-start dropdown-variables -$dropdown-min-width: 10rem !default; -$dropdown-padding-x: 0 !default; -$dropdown-padding-y: .5rem !default; -$dropdown-spacer: .125rem !default; -$dropdown-font-size: $font-size-base !default; -$dropdown-color: var(--#{$prefix}body-color) !default; -$dropdown-bg: var(--#{$prefix}body-bg) !default; -$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default; -$dropdown-border-radius: var(--#{$prefix}border-radius) !default; -$dropdown-border-width: var(--#{$prefix}border-width) !default; -$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default; // stylelint-disable-line function-disallowed-list -$dropdown-divider-bg: $dropdown-border-color !default; -$dropdown-divider-margin-y: $spacer * .5 !default; -$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default; - -$dropdown-link-color: var(--#{$prefix}body-color) !default; -$dropdown-link-hover-color: $dropdown-link-color !default; -$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default; - -$dropdown-link-active-color: $component-active-color !default; -$dropdown-link-active-bg: $component-active-bg !default; - -$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default; - -$dropdown-item-padding-y: $spacer * .25 !default; -$dropdown-item-padding-x: $spacer !default; - -$dropdown-header-color: $gray-600 !default; -$dropdown-header-padding-x: $dropdown-item-padding-x !default; -$dropdown-header-padding-y: $dropdown-padding-y !default; -// fusv-disable -$dropdown-header-padding: $dropdown-header-padding-y $dropdown-header-padding-x !default; // Deprecated in v5.2.0 -// fusv-enable -// scss-docs-end dropdown-variables - -// scss-docs-start dropdown-dark-variables -$dropdown-dark-color: $gray-300 !default; -$dropdown-dark-bg: $gray-800 !default; -$dropdown-dark-border-color: $dropdown-border-color !default; -$dropdown-dark-divider-bg: $dropdown-divider-bg !default; -$dropdown-dark-box-shadow: null !default; -$dropdown-dark-link-color: $dropdown-dark-color !default; -$dropdown-dark-link-hover-color: $white !default; -$dropdown-dark-link-hover-bg: rgba($white, .15) !default; -$dropdown-dark-link-active-color: $dropdown-link-active-color !default; -$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default; -$dropdown-dark-link-disabled-color: $gray-500 !default; -$dropdown-dark-header-color: $gray-500 !default; -// scss-docs-end dropdown-dark-variables - - -// Pagination - -// scss-docs-start pagination-variables -$pagination-padding-y: .375rem !default; -$pagination-padding-x: .75rem !default; -$pagination-padding-y-sm: .25rem !default; -$pagination-padding-x-sm: .5rem !default; -$pagination-padding-y-lg: .75rem !default; -$pagination-padding-x-lg: 1.5rem !default; - -$pagination-font-size: $font-size-base !default; - -$pagination-color: var(--#{$prefix}link-color) !default; -$pagination-bg: var(--#{$prefix}body-bg) !default; -$pagination-border-radius: var(--#{$prefix}border-radius) !default; -$pagination-border-width: var(--#{$prefix}border-width) !default; -$pagination-margin-start: calc(-1 * #{$pagination-border-width}) !default; // stylelint-disable-line function-disallowed-list -$pagination-border-color: var(--#{$prefix}border-color) !default; - -$pagination-focus-color: var(--#{$prefix}link-hover-color) !default; -$pagination-focus-bg: var(--#{$prefix}secondary-bg) !default; -$pagination-focus-box-shadow: $focus-ring-box-shadow !default; -$pagination-focus-outline: 0 !default; - -$pagination-hover-color: var(--#{$prefix}link-hover-color) !default; -$pagination-hover-bg: var(--#{$prefix}tertiary-bg) !default; -$pagination-hover-border-color: var(--#{$prefix}border-color) !default; // Todo in v6: remove this? - -$pagination-active-color: $component-active-color !default; -$pagination-active-bg: $component-active-bg !default; -$pagination-active-border-color: $component-active-bg !default; - -$pagination-disabled-color: var(--#{$prefix}secondary-color) !default; -$pagination-disabled-bg: var(--#{$prefix}secondary-bg) !default; -$pagination-disabled-border-color: var(--#{$prefix}border-color) !default; - -$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$pagination-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$pagination-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; -// scss-docs-end pagination-variables - - -// Placeholders - -// scss-docs-start placeholders -$placeholder-opacity-max: .5 !default; -$placeholder-opacity-min: .2 !default; -// scss-docs-end placeholders - -// Cards - -// scss-docs-start card-variables -$card-spacer-y: $spacer !default; -$card-spacer-x: $spacer !default; -$card-title-spacer-y: $spacer * .5 !default; -$card-title-color: null !default; -$card-subtitle-color: null !default; -$card-border-width: var(--#{$prefix}border-width) !default; -$card-border-color: var(--#{$prefix}border-color-translucent) !default; -$card-border-radius: var(--#{$prefix}border-radius) !default; -$card-box-shadow: null !default; -$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default; -$card-cap-padding-y: $card-spacer-y * .5 !default; -$card-cap-padding-x: $card-spacer-x !default; -$card-cap-bg: rgba(var(--#{$prefix}body-color-rgb), .03) !default; -$card-cap-color: null !default; -$card-height: null !default; -$card-color: null !default; -$card-bg: var(--#{$prefix}body-bg) !default; -$card-img-overlay-padding: $spacer !default; -$card-group-margin: $grid-gutter-width * .5 !default; -// scss-docs-end card-variables - -// Accordion - -// scss-docs-start accordion-variables -$accordion-padding-y: 1rem !default; -$accordion-padding-x: 1.25rem !default; -$accordion-color: var(--#{$prefix}body-color) !default; -$accordion-bg: var(--#{$prefix}body-bg) !default; -$accordion-border-width: var(--#{$prefix}border-width) !default; -$accordion-border-color: var(--#{$prefix}border-color) !default; -$accordion-border-radius: var(--#{$prefix}border-radius) !default; -$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default; - -$accordion-body-padding-y: $accordion-padding-y !default; -$accordion-body-padding-x: $accordion-padding-x !default; - -$accordion-button-padding-y: $accordion-padding-y !default; -$accordion-button-padding-x: $accordion-padding-x !default; -$accordion-button-color: var(--#{$prefix}body-color) !default; -$accordion-button-bg: var(--#{$prefix}accordion-bg) !default; -$accordion-transition: $btn-transition, border-radius .15s ease !default; -$accordion-button-active-bg: var(--#{$prefix}primary-bg-subtle) !default; -$accordion-button-active-color: var(--#{$prefix}primary-text-emphasis) !default; - -// fusv-disable -$accordion-button-focus-border-color: $input-focus-border-color !default; // Deprecated in v5.3.3 -// fusv-enable -$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default; - -$accordion-icon-width: 1.25rem !default; -$accordion-icon-color: $body-color !default; -$accordion-icon-active-color: $primary-text-emphasis !default; -$accordion-icon-transition: transform .2s ease-in-out !default; -$accordion-icon-transform: rotate(-180deg) !default; - -$accordion-button-icon: url("data:image/svg+xml,") !default; -$accordion-button-active-icon: url("data:image/svg+xml,") !default; -// scss-docs-end accordion-variables - -// Tooltips - -// scss-docs-start tooltip-variables -$tooltip-font-size: $font-size-sm !default; -$tooltip-max-width: 200px !default; -$tooltip-color: var(--#{$prefix}body-bg) !default; -$tooltip-bg: var(--#{$prefix}emphasis-color) !default; -$tooltip-border-radius: var(--#{$prefix}border-radius) !default; -$tooltip-opacity: .9 !default; -$tooltip-padding-y: $spacer * .25 !default; -$tooltip-padding-x: $spacer * .5 !default; -$tooltip-margin: null !default; // TODO: remove this in v6 - -$tooltip-arrow-width: .8rem !default; -$tooltip-arrow-height: .4rem !default; -// fusv-disable -$tooltip-arrow-color: null !default; // Deprecated in Bootstrap 5.2.0 for CSS variables -// fusv-enable -// scss-docs-end tooltip-variables - -// Form tooltips must come after regular tooltips -// scss-docs-start tooltip-feedback-variables -$form-feedback-tooltip-padding-y: $tooltip-padding-y !default; -$form-feedback-tooltip-padding-x: $tooltip-padding-x !default; -$form-feedback-tooltip-font-size: $tooltip-font-size !default; -$form-feedback-tooltip-line-height: null !default; -$form-feedback-tooltip-opacity: $tooltip-opacity !default; -$form-feedback-tooltip-border-radius: $tooltip-border-radius !default; -// scss-docs-end tooltip-feedback-variables - - -// Popovers - -// scss-docs-start popover-variables -$popover-font-size: $font-size-sm !default; -$popover-bg: var(--#{$prefix}body-bg) !default; -$popover-max-width: 276px !default; -$popover-border-width: var(--#{$prefix}border-width) !default; -$popover-border-color: var(--#{$prefix}border-color-translucent) !default; -$popover-border-radius: var(--#{$prefix}border-radius-lg) !default; -$popover-inner-border-radius: calc(#{$popover-border-radius} - #{$popover-border-width}) !default; // stylelint-disable-line function-disallowed-list -$popover-box-shadow: var(--#{$prefix}box-shadow) !default; - -$popover-header-font-size: $font-size-base !default; -$popover-header-bg: var(--#{$prefix}secondary-bg) !default; -$popover-header-color: $headings-color !default; -$popover-header-padding-y: .5rem !default; -$popover-header-padding-x: $spacer !default; - -$popover-body-color: var(--#{$prefix}body-color) !default; -$popover-body-padding-y: $spacer !default; -$popover-body-padding-x: $spacer !default; - -$popover-arrow-width: 1rem !default; -$popover-arrow-height: .5rem !default; -// scss-docs-end popover-variables - -// fusv-disable -// Deprecated in Bootstrap 5.2.0 for CSS variables -$popover-arrow-color: $popover-bg !default; -$popover-arrow-outer-color: var(--#{$prefix}border-color-translucent) !default; -// fusv-enable - - -// Toasts - -// scss-docs-start toast-variables -$toast-max-width: 350px !default; -$toast-padding-x: .75rem !default; -$toast-padding-y: .5rem !default; -$toast-font-size: .875rem !default; -$toast-color: null !default; -$toast-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; -$toast-border-width: var(--#{$prefix}border-width) !default; -$toast-border-color: var(--#{$prefix}border-color-translucent) !default; -$toast-border-radius: var(--#{$prefix}border-radius) !default; -$toast-box-shadow: var(--#{$prefix}box-shadow) !default; -$toast-spacing: $container-padding-x !default; - -$toast-header-color: var(--#{$prefix}secondary-color) !default; -$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; -$toast-header-border-color: $toast-border-color !default; -// scss-docs-end toast-variables - - -// Badges - -// scss-docs-start badge-variables -$badge-font-size: .75em !default; -$badge-font-weight: $font-weight-bold !default; -$badge-color: $white !default; -$badge-padding-y: .35em !default; -$badge-padding-x: .65em !default; -$badge-border-radius: var(--#{$prefix}border-radius) !default; -// scss-docs-end badge-variables - - -// Modals - -// scss-docs-start modal-variables -$modal-inner-padding: $spacer !default; - -$modal-footer-margin-between: .5rem !default; - -$modal-dialog-margin: .5rem !default; -$modal-dialog-margin-y-sm-up: 1.75rem !default; - -$modal-title-line-height: $line-height-base !default; - -$modal-content-color: var(--#{$prefix}body-color) !default; -$modal-content-bg: var(--#{$prefix}body-bg) !default; -$modal-content-border-color: var(--#{$prefix}border-color-translucent) !default; -$modal-content-border-width: var(--#{$prefix}border-width) !default; -$modal-content-border-radius: var(--#{$prefix}border-radius-lg) !default; -$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default; -$modal-content-box-shadow-xs: var(--#{$prefix}box-shadow-sm) !default; -$modal-content-box-shadow-sm-up: var(--#{$prefix}box-shadow) !default; - -$modal-backdrop-bg: $black !default; -$modal-backdrop-opacity: .5 !default; - -$modal-header-border-color: var(--#{$prefix}border-color) !default; -$modal-header-border-width: $modal-content-border-width !default; -$modal-header-padding-y: $modal-inner-padding !default; -$modal-header-padding-x: $modal-inner-padding !default; -$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility - -$modal-footer-bg: null !default; -$modal-footer-border-color: $modal-header-border-color !default; -$modal-footer-border-width: $modal-header-border-width !default; - -$modal-sm: 300px !default; -$modal-md: 500px !default; -$modal-lg: 800px !default; -$modal-xl: 1140px !default; - -$modal-fade-transform: translate(0, -50px) !default; -$modal-show-transform: none !default; -$modal-transition: transform .3s ease-out !default; -$modal-scale-transform: scale(1.02) !default; -// scss-docs-end modal-variables - - -// Alerts -// -// Define alert colors, border radius, and padding. - -// scss-docs-start alert-variables -$alert-padding-y: $spacer !default; -$alert-padding-x: $spacer !default; -$alert-margin-bottom: 1rem !default; -$alert-border-radius: var(--#{$prefix}border-radius) !default; -$alert-link-font-weight: $font-weight-bold !default; -$alert-border-width: var(--#{$prefix}border-width) !default; -$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side -// scss-docs-end alert-variables - -// fusv-disable -$alert-bg-scale: -80% !default; // Deprecated in v5.2.0, to be removed in v6 -$alert-border-scale: -70% !default; // Deprecated in v5.2.0, to be removed in v6 -$alert-color-scale: 40% !default; // Deprecated in v5.2.0, to be removed in v6 -// fusv-enable - -// Progress bars - -// scss-docs-start progress-variables -$progress-height: 1rem !default; -$progress-font-size: $font-size-base * .75 !default; -$progress-bg: var(--#{$prefix}secondary-bg) !default; -$progress-border-radius: var(--#{$prefix}border-radius) !default; -$progress-box-shadow: var(--#{$prefix}box-shadow-inset) !default; -$progress-bar-color: $white !default; -$progress-bar-bg: $primary !default; -$progress-bar-animation-timing: 1s linear infinite !default; -$progress-bar-transition: width .6s ease !default; -// scss-docs-end progress-variables - - -// List group - -// scss-docs-start list-group-variables -$list-group-color: var(--#{$prefix}body-color) !default; -$list-group-bg: var(--#{$prefix}body-bg) !default; -$list-group-border-color: var(--#{$prefix}border-color) !default; -$list-group-border-width: var(--#{$prefix}border-width) !default; -$list-group-border-radius: var(--#{$prefix}border-radius) !default; - -$list-group-item-padding-y: $spacer * .5 !default; -$list-group-item-padding-x: $spacer !default; -// fusv-disable -$list-group-item-bg-scale: -80% !default; // Deprecated in v5.3.0 -$list-group-item-color-scale: 40% !default; // Deprecated in v5.3.0 -// fusv-enable - -$list-group-hover-bg: var(--#{$prefix}tertiary-bg) !default; -$list-group-active-color: $component-active-color !default; -$list-group-active-bg: $component-active-bg !default; -$list-group-active-border-color: $list-group-active-bg !default; - -$list-group-disabled-color: var(--#{$prefix}secondary-color) !default; -$list-group-disabled-bg: $list-group-bg !default; - -$list-group-action-color: var(--#{$prefix}secondary-color) !default; -$list-group-action-hover-color: var(--#{$prefix}emphasis-color) !default; - -$list-group-action-active-color: var(--#{$prefix}body-color) !default; -$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default; -// scss-docs-end list-group-variables - - -// Image thumbnails - -// scss-docs-start thumbnail-variables -$thumbnail-padding: .25rem !default; -$thumbnail-bg: var(--#{$prefix}body-bg) !default; -$thumbnail-border-width: var(--#{$prefix}border-width) !default; -$thumbnail-border-color: var(--#{$prefix}border-color) !default; -$thumbnail-border-radius: var(--#{$prefix}border-radius) !default; -$thumbnail-box-shadow: var(--#{$prefix}box-shadow-sm) !default; -// scss-docs-end thumbnail-variables - - -// Figures - -// scss-docs-start figure-variables -$figure-caption-font-size: $small-font-size !default; -$figure-caption-color: var(--#{$prefix}secondary-color) !default; -// scss-docs-end figure-variables - - -// Breadcrumbs - -// scss-docs-start breadcrumb-variables -$breadcrumb-font-size: null !default; -$breadcrumb-padding-y: 0 !default; -$breadcrumb-padding-x: 0 !default; -$breadcrumb-item-padding-x: .5rem !default; -$breadcrumb-margin-bottom: 1rem !default; -$breadcrumb-bg: null !default; -$breadcrumb-divider-color: var(--#{$prefix}secondary-color) !default; -$breadcrumb-active-color: var(--#{$prefix}secondary-color) !default; -$breadcrumb-divider: quote("/") !default; -$breadcrumb-divider-flipped: $breadcrumb-divider !default; -$breadcrumb-border-radius: null !default; -// scss-docs-end breadcrumb-variables - -// Carousel - -// scss-docs-start carousel-variables -$carousel-control-color: $white !default; -$carousel-control-width: 15% !default; -$carousel-control-opacity: .5 !default; -$carousel-control-hover-opacity: .9 !default; -$carousel-control-transition: opacity .15s ease !default; -$carousel-control-icon-filter: null !default; - -$carousel-indicator-width: 30px !default; -$carousel-indicator-height: 3px !default; -$carousel-indicator-hit-area-height: 10px !default; -$carousel-indicator-spacer: 3px !default; -$carousel-indicator-opacity: .5 !default; -$carousel-indicator-active-bg: $white !default; -$carousel-indicator-active-opacity: 1 !default; -$carousel-indicator-transition: opacity .6s ease !default; - -$carousel-caption-width: 70% !default; -$carousel-caption-color: $white !default; -$carousel-caption-padding-y: 1.25rem !default; -$carousel-caption-spacer: 1.25rem !default; - -$carousel-control-icon-width: 2rem !default; - -$carousel-control-prev-icon-bg: url("data:image/svg+xml,") !default; -$carousel-control-next-icon-bg: url("data:image/svg+xml,") !default; - -$carousel-transition-duration: .6s !default; -$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`) -// scss-docs-end carousel-variables - -// scss-docs-start carousel-dark-variables -$carousel-dark-indicator-active-bg: $black !default; // Deprecated in v5.3.4 -$carousel-dark-caption-color: $black !default; // Deprecated in v5.3.4 -$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default; // Deprecated in v5.3.4 -// scss-docs-end carousel-dark-variables - - -// Spinners - -// scss-docs-start spinner-variables -$spinner-width: 2rem !default; -$spinner-height: $spinner-width !default; -$spinner-vertical-align: -.125em !default; -$spinner-border-width: .25em !default; -$spinner-animation-speed: .75s !default; - -$spinner-width-sm: 1rem !default; -$spinner-height-sm: $spinner-width-sm !default; -$spinner-border-width-sm: .2em !default; -// scss-docs-end spinner-variables - - -// Close - -// scss-docs-start close-variables -$btn-close-width: 1em !default; -$btn-close-height: $btn-close-width !default; -$btn-close-padding-x: .25em !default; -$btn-close-padding-y: $btn-close-padding-x !default; -$btn-close-color: $black !default; -$btn-close-bg: url("data:image/svg+xml,") !default; -$btn-close-focus-shadow: $focus-ring-box-shadow !default; -$btn-close-opacity: .5 !default; -$btn-close-hover-opacity: .75 !default; -$btn-close-focus-opacity: 1 !default; -$btn-close-disabled-opacity: .25 !default; -$btn-close-filter: null !default; -$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default; // Deprecated in v5.3.4 -// scss-docs-end close-variables - - -// Offcanvas - -// scss-docs-start offcanvas-variables -$offcanvas-padding-y: $modal-inner-padding !default; -$offcanvas-padding-x: $modal-inner-padding !default; -$offcanvas-horizontal-width: 400px !default; -$offcanvas-vertical-height: 30vh !default; -$offcanvas-transition-duration: .3s !default; -$offcanvas-border-color: $modal-content-border-color !default; -$offcanvas-border-width: $modal-content-border-width !default; -$offcanvas-title-line-height: $modal-title-line-height !default; -$offcanvas-bg-color: var(--#{$prefix}body-bg) !default; -$offcanvas-color: var(--#{$prefix}body-color) !default; -$offcanvas-box-shadow: $modal-content-box-shadow-xs !default; -$offcanvas-backdrop-bg: $modal-backdrop-bg !default; -$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default; -// scss-docs-end offcanvas-variables - -// Code - -$code-font-size: $small-font-size !default; -$code-color: $pink !default; - -$kbd-padding-y: .1875rem !default; -$kbd-padding-x: .375rem !default; -$kbd-font-size: $code-font-size !default; -$kbd-color: var(--#{$prefix}body-bg) !default; -$kbd-bg: var(--#{$prefix}body-color) !default; -$nested-kbd-font-weight: null !default; // Deprecated in v5.2.0, removing in v6 - -$pre-color: null !default; - -@import "variables-dark"; // TODO: can be removed safely in v6, only here to avoid breaking changes in v5.3 diff --git a/assets/stylesheets/bootstrap/bootstrap-grid.scss b/assets/stylesheets/bootstrap/bootstrap-grid.scss new file mode 100644 index 00000000..7dff9bf7 --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-grid.scss @@ -0,0 +1,68 @@ +@use "banner" with ( + $file: "Grid" +); + +@use "config" as *; +@use "functions" as *; + +@forward "utilities"; // Make utilities available downstream +@use "utilities" as *; // Bring utilities into the current namespace + +@forward "layout/containers"; +@forward "layout/grid"; + +// stylelint-disable-next-line scss/dollar-variable-default +$utilities: map-get-multiple( + $utilities, + ( + "display", + "order", + "grid-column-counts", + "grid-columns", + "grid-auto-flow", + "gap", + "row-gap", + "column-gap", + "flex", + "flex-direction", + "flex-grow", + "flex-shrink", + "flex-wrap", + "justify-content", + "justify-items", + "align-items", + "align-content", + "align-self", + "place-items", + "margin", + "margin-x", + "margin-y", + "margin-top", + "margin-end", + "margin-bottom", + "margin-start", + "negative-margin", + "negative-margin-x", + "negative-margin-y", + "negative-margin-top", + "negative-margin-end", + "negative-margin-bottom", + "negative-margin-start", + "padding", + "padding-x", + "padding-y", + "padding-top", + "padding-end", + "padding-bottom", + "padding-start", + ) +); + +// check-unused-imports-disable-next-line — side-effect import: generates utility CSS. +@use "utilities/api"; + +:root { + @each $name, $value in $breakpoints { + --breakpoint-#{$name}: #{$value}; + } +} diff --git a/assets/stylesheets/bootstrap/bootstrap-reboot.scss b/assets/stylesheets/bootstrap/bootstrap-reboot.scss new file mode 100644 index 00000000..d8b59518 --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-reboot.scss @@ -0,0 +1,6 @@ +@use "banner" with ( + $file: "Reboot" +); + +@forward "root"; +@forward "content/reboot"; diff --git a/assets/stylesheets/bootstrap/bootstrap-utilities.scss b/assets/stylesheets/bootstrap/bootstrap-utilities.scss new file mode 100644 index 00000000..44ffa78f --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-utilities.scss @@ -0,0 +1,13 @@ +@use "banner" with ( + $file: "Utilities" +); + +// Layout & components +@forward "root"; + +// Helpers +@forward "helpers"; + +// Utilities +@forward "utilities"; +@forward "utilities/api"; diff --git a/assets/stylesheets/bootstrap/buttons/_button-group.scss b/assets/stylesheets/bootstrap/buttons/_button-group.scss new file mode 100644 index 00000000..0a86f95e --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_button-group.scss @@ -0,0 +1,135 @@ +@use "../mixins/border-radius" as *; + +@layer components { + // Make the div behave like a button + .btn-group, + .btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > [class*="btn-"] { + position: relative; + flex: 1 1 auto; + + &:hover { + z-index: 1; + } + } + + > .btn-check:has(input:checked), + > [class*="btn-"]:active, + > [class*="btn-"].active { + z-index: 2; + } + + > .btn-check:has(input:focus), + > [class*="btn-"]:focus { + z-index: 3; + } + } + + .btn-group-divider { + > [class*="btn-"] + [class*="btn-"] { + &::before { + position: absolute; + // top: 25%; + // bottom: 25%; + // left: calc(var(--btn-border-width) * -1); + z-index: 3; + // width: var(--btn-border-width); + content: ""; + background-color: var(--btn-color); + opacity: .25; + } + } + } + + .btn-group:where(.btn-group-divider) { + > [class*="btn-"] + [class*="btn-"] { + &::before { + top: 25%; + bottom: 25%; + left: calc(var(--btn-border-width) * -1); + width: var(--btn-border-width); + } + } + } + + .btn-group-vertical:where(.btn-group-divider) { + > [class*="btn-"] + [class*="btn-"] { + &::before { + top: calc(var(--btn-border-width) * -1); + right: var(--btn-padding-x); + left: var(--btn-padding-x); + height: var(--btn-border-width); + } + } + } + + // Optional: Group multiple button groups together for a toolbar + .btn-toolbar { + display: flex; + flex-wrap: wrap; + gap: .5rem; + justify-content: flex-start; + + .input-group { + width: auto; + } + } + + .btn-group { + @include border-radius(var(--btn-border-radius)); + + // Prevent double borders when buttons are next to each other + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) { + margin-inline-start: calc(-1 * var(--btn-border-width)); + } + + // Reset rounded corners + > [class*="btn-"]:not(:last-child, :has(+ .menu)), + > .btn-group:not(:last-child) > [class*="btn-"] { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is not the first child + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) > [class*="btn-"] { + @include border-start-radius(0); + } + } + + // + // Vertical button groups + // + + .btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > [class*="btn-"], + > .btn-group { + width: 100%; + } + + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: calc(-1 * var(--btn-border-width)); + } + + // Reset rounded corners + > [class*="btn-"]:not(:last-child, :has(+ .menu)), + > .btn-group:not(:last-child) > [class*="btn-"] { + @include border-bottom-radius(0); + } + + // The top radius should be 0 if the button is not the first child + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) > [class*="btn-"] { + @include border-top-radius(0); + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/_button.scss b/assets/stylesheets/bootstrap/buttons/_button.scss new file mode 100644 index 00000000..a778c09a --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_button.scss @@ -0,0 +1,451 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:meta"; +@use "sass:string"; +@use "../config" as *; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function, scss/dollar-variable-default + +$button-tokens: () !default; + +// scss-docs-start btn-tokens +$button-tokens: defaults( + ( + --btn-min-height: var(--btn-input-min-height), + --btn-padding-x: var(--btn-input-padding-x), + --btn-padding-y: var(--btn-input-padding-y), + --btn-font-size: var(--btn-input-font-size), + --btn-font-weight: var(--btn-input-font-weight), + --btn-line-height: var(--btn-input-line-height), + --btn-color: var(--fg-body), + --btn-white-space: nowrap, + --btn-border-width: var(--border-width), + --btn-border-color: transparent, + --btn-border-radius: var(--radius-5), + --btn-hover-border-color: transparent, + --btn-disabled-opacity: .65, + --btn-transition-timing: .15s ease-in-out, + --btn-transition-property: "color, background-color, border-color, box-shadow", + --btn-transition: var(--btn-transition-property) var(--btn-transition-timing), + ), + $button-tokens +); +// scss-docs-end btn-tokens + +$button-link-tokens: () !default; + +// scss-docs-start button-link-tokens +$button-link-tokens: defaults( + ( + --btn-font-weight: var(--font-weight-normal), + --btn-color: var(--link-color), + --btn-bg: transparent, + --btn-border-color: transparent, + --btn-hover-color: var(--link-hover-color), + --btn-hover-bg: transparent, + --btn-hover-border-color: transparent, + --btn-active-color: var(--link-hover-color), + --btn-active-bg: transparent, + --btn-active-border-color: transparent, + --btn-disabled-color: var(--fg-3), + --btn-disabled-border-color: transparent, + ), + $button-link-tokens +); +// scss-docs-end button-link-tokens + +$button-styled-tokens: () !default; + +// scss-docs-start button-styled-tokens +$button-styled-tokens: defaults( + ( + --btn-gradient-start: rgb(255 255 255 / 12.5%), + --btn-gradient-end: rgb(0 0 0 / 7.5%) , + --btn-border-mix-color: #000, + --btn-border-mix-amount: 10%, + --btn-border-hover-mix-amount: 12.5%, + --btn-border-active-mix-amount: 20%, + --btn-shadow: "0 1px 2px rgb(0 0 0 / 15%), inset 0 1px 0 rgb(255 255 255 / 10%)", + --btn-active-shadow: inset 0 2px 4px rgb(0 0 0 / .15) , + ), + $button-styled-tokens +); +// scss-docs-end button-styled-tokens + +// scss-docs-start button-sizes +$button-sizes: () !default; +$button-sizes: defaults( + ("xs", "sm", "lg"), + $button-sizes +); +// scss-docs-end button-sizes + +$button-variants: () !default; + +// scss-docs-start btn-variants +$button-variants: defaults( + ( + "solid": ( + "base": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ), + "hover": ( + "bg": "bg", + "border-color": "bg", + "color": "contrast" + ), + "active": ( + "bg": "bg", + "border-color": "bg", + "color": "contrast" + ) + ), + "outline": ( + "base": ( + "bg": "transparent", + "color": "fg", + "border-color": "border" + ), + "hover": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ), + "active": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ) + ), + "subtle": ( + "base": ( + "bg": "bg-subtle", + "color": "fg", + "border-color": "transparent" + ), + "hover": ( + "bg": ("bg-muted", "bg-subtle"), + "color": "fg-emphasis" + ), + "active": ( + "bg": "bg-subtle", + "color": "fg-emphasis" + ) + ), + "text": ( + "base": ( + "color": "fg", + "bg": "transparent", + "border-color": "transparent" + ), + "hover": ( + "color": "fg", + "bg": "bg-subtle" + ), + "active": ( + "color": "fg", + "bg": "bg-subtle" + ) + ) + ), + $button-variants +); +// scss-docs-end btn-variants +// stylelint-enable custom-property-no-missing-var-function, scss/dollar-variable-default + +// +// Base styles +// + +// scss-docs-start btn-variant-selectors +$btn-variant-selectors: (string.unquote(".btn"), string.unquote(".btn-link"), string.unquote(".btn-icon")) !default; +@each $variant, $config in $button-variants { + $btn-variant-selectors: list.append($btn-variant-selectors, string.unquote(".btn-#{$variant}"), comma); +} +// scss-docs-end btn-variant-selectors + +@layer components { + #{$btn-variant-selectors} { + @include tokens($button-tokens); + + display: inline-flex; + gap: var(--btn-gap, .25rem); + align-items: center; + justify-content: center; + min-height: var(--btn-min-height); + padding: var(--btn-padding-y) var(--btn-padding-x); + // font-family: var(--btn-font-family); + font-size: var(--btn-font-size); + font-weight: var(--btn-font-weight); + line-height: var(--btn-line-height); + color: var(--btn-color); + text-decoration: none; + white-space: var(--btn-white-space); + vertical-align: middle; + // stylelint-disable-next-line scss/at-function-named-arguments + cursor: if(sass($enable-button-pointers): pointer; else: null); + user-select: none; + background-color: var(--btn-bg, var(--bg-2)); + border: var(--btn-border-width) solid var(--btn-border-color); + @include border-radius(var(--btn-border-radius)); + @include transition(var(--btn-transition)); + + &:hover { + color: var(--btn-hover-color); + background-color: var(--btn-hover-bg, var(--bg-3)); + border-color: var(--btn-hover-border-color); + } + + &:focus-visible { + @include focus-ring(true); + --focus-ring-offset: 1px; + } + + &.active, + &.show { + color: var(--btn-active-color); + background-color: var(--btn-active-bg, var(--bg-3)); + border-color: var(--btn-active-border-color); + + &:focus-visible { + @include focus-ring(true); + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + color: var(--btn-disabled-color); + pointer-events: none; + background-color: var(--btn-disabled-bg, var(--bg-1)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-disabled-border-color); + opacity: var(--btn-disabled-opacity); + } + } + + // Main button style generator mixin + // Generate button variant classes (e.g., .btn-solid, .btn-outline, etc.) + // scss-docs-start btn-variant-mixin + @each $variant, $config in $button-variants { + .btn-#{$variant} { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-#{$property}: transparent; + } @else { + --btn-#{$property}: var(--theme-#{$value}); + } + } + + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --btn-active-#{$property}: var(--theme-#{$value}); + } @else { + --btn-active-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-disabled-#{$property}: transparent; + } @else { + --btn-disabled-#{$property}: var(--theme-#{$value}); + } + } + + &:hover { + @each $property, $value in map.get($button-variants, $variant, "hover") { + @if $value == "transparent" { + --btn-hover-#{$property}: transparent; + } @else if meta.type-of($value) == "list" { + $first-value: list.nth($value, 1); + $second-value: list.nth($value, 2); + --btn-hover-#{$property}: color-mix(in oklch, var(--theme-#{$first-value}) 50%, var(--theme-#{$second-value})); + } @else if $value == "bg-subtle" { + --btn-hover-#{$property}: var(--theme-#{$value}); + } @else { + --btn-hover-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .95) calc(c * 1.1) h); + } + } + } + + &:focus-visible { + outline-color: var(--theme-focus-ring); + } + + &:active, + &.active, + &.btn-check:has(input:checked) { + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --btn-active-#{$property}: var(--theme-#{$value}); + } @else { + --btn-active-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + } + + // Disabled state for toggle buttons + &:disabled, + &.disabled, + &.btn-check:has(input:disabled) { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-disabled-#{$property}: transparent; + } @else { + --btn-disabled-#{$property}: var(--theme-#{$value}); + } + } + } + } + } + // scss-docs-end btn-variant-mixin + + // + // Link buttons + // + + // Make a button look and behave like a link + .btn-link { + @include tokens($button-link-tokens); + + color: var(--theme-fg, var(--btn-color)); + text-decoration: var(--link-decoration); + + @if $enable-gradients { + background-image: none; + } + + &:focus-visible { + color: var(--theme-fg, var(--btn-color)); + } + + &:hover { + color: var(--theme-fg-emphasis, var(--btn-hover-color)); + } + + // No need for an active state here + } + + // + // Button Sizes + // + + // Generate button size classes from the $button-sizes map + // Skip "md" as it's the default size for .btn + + // scss-docs-start btn-sizes-loop + @each $size, $_ in $button-sizes { + .btn-#{$size}, + .btn-group-#{$size} > [class*="btn-"] { + --btn-min-height: var(--btn-input-#{$size}-min-height); + --btn-padding-y: var(--btn-input-#{$size}-padding-y); + --btn-padding-x: var(--btn-input-#{$size}-padding-x); + --btn-font-size: var(--btn-input-#{$size}-font-size); + --btn-line-height: var(--btn-input-#{$size}-line-height); + --btn-border-radius: var(--btn-input-#{$size}-border-radius); + } + } + // scss-docs-end btn-sizes-loop + + .btn-icon { + align-items: center; + justify-content: center; + aspect-ratio: 1; + padding: 0; + } + + // + // Toggle buttons (.btn-check) + // + // Checkbox and radio inputs that look like buttons. Add .btn-check to a + // label with button classes, with the input nested inside. + // + // Example: Toggle + + .btn-check { + > input { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; + } + + &:has(input:checked) { + color: var(--btn-active-color); + background-color: var(--btn-active-bg, var(--bg-3)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-active-border-color); + @include box-shadow(var(--btn-active-shadow)); + } + + &:has(input:focus-visible) { + @include focus-ring(true); + --focus-ring-offset: 1px; + } + + &:has(input:disabled) { + color: var(--btn-disabled-color); + pointer-events: none; + background-color: var(--btn-disabled-bg, var(--bg-1)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-disabled-border-color); + opacity: var(--btn-disabled-opacity); + @include box-shadow(none); + } + } + + // + // Styled buttons + // + // Add visual depth with gradients and shadows. Customize via CSS variables. + + .btn-styled { + @include tokens($button-styled-tokens); + + background-image: + linear-gradient( + to bottom, + var(--btn-gradient-start), + var(--btn-gradient-end) + ); + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-mix-amount)); + box-shadow: var(--btn-shadow); + + &:hover { + background-image: + linear-gradient( + to bottom, + var(--btn-gradient-start), + var(--btn-gradient-end) + ); + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-hover-mix-amount)); + } + + &:active, + &.active { + background-image: none; + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-active-mix-amount)); + box-shadow: var(--btn-active-shadow); + } + + &:disabled, + &.disabled { + background-image: none; + box-shadow: none; + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/_close.scss b/assets/stylesheets/bootstrap/buttons/_close.scss new file mode 100644 index 00000000..7cb76120 --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_close.scss @@ -0,0 +1,63 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; +@use "../mixins/tokens" as *; + +$btn-close-tokens: () !default; + +// scss-docs-start btn-close-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$btn-close-tokens: defaults( + ( + --btn-close-size: 1.5rem, + --btn-close-color: inherit, + --btn-close-icon: #{escape-svg(url("data:image/svg+xml,"))}, + --btn-close-opacity: .5, + --btn-close-hover-opacity: .75, + --btn-close-focus-opacity: .85, + --btn-close-disabled-opacity: .25, + ), + $btn-close-tokens +); +// scss-docs-end btn-close-tokens + +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +@layer components { + .btn-close { + @include tokens($btn-close-tokens); + + box-sizing: content-box; + min-width: var(--btn-close-size); + min-height: var(--btn-close-size); + padding: 0; + color: var(--btn-close-color); + background-color: currentcolor; + border: 0; // for button elements + @include border-radius(var(--radius-5)); + opacity: var(--btn-close-opacity); + @include mask-icon(var(--btn-close-icon)); + + // Override 's hover style + &:hover { + color: var(--btn-close-color); + text-decoration: none; + opacity: var(--btn-close-hover-opacity); + } + + &:focus-visible { + opacity: var(--btn-close-focus-opacity); + @include focus-ring(); + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: var(--btn-close-disabled-opacity); + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/index.scss b/assets/stylesheets/bootstrap/buttons/index.scss new file mode 100644 index 00000000..0122a4ef --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/index.scss @@ -0,0 +1,3 @@ +@forward "button"; +@forward "button-group"; +@forward "close"; diff --git a/assets/stylesheets/bootstrap/content/_images.scss b/assets/stylesheets/bootstrap/content/_images.scss new file mode 100644 index 00000000..c1f17212 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_images.scss @@ -0,0 +1,74 @@ +@use "../functions" as *; +@use "../mixins/image" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/tokens" as *; + +$thumbnail-tokens: () !default; + +// scss-docs-start thumbnail-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$thumbnail-tokens: defaults( + ( + --thumbnail-padding: .25rem, + --thumbnail-bg: var(--bg-body), + --thumbnail-border-width: var(--border-width), + --thumbnail-border-color: var(--border-color), + --thumbnail-border-radius: var(--radius-5), + --thumbnail-box-shadow: var(--box-shadow-sm), + ), + $thumbnail-tokens +); +// scss-docs-end thumbnail-tokens + +$figure-tokens: () !default; + +// scss-docs-start figure-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$figure-tokens: defaults( + ( + --figure-gap: calc(var(--spacer) * .5), + --figure-caption-font-size: var(--font-size-sm), + --figure-caption-color: var(--fg-3), + ), + $figure-tokens +); +// scss-docs-end figure-tokens + +@layer content { + // Responsive images (ensure images don't scale beyond their parents) + // + // This is purposefully opt-in via an explicit class rather than being the default for all ``s. + // We previously tried the "images are responsive by default" approach in Bootstrap v2, + // and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps) + // which weren't expecting the images within themselves to be involuntarily resized. + // See also https://github.com/twbs/bootstrap/issues/18178 + .img-fluid { + @include img-fluid(); + } + + .img-thumbnail { + @include tokens($thumbnail-tokens); + padding: var(--thumbnail-padding); + background-color: var(--thumbnail-bg); + border: var(--thumbnail-border-width) solid var(--thumbnail-border-color); + @include border-radius(var(--thumbnail-border-radius)); + @include box-shadow(var(--thumbnail-box-shadow)); + + // Keep them at most 100% wide + @include img-fluid(); + } + + .figure { + @include tokens($figure-tokens); + // Ensures the caption's text aligns with the image. + display: flex; + flex-direction: column; + gap: var(--figure-gap); + } + + .figure-caption { + font-size: var(--figure-caption-font-size); + color: var(--figure-caption-color); + } +} diff --git a/assets/stylesheets/bootstrap/content/_prose.scss b/assets/stylesheets/bootstrap/content/_prose.scss new file mode 100644 index 00000000..0408e9ba --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_prose.scss @@ -0,0 +1,143 @@ +@use "../functions" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function +$prose-tokens: () !default; + +// scss-docs-start prose-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$prose-tokens: defaults( + ( + --content-font-size: 1rem, + --content-line-height: 1.5, + --content-gap: calc(var(--content-font-size) * var(--content-line-height)), + --heading-color: light-dark(var(--gray-900), var(--white)), + ), + $prose-tokens +); +// scss-docs-end prose-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer content { + .prose { + @include tokens($prose-tokens); + position: relative; + display: flex; + flex-direction: column; + gap: var(--content-gap); + max-width: 1000px; + margin-inline: auto; + font-size: var(--content-font-size); + line-height: var(--content-line-height); + + @media (width >= 1024px) { + --content-font-size: var(--font-size-md); + --content-line-height: 1.625; + // --content-gap: calc(var(--content-font-size) * var(--content-line-height)); + } + + :where(p, ul, ol, dl, pre, table, blockquote):not(:where(.not-prose, .not-prose *)) { + margin-block: 0; + } + + :where(ul, ol):not([class], :where(.not-prose, .not-prose *)) li:not(:last-child) { + margin-bottom: calc(var(--content-gap) / 4); + } + + :where(li ul, li ol):not(:where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) / 4); + } + + :where(hr):not(:where(.not-prose, .not-prose *)) { + margin: calc(var(--content-gap) * 1.5) 0; + border: 0; + border-block-start: var(--border-width) solid var(--hr-border-color); + } + + :where(h1, h2, h3, h4, h5, h6):not([class], :where(.not-prose, .not-prose *)) { + margin-top: 0; + margin-bottom: calc(var(--content-gap) / -2); + font-weight: 500; + line-height: 1.25; + + code { + font-weight: 600; + color: inherit; + } + } + + :where(h1, h2):not(:first-child, :where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) * .75); + } + + :where(h3, h4, h5, h6):not(:first-child, :where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) * .5); + } + + :where(h1):not(:where(.not-prose, .not-prose *)) { + font-size: 2.25em; + line-height: 1.1; + } + :where(h2):not(:where(.not-prose, .not-prose *)) { + font-size: 1.75em; + } + :where(h3):not(:where(.not-prose, .not-prose *)) { + font-size: 1.5em; + } + :where(h4):not(:where(.not-prose, .not-prose *)) { + font-size: 1.25em; + } + :where(h5):not(:where(.not-prose, .not-prose *)) { + font-size: 1.125em; + } + :where(h6):not(:where(.not-prose, .not-prose *)) { + font-size: 1em; + } + + :where(a:not([class])):not(:where(.not-prose, .not-prose *)) { + color: var(--link-color); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--link-color) 25%, transparent); + text-underline-offset: 4px; + @include transition(.1s text-decoration-color ease-in-out); + + &:hover { + text-decoration-color: var(--link-hover-color); + } + } + + :where(img):not(:where(.not-prose, .not-prose *)) { + max-width: 100%; + } + + :where(blockquote):not(:where(.not-prose, .not-prose *)) { + padding-inline-start: calc(var(--content-gap) / 2); + margin: 0; + border-inline-start: 4px solid var(--border-color); + } + + :where(table):not(:where(.not-prose, .not-prose *)) { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + } + + :where(table:not([class])):not(:where(.not-prose, .not-prose *)) { + td, + th { + padding: 6px 12px; + text-align: inherit; + border: 1px solid var(--border-color); + } + } + + :where(dt):not(:where(.not-prose, .not-prose *)) { + font-weight: 500; + } + + :where(video, img):not(:where(.not-prose, .not-prose *)) { + max-width: 100%; + } + } +} diff --git a/assets/stylesheets/bootstrap/content/_reboot.scss b/assets/stylesheets/bootstrap/content/_reboot.scss new file mode 100644 index 00000000..578a8881 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_reboot.scss @@ -0,0 +1,635 @@ +@use "../config" as *; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix + +$reboot-kbd-tokens: () !default; +$reboot-mark-tokens: () !default; + +// scss-docs-start reboot-kbd-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$reboot-kbd-tokens: defaults( + ( + --kbd-padding-y: .125rem, + --kbd-padding-x: .25rem, + --kbd-font-size: var(--font-size-xs), + --kbd-color: var(--bg-body), + --kbd-bg: var(--fg-2), + --kbd-border-radius: var(--radius-5), + ), + $reboot-kbd-tokens +); +// scss-docs-end reboot-kbd-tokens + +// scss-docs-start reboot-mark-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$reboot-mark-tokens: defaults( + ( + --mark-padding: .1875em, + --mark-color: var(--fg-body), + --mark-bg: light-dark(var(--yellow-100), var(--yellow-900)), + ), + $reboot-mark-tokens +); +// scss-docs-end reboot-mark-tokens + +@layer reboot { + // Reboot + // + // Normalization of HTML elements, manually forked from Normalize.css to remove + // styles targeting irrelevant browsers while applying new styles. + // + // Normalize is licensed MIT. https://github.com/necolas/normalize.css + + // Document + // + // Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. + + *, + *::before, + *::after { + box-sizing: border-box; + } + + // Root + // + // Ability to the value of the root font sizes, affecting the value of `rem`. + // null by default, thus nothing is generated. + + :root { + // Assume browser default font-size of 16px, or a user's preference + accent-color: var(--primary-base); + + @if $enable-smooth-scroll { + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } + } + } + + // Reset iframe color-scheme + // + // If the color scheme of an iframe differs from parent document, iframe gets + // an opaque canvas background appropriate (resulting in a white background + // on the iframe when in a page with a dark color scheme). + iframe { + color-scheme: light dark; + border: 0; + } + + // Body + // + // 1. Remove the margin in all browsers. + // 2. As a best practice, apply a default `background-color`. + // 3. Prevent adjustments of font size after orientation changes in iOS. + // 4. Change the default tap highlight to be completely transparent in iOS. + + // scss-docs-start reboot-body-rules + body { + margin: 0; // 1 + font-family: var(--body-font-family); + font-size: var(--body-font-size); + font-weight: var(--body-font-weight); + line-height: var(--body-line-height); + color: var(--fg-body); + text-align: var(--body-text-align); + background-color: var(--bg-body); // 2 + -webkit-text-size-adjust: 100%; // 3 + -webkit-tap-highlight-color: transparent; // 4 + } + // scss-docs-end reboot-body-rules + + hr { + margin: var(--hr-margin-y, var(--spacer)) 0; + border: 0; + border-block-start: var(--border-width) solid var(--hr-border-color); + } + + // Typography + // + // 1. Remove top margins from headings + // By default, ``-`` all receive top and bottom margins. We nuke the top + // margin for easier control within type scales as it avoids margin collapsing. + + %heading { + margin-top: 0; // 1 + margin-bottom: $headings-margin-bottom; + font-family: $headings-font-family; + font-style: $headings-font-style; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + color: var(--heading-color); + } + + h1, + .h1 { + @extend %heading; + font-size: var(--font-size-3xl); + } + + h2, + .h2 { + @extend %heading; + font-size: var(--font-size-2xl); + } + + h3, + .h3 { + @extend %heading; + font-size: var(--font-size-xl); + } + + h4, + .h4 { + @extend %heading; + font-size: var(--font-size-lg); + } + + h5, + .h5 { + @extend %heading; + font-size: var(--font-size-md); + } + + h6, + .h6 { + @extend %heading; + font-size: var(--font-size-sm); + } + + // Reset margins on paragraphs + // + // Similarly, the top margin on ``s get reset. However, we also reset the + // bottom margin to use `rem` units instead of `em`. + + p { + margin-top: 0; + margin-bottom: $paragraph-margin-bottom; + } + + // Abbreviations + // + // 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. + // 2. Add explicit cursor to indicate changed behavior. + // 3. Prevent the text-decoration to be skipped. + + abbr[title] { + text-decoration: underline dotted; // 1 + cursor: help; // 2 + text-decoration-skip-ink: none; // 3 + } + + // Address + + address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; + } + + // Lists + + ol, + ul { + padding-inline-start: 2rem; + } + + ol, + ul, + dl { + margin-top: 0; + margin-bottom: 1rem; + } + + ol ol, + ul ul, + ol ul, + ul ol { + margin-bottom: 0; + } + + dt { + font-weight: $dt-font-weight; + } + + // 1. Undo browser default + + dd { + margin-inline-start: 0; // 1 + margin-bottom: .5rem; + } + + // Blockquote + + blockquote { + margin: 0 0 1rem; + > * { + margin-block: 0; + } + } + + // Strong + // + // Add the correct font weight in Chrome, Edge, and Safari + + b, + strong { + font-weight: $font-weight-bolder; + } + + // Small + // + // Add the correct font size in all browsers + + small, + .small { + font-size: var(--small-font-size, 87.5%); + } + + // Mark + + mark, + .mark { + @include tokens($reboot-mark-tokens); + padding: var(--mark-padding); + color: var(--mark-color); + background-color: var(--mark-bg); + } + + // Sub and Sup + // + // Prevent `sub` and `sup` elements from affecting the line height in + // all browsers. + + sub, + sup { + position: relative; + font-size: var(--sub-sup-font-size, .75em); + line-height: 0; + vertical-align: baseline; + } + + sub { bottom: -.25em; } + sup { top: -.5em; } + + // Links + + a { + color: var(--theme-fg, var(--link-color)); + text-decoration: var(--link-decoration); + text-underline-offset: $link-underline-offset; + + &:hover { + // --link-color: var(--link-hover-color); + // --link-decoration: var(--link-hover-decoration, var(--link-decoration)); + color: var(--theme-fg-emphasis, var(--link-hover-color)); + text-decoration: var(--link-hover-decoration, var(--link-decoration)); + } + } + + // And undo these styles for placeholder links/named anchors (without href). + // It would be more straightforward to just use a[href] in previous block, but that + // causes specificity issues in many other styles that are too complex to fix. + // See https://github.com/twbs/bootstrap/issues/19402 + + a:not([href], [class]) { + &, + &:hover { + color: inherit; + text-decoration: none; + } + } + + // Code + + pre, + code, + kbd, + samp { + font-family: var(--font-mono); + font-size: 1em; // Correct the odd `em` font sizing in all browsers. + } + + // 1. Remove browser default top margin + // 2. Reset browser default of `1em` to use `rem`s + // 3. Don't allow content to break outside + + pre { + display: block; + margin-top: 0; // 1 + margin-bottom: 1rem; // 2 + overflow: auto; // 3 + font-size: var(--code-font-size); + color: var(--code-color, inherit); + + // Account for some code outputs that place code tags in pre tags + code { + font-size: inherit; + color: inherit; + word-break: normal; + } + } + + code { + font-size: var(--code-font-size); + color: var(--code-color); + word-wrap: break-word; + + // Streamline the style when inside anchors to avoid broken underline and more + a > & { + color: inherit; + } + } + + kbd { + @include tokens($reboot-kbd-tokens); + padding: var(--kbd-padding-y) var(--kbd-padding-x); + font-size: var(--kbd-font-size); + color: var(--kbd-color); + background-color: var(--kbd-bg); + @include border-radius(var(--kbd-border-radius)); + + kbd { + padding: 0; + font-size: 1em; + font-weight: inherit; // mdo-do: check if this is needed + } + } + + // Figures + // + // Apply a consistent margin strategy (matches our type styles). + + figure { + margin: 0 0 1rem; + } + + // Images and content + + img, + svg { + vertical-align: middle; + } + + // Tables + // + // Prevent double borders + + table { + caption-side: bottom; + border-collapse: collapse; + } + + caption { + // padding-top: $table-cell-padding-y; + // padding-bottom: $table-cell-padding-y; + // color: $table-caption-color; + padding-block: .5rem; + color: var(--fg-3); + text-align: start; + } + + // 1. Removes font-weight bold by inheriting + // 2. Matches default `` alignment by inheriting `text-align`. + // 3. Fix alignment for Safari + + th { + // font-weight: $table-th-font-weight; // 1 // mdo-do: it's null by default. maybe we remove? + text-align: inherit; // 2 + text-align: -webkit-match-parent; // 3 + } + + thead, + tbody, + tfoot, + tr, + td, + th { + border-color: inherit; + border-style: solid; + border-width: 0; + } + + // Forms + // + // 1. Allow labels to use `margin` for spacing. + + label { + display: inline-block; // 1 + } + + // Remove the default `border-radius` that macOS Chrome adds. + // See https://github.com/twbs/bootstrap/issues/24093 + + button { + // stylelint-disable-next-line property-disallowed-list + border-radius: 0; + } + + // Explicitly remove focus outline in Chromium when it shouldn't be + // visible (e.g. as result of mouse click or touch tap). It already + // should be doing this automatically, but seems to currently be + // confused and applies its very visible two-tone outline anyway. + + button:focus:not(:focus-visible) { + outline: 0; + } + + // 1. Remove the margin in Firefox and Safari + + input, + button, + select, + optgroup, + textarea { + margin: 0; // 1 + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + // Set the cursor for non-`` buttons + // + // Details at https://github.com/twbs/bootstrap/pull/30562 + [role="button"] { + cursor: pointer; + } + + select { + // Remove the inheritance of word-wrap in Safari. + // See https://github.com/twbs/bootstrap/issues/24990 + word-wrap: normal; + + // Undo the opacity change from Chrome + &:disabled { + opacity: 1; + } + } + + // Remove the dropdown arrow only from text type inputs built with datalists in Chrome. + // See https://stackoverflow.com/a/54997118 + + [list]:not([type="date"], [type="datetime-local"], [type="month"], [type="week"], [type="time"])::-webkit-calendar-picker-indicator { + display: none !important; + } + + // 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + // controls in Android 4. + // 2. Correct the inability to style clickable types in iOS and Safari. + // 3. Opinionated: add "hand" cursor to non-disabled button elements. + + button, + [type="button"], // 1 + [type="reset"], + [type="submit"] { + -webkit-appearance: button; // 2 + + @if $enable-button-pointers { + &:not(:disabled) { + cursor: pointer; // 3 + } + } + } + + // 1. Textareas should really only resize vertically so they don't break their (horizontal) containers. + + textarea { + resize: vertical; // 1 + } + + // 1. Browsers set a default `min-width: min-content;` on fieldsets, + // unlike e.g. ``s, which have `min-width: 0;` by default. + // So we reset that to ensure fieldsets behave more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359 + // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements + // 2. Reset the default outline behavior of fieldsets so they don't affect page layout. + + fieldset { + min-width: 0; // 1 + padding: 0; // 2 + margin: 0; // 2 + border: 0; // 2 + } + + // 1. By using `float: inline-start`, the legend will behave like a block element. + // This way the border of a fieldset wraps around the legend if present. + // 2. Fix wrapping bug. + // See https://github.com/twbs/bootstrap/issues/29712 + + legend { + float: inline-start; // 1 + width: 100%; + padding: 0; + margin-bottom: $legend-margin-bottom; + font-size: $legend-font-size; + font-weight: $legend-font-weight; + line-height: inherit; + + + * { + clear: inline-start; // 2 + } + } + + // Fix height of inputs with a type of datetime-local, date, month, week, or time + // See https://github.com/twbs/bootstrap/issues/18842 + + ::-webkit-datetime-edit-fields-wrapper, + ::-webkit-datetime-edit-text, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-meridiem-field, // WebKit + ::-webkit-datetime-edit-ampm-field, // Chromium + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-week-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-year-field { + padding: 0; + } + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + // 1. This overrides the extra rounded corners on search inputs in iOS so that our + // `.form-control` class can properly style them. Note that this cannot simply + // be added to `.form-control` as it's not specific enough. For details, see + // https://github.com/twbs/bootstrap/issues/11586. + // 2. Correct the outline style in Safari. + + [type="search"] { + -webkit-appearance: textfield; // 1 + outline-offset: -2px; // 2 + + // 3. Better affordance and consistent appearance for search cancel button + &::-webkit-search-cancel-button { + cursor: pointer; + filter: grayscale(1); + } + } + + // A few input types should stay LTR regardless of document direction + // See https://rtlstyling.com/posts/rtl-styling#form-inputs + + [type="tel"], + [type="url"], + [type="email"], + [type="number"] { + direction: ltr; + } + + // Remove the inner padding in Chrome and Safari on macOS. + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + // Remove padding around color pickers in webkit browsers + + ::-webkit-color-swatch-wrapper { + padding: 0; + } + + // 1. Inherit font family and line height for file input buttons + // 2. Correct the inability to style clickable types in iOS and Safari. + + ::file-selector-button { + font: inherit; // 1 + -webkit-appearance: button; // 2 + } + + // Correct element displays + + output { + display: inline-block; + } + + // Summary + // + // 1. Add the correct display in all browsers + + summary { + display: list-item; // 1 + cursor: pointer; + } + + // Progress + // + // Add the correct vertical alignment in Chrome, Firefox, and Opera. + + progress { + vertical-align: baseline; + } + + // Hidden attribute + // + // Always hide an element with the `hidden` HTML attribute. + + [hidden] { + display: none !important; + } +} diff --git a/assets/stylesheets/bootstrap/content/_tables.scss b/assets/stylesheets/bootstrap/content/_tables.scss new file mode 100644 index 00000000..781d1f2c --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_tables.scss @@ -0,0 +1,255 @@ +@use "sass:map"; +@use "../config" as *; +@use "../functions" as *; +@use "../layout/breakpoints" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$table-tokens: () !default; + +// scss-docs-start table-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$table-tokens: defaults( + ( + --table-cell-padding-y: .5rem, + --table-cell-padding-x: .5rem, + --table-cell-vertical-align: top, + --table-color: var(--fg-body), + --table-bg: var(--bg-body), + --table-accent-bg: transparent, + --table-border-width: var(--border-width), + --table-border-color: var(--border-color), + --table-group-separator-color: currentcolor, + --table-striped-color: var(--table-color), + --table-striped-bg-factor: 5%, + --table-striped-bg: color-mix(in srgb, var(--table-color) var(--table-striped-bg-factor), transparent), + --table-active-color: var(--table-color), + --table-active-bg-factor: 10%, + --table-active-bg: color-mix(in srgb, var(--table-color) var(--table-active-bg-factor), transparent), + --table-hover-color: var(--table-color), + --table-hover-bg-factor: 7.5%, + --table-hover-bg: color-mix(in srgb, var(--table-color) var(--table-hover-bg-factor), transparent), + ), + $table-tokens +); +// scss-docs-end table-tokens +// stylelint-enable custom-property-no-missing-var-function + +$table-striped-order: odd !default; +$table-striped-columns-order: even !default; + +// +// Basic Bootstrap table +// + +@layer content { + .table { + @include tokens($table-tokens); + + // Reset needed for nesting tables + --table-color-type: initial; + --table-bg-type: initial; + --table-color-state: initial; + --table-bg-state: initial; + // End of reset + + width: 100%; + margin-bottom: var(--spacer); + vertical-align: var(--table-cell-vertical-align); + border-color: var(--theme-border, var(--table-border-color)); + + // Target th & td + // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class. + // We use the universal selectors here to simplify the selector (else we would need 6 different selectors). + // Another advantage is that this generates less code and makes the selector less specific making it easier to override. + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + padding: var(--table-cell-padding-y) var(--table-cell-padding-x); + // Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb + color: var(--table-color-state, var(--table-color-type, var(--theme-fg, var(--table-color)))); + background-color: var(--theme-bg-subtle, var(--table-bg)); + border-block-end-width: var(--table-border-width); + box-shadow: inset 0 0 0 9999px var(--table-bg-state, var(--table-bg-type, var(--theme-bg-subtle, var(--table-accent-bg)))); + } + + > tbody { + vertical-align: inherit; + } + + > thead { + vertical-align: bottom; + } + } + + .table-group-divider { + border-block-start: calc(var(--table-border-width) * 2) solid var(--table-group-separator-color); + } + + // + // Change placement of captions with a class + // + + .caption-top { + caption-side: top; + } + + // + // Condensed table w/ half padding + // + + .table-sm { + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + --table-cell-padding-y: .25rem; + --table-cell-padding-x: .25rem; + } + } + + // Border versions + // + // Add or remove borders all around the table and between all the columns. + // + // When borders are added on all sides of the cells, the corners can render odd when + // these borders do not have the same color or if they are semi-transparent. + // Therefore we add top and border bottoms to the `tr`s and left and right borders + // to the `td`s or `th`s + + .table-bordered { + > :not(caption) > * { + border-width: var(--table-border-width) 0; + + // stylelint-disable-next-line selector-max-universal + > * { + border-width: 0 var(--table-border-width); + } + } + } + + .table-borderless { + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + border-block-end-width: 0; + } + + > :not(:first-child) { + border-block-start-width: 0; + } + } + + // Zebra-striping + // + // Default zebra-stripe styles (alternating gray and transparent backgrounds) + + // For rows + .table-striped { + > tbody > tr:nth-of-type(#{$table-striped-order}) > * { + --table-color-type: var(--theme-fg, var(--table-striped-color)); + --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent); + } + } + + // For columns + .table-striped-columns { + > :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) { + --table-color-type: var(--theme-fg, var(--table-striped-color)); + --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent); + } + } + + // Active table + // + // The `.table-active` class can be added to highlight rows or cells + + .table-active { + --table-color-state: var(--theme-fg, var(--table-active-color)); + --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-active-bg-factor), transparent); + } + + // Hover effect + // + // Placed here since it has to come after the potential zebra striping + + .table-hover { + > tbody > tr:hover > * { + --table-color-state: var(--theme-fg, var(--table-hover-color)); + --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-hover-bg-factor), transparent); + } + } + + // Responsive tables + // + // Generate `.table-responsive` classes that act as container query contexts + // and enable horizontal scrolling when table content overflows. + + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + .#{$prefix}table-responsive { + container-type: inline-size; + + @include media-breakpoint-down($breakpoint) { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + } + } + + // Stacked tables + // + // Generate `.table-stacked` classes that convert table rows into stacked + // blocks using container queries. Requires a `.table-responsive` ancestor + // and `data-cell` attributes on `` elements for column labels. + + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + @include container-breakpoint-down($breakpoint) { + .#{$prefix}table-stacked { + > thead { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + > tbody > tr { + display: block; + padding-block: var(--table-cell-padding-y); + + + tr { + border-block-start: var(--table-border-width) solid var(--table-border-color); + } + + > td { + display: block; + padding: calc(var(--table-cell-padding-y) * .25) calc(var(--table-cell-padding-x) * 2); + border: 0; + + &:first-child { + font-weight: var(--font-weight-bold); + } + + // + td::before { + // margin-block-start: .25rem; + // } + + &[data-cell]:not(:first-child)::before { + display: block; + font-weight: var(--font-weight-semibold); + content: attr(data-cell); + } + } + + > td:not(:first-child) + td::before { + margin-block-start: .25rem; + } + } + } + } + } +} diff --git a/assets/stylesheets/bootstrap/content/_type.scss b/assets/stylesheets/bootstrap/content/_type.scss new file mode 100644 index 00000000..fec15e14 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_type.scss @@ -0,0 +1,86 @@ +@use "../functions" as *; +@use "../mixins/lists" as *; +@use "../mixins/tokens" as *; + +$blockquote-tokens: () !default; + +// scss-docs-start blockquote-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$blockquote-tokens: defaults( + ( + --blockquote-gap: calc(var(--spacer) / 2), + --blockquote-padding-x: var(--spacer), + --blockquote-margin-y: 1rem, + --blockquote-font-size: var(--font-size-md), + --blockquote-border-width: .25rem, + --blockquote-border-color: var(--border-color), + --blockquote-footer-font-size: var(--font-size-sm), + --blockquote-footer-color: var(--fg-3), + ), + $blockquote-tokens +); +// scss-docs-end blockquote-tokens + +@layer content { + // + // Lists + // + + .list-unstyled { + @include list-unstyled(); + } + + // Inline turns list items into inline-block + .list-inline { + @include list-unstyled(); + } + .list-inline-item { + display: inline-block; + + &:not(:last-child) { + margin-inline-end: var(--list-inline-padding, var(--spacer) / 2); + } + } + + // + // Misc + // + + // Builds on `abbr` + .initialism { + font-size: var(--initialism-font-size, var(--font-size-xs)); + text-transform: uppercase; + } + + // Blockquotes + .blockquote { + @include tokens($blockquote-tokens); + display: flex; + flex-direction: column; + gap: var(--blockquote-gap); + padding-inline-start: var(--blockquote-padding-x); + margin-bottom: var(--blockquote-margin-y); + font-size: var(--blockquote-font-size); + border-inline-start: var(--blockquote-border-width) solid var(--blockquote-border-color); + + > * { + margin-bottom: 0; + } + } + + // stylelint-disable-next-line selector-no-qualifying-type + figure.blockquote { + blockquote { + margin-bottom: 0; + } + } + + .blockquote-footer { + font-size: var(--blockquote-footer-font-size); + color: var(--blockquote-footer-color); + + &::before { + content: "\2014\00A0"; // em dash, nbsp + } + } +} diff --git a/assets/stylesheets/bootstrap/content/index.scss b/assets/stylesheets/bootstrap/content/index.scss new file mode 100644 index 00000000..8b141c94 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/index.scss @@ -0,0 +1,5 @@ +@forward "reboot"; +@forward "type"; +@forward "tables"; +@forward "images"; +@forward "prose"; diff --git a/assets/stylesheets/bootstrap/forms/_check.scss b/assets/stylesheets/bootstrap/forms/_check.scss new file mode 100644 index 00000000..86c354a0 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_check.scss @@ -0,0 +1,105 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$check-tokens: () !default; + +// scss-docs-start check-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$check-tokens: defaults( + ( + --check-size: 1.25rem, + --check-margin-block: .125rem, + --check-bg: var(--bg-body), + --check-border-color: var(--border-color), + --check-border-radius: var(--radius-5), + --check-icon-checked: #{escape-svg(url("data:image/svg+xml,"))}, + --check-icon-indeterminate: #{escape-svg(url("data:image/svg+xml,"))}, + --check-checked-bg: var(--control-checked-bg), + --check-checked-border-color: var(--control-checked-border-color), + --check-indeterminate-bg: var(--control-checked-bg), + --check-indeterminate-border-color: var(--control-checked-border-color), + --check-active-bg: var(--control-active-bg), + --check-active-border-color: var(--control-active-border-color), + --check-disabled-bg: var(--control-disabled-bg), + --check-disabled-opacity: var(--control-disabled-opacity), + ), + $check-tokens +); +// scss-docs-end check-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + // The class lives on the `` itself; `appearance: none` controls render + // pseudo-elements, so the mark is drawn directly on the input — no wrapper. + .check { + @include tokens($check-tokens); + + position: relative; + flex-shrink: 0; + width: var(--check-size); + height: var(--check-size); + margin-block: var(--check-margin-block); + appearance: none; + // later: maybe set a tertiary bg color? + background-color: var(--theme-bg, var(--check-bg)); + border: 1px solid var(--theme-bg, var(--check-border-color)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 33%; + + &:checked, + &:indeterminate { + background-color: var(--theme-bg, var(--check-checked-bg)); + border-color: var(--theme-bg, var(--check-checked-border-color)); + + // Check/indeterminate mark, overlaid on the input and rendered via a CSS + // mask so it inherits the contrast color without an inline SVG. + &::before { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + background-color: var(--theme-contrast, var(--primary-contrast)); + @include mask-icon(); + } + } + + &:checked::before { mask-image: var(--check-icon-checked); } + &:indeterminate::before { mask-image: var(--check-icon-indeterminate); } + + &:focus-visible { + @include focus-ring(true); + --focus-ring-offset: -1px; + } + + &:disabled { + --check-bg: var(--check-disabled-bg); + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + &:disabled:checked { + opacity: var(--check-disabled-opacity); + } + } + + .check-sm { + --check-size: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .check-lg { + --check-size: 1.5rem; + --check-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_chip-input.scss b/assets/stylesheets/bootstrap/forms/_chip-input.scss new file mode 100644 index 00000000..1fb6db31 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_chip-input.scss @@ -0,0 +1,74 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +$chip-input-tokens: () !default; + +// scss-docs-start chip-input-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$chip-input-tokens: defaults( + ( + --chip-input-padding-y: .75rem, + --chip-input-padding-x: .75rem, + --chip-input-gap: .375rem, + --chip-input-ghost-min-width: 5rem, + --control-fg: var(--btn-input-fg), + --control-bg: var(--btn-input-bg), + --control-border-width: var(--border-width), + --control-border-color: var(--border-color), + --control-border-radius: var(--radius-5), + ), + $chip-input-tokens +); +// scss-docs-end chip-input-tokens + +@layer forms { + .chip-input { + @include tokens($chip-input-tokens); + + // Flexbox wrapping layout + display: flex; + flex-wrap: wrap; + gap: var(--chip-input-gap); + align-items: center; + padding: var(--chip-input-padding-y) var(--chip-input-padding-x); + + color: var(--control-fg); + background-color: var(--control-bg); + border: var(--control-border-width) solid var(--control-border-color); + @include border-radius(var(--control-border-radius), 0); + + // Focus state when ghost input is focused + &:focus-within { + --focus-ring-offset: -1px; + border-color: var(--focus-ring-color); + @include focus-ring(true); + } + + // Ghost input fills remaining space + > .form-ghost { + flex: 1 1 0; + min-width: var(--chip-input-ghost-min-width); + min-height: 1.75rem; + } + + // Disabled state + &.disabled, + &:has(.form-ghost:disabled) { + cursor: not-allowed; + background-color: var(--bg-2); + opacity: 1; + + &:focus-within { + border-color: var(--control-border-color); + outline: 0; + } + + > .chip { + pointer-events: none; + opacity: var(--control-disabled-opacity); + } + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_combobox.scss b/assets/stylesheets/bootstrap/forms/_combobox.scss new file mode 100644 index 00000000..9b99994f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_combobox.scss @@ -0,0 +1,71 @@ +@use "../mixins/transition" as *; + +@layer components { + .combobox-toggle { + display: inline-flex; + gap: .5rem; + align-items: center; + justify-content: space-between; + width: 100%; + padding-inline-end: var(--control-padding-x); + text-align: start; + cursor: pointer; + + &.show { + background-color: var(--bg-1); + } + + &:disabled, + &.disabled { + cursor: not-allowed; + opacity: .65; + } + } + + .combobox-value { + display: flex; + flex: 1; + gap: .5rem; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .combobox-placeholder { + color: color-mix(in oklch, currentcolor 65%, transparent); + } + + .combobox-caret { + flex-shrink: 0; + @include transition(transform .2s ease-in-out); + + .show > & { + transform: rotate(180deg); + } + } + + .combobox-toggle + .menu { + --menu-max-height: 300px; + --menu-overflow-y: auto; + } + + .combobox-search { + position: sticky; + top: 0; + z-index: 1; + padding: var(--menu-padding-x, .25rem); + background-color: var(--menu-bg, var(--bg-body)); + } + + .combobox-search-input { + width: 100%; + } + + .combobox-no-results { + padding: 1rem; + font-size: var(--font-size-sm); + color: var(--fg-3); + text-align: center; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_floating-labels.scss b/assets/stylesheets/bootstrap/forms/_floating-labels.scss index 38df1155..8f9b79aa 100644 --- a/assets/stylesheets/bootstrap/forms/_floating-labels.scss +++ b/assets/stylesheets/bootstrap/forms/_floating-labels.scss @@ -1,97 +1,129 @@ -.form-floating { - position: relative; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; - > .form-control, - > .form-control-plaintext, - > .form-select { - height: $form-floating-height; - min-height: $form-floating-height; - line-height: $form-floating-line-height; - } +$form-floating-tokens: () !default; - > label { - position: absolute; - top: 0; - left: 0; - z-index: 2; - max-width: 100%; - height: 100%; // allow textareas - padding: $form-floating-padding-y $form-floating-padding-x; - overflow: hidden; - color: rgba(var(--#{$prefix}body-color-rgb), #{$form-floating-label-opacity}); - text-align: start; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model - transform-origin: 0 0; - @include transition($form-floating-transition); - } +// scss-docs-start form-floating-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-floating-tokens: defaults( + ( + --form-floating-height: calc(3.75rem + (var(--border-width) * 2)), + --form-floating-line-height: 1.25, + --form-floating-padding-x: calc(var(--btn-input-padding-x) * 1.25), + --form-floating-padding-y: 1rem, + --form-floating-input-padding-t: 1.625rem, + --form-floating-input-padding-b: .625rem, + --form-floating-label-height: 1.5em, + // Backgrounds for the textarea label's masking pseudo-element. Mirrors + // `.form-control` here because the label is a sibling of the control, so it + // can't inherit the control's own `--control-bg`/`--control-disabled-bg`. + --form-floating-label-bg: var(--btn-input-bg), + --form-floating-label-disabled-bg: var(--bg-2), + --form-floating-label-opacity: .65, + --form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem), + --form-floating-label-disabled-color: var(--fg-3), + --form-floating-transition-property: "opacity, transform", + --form-floating-transition-timing: .1s ease-in-out, + --form-floating-transition: var(--form-floating-transition-property) var(--form-floating-transition-timing), + ), + $form-floating-tokens +); +// scss-docs-end form-floating-tokens + +@layer forms { + .form-floating { + @include tokens($form-floating-tokens); - > .form-control, - > .form-control-plaintext { - padding: $form-floating-padding-y $form-floating-padding-x; + position: relative; - &::placeholder { - color: transparent; + > label { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + z-index: 2; + display: flex; + align-items: center; + max-width: 100%; + height: 100%; // allow textareas + padding: var(--form-floating-padding-y) var(--form-floating-padding-x); + overflow: hidden; + color: color-mix(in oklch, var(--fg-body) var(--form-floating-label-opacity), transparent); + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--border-width) solid transparent; // Required for aligning label's text with the input as it affects inner box model + transform-origin: 0 0; + @include transition(var(--form-floating-transition)); } - &:focus, - &:not(:placeholder-shown) { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; + // Anchor the label to the top for textareas so it floats correctly at any height + > label:has(~ textarea) { + align-items: flex-start; } - // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped - &:-webkit-autofill { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; + + > .form-control, + > .form-control-plaintext { + height: var(--form-floating-height); + min-height: var(--form-floating-height); + padding: var(--form-floating-padding-y) var(--form-floating-padding-x); + line-height: var(--form-floating-line-height); + + &::placeholder { + color: transparent; + } + + &:focus, + &:not(:placeholder-shown) { + padding-top: var(--form-floating-input-padding-t); + padding-bottom: var(--form-floating-input-padding-b); + } + // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped + &:-webkit-autofill { + padding-top: var(--form-floating-input-padding-t); + padding-bottom: var(--form-floating-input-padding-b); + } } - } - > .form-select { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; - padding-left: $form-floating-padding-x; - } + // The label precedes the control in the DOM so screen readers announce it + // before the field's value, so we look forward with `:has()` to react to the + // control's state (focus, value, disabled, etc.). + > label:has(~ .form-control:focus), + > label:has(~ .form-control:not(:placeholder-shown)), + > label:has(~ .form-control-plaintext) { + transform: var(--form-floating-label-transform); + } - > .form-control:focus, - > .form-control:not(:placeholder-shown), - > .form-control-plaintext, - > .form-select { - ~ label { - transform: $form-floating-label-transform; + // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped + > label:has(~ .form-control:-webkit-autofill) { + transform: var(--form-floating-label-transform); } - } - // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped - > .form-control:-webkit-autofill { - ~ label { - transform: $form-floating-label-transform; + + > label:has(~ textarea:focus), + > label:has(~ textarea:not(:placeholder-shown)) { + &::after { + position: absolute; + inset: var(--form-floating-padding-y) calc(var(--form-floating-padding-x) * .5); + z-index: -1; + height: var(--form-floating-label-height); + content: ""; + background-color: var(--form-floating-label-bg); + @include border-radius(var(--btn-input-border-radius)); + } } - } - > textarea:focus, - > textarea:not(:placeholder-shown) { - ~ label::after { - position: absolute; - inset: $form-floating-padding-y ($form-floating-padding-x * .5); - z-index: -1; - height: $form-floating-label-height; - content: ""; - background-color: $input-bg; - @include border-radius($input-border-radius); + > label:has(~ textarea:disabled)::after { + background-color: var(--form-floating-label-disabled-bg); } - } - > textarea:disabled ~ label::after { - background-color: $input-disabled-bg; - } - > .form-control-plaintext { - ~ label { - border-width: $input-border-width 0; // Required to properly position label text - as explained above + > label:has(~ .form-control-plaintext) { + border-width: var(--control-border-width) 0; // Required to properly position label text - as explained above } - } - > :disabled ~ label, - > .form-control:disabled ~ label { // Required for `.form-control`s because of specificity - color: $form-floating-label-disabled-color; + > label:has(~ :disabled), + > label:has(~ .form-control:disabled) { // Required for `.form-control`s because of specificity + color: var(--form-floating-label-disabled-color); + } } } diff --git a/assets/stylesheets/bootstrap/forms/_form-adorn.scss b/assets/stylesheets/bootstrap/forms/_form-adorn.scss new file mode 100644 index 00000000..bd33d923 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_form-adorn.scss @@ -0,0 +1,68 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +$form-adorn-tokens: () !default; + +// scss-docs-start form-adorn-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-adorn-tokens: defaults( + ( + --form-adorn-gap: .375rem, + --form-adorn-icon-size: 1rem, + --form-adorn-icon-color: var(--fg-2), + ), + $form-adorn-tokens +); +// scss-docs-end form-adorn-tokens + +@layer forms { + .form-adorn { + @include tokens($form-adorn-tokens); + + gap: var(--form-adorn-gap); + align-items: center; + + // Prevent default `.form-control` focus + &:focus-visible { + outline: 0; + } + + &:focus-within { + --focus-ring-offset: -1px; + border-color: var(--focus-ring-color); + @include focus-ring(true); + } + + // Ghost input fills remaining space + > .form-ghost { + flex: 1; + min-width: 0; // Prevent text overflow + } + + &.form-adorn-end > .form-ghost { + order: -1; + } + } + + .form-adorn-icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + color: var(--form-adorn-icon-color); + pointer-events: none; + + > svg { + width: var(--form-adorn-icon-size); + height: var(--form-adorn-icon-size); + } + } + + .form-adorn-text { + flex-shrink: 0; + color: var(--form-adorn-icon-color); + pointer-events: none; + user-select: none; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_form-check.scss b/assets/stylesheets/bootstrap/forms/_form-check.scss deleted file mode 100644 index 8a1b639d..00000000 --- a/assets/stylesheets/bootstrap/forms/_form-check.scss +++ /dev/null @@ -1,189 +0,0 @@ -// -// Check/radio -// - -.form-check { - display: block; - min-height: $form-check-min-height; - padding-left: $form-check-padding-start; - margin-bottom: $form-check-margin-bottom; - - .form-check-input { - float: left; - margin-left: $form-check-padding-start * -1; - } -} - -.form-check-reverse { - padding-right: $form-check-padding-start; - padding-left: 0; - text-align: right; - - .form-check-input { - float: right; - margin-right: $form-check-padding-start * -1; - margin-left: 0; - } -} - -.form-check-input { - --#{$prefix}form-check-bg: #{$form-check-input-bg}; - - flex-shrink: 0; - width: $form-check-input-width; - height: $form-check-input-width; - margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height - vertical-align: top; - appearance: none; - background-color: var(--#{$prefix}form-check-bg); - background-image: var(--#{$prefix}form-check-bg-image); - background-repeat: no-repeat; - background-position: center; - background-size: contain; - border: $form-check-input-border; - print-color-adjust: exact; // Keep themed appearance for print - @include transition($form-check-transition); - - &[type="checkbox"] { - @include border-radius($form-check-input-border-radius); - } - - &[type="radio"] { - // stylelint-disable-next-line property-disallowed-list - border-radius: $form-check-radio-border-radius; - } - - &:active { - filter: $form-check-input-active-filter; - } - - &:focus { - border-color: $form-check-input-focus-border; - outline: 0; - box-shadow: $form-check-input-focus-box-shadow; - } - - &:checked { - background-color: $form-check-input-checked-bg-color; - border-color: $form-check-input-checked-border-color; - - &[type="checkbox"] { - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)}; - } - } - - &[type="radio"] { - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)}; - } - } - } - - &[type="checkbox"]:indeterminate { - background-color: $form-check-input-indeterminate-bg-color; - border-color: $form-check-input-indeterminate-border-color; - - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)}; - } - } - - &:disabled { - pointer-events: none; - filter: none; - opacity: $form-check-input-disabled-opacity; - } - - // Use disabled attribute in addition of :disabled pseudo-class - // See: https://github.com/twbs/bootstrap/issues/28247 - &[disabled], - &:disabled { - ~ .form-check-label { - cursor: default; - opacity: $form-check-label-disabled-opacity; - } - } -} - -.form-check-label { - color: $form-check-label-color; - cursor: $form-check-label-cursor; -} - -// -// Switch -// - -.form-switch { - padding-left: $form-switch-padding-start; - - .form-check-input { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image)}; - - width: $form-switch-width; - margin-left: $form-switch-padding-start * -1; - background-image: var(--#{$prefix}form-switch-bg); - background-position: left center; - @include border-radius($form-switch-border-radius, 0); - @include transition($form-switch-transition); - - &:focus { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-focus-bg-image)}; - } - - &:checked { - background-position: $form-switch-checked-bg-position; - - @if $enable-gradients { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)}; - } - } - } - - &.form-check-reverse { - padding-right: $form-switch-padding-start; - padding-left: 0; - - .form-check-input { - margin-right: $form-switch-padding-start * -1; - margin-left: 0; - } - } -} - -.form-check-inline { - display: inline-block; - margin-right: $form-check-inline-margin-end; -} - -.btn-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; - - &[disabled], - &:disabled { - + .btn { - pointer-events: none; - filter: none; - opacity: $form-check-btn-check-disabled-opacity; - } - } -} - -@if $enable-dark-mode { - @include color-mode(dark) { - .form-switch .form-check-input:not(:checked):not(:focus) { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image-dark)}; - } - } -} diff --git a/assets/stylesheets/bootstrap/forms/_form-control.scss b/assets/stylesheets/bootstrap/forms/_form-control.scss index 67ae5f4f..9eedb198 100644 --- a/assets/stylesheets/bootstrap/forms/_form-control.scss +++ b/assets/stylesheets/bootstrap/forms/_form-control.scss @@ -1,214 +1,284 @@ -// -// General form controls (plus a few specific high-level interventions) -// - -.form-control { - display: block; - width: 100%; - padding: $input-padding-y $input-padding-x; - font-family: $input-font-family; - @include font-size($input-font-size); - font-weight: $input-font-weight; - line-height: $input-line-height; - color: $input-color; - appearance: none; // Fix appearance for date inputs in Safari - background-color: $input-bg; - background-clip: padding-box; - border: $input-border-width solid $input-border-color; - - // Note: This has no effect on s in some browsers, due to the limited stylability of ``s in CSS. - @include border-radius($input-border-radius, 0); - - @include box-shadow($input-box-shadow); - @include transition($input-transition); - - &[type="file"] { - overflow: hidden; // prevent pseudo element button overlap - - &:not(:disabled):not([readonly]) { - cursor: pointer; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +$form-control-tokens: () !default; + +// scss-docs-start form-control-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$form-control-tokens: defaults( + ( + --control-min-height: var(--btn-input-min-height), + --control-padding-y: var(--btn-input-padding-y), + --control-padding-x: var(--btn-input-padding-x), + --control-font-size: var(--btn-input-font-size), + --control-line-height: var(--btn-input-line-height), + --control-fg: var(--btn-input-fg), + --control-bg: var(--btn-input-bg), + --control-border-width: var(--border-width), + --control-border-color: var(--border-color), + --control-border-radius: var(--radius-5), + --control-box-shadow: var(--box-shadow-inset), + --control-action-bg: var(--bg-1), + --control-action-hover-bg: var(--bg-2), + --control-transition-property: "border-color, box-shadow", + --control-transition-timing: .15s ease-in-out, + --control-transition: var(--control-transition-property) var(--control-transition-timing), + --control-placeholder-color: var(--fg-3), + --control-disabled-color: var(--control-fg), + --control-disabled-bg: var(--bg-2), + --control-disabled-border-color: var(--control-border-color), + --control-select-bg: #{escape-svg(url("data:image/svg+xml,"))}, + --control-select-bg-position: right .75rem center, + --control-select-bg-size: 16px 12px, + --control-select-bg-dark: #{escape-svg(url("data:image/svg+xml,"))}, + ), + $form-control-tokens +); +// scss-docs-end form-control-tokens + +// scss-docs-start form-control-sizes +$form-control-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$form-control-sizes: defaults( + ("sm", "lg"), + $form-control-sizes +); +// scss-docs-end form-control-sizes +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .form-control { + @include tokens($form-control-tokens); + + display: flex; + width: 100%; + min-height: var(--control-min-height); + padding: var(--control-padding-y) var(--control-padding-x); + font-size: var(--control-font-size); + line-height: var(--control-line-height); + color: var(--control-fg); + appearance: none; + background-color: var(--control-bg); + background-clip: padding-box; + border: var(--control-border-width) solid var(--control-border-color); + @include border-radius(var(--control-border-radius), 0); + @include box-shadow(var(--control-box-shadow)); + @include transition(var(--control-transition)); + + // Customize the `:focus` state to imitate native WebKit styles. + &:focus-visible { + --focus-ring-offset: -1px; + @include focus-ring(true); } - } - // Customize the `:focus` state to imitate native WebKit styles. - &:focus { - color: $input-focus-color; - background-color: $input-focus-bg; - border-color: $input-focus-border-color; - outline: 0; - @if $enable-shadows { - @include box-shadow($input-box-shadow, $input-focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $input-focus-box-shadow; + // Placeholder + &::placeholder { + color: var(--control-placeholder-color); + // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526. + opacity: 1; } - } - &::-webkit-date-and-time-value { - // On Android Chrome, form-control's "width: 100%" makes the input width too small - // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // Disabled inputs // - // On iOS Safari, form-control's "appearance: none" + "width: 100%" makes the input width too small - // Tested under iOS 16.2 / Safari 16.2 - min-width: 85px; // Seems to be a good minimum safe width - - // Add some height to date inputs on iOS - // https://github.com/twbs/bootstrap/issues/23307 - // TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved - // Multiply line-height by 1em if it has no unit - height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height); - - // Android Chrome type="date" is taller than the other inputs - // because of "margin: 1px 24px 1px 4px" inside the shadow DOM - // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 - margin: 0; - } + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &:disabled { + color: var(--control-disabled-color); + background-color: var(--control-disabled-bg); + border-color: var(--control-disabled-border-color); + // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655. + opacity: 1; + } - // Prevent excessive date input height in Webkit - // https://github.com/twbs/bootstrap/issues/34433 - &::-webkit-datetime-edit { - display: block; - padding: 0; - } + // Date and time inputs + // &::-webkit-date-and-time-value { + // // On Android Chrome, form-control's "width: 100%" makes the input width too small + // // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // // + // // On iOS Safari, form-control's "appearance: none" + "width: 100%" makes the input width too small + // // Tested under iOS 16.2 / Safari 16.2 + // min-width: 85px; // Seems to be a good minimum safe width - // Placeholder - &::placeholder { - color: $input-placeholder-color; - // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526. - opacity: 1; - } + // // Add some height to date inputs on iOS + // // https://github.com/twbs/bootstrap/issues/23307 + // // TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved + // // Multiply line-height by 1em if it has no unit + // height: 1.5em; - // Disabled inputs - // - // HTML5 says that controls under a fieldset > legend:first-child won't be - // disabled if the fieldset is disabled. Due to implementation difficulty, we - // don't honor that edge case; we style them as disabled anyway. - &:disabled { - color: $input-disabled-color; - background-color: $input-disabled-bg; - border-color: $input-disabled-border-color; - // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655. - opacity: 1; - } + // // Android Chrome type="date" is taller than the other inputs + // // because of "margin: 1px 24px 1px 4px" inside the shadow DOM + // // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // margin: 0; + // background-color: var(--red-500); + // } - // File input buttons theming - &::file-selector-button { - padding: $input-padding-y $input-padding-x; - margin: (-$input-padding-y) (-$input-padding-x); - margin-inline-end: $input-padding-x; - color: $form-file-button-color; - @include gradient-bg($form-file-button-bg); - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: $input-border-width; - border-radius: 0; // stylelint-disable-line property-disallowed-list - @include transition($btn-transition); - } + // Prevent excessive date input height in Webkit + // https://github.com/twbs/bootstrap/issues/34433 - &:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: $form-file-button-hover-bg; - } -} + // mdo-do: need to check this stuff out across browsers + &::-webkit-datetime-edit { + display: block; + height: 1.5rem; + padding: 0; + margin-bottom: -.125rem; + } + &::-webkit-datetime-edit-fields-wrapper { + height: 1.5rem; + } -// Readonly controls as plain text -// -// Apply class to a readonly input to make it appear like regular plain -// text (without any border, background color, focus indicator) - -.form-control-plaintext { - display: block; - width: 100%; - padding: $input-padding-y 0; - margin-bottom: 0; // match inputs if this class comes on inputs with default margins - line-height: $input-line-height; - color: $input-plaintext-color; - background-color: transparent; - border: solid transparent; - border-width: $input-border-width 0; - - &:focus { - outline: 0; - } + // File inputs + &[type="file"] { + overflow: hidden; // prevent pseudo element button overlap - &.form-control-sm, - &.form-control-lg { - padding-right: 0; - padding-left: 0; - } -} + &:not(:disabled, [readonly]) { + cursor: pointer; + } + } + &::file-selector-button { + min-height: var(--control-min-height); + padding: var(--control-padding-y) var(--control-padding-x); + margin: calc(var(--control-padding-y) * -1) calc(var(--control-padding-x) * -1); + margin-inline-end: var(--control-padding-x); + color: var(--control-fg); + // @include gradient-bg(var(--control-action-bg)); + pointer-events: none; + background-color: var(--control-action-bg); + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--control-border-width); + border-radius: 0; // stylelint-disable-line property-disallowed-list + @include transition(var(--control-transition)); + } -// Form control sizing -// -// Build on `.form-control` with modifier classes to decrease or increase the -// height and font-size of form controls. -// -// Repeated in `_input_group.scss` to avoid Sass extend issues. - -.form-control-sm { - min-height: $input-height-sm; - padding: $input-padding-y-sm $input-padding-x-sm; - @include font-size($input-font-size-sm); - @include border-radius($input-border-radius-sm); - - &::file-selector-button { - padding: $input-padding-y-sm $input-padding-x-sm; - margin: (-$input-padding-y-sm) (-$input-padding-x-sm); - margin-inline-end: $input-padding-x-sm; + &:hover:not(:disabled, [readonly])::file-selector-button { + background-color: var(--control-action-hover-bg); + } } -} -.form-control-lg { - min-height: $input-height-lg; - padding: $input-padding-y-lg $input-padding-x-lg; - @include font-size($input-font-size-lg); - @include border-radius($input-border-radius-lg); + // Readonly controls as plain text + // + // Apply class to a readonly input to make it appear like regular plain + // text (without any border, background color, focus indicator) - &::file-selector-button { - padding: $input-padding-y-lg $input-padding-x-lg; - margin: (-$input-padding-y-lg) (-$input-padding-x-lg); - margin-inline-end: $input-padding-x-lg; + .form-control-plaintext { + // Plaintext is a standalone class (not combined with `.form-control`), so it + // needs its own copy of the control tokens. Without them the `var(--control-*)` + // references below are invalid and fall back to their initial values (e.g. + // `border-width: medium`), which adds phantom inline borders and misaligns the + // text from a floating label. + @include tokens($form-control-tokens); + + display: block; + width: 100%; + padding: var(--control-padding-y) 0; + margin-bottom: 0; // match inputs if this class comes on inputs with default margins + line-height: var(--control-line-height); + color: var(--control-fg); + background-color: transparent; + border: solid transparent; + border-width: var(--control-border-width) 0; + + &:focus { + outline: 0; + } + + &.form-control-sm, + &.form-control-lg { + padding-inline: 0; + } } -} -// Make sure textareas don't shrink too much when resized -// https://github.com/twbs/bootstrap/pull/29124 -// stylelint-disable selector-no-qualifying-type -textarea { - &.form-control { - min-height: $input-height; + // stylelint-disable selector-no-qualifying-type + select.form-control, + .form-control-caret { + padding-inline-end: calc(var(--control-padding-x) * 3); + background-image: var(--control-select-bg); + background-repeat: no-repeat; + background-position: var(--control-select-bg-position); + background-size: var(--control-select-bg-size); + + &[multiple], + &[size]:not([size="1"]) { + padding-inline-end: var(--control-padding-x); + background-image: none; + } } - &.form-control-sm { - min-height: $input-height-sm; + [data-bs-theme="dark"] { + select.form-control, + .form-control-caret { + background-image: var(--control-select-bg-dark); + } } + // stylelint-enable selector-no-qualifying-type - &.form-control-lg { - min-height: $input-height-lg; + // Form control sizing + // + // Build on `.form-control` with modifier classes to decrease or increase the + // height and font-size of form controls. + // + // Repeated in `_input_group.scss` to avoid Sass extend issues. + @each $size, $_ in $form-control-sizes { + .form-control-#{$size} { + --control-min-height: var(--btn-input-#{$size}-min-height); + --control-padding-y: var(--btn-input-#{$size}-padding-y); + --control-padding-x: var(--btn-input-#{$size}-padding-x); + --control-font-size: var(--btn-input-#{$size}-font-size); + --control-line-height: var(--btn-input-#{$size}-line-height); + --control-border-radius: var(--btn-input-#{$size}-border-radius); + } } -} -// stylelint-enable selector-no-qualifying-type -.form-control-color { - width: $form-color-width; - height: $input-height; - padding: $input-padding-y; + .form-control-color { + width: var(--control-min-height); + padding: var(--control-padding-y); - &:not(:disabled):not([readonly]) { - cursor: pointer; - } + &:not(:disabled, [readonly]) { + cursor: pointer; + } - &::-moz-color-swatch { - border: 0 !important; // stylelint-disable-line declaration-no-important - @include border-radius($input-border-radius); - } + &::-moz-color-swatch { + border: 0 !important; // stylelint-disable-line declaration-no-important + @include border-radius(var(--radius-5)); + } - &::-webkit-color-swatch { - border: 0 !important; // stylelint-disable-line declaration-no-important - @include border-radius($input-border-radius); + &::-webkit-color-swatch { + border: 0 !important; // stylelint-disable-line declaration-no-important + @include border-radius(var(--radius-5)); + } } - &.form-control-sm { height: $input-height-sm; } - &.form-control-lg { height: $input-height-lg; } + // Ghost input - removes all visual styling + // Used inside custom wrappers that handle their own styling + .form-ghost { + display: block; + width: 100%; + padding: 0; + font: inherit; + color: inherit; + appearance: none; + background: transparent; + border: 0; + + &:focus { + outline: 0; + } + + &::placeholder { + color: var(--fg-3); + opacity: 1; + } + + &:disabled { + color: var(--fg-4); + cursor: not-allowed; + } + } } diff --git a/assets/stylesheets/bootstrap/forms/_form-field.scss b/assets/stylesheets/bootstrap/forms/_form-field.scss new file mode 100644 index 00000000..3f58835f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_form-field.scss @@ -0,0 +1,79 @@ +@use "../mixins/border-radius" as *; + +// scss-docs-start form-field +@layer forms { + .form-field { + position: relative; + display: grid; + gap: .5rem; + // width: 100%; + + > label, + > .form-label { + justify-self: start; + margin-bottom: 0; + } + + &:has(> .check, > .radio, > .switch) { + grid-template-columns: auto 1fr; + column-gap: .5rem; + align-items: start; + + > .check, + > .radio, + > .switch { + grid-column: 1; + } + + > :not(.check, .radio, .switch) { + grid-column: 2; + } + + > .form-label { + grid-column: 1 / -1; + } + } + } + + .form-field-content { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .form-field-card { + position: relative; + padding: calc(var(--spacer) * .75); + cursor: pointer; + border: var(--border-width) solid transparent; + @include border-radius(var(--radius-7)); + + &:hover { + background-color: var(--bg-1); + } + + &:has(:checked) { + background-color: var(--bg-1); + border-color: var(--border-color); + } + + label::before { + position: absolute; + inset: 0; + content: ""; + } + } + + .form-group { + display: grid; + gap: .5rem; + + > label, + > .form-label, + > legend { + justify-self: start; + margin-bottom: 0; + } + } +} +// scss-docs-end form-field diff --git a/assets/stylesheets/bootstrap/forms/_form-range.scss b/assets/stylesheets/bootstrap/forms/_form-range.scss index 4732213e..fa051549 100644 --- a/assets/stylesheets/bootstrap/forms/_form-range.scss +++ b/assets/stylesheets/bootstrap/forms/_form-range.scss @@ -1,91 +1,216 @@ -// Range -// -// Style range inputs the same across browsers. Vendor-specific rules for pseudo -// elements cannot be mixed. As such, there are no shared styles for focus or -// active states on prefixed selectors. - -.form-range { - width: 100%; - height: add($form-range-thumb-height, $form-range-thumb-focus-box-shadow-width * 2); - padding: 0; // Need to reset padding - appearance: none; - background-color: transparent; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/transition" as *; +@use "../mixins/gradients" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$range-tokens: () !default; - &:focus { - outline: 0; +// scss-docs-start range-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$range-tokens: defaults( + ( + --range-track-width: 100%, + --range-track-height: .5rem, + --range-track-cursor: pointer, + --range-track-bg: var(--bg-3), + --range-track-border-radius: 1rem, + --range-track-fill-bg: var(--primary-base), + --range-track-disabled-bg: color-mix(in oklch, var(--bg-4), var(--fg-3)), + --range-thumb-width: 1rem, + --range-thumb-height: var(--range-thumb-width), + --range-thumb-bg: var(--primary-base), + --range-thumb-border: var(--range-thumb-bg) solid var(--border-color), + --range-thumb-border-radius: 1rem, + --range-thumb-box-shadow: "0 1px 2px rgb(0 0 0 / 7.5%), 0 2px 4px rgb(0 0 0 / 7.5%)", + --range-thumb-active-bg: color-mix(in oklch, var(--primary-base) 70%, var(--bg-body)), + --range-thumb-disabled-bg: var(--fg-3), + --range-thumb-transition-property: "background-color, border-color, box-shadow", + --range-thumb-transition-timing: .15s ease-in-out, + --range-thumb-transition: var(--range-thumb-transition-property) var(--range-thumb-transition-timing), + --range-tick-width: var(--border-width), + --range-tick-height: .5rem, + --range-tick-bg: var(--border-color), + ), + $range-tokens +); +// scss-docs-end range-tokens +// stylelint-enable custom-property-no-missing-var-function - // Pseudo-elements must be split across multiple rulesets to have an effect. - // No box-shadow() mixin for focus accessibility. - &::-webkit-slider-thumb { box-shadow: $form-range-thumb-focus-box-shadow; } - &::-moz-range-thumb { box-shadow: $form-range-thumb-focus-box-shadow; } +// scss-docs-start range-mixins +@mixin range-thumb() { + width: var(--range-thumb-width); + height: var(--range-thumb-height); + appearance: none; + @include gradient-bg(var(--range-thumb-bg)); + border: var(--range-thumb-border); + @include border-radius(var(--range-thumb-border-radius)); + @include box-shadow(var(--range-thumb-box-shadow)); + @include transition(var(--range-thumb-transition)); + + &:active { + @include gradient-bg(var(--range-thumb-active-bg)); } +} - &::-moz-focus-outer { - border: 0; +@mixin range-track() { + width: var(--range-track-width); + height: var(--range-track-height); + color: transparent; + cursor: var(--range-track-cursor); + // Fill (progress) up to the thumb. The Range plugin keeps `--range-fill` (0–1) in sync. + background-color: var(--range-track-bg); + background-image: + linear-gradient( + to right, + var(--range-track-fill-bg) calc(var(--range-fill, 0) * 100%), + transparent calc(var(--range-fill, 0) * 100%) + ); + border-color: transparent; + @include border-radius(var(--range-track-border-radius)); + @include box-shadow(var(--range-track-box-shadow)); +} +// scss-docs-end range-mixins + +@layer forms { + .form-range { + @include tokens($range-tokens); + + position: relative; + display: block; + width: 100%; } - &::-webkit-slider-thumb { - width: $form-range-thumb-width; - height: $form-range-thumb-height; - margin-top: ($form-range-track-height - $form-range-thumb-height) * .5; // Webkit specific + .form-range-input { + display: block; + width: 100%; + height: calc(var(--range-thumb-height) + (var(--focus-ring-width) * 2)); + padding: 0; appearance: none; - @include gradient-bg($form-range-thumb-bg); - border: $form-range-thumb-border; - @include border-radius($form-range-thumb-border-radius); - @include box-shadow($form-range-thumb-box-shadow); - @include transition($form-range-thumb-transition); - - &:active { - @include gradient-bg($form-range-thumb-active-bg); + background-color: transparent; + + &:hover { + &::-webkit-slider-thumb { + @include focus-ring(false, color-mix(in oklch, var(--primary-focus-ring), transparent)); + } + &::-moz-range-thumb { + @include focus-ring(false, color-mix(in oklch, var(--primary-focus-ring), transparent)); + } + } + + &:focus-visible { + outline: 0; + + &::-webkit-slider-thumb { + @include focus-ring(true); + --focus-ring-offset: 0; + } + &::-moz-range-thumb { + @include focus-ring(true); + --focus-ring-offset: 0; + } + } + + &::-moz-focus-outer { + border: 0; + } + + &::-webkit-slider-thumb { + @include range-thumb(); + margin-top: calc((var(--range-track-height) - var(--range-thumb-height)) * .5); + } + + &::-moz-range-thumb { + @include range-thumb(); + } + + &::-webkit-slider-runnable-track { + @include range-track(); + } + + &::-moz-range-track { + @include range-track(); } - } - &::-webkit-slider-runnable-track { - width: $form-range-track-width; - height: $form-range-track-height; - color: transparent; // Why? - cursor: $form-range-track-cursor; - background-color: $form-range-track-bg; - border-color: transparent; - @include border-radius($form-range-track-border-radius); - @include box-shadow($form-range-track-box-shadow); + &:disabled { + pointer-events: none; + + &::-webkit-slider-thumb { + background-color: var(--range-thumb-disabled-bg); + } + + &::-moz-range-thumb { + background-color: var(--range-thumb-disabled-bg); + } + + &::-webkit-slider-runnable-track { + --range-track-fill-bg: var(--range-track-disabled-bg); + } + + &::-moz-range-track { + --range-track-fill-bg: var(--range-track-disabled-bg); + } + } } - &::-moz-range-thumb { - width: $form-range-thumb-width; - height: $form-range-thumb-height; - appearance: none; - @include gradient-bg($form-range-thumb-bg); - border: $form-range-thumb-border; - @include border-radius($form-range-thumb-border-radius); - @include box-shadow($form-range-thumb-box-shadow); - @include transition($form-range-thumb-transition); - - &:active { - @include gradient-bg($form-range-thumb-active-bg); + // Value bubble: reuses the tooltip styles (`.tooltip` markup) so we don't duplicate the + // pill and arrow. We only add the static positioning the Tooltip plugin would normally do. + .form-range-bubble { + position: absolute; + bottom: 100%; + left: calc((var(--range-thumb-width) * .5) + var(--range-fill, 0) * (100% - var(--range-thumb-width))); + margin-bottom: var(--tooltip-arrow-height); + pointer-events: none; + transform: translateX(-50%); + + .tooltip-arrow { + position: absolute; + bottom: calc(-1 * var(--tooltip-arrow-height)); + left: 50%; + transform: translateX(-50%); } } - &::-moz-range-track { - width: $form-range-track-width; - height: $form-range-track-height; - color: transparent; - cursor: $form-range-track-cursor; - background-color: $form-range-track-bg; - border-color: transparent; // Firefox specific? - @include border-radius($form-range-track-border-radius); - @include box-shadow($form-range-track-box-shadow); + // Tick marks generated from the linked . Plugin builds `grid-template-columns` + // from the gaps between values so each tick lands on a grid line (handles uneven values). + // Track is inset by 1/4th of the thumb width to keep alignment. + .form-range-ticks { + display: grid; + padding-inline: calc(var(--range-thumb-width) * .25); } - &:disabled { - pointer-events: none; + .form-range-tick { + display: flex; + flex-direction: column; + align-items: center; + justify-self: start; + // Zero-width items so labels never widen their `fr` column; the tick line and label + // overflow centered on the grid line via `align-items`. + width: 0; - &::-webkit-slider-thumb { - background-color: $form-range-thumb-disabled-bg; + &::before { + width: var(--range-tick-width); + height: var(--range-tick-height); + content: ""; + background-color: var(--range-tick-bg); } - &::-moz-range-thumb { - background-color: $form-range-thumb-disabled-bg; + &:first-child { + align-items: flex-start; + } + + &:last-child { + align-items: flex-end; } } + + .form-range-tick-label { + margin-top: .125rem; + font-size: var(--font-size-sm); + color: var(--fg-2); + white-space: nowrap; + } } diff --git a/assets/stylesheets/bootstrap/forms/_form-select.scss b/assets/stylesheets/bootstrap/forms/_form-select.scss deleted file mode 100644 index 69ace529..00000000 --- a/assets/stylesheets/bootstrap/forms/_form-select.scss +++ /dev/null @@ -1,80 +0,0 @@ -// Select -// -// Replaces the browser default select with a custom one, mostly pulled from -// https://primer.github.io/. - -.form-select { - --#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator)}; - - display: block; - width: 100%; - padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x; - font-family: $form-select-font-family; - @include font-size($form-select-font-size); - font-weight: $form-select-font-weight; - line-height: $form-select-line-height; - color: $form-select-color; - appearance: none; - background-color: $form-select-bg; - background-image: var(--#{$prefix}form-select-bg-img), var(--#{$prefix}form-select-bg-icon, none); - background-repeat: no-repeat; - background-position: $form-select-bg-position; - background-size: $form-select-bg-size; - border: $form-select-border-width solid $form-select-border-color; - @include border-radius($form-select-border-radius, 0); - @include box-shadow($form-select-box-shadow); - @include transition($form-select-transition); - - &:focus { - border-color: $form-select-focus-border-color; - outline: 0; - @if $enable-shadows { - @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $form-select-focus-box-shadow; - } - } - - &[multiple], - &[size]:not([size="1"]) { - padding-right: $form-select-padding-x; - background-image: none; - } - - &:disabled { - color: $form-select-disabled-color; - background-color: $form-select-disabled-bg; - border-color: $form-select-disabled-border-color; - } - - // Remove outline from select box in FF - &:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 $form-select-color; - } -} - -.form-select-sm { - padding-top: $form-select-padding-y-sm; - padding-bottom: $form-select-padding-y-sm; - padding-left: $form-select-padding-x-sm; - @include font-size($form-select-font-size-sm); - @include border-radius($form-select-border-radius-sm); -} - -.form-select-lg { - padding-top: $form-select-padding-y-lg; - padding-bottom: $form-select-padding-y-lg; - padding-left: $form-select-padding-x-lg; - @include font-size($form-select-font-size-lg); - @include border-radius($form-select-border-radius-lg); -} - -@if $enable-dark-mode { - @include color-mode(dark) { - .form-select { - --#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator-dark)}; - } - } -} diff --git a/assets/stylesheets/bootstrap/forms/_form-text.scss b/assets/stylesheets/bootstrap/forms/_form-text.scss index f080d1a2..81259b73 100644 --- a/assets/stylesheets/bootstrap/forms/_form-text.scss +++ b/assets/stylesheets/bootstrap/forms/_form-text.scss @@ -1,11 +1,30 @@ -// -// Form text -// - -.form-text { - margin-top: $form-text-margin-top; - @include font-size($form-text-font-size); - font-style: $form-text-font-style; - font-weight: $form-text-font-weight; - color: $form-text-color; +@use "../functions" as *; +@use "../mixins/tokens" as *; + +$form-text-tokens: () !default; + +// scss-docs-start form-text-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-text-tokens: defaults( + ( + --form-text-margin-top: .25rem, + --form-text-font-size: var(--font-size-sm), + --form-text-font-style: null, + --form-text-font-weight: null, + --form-text-color: var(--fg-2), + ), + $form-text-tokens +); +// scss-docs-end form-text-tokens + +@layer forms { + .form-text { + @include tokens($form-text-tokens); + + // margin-top: var(--form-text-margin-top); + font-size: var(--form-text-font-size); + font-style: var(--form-text-font-style); + font-weight: var(--form-text-font-weight); + color: var(--form-text-color); + } } diff --git a/assets/stylesheets/bootstrap/forms/_input-group.scss b/assets/stylesheets/bootstrap/forms/_input-group.scss index 8078ebb1..c73598e5 100644 --- a/assets/stylesheets/bootstrap/forms/_input-group.scss +++ b/assets/stylesheets/bootstrap/forms/_input-group.scss @@ -1,132 +1,135 @@ -// -// Base styles -// - -.input-group { - position: relative; - display: flex; - flex-wrap: wrap; // For form validation feedback - align-items: stretch; - width: 100%; - - > .form-control, - > .form-select, - > .form-floating { - position: relative; // For focus state's z-index - flex: 1 1 auto; - width: 1%; - min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size - } - - // Bring the "active" form control to the top of surrounding elements - > .form-control:focus, - > .form-select:focus, - > .form-floating:focus-within { - z-index: 5; - } +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; + +$input-group-addon-tokens: () !default; + +// scss-docs-start input-group-addon-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$input-group-addon-tokens: defaults( + ( + --input-group-addon-padding-y: var(--btn-input-padding-y), + --input-group-addon-padding-x: var(--btn-input-padding-x), + --input-group-addon-font-size: var(--btn-input-font-size), + --input-group-addon-line-height: var(--btn-input-line-height), + --input-group-addon-color: var(--fg-body), + --input-group-addon-bg: var(--bg-2), + --input-group-addon-border-color: var(--border-color), + ), + $input-group-addon-tokens +); +// scss-docs-end input-group-addon-tokens + +// scss-docs-start input-group-sizes +$input-group-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$input-group-sizes: defaults( + ("sm", "lg"), + $input-group-sizes +); +// scss-docs-end input-group-sizes + +@layer components { + .input-group { + @include tokens($input-group-addon-tokens); - // Ensure buttons are always above inputs for more visually pleasing borders. - // This isn't needed for `.input-group-text` since it shares the same border-color - // as our inputs. - .btn { position: relative; - z-index: 2; + display: flex; + align-items: stretch; + width: 100%; + + > .form-control, + > .form-floating { + position: relative; // For focus state's z-index + flex: 1 1 auto; + width: 1%; + min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size + } - &:focus { + // Bring the "active" form control to the top of surrounding elements + > .form-control:focus, + > .form-floating:focus-within { z-index: 5; } - } -} - - -// Textual addons -// -// Serves as a catch-all element for any text or radio/checkbox input you wish -// to prepend or append to an input. - -.input-group-text { - display: flex; - align-items: center; - padding: $input-group-addon-padding-y $input-group-addon-padding-x; - @include font-size($input-font-size); // Match inputs - font-weight: $input-group-addon-font-weight; - line-height: $input-line-height; - color: $input-group-addon-color; - text-align: center; - white-space: nowrap; - background-color: $input-group-addon-bg; - border: $input-border-width solid $input-group-addon-border-color; - @include border-radius($input-border-radius); -} + // Ensure buttons are always above inputs for more visually pleasing borders. + // This isn't needed for `.input-group-text` since it shares the same border-color + // as our inputs. + > .input-group-btn { + position: relative; + z-index: 2; -// Sizing -// -// Remix the default form control sizing classes into new ones for easier -// manipulation. - -.input-group-lg > .form-control, -.input-group-lg > .form-select, -.input-group-lg > .input-group-text, -.input-group-lg > .btn { - padding: $input-padding-y-lg $input-padding-x-lg; - @include font-size($input-font-size-lg); - @include border-radius($input-border-radius-lg); -} - -.input-group-sm > .form-control, -.input-group-sm > .form-select, -.input-group-sm > .input-group-text, -.input-group-sm > .btn { - padding: $input-padding-y-sm $input-padding-x-sm; - @include font-size($input-font-size-sm); - @include border-radius($input-border-radius-sm); -} + &:focus { + z-index: 5; + } + } + } -.input-group-lg > .form-select, -.input-group-sm > .form-select { - padding-right: $form-select-padding-x + $form-select-indicator-padding; -} + // Textual addons + // + // Serves as a catch-all element for any text or radio/checkbox input you wish + // to prepend or append to an input. + + .input-group-text { + display: flex; + align-items: center; + padding: var(--input-group-addon-padding-y) var(--input-group-addon-padding-x); + font-size: var(--input-group-addon-font-size); // Match inputs + // font-weight: $input-group-addon-font-weight; + line-height: var(--input-group-addon-line-height); + color: var(--input-group-addon-color); + text-align: center; + white-space: nowrap; + background-color: var(--input-group-addon-bg); + border: var(--border-width) solid var(--input-group-addon-border-color); + @include border-radius(var(--btn-input-border-radius)); + } + // Sizing + // + // Remix the default form control sizing classes into new ones for easier + // manipulation. + + @each $size, $_ in $input-group-sizes { + .input-group-#{$size} { + > .form-control, + > .input-group-text, + > .btn { + min-height: var(--btn-input-#{$size}-min-height); + padding: var(--btn-input-#{$size}-padding-y) var(--btn-input-#{$size}-padding-x); + font-size: var(--btn-input-#{$size}-font-size); + @include border-radius(var(--btn-input-#{$size}-border-radius)); + } + } + } -// Rounded corners -// -// These rulesets must come after the sizing ones to properly override sm and lg -// border-radius values when extending. They're more specific than we'd like -// with the `.input-group >` part, but without it, we cannot override the sizing. + // Rounded corners + // + // These rulesets must come after the sizing ones to properly override sm and lg + // border-radius values when extending. They're more specific than we'd like + // with the `.input-group >` part, but without it, we cannot override the sizing. -// stylelint-disable-next-line no-duplicate-selectors -.input-group { - &:not(.has-validation) { - > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), - > .dropdown-toggle:nth-last-child(n + 3), + // stylelint-disable-next-line no-duplicate-selectors + .input-group { + > :not(:last-child, .menu-toggle-split, .menu, .input-group-ignore, .form-floating, :has(+ :is(.menu, .input-group-ignore):last-child)), + > .menu-toggle-split:nth-last-child(n + 3), > .form-floating:not(:last-child) > .form-control, > .form-floating:not(:last-child) > .form-select { @include border-end-radius(0); } - } - &.has-validation { - > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), - > .dropdown-toggle:nth-last-child(n + 4), - > .form-floating:nth-last-child(n + 3) > .form-control, - > .form-floating:nth-last-child(n + 3) > .form-select { - @include border-end-radius(0); + > :not(:first-child, .menu, .input-group-ignore) { + margin-inline-start: calc(-1 * var(--border-width)); + @include border-start-radius(0); } - } - - $validation-messages: ""; - @each $state in map-keys($form-validation-states) { - $validation-messages: $validation-messages + ":not(." + unquote($state) + "-tooltip)" + ":not(." + unquote($state) + "-feedback)"; - } - > :not(:first-child):not(.dropdown-menu)#{$validation-messages} { - margin-left: calc(-1 * #{$input-border-width}); // stylelint-disable-line function-disallowed-list - @include border-start-radius(0); - } + > :first-child:is(.input-group-ignore) + :not(.menu, .input-group-ignore) { + @include border-start-radius(var(--btn-input-border-radius)); + } - > .form-floating:not(:first-child) > .form-control, - > .form-floating:not(:first-child) > .form-select { - @include border-start-radius(0); + > .form-floating:not(:first-child) > .form-control, + > .form-floating:not(:first-child) > .form-select { + @include border-start-radius(0); + } } } diff --git a/assets/stylesheets/bootstrap/forms/_labels.scss b/assets/stylesheets/bootstrap/forms/_labels.scss index 39ecafcd..462a1bcc 100644 --- a/assets/stylesheets/bootstrap/forms/_labels.scss +++ b/assets/stylesheets/bootstrap/forms/_labels.scss @@ -1,36 +1,49 @@ -// -// Labels -// +@use "../functions" as *; -.form-label { - margin-bottom: $form-label-margin-bottom; - @include font-size($form-label-font-size); - font-style: $form-label-font-style; - font-weight: $form-label-font-weight; - color: $form-label-color; -} +$form-label-tokens: () !default; -// For use with horizontal and inline forms, when you need the label (or legend) -// text to align with the form controls. -.col-form-label { - padding-top: add($input-padding-y, $input-border-width); - padding-bottom: add($input-padding-y, $input-border-width); - margin-bottom: 0; // Override the `` default - @include font-size(inherit); // Override the `` default - font-style: $form-label-font-style; - font-weight: $form-label-font-weight; - line-height: $input-line-height; - color: $form-label-color; -} +// scss-docs-start form-label-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-label-tokens: defaults( + ( + --label-margin-bottom: calc(var(--spacer) / 2), + --label-font-size: null, + --label-font-style: null, + --label-font-weight: null, + --label-color: null, + ), + $form-label-tokens +); +// scss-docs-end form-label-tokens -.col-form-label-lg { - padding-top: add($input-padding-y-lg, $input-border-width); - padding-bottom: add($input-padding-y-lg, $input-border-width); - @include font-size($input-font-size-lg); -} +@layer forms { + .form-label, + .col-form-label { + font-size: var(--label-font-size, inherit); + font-style: var(--label-font-style, inherit); + font-weight: var(--label-font-weight, 500); + color: var(--label-color, var(--fg-body)); + } + + .form-label { + margin-bottom: var(--label-margin-bottom, calc(var(--spacer) / 2)); + } + + // For use with horizontal and inline forms, when you need the label (or legend) + // text to align with the form controls. + .col-form-label { + --label-padding-y: calc(var(--btn-input-padding-y) + var(--border-width)); + padding-block: var(--label-padding-y); + margin-bottom: 0; // Override the `` default + } + + .col-form-label-lg { + --label-padding-y: calc(var(--btn-input-lg-padding-y) + var(--border-width)); + font-size: var(--btn-input-lg-font-size); + } -.col-form-label-sm { - padding-top: add($input-padding-y-sm, $input-border-width); - padding-bottom: add($input-padding-y-sm, $input-border-width); - @include font-size($input-font-size-sm); + .col-form-label-sm { + --label-padding-y: calc(var(--btn-input-sm-padding-y) + var(--border-width)); + font-size: var(--btn-input-sm-font-size); + } } diff --git a/assets/stylesheets/bootstrap/forms/_otp-input.scss b/assets/stylesheets/bootstrap/forms/_otp-input.scss new file mode 100644 index 00000000..06e34a57 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_otp-input.scss @@ -0,0 +1,159 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +$otp-tokens: () !default; + +// scss-docs-start otp-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$otp-tokens: defaults( + ( + --otp-size: var(--btn-input-lg-min-height), + --otp-font-size: var(--btn-input-font-size), + --otp-gap: .5rem, + --otp-slot-fg: var(--btn-input-fg), + --otp-slot-bg: var(--btn-input-bg), + --otp-slot-border-width: var(--border-width), + --otp-slot-border-color: var(--border-color), + --otp-slot-border-radius: var(--radius-5), + ), + $otp-tokens +); +// scss-docs-end otp-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start otp-sizes +$otp-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$otp-sizes: defaults( + ("sm", "lg"), + $otp-sizes +); +// scss-docs-end otp-sizes + +@layer components { + .otp { + @include tokens($otp-tokens); + + position: relative; + display: flex; + } + + // A single real input backs the whole control. Once the JS renders the + // visual slots (`.otp-rendered`), the input becomes a transparent overlay + // that captures all interaction while the slots display the value. + .otp-rendered .otp-input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 0; + color: transparent; + text-align: center; + cursor: default; + caret-color: transparent; + background-color: transparent; + border: 0; + outline: 0; + box-shadow: none; + + // Never reveal the underlying characters, even on selection + &::selection { + color: transparent; + background-color: transparent; + } + } + + .otp-slots { + display: inline-flex; + gap: var(--otp-gap); + pointer-events: none; // let clicks fall through to the input overlay + } + + .otp-slot { + display: flex; + align-items: center; + justify-content: center; + width: var(--otp-size); + min-height: var(--otp-size); + font-size: var(--otp-font-size); + font-weight: 500; + line-height: 1; + color: var(--otp-slot-fg); + background-color: var(--otp-slot-bg); + border: var(--otp-slot-border-width) solid var(--otp-slot-border-color); + @include border-radius(var(--otp-slot-border-radius)); + @include box-shadow(var(--box-shadow-inset)); + @include transition(border-color .15s ease-in-out, box-shadow .15s ease-in-out); + } + + // The slot at the caret gets the focus ring; empty active slots show a + // blinking caret so the entry point is obvious. + .otp-slot-active { + --focus-ring-offset: -1px; + z-index: 1; + @include focus-ring(true); + + &:not(.otp-slot-filled)::after { + width: 1px; + height: 50%; + content: ""; + background-color: var(--otp-slot-fg); + animation: otp-caret-blink 1s step-end infinite; + } + } + + // Disabled state mirrors disabled form controls + .otp-input:disabled ~ .otp-slots .otp-slot { + background-color: var(--bg-2); + } + + // Connected slots share borders for a single cohesive field + .otp-connected .otp-slots { + gap: 0; + } + .otp-connected .otp-slot { + border-radius: 0; // stylelint-disable-line property-disallowed-list + + &:not(:first-child) { + margin-inline-start: calc(var(--otp-slot-border-width) * -1); + } + &:first-child { + @include border-start-radius(var(--otp-slot-border-radius)); + } + &:last-child { + @include border-end-radius(var(--otp-slot-border-radius)); + } + } + + .otp-separator { + display: flex; + align-items: center; + padding-inline: var(--otp-gap); + font-size: var(--otp-font-size); + color: var(--fg-4); + user-select: none; + } + + // OTP input sizing — keep in sync with `$form-control-sizes`. + @each $size, $_ in $otp-sizes { + .otp-#{$size} { + --otp-size: var(--btn-input-#{$size}-min-height); + --otp-font-size: var(--btn-input-#{$size}-font-size); + } + } +} + +@keyframes otp-caret-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_radio.scss b/assets/stylesheets/bootstrap/forms/_radio.scss new file mode 100644 index 00000000..71dd811c --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_radio.scss @@ -0,0 +1,88 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$radio-tokens: () !default; + +// scss-docs-start radio-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$radio-tokens: defaults( + ( + --radio-size: 1.25rem, + --radio-margin-block: .125rem, + --radio-bg: var(--bg-body), + --radio-border-color: var(--border-color), + --radio-checked-bg: var(--control-checked-bg), + --radio-checked-border-color: var(--control-checked-border-color), + --radio-disabled-bg: var(--control-disabled-bg), + --radio-disabled-opacity: var(--control-disabled-opacity), + ), + $radio-tokens +); +// scss-docs-end radio-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .radio { + @include tokens($radio-tokens); + + position: relative; + flex-shrink: 0; + width: var(--radio-size); + height: var(--radio-size); + margin-block: var(--radio-margin-block); + appearance: none; + background-color: var(--theme-bg, var(--radio-bg)); + border: 1px solid var(--theme-bg, var(--radio-border-color)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + + &:checked { + color: var(--theme-contrast, var(--primary-contrast)); + background-color: var(--theme-bg, var(--radio-checked-bg)); + border-color: var(--theme-bg, var(--radio-checked-border-color)); + + &::before { + position: absolute; + inset: calc(var(--radio-size) * .25); + content: ""; + background-color: currentcolor; + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + } + } + + &:disabled { + --radio-bg: var(--radio-disabled-bg); + + ~ label { + color: var(--secondary-fg); + cursor: default; + } + } + &:disabled:checked { + opacity: var(--radio-disabled-opacity); + } + + &:focus-visible { + @include focus-ring(true); + } + } + + .radio-sm { + --radio-size: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .radio-lg { + --radio-size: 1.5rem; + --radio-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_strength.scss b/assets/stylesheets/bootstrap/forms/_strength.scss new file mode 100644 index 00000000..a4140234 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_strength.scss @@ -0,0 +1,111 @@ +@use "sass:list"; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function +$strength-tokens: () !default; + +// scss-docs-start strength-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$strength-tokens: defaults( + ( + --strength-height: .375rem, + --strength-gap: .25rem, + --strength-margin-top: .25rem, + --strength-border-radius: var(--radius-pill), + --strength-bg: var(--bg-2), + --strength-color: var(--bg-2), + --strength-weak-color: var(--danger-bg), + --strength-fair-color: var(--warning-bg), + --strength-good-color: var(--info-bg), + --strength-strong-color: var(--success-bg), + ), + $strength-tokens +); +// scss-docs-end strength-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start strength-levels +$strength-levels: weak, fair, good, strong !default; +// scss-docs-end strength-levels + +$strength-transition: background-color .2s ease-in-out, width .3s ease-in-out !default; + +@layer forms { + // Strength meter container + .strength { + @include tokens($strength-tokens); + + display: flex; + gap: var(--strength-gap); + width: 100%; + margin-top: var(--strength-margin-top); + } + + // Individual strength segments + .strength-segment { + flex: 1; + height: var(--strength-height); + background-color: var(--strength-bg); + @include border-radius(var(--strength-border-radius)); + @include transition($strength-transition); + + // Filled state + &.active { + background-color: var(--strength-color); + } + } + + @each $level in $strength-levels { + .strength[data-bs-strength="#{$level}"] { + --strength-color: var(--strength-#{$level}-color); + } + } + // Optional text feedback + .strength-text { + display: block; + margin-top: var(--strength-margin-top); + font-size: var(--font-size-xs); + color: var(--strength-color, var(--fg-3)); + @include transition(color .2s ease-in-out); + + // Hide when empty + &:empty { + display: none; + } + } + + // Alternative: Single bar variant (like a progress bar) + .strength-bar { + @include tokens($strength-tokens); + + --strength-color: transparent; + --strength-width: 0%; + + width: 100%; + height: var(--strength-height); + margin-top: var(--strength-margin-top); + overflow: hidden; + background-color: var(--strength-bg); + @include border-radius(var(--strength-border-radius)); + + &::after { + display: block; + width: var(--strength-width); + height: 100%; + content: ""; + background-color: var(--strength-color); + @include border-radius(var(--strength-border-radius)); + @include transition($strength-transition); + } + + @each $level in $strength-levels { + &[data-bs-strength="#{$level}"] { + --strength-color: var(--strength-#{$level}-color); + --strength-width: #{list.index($strength-levels, $level) * 25%}; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_switch.scss b/assets/stylesheets/bootstrap/forms/_switch.scss new file mode 100644 index 00000000..2357b744 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_switch.scss @@ -0,0 +1,123 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$switch-tokens: () !default; + +// scss-docs-start switch-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$switch-tokens: defaults( + ( + --switch-height: 1.25rem, + --switch-width: calc(var(--switch-height) * 1.75), + --switch-padding: .0625rem, + --switch-margin-block: .125rem, + --switch-bg: var(--bg-3), + --switch-border-width: var(--border-width), + --switch-border-color: var(--border-color), + --switch-indicator-bg: var(--white), + --switch-indicator-width: calc(var(--switch-height) - calc(var(--switch-padding) * 2) - var(--switch-border-width) * 2), + --switch-indicator-height: calc(var(--switch-height) - calc(var(--switch-padding) * 2) - var(--switch-border-width) * 2), + --switch-checked-bg: var(--control-checked-bg), + --switch-checked-border-color: var(--switch-checked-bg), + --switch-checked-indicator-bg: var(--white), + --switch-disabled-bg: var(--control-disabled-bg), + --switch-disabled-indicator-bg: var(--fg-3), + ), + $switch-tokens +); +// scss-docs-end switch-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .switch { + @include tokens($switch-tokens); + + position: relative; + flex-shrink: 0; + width: var(--switch-width); + height: var(--switch-height); + padding: var(--switch-padding); + margin-block: var(--switch-margin-block); + background-color: var(--switch-bg); + border: var(--switch-border-width) solid var(--switch-border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: 10rem; + box-shadow: inset 0 1px 2px rgb(0 0 0 / .05); + // stylelint-disable-next-line property-disallowed-list + transition: background-color .15s ease-in-out; + + &::before { + position: absolute; + inset-block: var(--switch-padding); + inset-inline-start: var(--switch-padding); + width: var(--switch-indicator-width); + height: var(--switch-indicator-height); + content: ""; + background-color: var(--theme-contrast, var(--switch-indicator-bg)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 10rem; + box-shadow: 0 1px 2px rgb(0 0 0 / .1); + // stylelint-disable-next-line property-disallowed-list + transition: inset-inline-start .15s ease-in-out; + } + + input { + position: absolute; + inset: 0; + appearance: none; + background-color: transparent; + outline: 0; + } + + &:focus-within { + @include focus-ring(true); + } + + &:has(input:disabled:not(:checked)) { + --switch-bg: var(--switch-disabled-bg); + --switch-indicator-bg: var(--switch-disabled-indicator-bg); + + &::before { opacity: .4; } + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + + &:has(input:checked) { + background-color: var(--theme-bg, var(--switch-checked-bg)); + border-color: var(--theme-bg, var(--switch-checked-border-color)); + + &::before { + inset-inline-start: calc(100% - var(--switch-indicator-width) - var(--switch-padding)); + } + } + + &:has(input:checked:disabled) { + opacity: .65; + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + } + .switch-sm { + --switch-height: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .switch-lg { + --switch-height: 1.5rem; + --switch-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_validation.scss b/assets/stylesheets/bootstrap/forms/_validation.scss index c48123a7..acf51fd2 100644 --- a/assets/stylesheets/bootstrap/forms/_validation.scss +++ b/assets/stylesheets/bootstrap/forms/_validation.scss @@ -1,12 +1,360 @@ +@use "../config" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/form-validation" as *; + // Form validation // -// Provide feedback to users when form field values are valid or invalid. Works -// primarily for client-side validation via scoped `:invalid` and `:valid` -// pseudo-classes but also includes `.is-invalid` and `.is-valid` classes for -// server-side validation. - -// scss-docs-start form-validation-states-loop -@each $state, $data in $form-validation-states { - @include form-validation-state($state, $data...); +// Provide feedback to users when form field values are valid or invalid. +// Server-side: `.is-invalid` / `.is-valid` classes work globally. +// Client-side: `:user-invalid` pseudo-class is scoped behind `[data-bs-validate]`. +// `:user-valid` is scoped behind `[data-bs-validate~="valid"]` so success styling is opt-in. +// Custom states (e.g., "warning") use only `.is-*` classes. + +// scss-docs-start form-validation-states +$validation-states: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$validation-states: defaults( + ( + "valid": "success", + "invalid": "danger", + ), + $validation-states +); +// scss-docs-end form-validation-states + +// scss-docs-start form-validation-state-mixin +@mixin form-validation-state($state, $theme) { + .#{$state}-feedback { + display: none; + width: 100%; + font-size: var(--font-size-sm); + color: var(--#{$theme}-fg); + } + + // More specific to override base tooltip styles + .tooltip.#{$state}-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: var(--tooltip-padding-y) var(--tooltip-padding-x); + margin-top: .1rem; + color: var(--#{$theme}-contrast); + text-align: center; + background-color: var(--#{$theme}-bg); + opacity: 1; + @include border-radius(var(--tooltip-border-radius)); + } + + // Generic sibling feedback display — works for .form-control, .form-range, + // and any element where feedback is a direct sibling. + @include form-validation-state-selector($state) { + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { + display: block; + } + } + + // Form control + .form-control { + @include form-validation-state-selector($state) { + --control-border-color: var(--#{$theme}-border); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + --control-border-color: var(--#{$theme}-border); + } + } + } + + // Checkbox — control-level styling (border, checked bg, focus ring). + .check { + @include form-validation-state-selector($state) { + --check-border-color: var(--#{$theme}-border); + --check-checked-bg: var(--#{$theme}-bg); + --check-checked-border-color: var(--#{$theme}-bg); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Checkbox — label color and feedback display via .form-field:has(). + .form-field:has(.check.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.check:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.check:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Radio — control-level styling. + .radio { + @include form-validation-state-selector($state) { + --radio-border-color: var(--#{$theme}-border); + --radio-checked-bg: var(--#{$theme}-bg); + --radio-checked-border-color: var(--#{$theme}-bg); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Radio — label color and feedback display via .form-field:has(). + .form-field:has(.radio.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.radio:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.radio:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Switch — control-level styling. The input is an invisible overlay; + // all visuals are on the .switch wrapper. + .switch:has(input.is-#{$state}) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + + @if $state == "invalid" { + [data-bs-validate] .switch:has(input:user-invalid) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .switch:has(input:user-valid) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Switch — label color and feedback display via .form-field:has(). + .form-field:has(.switch input.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.switch input:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.switch input:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Chip input — wrapper has the visible border; the .form-ghost inside + // receives the native pseudo-class. + .chip-input:has(.form-ghost.is-#{$state}) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .chip-input:has(.form-ghost:user-invalid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .invalid-feedback, + ~ .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .chip-input:has(.form-ghost:user-valid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .valid-feedback, + ~ .valid-tooltip { display: block; } + } + } + + // Form adorn — :user-invalid fires on the inner .form-ghost, so we + // propagate it to the visible wrapper with :has(). + .form-adorn:has(.form-ghost.is-#{$state}) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-adorn:has(.form-ghost:user-invalid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .invalid-feedback, + ~ .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-adorn:has(.form-ghost:user-valid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .valid-feedback, + ~ .valid-tooltip { display: block; } + } + } + + // Range — the validation class lives on .form-range-input, while feedback sits outside + // the .form-range wrapper, so we use :has() to toggle it. + .form-range-input { + @include form-validation-state-selector($state) { + &::-webkit-slider-thumb { background: var(--#{$theme}-bg); } + &::-moz-range-thumb { background: var(--#{$theme}-bg); } + + &:focus-visible { + &::-webkit-slider-thumb { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + &::-moz-range-thumb { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + } + + .form-range:has(.form-range-input.is-#{$state}) { + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + // Input group — feedback lives outside the input-group in the parent + // .form-field, so we use :has() to toggle display. + .form-field:has(.input-group .form-control.is-#{$state}) { + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.input-group .form-control:user-invalid) { + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.input-group .form-control:user-valid) { + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + .input-group { + > .form-control:not(:focus), + > .form-floating:not(:focus-within) { + @include form-validation-state-selector($state) { + @if $state == "valid" { + z-index: 3; + } @else if $state == "invalid" { + z-index: 4; + } + } + } + } + + // OTP — validation applies to the wrapper; the visual slots inherit the state. + .otp { + @include form-validation-state-selector($state) { + .otp-slot { + --otp-slot-border-color: var(--#{$theme}-border); + } + + .otp-slot-active { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } +} +// scss-docs-end form-validation-state-mixin + +@layer components { + // scss-docs-start form-validation-states-loop + @each $state, $theme in $validation-states { + @include form-validation-state($state, $theme); + } + // scss-docs-end form-validation-states-loop } -// scss-docs-end form-validation-states-loop diff --git a/assets/stylesheets/bootstrap/forms/index.scss b/assets/stylesheets/bootstrap/forms/index.scss new file mode 100644 index 00000000..58cd150f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/index.scss @@ -0,0 +1,16 @@ +@forward "labels"; +@forward "form-text"; +@forward "form-control"; +@forward "check"; +@forward "radio"; +@forward "switch"; +@forward "form-range"; +@forward "floating-labels"; +@forward "input-group"; +@forward "strength"; +@forward "otp-input"; +@forward "form-adorn"; +@forward "chip-input"; +@forward "combobox"; +@forward "form-field"; +@forward "validation"; diff --git a/assets/stylesheets/bootstrap/helpers/_clearfix.scss b/assets/stylesheets/bootstrap/helpers/_clearfix.scss deleted file mode 100644 index e92522a9..00000000 --- a/assets/stylesheets/bootstrap/helpers/_clearfix.scss +++ /dev/null @@ -1,3 +0,0 @@ -.clearfix { - @include clearfix(); -} diff --git a/assets/stylesheets/bootstrap/helpers/_color-bg.scss b/assets/stylesheets/bootstrap/helpers/_color-bg.scss deleted file mode 100644 index 1a3a4cff..00000000 --- a/assets/stylesheets/bootstrap/helpers/_color-bg.scss +++ /dev/null @@ -1,7 +0,0 @@ -// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251 -@each $color, $value in $theme-colors { - .text-bg-#{$color} { - color: color-contrast($value) if($enable-important-utilities, !important, null); - background-color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}bg-opacity, 1)) if($enable-important-utilities, !important, null); - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_colored-links.scss b/assets/stylesheets/bootstrap/helpers/_colored-links.scss deleted file mode 100644 index 5f868578..00000000 --- a/assets/stylesheets/bootstrap/helpers/_colored-links.scss +++ /dev/null @@ -1,30 +0,0 @@ -// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251 -@each $color, $value in $theme-colors { - .link-#{$color} { - color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - - @if $link-shade-percentage != 0 { - &:hover, - &:focus { - $hover-color: if(color-contrast($value) == $color-contrast-light, shade-color($value, $link-shade-percentage), tint-color($value, $link-shade-percentage)); - color: RGBA(#{to-rgb($hover-color)}, var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(to-rgb($hover-color), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - } - } - } -} - -// One-off special link helper as a bridge until v6 -.link-body-emphasis { - color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - - @if $link-shade-percentage != 0 { - &:hover, - &:focus { - color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-opacity, .75)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-underline-opacity, .75)) if($enable-important-utilities, !important, null); - } - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_focus-ring.scss b/assets/stylesheets/bootstrap/helpers/_focus-ring.scss index 26508a8d..c210d8d3 100644 --- a/assets/stylesheets/bootstrap/helpers/_focus-ring.scss +++ b/assets/stylesheets/bootstrap/helpers/_focus-ring.scss @@ -1,5 +1,6 @@ -.focus-ring:focus { - outline: 0; - // By default, there is no `--bs-focus-ring-x`, `--bs-focus-ring-y`, or `--bs-focus-ring-blur`, but we provide CSS variables with fallbacks to initial `0` values - box-shadow: var(--#{$prefix}focus-ring-x, 0) var(--#{$prefix}focus-ring-y, 0) var(--#{$prefix}focus-ring-blur, 0) var(--#{$prefix}focus-ring-width) var(--#{$prefix}focus-ring-color); +@layer helpers { + .focus-ring:focus-visible { + // outline: var(--focus-ring); + outline: var(--focus-ring-width) solid var(--theme-focus-ring, var(--focus-ring-color)); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_icon-link.scss b/assets/stylesheets/bootstrap/helpers/_icon-link.scss index 3f8bcb33..23f0ad71 100644 --- a/assets/stylesheets/bootstrap/helpers/_icon-link.scss +++ b/assets/stylesheets/bootstrap/helpers/_icon-link.scss @@ -1,25 +1,30 @@ -.icon-link { - display: inline-flex; - gap: $icon-link-gap; - align-items: center; - text-decoration-color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, .5)); - text-underline-offset: $icon-link-underline-offset; - backface-visibility: hidden; +@use "../config" as *; +@use "../mixins/transition" as *; - > .bi { - flex-shrink: 0; - width: $icon-link-icon-size; - height: $icon-link-icon-size; - fill: currentcolor; - @include transition($icon-link-icon-transition); - } -} +@layer helpers { + .icon-link { + display: inline-flex; + gap: $icon-link-gap; + align-items: center; + text-decoration-color: rgba(var(--link-color-rgb), var(--link-opacity, .5)); + text-underline-offset: $icon-link-underline-offset; + backface-visibility: hidden; -.icon-link-hover { - &:hover, - &:focus-visible { > .bi { - transform: var(--#{$prefix}icon-link-transform, $icon-link-icon-transform); + flex-shrink: 0; + width: $icon-link-icon-size; + height: $icon-link-icon-size; + fill: currentcolor; + @include transition($icon-link-icon-transition); + } + } + + .icon-link-hover { + &:hover, + &:focus-visible { + > .bi { + transform: var(--icon-link-transform, $icon-link-icon-transform); + } } } } diff --git a/assets/stylesheets/bootstrap/helpers/_position.scss b/assets/stylesheets/bootstrap/helpers/_position.scss index 59103d94..3c94967d 100644 --- a/assets/stylesheets/bootstrap/helpers/_position.scss +++ b/assets/stylesheets/bootstrap/helpers/_position.scss @@ -1,36 +1,36 @@ -// Shorthand +@use "sass:map"; +@use "../config" as *; +@use "../layout/breakpoints" as *; -.fixed-top { - position: fixed; - top: 0; - right: 0; - left: 0; - z-index: $zindex-fixed; -} +@layer helpers { + .fixed-top { + position: fixed; + inset: 0 0 auto; + z-index: $zindex-fixed; + } -.fixed-bottom { - position: fixed; - right: 0; - bottom: 0; - left: 0; - z-index: $zindex-fixed; -} + .fixed-bottom { + position: fixed; + inset: auto 0 0; + z-index: $zindex-fixed; + } -// Responsive sticky top and bottom -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + // Responsive sticky top and bottom + @each $breakpoint in map.keys($breakpoints) { + @include media-breakpoint-up($breakpoint) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); - .sticky#{$infix}-top { - position: sticky; - top: 0; - z-index: $zindex-sticky; - } + .#{$prefix}sticky-top { + position: sticky; + top: 0; + z-index: $zindex-sticky; + } - .sticky#{$infix}-bottom { - position: sticky; - bottom: 0; - z-index: $zindex-sticky; + .#{$prefix}sticky-bottom { + position: sticky; + bottom: 0; + z-index: $zindex-sticky; + } } } } diff --git a/assets/stylesheets/bootstrap/helpers/_ratio.scss b/assets/stylesheets/bootstrap/helpers/_ratio.scss deleted file mode 100644 index b6a7654c..00000000 --- a/assets/stylesheets/bootstrap/helpers/_ratio.scss +++ /dev/null @@ -1,26 +0,0 @@ -// Credit: Nicolas Gallagher and SUIT CSS. - -.ratio { - position: relative; - width: 100%; - - &::before { - display: block; - padding-top: var(--#{$prefix}aspect-ratio); - content: ""; - } - - > * { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } -} - -@each $key, $ratio in $aspect-ratios { - .ratio-#{$key} { - --#{$prefix}aspect-ratio: #{$ratio}; - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_stacks.scss b/assets/stylesheets/bootstrap/helpers/_stacks.scss index 6cd237ae..1ed716e5 100644 --- a/assets/stylesheets/bootstrap/helpers/_stacks.scss +++ b/assets/stylesheets/bootstrap/helpers/_stacks.scss @@ -1,15 +1,33 @@ -// scss-docs-start stacks -.hstack { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; -} +@use "../layout/breakpoints" as *; + +@layer helpers { + // scss-docs-start stacks + .stack-container { + @include set-container(); + } + + [class*="hstack"], + [class*="vstack"] { + display: flex; + flex: var(--stack-flex, 1 1 auto); + flex-direction: var(--stack-direction, row); + align-items: var(--stack-align-items, center); + align-self: var(--stack-align-self, stretch); + } -.vstack { - display: flex; - flex: 1 1 auto; - flex-direction: column; - align-self: stretch; + @include loop-breakpoints-up() using ($breakpoint, $prefix) { + .#{$prefix}vstack { + @include container-breakpoint-up($breakpoint) { + --stack-direction: column; + --stack-align-items: stretch; + } + } + .#{$prefix}hstack { + @include container-breakpoint-up($breakpoint) { + --stack-direction: row; + --stack-align-items: flex-start; + } + } + } + // scss-docs-end stacks } -// scss-docs-end stacks diff --git a/assets/stylesheets/bootstrap/helpers/_stretched-link.scss b/assets/stylesheets/bootstrap/helpers/_stretched-link.scss index 71a1c755..c3a319b6 100644 --- a/assets/stylesheets/bootstrap/helpers/_stretched-link.scss +++ b/assets/stylesheets/bootstrap/helpers/_stretched-link.scss @@ -1,15 +1,12 @@ -// -// Stretched link -// +@use "../config" as *; -.stretched-link { - &::#{$stretched-link-pseudo-element} { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: $stretched-link-z-index; - content: ""; +@layer helpers { + .stretched-link { + &::#{$stretched-link-pseudo-element} { + position: absolute; + inset: 0; + z-index: $stretched-link-z-index; + content: ""; + } } } diff --git a/assets/stylesheets/bootstrap/helpers/_text-truncation.scss b/assets/stylesheets/bootstrap/helpers/_text-truncation.scss index 6421dac9..b2f423cf 100644 --- a/assets/stylesheets/bootstrap/helpers/_text-truncation.scss +++ b/assets/stylesheets/bootstrap/helpers/_text-truncation.scss @@ -1,7 +1,7 @@ -// -// Text truncation -// +@use "../mixins/text-truncate" as *; -.text-truncate { - @include text-truncate(); +@layer helpers { + .text-truncate { + @include text-truncate(); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_theme-colors.scss b/assets/stylesheets/bootstrap/helpers/_theme-colors.scss new file mode 100644 index 00000000..b40fa196 --- /dev/null +++ b/assets/stylesheets/bootstrap/helpers/_theme-colors.scss @@ -0,0 +1,6 @@ +@use "../theme" as *; + +// Generate theme modifier classes (e.g., .theme-primary, .theme-accent, etc.) +@layer helpers { + @include generate-theme-classes(); +} diff --git a/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss b/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss index 4760ff03..327dc0cb 100644 --- a/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss +++ b/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss @@ -1,8 +1,8 @@ -// -// Visually hidden -// +@use "../mixins/visually-hidden" as *; -.visually-hidden, -.visually-hidden-focusable:not(:focus):not(:focus-within) { - @include visually-hidden(); +@layer helpers { + .visually-hidden, + .visually-hidden-focusable:not(:focus, :focus-within) { + @include visually-hidden(); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_vr.scss b/assets/stylesheets/bootstrap/helpers/_vr.scss index b6f9d42c..56b57c97 100644 --- a/assets/stylesheets/bootstrap/helpers/_vr.scss +++ b/assets/stylesheets/bootstrap/helpers/_vr.scss @@ -1,8 +1,9 @@ -.vr { - display: inline-block; - align-self: stretch; - width: $vr-border-width; - min-height: 1em; - background-color: currentcolor; - opacity: $hr-opacity; +@layer helpers { + .vr { + display: inline-block; + align-self: stretch; + width: var(--vr-border-width, var(--border-width)); + min-height: 1em; + background-color: var(--border-color); + } } diff --git a/assets/stylesheets/bootstrap/helpers/index.scss b/assets/stylesheets/bootstrap/helpers/index.scss new file mode 100644 index 00000000..07f3c267 --- /dev/null +++ b/assets/stylesheets/bootstrap/helpers/index.scss @@ -0,0 +1,9 @@ +@forward "focus-ring"; +@forward "icon-link"; +@forward "position"; +@forward "stacks"; +@forward "theme-colors"; +@forward "visually-hidden"; +@forward "stretched-link"; +@forward "text-truncation"; +@forward "vr"; diff --git a/assets/stylesheets/bootstrap/layout/_breakpoints.scss b/assets/stylesheets/bootstrap/layout/_breakpoints.scss new file mode 100644 index 00000000..0d9163b7 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_breakpoints.scss @@ -0,0 +1,324 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:string"; +@use "../config" as *; + +// Breakpoint viewport sizes and media queries. +// +// Breakpoints are defined as a map of (name: minimum width), order from small to large: +// +// (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px) +// +// The map defined in the `$breakpoints` global variable is used as the `$breakpoints` argument by default. + +// Name of the next breakpoint, or null for the last breakpoint. +// +// >> breakpoint-next(sm) +// md +// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// md +// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl 2xl)) +// md +@function breakpoint-next($name, $breakpoints: $breakpoints, $breakpoint-names: map.keys($breakpoints)) { + $n: list.index($breakpoint-names, $name); + @if not $n { + @error "breakpoint `#{$name}` not found in `#{$breakpoint-names}`"; + } + // Use @if/@else because list.nth would error if evaluated when $n equals list length + @if $n < list.length($breakpoint-names) { + @return list.nth($breakpoint-names, $n + 1); + } @else { + @return null; + } +} + +// Minimum breakpoint width. Null for the smallest (first) breakpoint. +// +// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// 576px +@function breakpoint-min($name, $breakpoints: $breakpoints) { + $min: map.get($breakpoints, $name); + @return if(sass($min != 0): $min; else: null); +} + +// Maximum breakpoint width for range media queries. +// Returns the breakpoint value to use as an upper bound in range queries. +// +// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// 576px +// >> breakpoint-max(xxl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// null +@function breakpoint-max($name, $breakpoints: $breakpoints) { + @if $name == null { + @return null; + } + $max: map.get($breakpoints, $name); + @return if(sass($max and $max > 0): $max; else: null); +} + +// Escape a name for use at the start of a CSS identifier. +// Leading digits are hex-escaped (e.g., 2xl becomes \32 xl). +@function css-escape-ident($name) { + $name-str: "#{$name}"; + $digits: "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"; + $first: string.slice($name-str, 1, 1); + + @if list.index($digits, $first) { + @return "\\3#{$first} #{string.slice($name-str, 2)}"; + } + + @return $name-str; +} + +// Returns a blank string if smallest breakpoint, otherwise returns the name +// with an escaped colon as a Tailwind-style prefix for responsive class names. +// Leading digits are CSS-escaped (e.g., 2xl becomes \32 xl) for valid identifiers. +// +// >> breakpoint-prefix(xs, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "" (Returns a blank string) +// >> breakpoint-prefix(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "sm\:" +// >> breakpoint-prefix(2xl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "\32 xl\:" +@function breakpoint-prefix($name, $breakpoints: $breakpoints) { + @if breakpoint-min($name, $breakpoints) == null { + @return ""; + } + + @return "#{css-escape-ident($name)}\\:"; +} + +// Iterate all breakpoints and provide the current name and prefix. +// +// @include loop-breakpoints-up() using ($breakpoint, $prefix) { +// // ... +// } +@mixin loop-breakpoints-up($breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + @content($breakpoint, $prefix); + } +} + +// Iterate all breakpoints and provide the current name, next name, and next prefix. +// +// @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { +// // ... +// } +@mixin loop-breakpoints-down($breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $next: breakpoint-next($breakpoint, $breakpoints); + $prefix: breakpoint-prefix($next, $breakpoints); + @content($breakpoint, $next, $prefix); + } +} + +// Backwards-compatible alias for next/down breakpoint loops. +@mixin loop-breakpoints($breakpoints: $breakpoints) { + @include loop-breakpoints-down($breakpoints) using ($breakpoint, $next, $prefix) { + @content($breakpoint, $next, $prefix); + } +} + +// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider. +@mixin media-breakpoint-up($name, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + @if $min { + @media (width >= $min) { + @content; + } + } @else { + @content; + } +} + +// Media of at most the maximum breakpoint width. No query for the largest breakpoint. +// Makes the @content apply to the given breakpoint and narrower. +@mixin media-breakpoint-down($name, $breakpoints: $breakpoints) { + $max: breakpoint-max($name, $breakpoints); + @if $max { + @media (width < $max) { + @content; + } + } @else { + @content; + } +} + +// Media that spans multiple breakpoint widths. +// Makes the @content apply between the min and max breakpoints +@mixin media-breakpoint-between($lower, $upper, $breakpoints: $breakpoints) { + $min: breakpoint-min($lower, $breakpoints); + $max: breakpoint-max($upper, $breakpoints); + + @if $min != null and $max != null { + @media (width >= $min) and (width < $max) { + @content; + } + } @else if $max == null { + @include media-breakpoint-up($lower, $breakpoints) { + @content; + } + } @else if $min == null { + @include media-breakpoint-down($upper, $breakpoints) { + @content; + } + } +} + +// Media between the breakpoint's minimum and maximum widths. +// No minimum for the smallest breakpoint, and no maximum for the largest one. +// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower. +@mixin media-breakpoint-only($name, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + $next: breakpoint-next($name, $breakpoints); + $max: breakpoint-max($next, $breakpoints); + + @if $min != null and $max != null { + @media (width >= $min) and (width < $max) { + @content; + } + } @else if $max == null { + @include media-breakpoint-up($name, $breakpoints) { + @content; + } + } @else if $min == null { + @include media-breakpoint-down($next, $breakpoints) { + @content; + } + } +} + + +// Container queries +// +// Container queries allow elements to respond to the size of a containing element +// rather than the viewport. These mixins mirror the media-breakpoint-* mixins above. +// +// scss-docs-start container-query-mixins + +// Set an element as a query container. +// +// @include set-container(); // container-type: inline-size +// @include set-container(size); // container-type: size +// @include set-container(inline-size, sidebar); // container: sidebar / inline-size +// +@mixin set-container($type: inline-size, $name: null) { + @if $name { + container: #{$name} / #{$type}; + } @else { + container-type: #{$type}; + } +} + +// Container query of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider within the container. +// +// @include container-breakpoint-up(md) { ... } +// @include container-breakpoint-up(lg, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-up($name, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + @if $min { + @if $container-name { + @container #{$container-name} (width >= #{$min}) { + @content; + } + } @else { + @container (width >= #{$min}) { + @content; + } + } + } @else { + @content; + } +} + +// Container query of at most the maximum breakpoint width. No query for the largest breakpoint. +// Makes the @content apply to the given breakpoint and narrower within the container. +// +// @include container-breakpoint-down(lg) { ... } +// @include container-breakpoint-down(lg, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-down($name, $container-name: null, $breakpoints: $breakpoints) { + $max: breakpoint-max($name, $breakpoints); + @if $max { + @if $container-name { + @container #{$container-name} (width < #{$max}) { + @content; + } + } @else { + @container (width < #{$max}) { + @content; + } + } + } @else { + @content; + } +} + +// Container query that spans multiple breakpoint widths. +// Makes the @content apply between the min and max breakpoints within the container. +// +// @include container-breakpoint-between(md, xl) { ... } +// @include container-breakpoint-between(md, xl, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-between($lower, $upper, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($lower, $breakpoints); + $max: breakpoint-max($upper, $breakpoints); + + @if $min != null and $max != null { + @if $container-name { + @container #{$container-name} (width >= #{$min}) and (width < #{$max}) { + @content; + } + } @else { + @container (width >= #{$min}) and (width < #{$max}) { + @content; + } + } + } @else if $max == null { + @include container-breakpoint-up($lower, $container-name, $breakpoints) { + @content; + } + } @else if $min == null { + @include container-breakpoint-down($upper, $container-name, $breakpoints) { + @content; + } + } +} + +// Container query between the breakpoint's minimum and maximum widths. +// No minimum for the smallest breakpoint, and no maximum for the largest one. +// Makes the @content apply only to the given breakpoint within the container. +// +// @include container-breakpoint-only(md) { ... } +// @include container-breakpoint-only(md, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-only($name, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + $next: breakpoint-next($name, $breakpoints); + $max: breakpoint-max($next, $breakpoints); + + @if $min != null and $max != null { + @if $container-name { + @container #{$container-name} (width >= #{$min}) and (width < #{$max}) { + @content; + } + } @else { + @container (width >= #{$min}) and (width < #{$max}) { + @content; + } + } + } @else if $max == null { + @include container-breakpoint-up($name, $container-name, $breakpoints) { + @content; + } + } @else if $min == null { + @include container-breakpoint-down($next, $container-name, $breakpoints) { + @content; + } + } +} +// scss-docs-end container-query-mixins diff --git a/assets/stylesheets/bootstrap/layout/_containers.scss b/assets/stylesheets/bootstrap/layout/_containers.scss new file mode 100644 index 00000000..a190197f --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_containers.scss @@ -0,0 +1,55 @@ +@use "../config" as *; +@use "breakpoints" as *; + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. +// Container mixins + +@mixin make-container($gutter: $container-padding-x) { + --gutter-x: #{$gutter}; + --gutter-y: 0; + width: 100%; + padding-inline: calc(var(--gutter-x) * .5); + margin-inline: auto; +} + +@layer layout { + @if $enable-container-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .#{breakpoint-prefix($breakpoint, $breakpoints)}container { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $breakpoints) { + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + @each $name, $width in $breakpoints { + @if ($extend-breakpoint) { + .#{breakpoint-prefix($name, $breakpoints)}container { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } + } +} diff --git a/assets/stylesheets/bootstrap/layout/_grid.scss b/assets/stylesheets/bootstrap/layout/_grid.scss new file mode 100644 index 00000000..dab87eb7 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_grid.scss @@ -0,0 +1,68 @@ +@use "../config" as *; +@use "../mixins/grid" as *; + +// mdo-do +// - check gap utilities as replacement for gutter classes from v5 + +@layer layout { + @if $enable-grid-classes { + .row { + @include make-row(); + + > * { + @include make-col-ready(); + } + } + + @include make-grid-columns(); + } + + @if $enable-cssgrid { + .grid { + --columns: #{$grid-columns}; + --rows: 1; + --gap: #{$grid-gutter-x}; + + display: grid; + grid-template-rows: repeat(var(--rows), 1fr); + grid-template-columns: repeat(var(--columns), 1fr); + gap: var(--gap); + + } + + @include make-cssgrid(); + } + + // mdo-do: add to utilities? + .grid-cols-subgrid { + grid-template-columns: subgrid; + } + + .grid-fill { + --gap: #{$grid-gutter-x}; + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + grid-auto-flow: row; + gap: var(--gap); + } + + // .g-col-auto { + // grid-column: auto; + // } + + // mdo-do: add to utilities? + // .grid-cols-3 { + // --columns: 3; + // } + // .grid-cols-4 { + // --columns: 4; + // } + // .grid-cols-6 { + // --columns: 6; + // } + + // .grid-full { + // grid-column: 1 / -1; + // } +} diff --git a/assets/stylesheets/bootstrap/layout/index.scss b/assets/stylesheets/bootstrap/layout/index.scss new file mode 100644 index 00000000..df0a0f29 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/index.scss @@ -0,0 +1,3 @@ +@forward "breakpoints"; +@forward "containers"; +@forward "grid"; diff --git a/assets/stylesheets/bootstrap/mixins/_alert.scss b/assets/stylesheets/bootstrap/mixins/_alert.scss deleted file mode 100644 index fb524af1..00000000 --- a/assets/stylesheets/bootstrap/mixins/_alert.scss +++ /dev/null @@ -1,18 +0,0 @@ -@include deprecate("`alert-variant()`", "v5.3.0", "v6.0.0"); - -// scss-docs-start alert-variant-mixin -@mixin alert-variant($background, $border, $color) { - --#{$prefix}alert-color: #{$color}; - --#{$prefix}alert-bg: #{$background}; - --#{$prefix}alert-border-color: #{$border}; - --#{$prefix}alert-link-color: #{shade-color($color, 20%)}; - - @if $enable-gradients { - background-image: var(--#{$prefix}gradient); - } - - .alert-link { - color: var(--#{$prefix}alert-link-color); - } -} -// scss-docs-end alert-variant-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_backdrop.scss b/assets/stylesheets/bootstrap/mixins/_backdrop.scss index 9705ae9e..9b6c1fbe 100644 --- a/assets/stylesheets/bootstrap/mixins/_backdrop.scss +++ b/assets/stylesheets/bootstrap/mixins/_backdrop.scss @@ -1,14 +1,14 @@ -// Shared between modals and offcanvases -@mixin overlay-backdrop($zindex, $backdrop-bg, $backdrop-opacity) { +// Shared between modals and drawers +@mixin overlay-backdrop($zindex, $backdrop-bg, $backdrop-opacity, $backdrop-blur) { position: fixed; - top: 0; - left: 0; + inset: 0; z-index: $zindex; - width: 100vw; - height: 100vh; - background-color: $backdrop-bg; + background-color: color-mix(in oklch, var(--drawer-backdrop-bg) var(--drawer-backdrop-opacity), transparent); + @if $backdrop-blur { + backdrop-filter: blur($backdrop-blur); + } // Fade for backdrop &.fade { opacity: 0; } - &.show { opacity: $backdrop-opacity; } + &.show { opacity: 1; } } diff --git a/assets/stylesheets/bootstrap/mixins/_banner.scss b/assets/stylesheets/bootstrap/mixins/_banner.scss index dd8a5103..1fd399c1 100644 --- a/assets/stylesheets/bootstrap/mixins/_banner.scss +++ b/assets/stylesheets/bootstrap/mixins/_banner.scss @@ -1,7 +1,7 @@ @mixin bsBanner($file) { /*! - * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors + * Bootstrap #{$file} v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ } diff --git a/assets/stylesheets/bootstrap/mixins/_border-radius.scss b/assets/stylesheets/bootstrap/mixins/_border-radius.scss index 616decbc..6dd7e617 100644 --- a/assets/stylesheets/bootstrap/mixins/_border-radius.scss +++ b/assets/stylesheets/bootstrap/mixins/_border-radius.scss @@ -1,3 +1,8 @@ +@use "sass:list"; +@use "sass:math"; +@use "sass:meta"; +@use "../config" as *; + // stylelint-disable property-disallowed-list // Single side border-radius @@ -5,17 +10,17 @@ @function valid-radius($radius) { $return: (); @each $value in $radius { - @if type-of($value) == number { - $return: append($return, max($value, 0)); + @if meta.type-of($value) == number { + $return: list.append($return, math.max($value, 0)); } @else { - $return: append($return, $value); + $return: list.append($return, $value); } } @return $return; } // scss-docs-start border-radius-mixins -@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) { +@mixin border-radius($radius: var(--radius-5), $fallback-border-radius: false) { @if $enable-rounded { border-radius: valid-radius($radius); } @@ -24,55 +29,55 @@ } } -@mixin border-top-radius($radius: $border-radius) { +@mixin border-top-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); - border-top-right-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); } } -@mixin border-end-radius($radius: $border-radius) { +@mixin border-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-right-radius: valid-radius($radius); - border-bottom-right-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-bottom-radius($radius: $border-radius) { +@mixin border-bottom-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-right-radius: valid-radius($radius); - border-bottom-left-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-start-radius($radius: $border-radius) { +@mixin border-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); - border-bottom-left-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); } } -@mixin border-top-start-radius($radius: $border-radius) { +@mixin border-top-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); } } -@mixin border-top-end-radius($radius: $border-radius) { +@mixin border-top-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-right-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); } } -@mixin border-bottom-end-radius($radius: $border-radius) { +@mixin border-bottom-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-right-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-bottom-start-radius($radius: $border-radius) { +@mixin border-bottom-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-left-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); } } // scss-docs-end border-radius-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_box-shadow.scss b/assets/stylesheets/bootstrap/mixins/_box-shadow.scss index 0bb6bf7e..fa6c2227 100644 --- a/assets/stylesheets/bootstrap/mixins/_box-shadow.scss +++ b/assets/stylesheets/bootstrap/mixins/_box-shadow.scss @@ -1,3 +1,6 @@ +@use "sass:list"; +@use "../config" as *; + @mixin box-shadow($shadow...) { @if $enable-shadows { $result: (); @@ -10,14 +13,14 @@ $has-single-value: true; $single-value: $value; } @else { - $result: append($result, $value, "comma"); + $result: list.append($result, $value, "comma"); } } } @if $has-single-value { box-shadow: $single-value; - } @else if (length($result) > 0) { + } @else if (list.length($result) > 0) { box-shadow: $result; } } diff --git a/assets/stylesheets/bootstrap/mixins/_breakpoints.scss b/assets/stylesheets/bootstrap/mixins/_breakpoints.scss deleted file mode 100644 index 286be893..00000000 --- a/assets/stylesheets/bootstrap/mixins/_breakpoints.scss +++ /dev/null @@ -1,127 +0,0 @@ -// Breakpoint viewport sizes and media queries. -// -// Breakpoints are defined as a map of (name: minimum width), order from small to large: -// -// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px) -// -// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default. - -// Name of the next breakpoint, or null for the last breakpoint. -// -// >> breakpoint-next(sm) -// md -// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// md -// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl)) -// md -@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { - $n: index($breakpoint-names, $name); - @if not $n { - @error "breakpoint `#{$name}` not found in `#{$breakpoints}`"; - } - @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null); -} - -// Minimum breakpoint width. Null for the smallest (first) breakpoint. -// -// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// 576px -@function breakpoint-min($name, $breakpoints: $grid-breakpoints) { - $min: map-get($breakpoints, $name); - @return if($min != 0, $min, null); -} - -// Maximum breakpoint width. -// The maximum value is reduced by 0.02px to work around the limitations of -// `min-` and `max-` prefixes and viewports with fractional widths. -// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max -// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari. -// See https://bugs.webkit.org/show_bug.cgi?id=178261 -// -// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// 767.98px -@function breakpoint-max($name, $breakpoints: $grid-breakpoints) { - $max: map-get($breakpoints, $name); - @return if($max and $max > 0, $max - .02, null); -} - -// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front. -// Useful for making responsive utilities. -// -// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// "" (Returns a blank string) -// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// "-sm" -@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) { - @return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}"); -} - -// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. -// Makes the @content apply to the given breakpoint and wider. -@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($name, $breakpoints); - @if $min { - @media (min-width: $min) { - @content; - } - } @else { - @content; - } -} - -// Media of at most the maximum breakpoint width. No query for the largest breakpoint. -// Makes the @content apply to the given breakpoint and narrower. -@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) { - $max: breakpoint-max($name, $breakpoints); - @if $max { - @media (max-width: $max) { - @content; - } - } @else { - @content; - } -} - -// Media that spans multiple breakpoint widths. -// Makes the @content apply between the min and max breakpoints -@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($lower, $breakpoints); - $max: breakpoint-max($upper, $breakpoints); - - @if $min != null and $max != null { - @media (min-width: $min) and (max-width: $max) { - @content; - } - } @else if $max == null { - @include media-breakpoint-up($lower, $breakpoints) { - @content; - } - } @else if $min == null { - @include media-breakpoint-down($upper, $breakpoints) { - @content; - } - } -} - -// Media between the breakpoint's minimum and maximum widths. -// No minimum for the smallest breakpoint, and no maximum for the largest one. -// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower. -@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($name, $breakpoints); - $next: breakpoint-next($name, $breakpoints); - $max: breakpoint-max($next, $breakpoints); - - @if $min != null and $max != null { - @media (min-width: $min) and (max-width: $max) { - @content; - } - } @else if $max == null { - @include media-breakpoint-up($name, $breakpoints) { - @content; - } - } @else if $min == null { - @include media-breakpoint-down($next, $breakpoints) { - @content; - } - } -} diff --git a/assets/stylesheets/bootstrap/mixins/_buttons.scss b/assets/stylesheets/bootstrap/mixins/_buttons.scss deleted file mode 100644 index cf087fda..00000000 --- a/assets/stylesheets/bootstrap/mixins/_buttons.scss +++ /dev/null @@ -1,70 +0,0 @@ -// Button variants -// -// Easily pump out default styles, as well as :hover, :focus, :active, -// and disabled options for all buttons - -// scss-docs-start btn-variant-mixin -@mixin button-variant( - $background, - $border, - $color: color-contrast($background), - $hover-background: if($color == $color-contrast-light, shade-color($background, $btn-hover-bg-shade-amount), tint-color($background, $btn-hover-bg-tint-amount)), - $hover-border: if($color == $color-contrast-light, shade-color($border, $btn-hover-border-shade-amount), tint-color($border, $btn-hover-border-tint-amount)), - $hover-color: color-contrast($hover-background), - $active-background: if($color == $color-contrast-light, shade-color($background, $btn-active-bg-shade-amount), tint-color($background, $btn-active-bg-tint-amount)), - $active-border: if($color == $color-contrast-light, shade-color($border, $btn-active-border-shade-amount), tint-color($border, $btn-active-border-tint-amount)), - $active-color: color-contrast($active-background), - $disabled-background: $background, - $disabled-border: $border, - $disabled-color: color-contrast($disabled-background) -) { - --#{$prefix}btn-color: #{$color}; - --#{$prefix}btn-bg: #{$background}; - --#{$prefix}btn-border-color: #{$border}; - --#{$prefix}btn-hover-color: #{$hover-color}; - --#{$prefix}btn-hover-bg: #{$hover-background}; - --#{$prefix}btn-hover-border-color: #{$hover-border}; - --#{$prefix}btn-focus-shadow-rgb: #{to-rgb(mix($color, $border, 15%))}; - --#{$prefix}btn-active-color: #{$active-color}; - --#{$prefix}btn-active-bg: #{$active-background}; - --#{$prefix}btn-active-border-color: #{$active-border}; - --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow}; - --#{$prefix}btn-disabled-color: #{$disabled-color}; - --#{$prefix}btn-disabled-bg: #{$disabled-background}; - --#{$prefix}btn-disabled-border-color: #{$disabled-border}; -} -// scss-docs-end btn-variant-mixin - -// scss-docs-start btn-outline-variant-mixin -@mixin button-outline-variant( - $color, - $color-hover: color-contrast($color), - $active-background: $color, - $active-border: $color, - $active-color: color-contrast($active-background) -) { - --#{$prefix}btn-color: #{$color}; - --#{$prefix}btn-border-color: #{$color}; - --#{$prefix}btn-hover-color: #{$color-hover}; - --#{$prefix}btn-hover-bg: #{$active-background}; - --#{$prefix}btn-hover-border-color: #{$active-border}; - --#{$prefix}btn-focus-shadow-rgb: #{to-rgb($color)}; - --#{$prefix}btn-active-color: #{$active-color}; - --#{$prefix}btn-active-bg: #{$active-background}; - --#{$prefix}btn-active-border-color: #{$active-border}; - --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow}; - --#{$prefix}btn-disabled-color: #{$color}; - --#{$prefix}btn-disabled-bg: transparent; - --#{$prefix}btn-disabled-border-color: #{$color}; - --#{$prefix}gradient: none; -} -// scss-docs-end btn-outline-variant-mixin - -// scss-docs-start btn-size-mixin -@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) { - --#{$prefix}btn-padding-y: #{$padding-y}; - --#{$prefix}btn-padding-x: #{$padding-x}; - @include rfs($font-size, --#{$prefix}btn-font-size); - --#{$prefix}btn-border-radius: #{$border-radius}; -} -// scss-docs-end btn-size-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_caret.scss b/assets/stylesheets/bootstrap/mixins/_caret.scss index be731165..f227dcec 100644 --- a/assets/stylesheets/bootstrap/mixins/_caret.scss +++ b/assets/stylesheets/bootstrap/mixins/_caret.scss @@ -1,29 +1,37 @@ +@use "../config" as *; + +// scss-docs-start caret-variables +$caret-width: .3em !default; +$caret-vertical-align: $caret-width * .85 !default; +$caret-spacing: $caret-width * .85 !default; +// scss-docs-end caret-variables + // scss-docs-start caret-mixins @mixin caret-down($width: $caret-width) { - border-top: $width solid; - border-right: $width solid transparent; - border-bottom: 0; - border-left: $width solid transparent; + border-block-start: $width solid; + border-block-end: 0; + border-inline-start: $width solid transparent; + border-inline-end: $width solid transparent; } @mixin caret-up($width: $caret-width) { - border-top: 0; - border-right: $width solid transparent; - border-bottom: $width solid; - border-left: $width solid transparent; + border-block-start: 0; + border-block-end: $width solid; + border-inline-start: $width solid transparent; + border-inline-end: $width solid transparent; } @mixin caret-end($width: $caret-width) { - border-top: $width solid transparent; - border-right: 0; - border-bottom: $width solid transparent; - border-left: $width solid; + border-block-start: $width solid transparent; + border-block-end: $width solid transparent; + border-inline-start: $width solid; + border-inline-end: 0; } @mixin caret-start($width: $caret-width) { - border-top: $width solid transparent; - border-right: $width solid; - border-bottom: $width solid transparent; + border-block-start: $width solid transparent; + border-block-end: $width solid transparent; + border-inline-end: $width solid; } @mixin caret( @@ -35,7 +43,7 @@ @if $enable-caret { &::after { display: inline-block; - margin-left: $spacing; + margin-inline-start: $spacing; vertical-align: $vertical-align; content: ""; @if $direction == down { @@ -54,7 +62,7 @@ &::before { display: inline-block; - margin-right: $spacing; + margin-inline-end: $spacing; vertical-align: $vertical-align; content: ""; @include caret-start($width); @@ -62,7 +70,7 @@ } &:empty::after { - margin-left: 0; + margin-inline-start: 0; } } } diff --git a/assets/stylesheets/bootstrap/mixins/_clearfix.scss b/assets/stylesheets/bootstrap/mixins/_clearfix.scss deleted file mode 100644 index ffc62bb2..00000000 --- a/assets/stylesheets/bootstrap/mixins/_clearfix.scss +++ /dev/null @@ -1,9 +0,0 @@ -// scss-docs-start clearfix -@mixin clearfix() { - &::after { - display: block; - clear: both; - content: ""; - } -} -// scss-docs-end clearfix diff --git a/assets/stylesheets/bootstrap/mixins/_color-mode.scss b/assets/stylesheets/bootstrap/mixins/_color-mode.scss index 03338b02..518b0b09 100644 --- a/assets/stylesheets/bootstrap/mixins/_color-mode.scss +++ b/assets/stylesheets/bootstrap/mixins/_color-mode.scss @@ -1,3 +1,5 @@ +@use "../config" as *; + // scss-docs-start color-mode-mixin @mixin color-mode($mode: light, $root: false) { @if $color-mode-type == "media-query" { diff --git a/assets/stylesheets/bootstrap/mixins/_container.scss b/assets/stylesheets/bootstrap/mixins/_container.scss deleted file mode 100644 index b9f33519..00000000 --- a/assets/stylesheets/bootstrap/mixins/_container.scss +++ /dev/null @@ -1,11 +0,0 @@ -// Container mixins - -@mixin make-container($gutter: $container-padding-x) { - --#{$prefix}gutter-x: #{$gutter}; - --#{$prefix}gutter-y: 0; - width: 100%; - padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - margin-right: auto; - margin-left: auto; -} diff --git a/assets/stylesheets/bootstrap/mixins/_deprecate.scss b/assets/stylesheets/bootstrap/mixins/_deprecate.scss index df070bc5..862823df 100644 --- a/assets/stylesheets/bootstrap/mixins/_deprecate.scss +++ b/assets/stylesheets/bootstrap/mixins/_deprecate.scss @@ -1,3 +1,5 @@ +@use "../config" as *; + // Deprecate mixin // // This mixin can be used to deprecate mixins or functions. diff --git a/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss b/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss new file mode 100644 index 00000000..28f58fb4 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss @@ -0,0 +1,49 @@ +// Shared mixins for Dialog and Drawer sub-components. +// Both components use identical header/footer/body/title patterns +// with different token namespaces. + +@use "transition" as *; + +// Header: flex row with close button alignment +@mixin dialog-header($padding) { + display: flex; + flex-shrink: 0; + align-items: center; + padding: $padding; +} + +// Footer: flex row with end-aligned actions +@mixin dialog-footer($padding, $gap, $border-width, $border-color) { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + gap: $gap; + align-items: center; + justify-content: flex-end; + padding: $padding; + border-block-start: $border-width solid $border-color; +} + +// Body: flexible scrollable content area +@mixin dialog-body($padding) { + flex: 1 1 auto; + padding: $padding; +} + +// Title: reset margin, set line-height +@mixin dialog-title($line-height: 1.5) { + margin-bottom: 0; + line-height: $line-height; +} + +// Backdrop transitions for ::backdrop pseudo-element. +// Both Dialog and Drawer use identical allow-discrete transitions +// on display and overlay to keep ::backdrop in the top layer. +@mixin backdrop-transitions($duration, $timing) { + @include transition( + background-color $duration $timing, + backdrop-filter $duration $timing, + display $duration allow-discrete, + overlay $duration allow-discrete + ); +} diff --git a/assets/stylesheets/bootstrap/mixins/_focus-ring.scss b/assets/stylesheets/bootstrap/mixins/_focus-ring.scss new file mode 100644 index 00000000..156a2173 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_focus-ring.scss @@ -0,0 +1,10 @@ +@mixin focus-ring($offset: false, $color: null) { + @if $color != null { + outline: var(--focus-ring-width) solid #{$color}; + } @else { + outline: var(--focus-ring); + } + @if $offset { + outline-offset: var(--focus-ring-offset); + } +} diff --git a/assets/stylesheets/bootstrap/mixins/_form-validation.scss b/assets/stylesheets/bootstrap/mixins/_form-validation.scss new file mode 100644 index 00000000..3c575e10 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_form-validation.scss @@ -0,0 +1,33 @@ +// scss-docs-start form-validation-state-selector +@mixin form-validation-state-selector($state) { + @if & { + &.is-#{$state} { + @content; + } + + @if $state == "invalid" { + @at-root [data-bs-validate] #{&}:user-invalid { + @content; + } + } @else if $state == "valid" { + @at-root [data-bs-validate~="valid"] #{&}:user-valid { + @content; + } + } + } @else { + .is-#{$state} { + @content; + } + + @if $state == "invalid" { + [data-bs-validate] :user-invalid { + @content; + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] :user-valid { + @content; + } + } + } +} +// scss-docs-end form-validation-state-selector diff --git a/assets/stylesheets/bootstrap/mixins/_forms.scss b/assets/stylesheets/bootstrap/mixins/_forms.scss deleted file mode 100644 index 00b47641..00000000 --- a/assets/stylesheets/bootstrap/mixins/_forms.scss +++ /dev/null @@ -1,163 +0,0 @@ -// This mixin uses an `if()` technique to be compatible with Dart Sass -// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details - -// scss-docs-start form-validation-mixins -@mixin form-validation-state-selector($state) { - @if ($state == "valid" or $state == "invalid") { - .was-validated #{if(&, "&", "")}:#{$state}, - #{if(&, "&", "")}.is-#{$state} { - @content; - } - } @else { - #{if(&, "&", "")}.is-#{$state} { - @content; - } - } -} - -@mixin form-validation-state( - $state, - $color, - $icon, - $tooltip-color: color-contrast($color), - $tooltip-bg-color: rgba($color, $form-feedback-tooltip-opacity), - $focus-box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity), - $border-color: $color -) { - .#{$state}-feedback { - display: none; - width: 100%; - margin-top: $form-feedback-margin-top; - @include font-size($form-feedback-font-size); - font-style: $form-feedback-font-style; - color: $color; - } - - .#{$state}-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; // Contain to parent when possible - padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x; - margin-top: .1rem; - @include font-size($form-feedback-tooltip-font-size); - line-height: $form-feedback-tooltip-line-height; - color: $tooltip-color; - background-color: $tooltip-bg-color; - @include border-radius($form-feedback-tooltip-border-radius); - } - - @include form-validation-state-selector($state) { - ~ .#{$state}-feedback, - ~ .#{$state}-tooltip { - display: block; - } - } - - .form-control { - @include form-validation-state-selector($state) { - border-color: $border-color; - - @if $enable-validation-icons { - padding-right: $input-height-inner; - background-image: escape-svg($icon); - background-repeat: no-repeat; - background-position: right $input-height-inner-quarter center; - background-size: $input-height-inner-half $input-height-inner-half; - } - - &:focus { - border-color: $border-color; - @if $enable-shadows { - @include box-shadow($input-box-shadow, $focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $focus-box-shadow; - } - } - } - } - - // stylelint-disable-next-line selector-no-qualifying-type - textarea.form-control { - @include form-validation-state-selector($state) { - @if $enable-validation-icons { - padding-right: $input-height-inner; - background-position: top $input-height-inner-quarter right $input-height-inner-quarter; - } - } - } - - .form-select { - @include form-validation-state-selector($state) { - border-color: $border-color; - - @if $enable-validation-icons { - &:not([multiple]):not([size]), - &:not([multiple])[size="1"] { - --#{$prefix}form-select-bg-icon: #{escape-svg($icon)}; - padding-right: $form-select-feedback-icon-padding-end; - background-position: $form-select-bg-position, $form-select-feedback-icon-position; - background-size: $form-select-bg-size, $form-select-feedback-icon-size; - } - } - - &:focus { - border-color: $border-color; - @if $enable-shadows { - @include box-shadow($form-select-box-shadow, $focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $focus-box-shadow; - } - } - } - } - - .form-control-color { - @include form-validation-state-selector($state) { - @if $enable-validation-icons { - width: add($form-color-width, $input-height-inner); - } - } - } - - .form-check-input { - @include form-validation-state-selector($state) { - border-color: $border-color; - - &:checked { - background-color: $color; - } - - &:focus { - box-shadow: $focus-box-shadow; - } - - ~ .form-check-label { - color: $color; - } - } - } - .form-check-inline .form-check-input { - ~ .#{$state}-feedback { - margin-left: .5em; - } - } - - .input-group { - > .form-control:not(:focus), - > .form-select:not(:focus), - > .form-floating:not(:focus-within) { - @include form-validation-state-selector($state) { - @if $state == "valid" { - z-index: 3; - } @else if $state == "invalid" { - z-index: 4; - } - } - } - } -} -// scss-docs-end form-validation-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_gradients.scss b/assets/stylesheets/bootstrap/mixins/_gradients.scss index 608e18df..1789d35d 100644 --- a/assets/stylesheets/bootstrap/mixins/_gradients.scss +++ b/assets/stylesheets/bootstrap/mixins/_gradients.scss @@ -1,3 +1,6 @@ +@use "../colors" as *; +@use "../config" as *; + // Gradients // scss-docs-start gradient-bg-mixin @@ -5,7 +8,7 @@ background-color: $color; @if $enable-gradients { - background-image: var(--#{$prefix}gradient); + background-image: var(--gradient); } } // scss-docs-end gradient-bg-mixin @@ -14,18 +17,18 @@ // Horizontal gradient, from left to right // // Creates two color stops, start and end, by specifying a color and position for each color stop. -@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) { +@mixin gradient-x($start-color: var(--gray-700), $end-color: var(--gray-800), $start-percent: 0%, $end-percent: 100%) { background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent); } // Vertical gradient, from top to bottom // // Creates two color stops, start and end, by specifying a color and position for each color stop. -@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: null, $end-percent: null) { +@mixin gradient-y($start-color: var(--gray-700), $end-color: var(--gray-800), $start-percent: null, $end-percent: null) { background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent); } -@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) { +@mixin gradient-directional($start-color: var(--gray-700), $end-color: var(--gray-800), $deg: 45deg) { background-image: linear-gradient($deg, $start-color, $end-color); } @@ -37,11 +40,11 @@ background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color); } -@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) { +@mixin gradient-radial($inner-color: var(--gray-700), $outer-color: var(--gray-800)) { background-image: radial-gradient(circle, $inner-color, $outer-color); } -@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) { +@mixin gradient-striped($color: rgb(255 255 255 / .15), $angle: 45deg) { background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent); } // scss-docs-end gradient-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_grid.scss b/assets/stylesheets/bootstrap/mixins/_grid.scss index db77e07f..a25007af 100644 --- a/assets/stylesheets/bootstrap/mixins/_grid.scss +++ b/assets/stylesheets/bootstrap/mixins/_grid.scss @@ -1,36 +1,41 @@ +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +@use "../config" as *; +@use "../layout/breakpoints" as *; + // Grid system // // Generate semantic grid columns with these mixins. -@mixin make-row($gutter: $grid-gutter-width) { - --#{$prefix}gutter-x: #{$gutter}; - --#{$prefix}gutter-y: 0; +@mixin make-row($gutter-x: $grid-gutter-x, $gutter-y: $grid-gutter-y) { + --gutter-x: #{$gutter-x}; + --gutter-y: #{$gutter-y}; display: flex; flex-wrap: wrap; // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed - margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list - margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list + margin-inline: calc(-.5 * var(--gutter-x)); + margin-top: calc(-1 * var(--gutter-y)); } @mixin make-col-ready() { // Add box sizing if only the grid is loaded - box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null); + // stylelint-disable-next-line scss/at-function-named-arguments + box-sizing: if(sass(meta.variable-exists(include-column-box-sizing) and $include-column-box-sizing): border-box; else: null); // Prevent columns from becoming too narrow when at smaller grid tiers by // always setting `width: 100%;`. This works because we set the width // later on to override this initial width. flex-shrink: 0; width: 100%; max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid - padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - margin-top: var(--#{$prefix}gutter-y); + padding-inline: calc(var(--gutter-x) * .5); + margin-top: var(--gutter-y); } @mixin make-col($size: false, $columns: $grid-columns) { @if $size { flex: 0 0 auto; - width: percentage(divide($size, $columns)); + width: math.percentage(math.div($size, $columns)); } @else { flex: 1 1 0; @@ -44,8 +49,9 @@ } @mixin make-col-offset($size, $columns: $grid-columns) { - $num: divide($size, $columns); - margin-left: if($num == 0, 0, percentage($num)); + $num: math.div($size, $columns); + // stylelint-disable-next-line scss/at-function-named-arguments + margin-inline-start: if(sass($num == 0): 0; else: math.percentage($num)); } // Row columns @@ -56,7 +62,7 @@ @mixin row-cols($count) { > * { flex: 0 0 auto; - width: percentage(divide(1, $count)); + width: math.percentage(math.div(1, $count)); } } @@ -65,43 +71,42 @@ // Used only by Bootstrap to generate the correct number of grid classes given // any value of `$grid-columns`. -@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) { - @each $breakpoint in map-keys($breakpoints) { - $infix: breakpoint-infix($breakpoint, $breakpoints); +@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-x, $breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); @include media-breakpoint-up($breakpoint, $breakpoints) { - // Provide basic `.col-{bp}` classes for equal-width flexbox columns - .col#{$infix} { + .#{$prefix}col { flex: 1 0 0; } - .row-cols#{$infix}-auto > * { + .#{$prefix}row-cols-auto > * { @include make-col-auto(); } @if $grid-row-columns > 0 { @for $i from 1 through $grid-row-columns { - .row-cols#{$infix}-#{$i} { + .#{$prefix}row-cols-#{$i} { @include row-cols($i); } } } - .col#{$infix}-auto { + .#{$prefix}col-auto { @include make-col-auto(); } @if $columns > 0 { @for $i from 1 through $columns { - .col#{$infix}-#{$i} { + .#{$prefix}col-#{$i} { @include make-col($i, $columns); } } // `$columns - 1` because offsetting by the width of an entire row isn't possible @for $i from 0 through ($columns - 1) { - @if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0 - .offset#{$infix}-#{$i} { + @if not ($prefix == "" and $i == 0) { // Avoid emitting useless .offset-0 + .#{$prefix}offset-#{$i} { @include make-col-offset($i, $columns); } } @@ -112,28 +117,28 @@ // // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns. @each $key, $value in $gutters { - .g#{$infix}-#{$key}, - .gx#{$infix}-#{$key} { - --#{$prefix}gutter-x: #{$value}; + .#{$prefix}g-#{$key}, + .#{$prefix}gx-#{$key} { + --gutter-x: #{$value}; } - .g#{$infix}-#{$key}, - .gy#{$infix}-#{$key} { - --#{$prefix}gutter-y: #{$value}; + .#{$prefix}g-#{$key}, + .#{$prefix}gy-#{$key} { + --gutter-y: #{$value}; } } } } } -@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) { - @each $breakpoint in map-keys($breakpoints) { - $infix: breakpoint-infix($breakpoint, $breakpoints); +@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); @include media-breakpoint-up($breakpoint, $breakpoints) { @if $columns > 0 { @for $i from 1 through $columns { - .g-col#{$infix}-#{$i} { + .#{$prefix}g-col-#{$i} { grid-column: auto / span $i; } } @@ -141,7 +146,7 @@ // Start with `1` because `0` is an invalid value. // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible. @for $i from 1 through ($columns - 1) { - .g-start#{$infix}-#{$i} { + .#{$prefix}g-start-#{$i} { grid-column-start: $i; } } diff --git a/assets/stylesheets/bootstrap/mixins/_list-group.scss b/assets/stylesheets/bootstrap/mixins/_list-group.scss deleted file mode 100644 index 6274f343..00000000 --- a/assets/stylesheets/bootstrap/mixins/_list-group.scss +++ /dev/null @@ -1,26 +0,0 @@ -@include deprecate("`list-group-item-variant()`", "v5.3.0", "v6.0.0"); - -// List Groups - -// scss-docs-start list-group-mixin -@mixin list-group-item-variant($state, $background, $color) { - .list-group-item-#{$state} { - color: $color; - background-color: $background; - - &.list-group-item-action { - &:hover, - &:focus { - color: $color; - background-color: shade-color($background, 10%); - } - - &.active { - color: $white; - background-color: $color; - border-color: $color; - } - } - } -} -// scss-docs-end list-group-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_lists.scss b/assets/stylesheets/bootstrap/mixins/_lists.scss index 25185626..acc3b53d 100644 --- a/assets/stylesheets/bootstrap/mixins/_lists.scss +++ b/assets/stylesheets/bootstrap/mixins/_lists.scss @@ -2,6 +2,6 @@ // Unstyled keeps list items block level, just removes default browser padding and list-style @mixin list-unstyled { - padding-left: 0; - list-style: none; + padding-inline-start: 0; + list-style-type: ""; } diff --git a/assets/stylesheets/bootstrap/mixins/_mask-icon.scss b/assets/stylesheets/bootstrap/mixins/_mask-icon.scss new file mode 100644 index 00000000..f4d1fd7f --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_mask-icon.scss @@ -0,0 +1,21 @@ +// Mask icon +// +// Renders an SVG icon via a CSS mask so the shape is painted with the +// element's `background-color` and therefore inherits theme/dark-mode color. +// Set `background-color` on the element itself; pass `null` for `$icon` when +// the mask image is applied conditionally (e.g. per state or direction). + +// scss-docs-start mask-icon-mixin +@mixin mask-icon($icon: null, $size: contain, $position: center) { + @if $icon != null { + mask-image: $icon; + } + mask-repeat: no-repeat; + @if $position != null { + mask-position: $position; + } + @if $size != null { + mask-size: $size; + } +} +// scss-docs-end mask-icon-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_pagination.scss b/assets/stylesheets/bootstrap/mixins/_pagination.scss deleted file mode 100644 index 0d657964..00000000 --- a/assets/stylesheets/bootstrap/mixins/_pagination.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Pagination - -// scss-docs-start pagination-mixin -@mixin pagination-size($padding-y, $padding-x, $font-size, $border-radius) { - --#{$prefix}pagination-padding-x: #{$padding-x}; - --#{$prefix}pagination-padding-y: #{$padding-y}; - @include rfs($font-size, --#{$prefix}pagination-font-size); - --#{$prefix}pagination-border-radius: #{$border-radius}; -} -// scss-docs-end pagination-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_reset-text.scss b/assets/stylesheets/bootstrap/mixins/_reset-text.scss index f5bd1afe..4dac3c7e 100644 --- a/assets/stylesheets/bootstrap/mixins/_reset-text.scss +++ b/assets/stylesheets/bootstrap/mixins/_reset-text.scss @@ -1,10 +1,9 @@ @mixin reset-text { - font-family: $font-family-base; + font-family: var(--body-font-family); // We deliberately do NOT reset font-size or overflow-wrap / word-wrap. font-style: normal; - font-weight: $font-weight-normal; - line-height: $line-height-base; - text-align: left; // Fallback for where `start` is not supported + font-weight: var(--body-font-weight); + line-height: var(--body-line-height); text-align: start; text-decoration: none; text-shadow: none; diff --git a/assets/stylesheets/bootstrap/mixins/_table-variants.scss b/assets/stylesheets/bootstrap/mixins/_table-variants.scss deleted file mode 100644 index 5fe1b9b2..00000000 --- a/assets/stylesheets/bootstrap/mixins/_table-variants.scss +++ /dev/null @@ -1,24 +0,0 @@ -// scss-docs-start table-variant -@mixin table-variant($state, $background) { - .table-#{$state} { - $color: color-contrast(opaque($body-bg, $background)); - $hover-bg: mix($color, $background, percentage($table-hover-bg-factor)); - $striped-bg: mix($color, $background, percentage($table-striped-bg-factor)); - $active-bg: mix($color, $background, percentage($table-active-bg-factor)); - $table-border-color: mix($color, $background, percentage($table-border-factor)); - - --#{$prefix}table-color: #{$color}; - --#{$prefix}table-bg: #{$background}; - --#{$prefix}table-border-color: #{$table-border-color}; - --#{$prefix}table-striped-bg: #{$striped-bg}; - --#{$prefix}table-striped-color: #{color-contrast($striped-bg)}; - --#{$prefix}table-active-bg: #{$active-bg}; - --#{$prefix}table-active-color: #{color-contrast($active-bg)}; - --#{$prefix}table-hover-bg: #{$hover-bg}; - --#{$prefix}table-hover-color: #{color-contrast($hover-bg)}; - - color: var(--#{$prefix}table-color); - border-color: var(--#{$prefix}table-border-color); - } -} -// scss-docs-end table-variant diff --git a/assets/stylesheets/bootstrap/mixins/_tokens.scss b/assets/stylesheets/bootstrap/mixins/_tokens.scss new file mode 100644 index 00000000..d4132add --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_tokens.scss @@ -0,0 +1,9 @@ +// Mixin to output tokens as CSS custom properties + +// scss-docs-start mixin-tokens +@mixin tokens($map) { + @each $prop, $value in $map { + #{$prop}: #{$value}; + } +} +// scss-docs-end mixin-tokens diff --git a/assets/stylesheets/bootstrap/mixins/_transition.scss b/assets/stylesheets/bootstrap/mixins/_transition.scss index d437f6d8..c9f2ca41 100644 --- a/assets/stylesheets/bootstrap/mixins/_transition.scss +++ b/assets/stylesheets/bootstrap/mixins/_transition.scss @@ -1,10 +1,13 @@ +@use "sass:list"; +@use "../config" as *; + // stylelint-disable property-disallowed-list @mixin transition($transition...) { - @if length($transition) == 0 { + @if list.length($transition) == 0 { $transition: $transition-base; } - @if length($transition) > 1 { + @if list.length($transition) > 1 { @each $value in $transition { @if $value == null or $value == none { @warn "The keyword 'none' or 'null' must be used as a single argument."; @@ -13,11 +16,11 @@ } @if $enable-transitions { - @if nth($transition, 1) != null { + @if list.nth($transition, 1) != null { transition: $transition; } - @if $enable-reduced-motion and nth($transition, 1) != null and nth($transition, 1) != none { + @if $enable-reduced-motion and list.nth($transition, 1) != null and list.nth($transition, 1) != none { @media (prefers-reduced-motion: reduce) { transition: none; } diff --git a/assets/stylesheets/bootstrap/mixins/_utilities.scss b/assets/stylesheets/bootstrap/mixins/_utilities.scss index 4795e894..2d78150d 100644 --- a/assets/stylesheets/bootstrap/mixins/_utilities.scss +++ b/assets/stylesheets/bootstrap/mixins/_utilities.scss @@ -1,96 +1,291 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:meta"; +@use "../layout/breakpoints" as bp; + // Utility generator -// Used to generate utilities & print utilities -@mixin generate-utility($utility, $infix: "", $is-rfs-media-query: false) { - $values: map-get($utility, values); - // If the values are a list or string, convert it into a map - @if type-of($values) == "string" or type-of(nth($values, 1)) != "list" { - $values: zip($values, $values); - } +// - Utilities can use three different types of selectors: +// - class: .class +// - attr-starts: [class^="class"] +// - attr-includes: [class*="class"] +// - Utilities can target children via `child-selector`, wrapped in :where() for zero specificity +// - Utilities can generate regular CSS properties and CSS custom properties +// - Utilities can be responsive or not +// - Utilities can have state variants (e.g., hover, focus, active) +// - Utilities can define local CSS variables +// +// CSS custom properties can be generated in two ways: +// +// 1. Property map with null values (CSS var receives the utility value): +// "bg-color": ( +// property: ( +// "--bg": null, +// "background-color": var(--bg) +// ), +// class: bg, +// values: ( +// primary: var(--blue-500), +// ) +// ) +// Generates: +// .bg-primary { +// --bs-bg: var(--bs-blue-500); +// background-color: var(--bs-bg); +// } +// +// 2. Variables map (static CSS custom properties on every class): +// "link-underline": ( +// property: text-decoration-color, +// class: link-underline, +// variables: ( +// "link-underline-opacity": 1 +// ), +// values: (...) +// ) +// Generates: +// .link-underline { +// --bs-link-underline-opacity: 1; +// text-decoration-color: ...; +// } - @each $key, $value in $values { - $properties: map-get($utility, property); +// Helper mixin to emit CSS custom properties from a utility's `variables` key. +// When variables is a map, the provided static values are used on each class. +// When variables is a list or single identifier, each variable receives the current utility value. +@mixin generate-variables($utility, $value) { + @if map.has-key($utility, variables) { + $variables: map.get($utility, variables); + @if meta.type-of($variables) == "map" { + @each $var-key, $var-value in $variables { + --#{$var-key}: #{$var-value}; + } + } @else { + // Treat as a list (or single identifier) — each variable gets the utility value + @each $var-name in $variables { + --#{$var-name}: #{$value}; + } + } + } +} - // Multiple properties are possible, for example with vertical or horizontal margins or paddings - @if type-of($properties) == "string" { - $properties: append((), $properties); +// Helper mixin to generate CSS properties for both legacy and property map approaches +@mixin generate-properties($utility, $property-map, $properties, $value) { + @if $property-map != null { + // Property-Value Mapping approach + @each $property, $default-value in $property-map { + // If value is a map, check if it has a key for this property. + // Otherwise, use default-value (or $value if default-value is null). + $actual-value: $default-value; + @if meta.type-of($value) == "map" and map.has-key($value, $property) { + $actual-value: map.get($value, $property); + } @else if $default-value == null { + $actual-value: $value; + } + @if map.get($utility, important) { + #{$property}: $actual-value !important; // stylelint-disable-line declaration-no-important + } @else { + #{$property}: $actual-value; + } + } + } @else { + // Legacy approach + @each $property in $properties { + @if map.get($utility, important) { + #{$property}: $value !important; // stylelint-disable-line declaration-no-important + } @else { + #{$property}: $value; + } } + } +} - // Use custom class if present - $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1)); - $property-class: if($property-class == null, "", $property-class); +@mixin generate-utility($utility, $prefix: "") { + // Validate required keys + @if not map.has-key($utility, property) { + @error "Utility is missing required `property` key: #{$utility}"; + } + @if not map.has-key($utility, values) { + @error "Utility is missing required `values` key: #{$utility}"; + } - // Use custom CSS variable name if present, otherwise default to `class` - $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class)); + // Warn on unknown keys (likely typos) + $valid-keys: property, values, class, selector, responsive, print, dark, important, state, variables, child-selector, enabled; + @each $key in map.keys($utility) { + @if not list.index($valid-keys, $key) { + @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}"; + } + } - // State params to generate pseudo-classes - $state: if(map-has-key($utility, state), map-get($utility, state), ()); + // Validate boolean keys + @each $bool-key in (responsive, print, dark, important, enabled) { + @if map.has-key($utility, $bool-key) { + $val: map.get($utility, $bool-key); + @if $val != true and $val != false { + @error "Utility key `#{$bool-key}` should be a boolean (true or false), got: #{$val}"; + } + } + } - $infix: if($property-class == "" and str-slice($infix, 1, 1) == "-", str-slice($infix, 2), $infix); + // Determine if we're generating a class, or an attribute selector + $selector-type: "class"; + @if map.has-key($utility, selector) { + $selector-type: map.get($utility, selector); + // Validate selector type + $valid-selectors: "class", "attr-starts", "attr-includes"; + @if not list.index($valid-selectors, $selector-type) { + @error "Invalid `selector` value `#{$selector-type}`. Must be one of: #{$valid-selectors}"; + } + } + // Then get the class name to use in a class (e.g., .class) or in an attribute selector (e.g., [class^="class"]) + $selector-class: map.get($utility, class); - // Don't prefix if value key is null (e.g. with shadow class) - $property-class-modifier: if($key, if($property-class == "" and $infix == "", "", "-") + $key, ""); + // Attribute selectors require a `class` key + @if $selector-type != "class" and not map.has-key($utility, class) { + @error "Utility with `selector: #{$selector-type}` requires a `class` key."; + } - @if map-get($utility, rfs) { - // Inside the media query - @if $is-rfs-media-query { - $val: rfs-value($value); + // Get the list or map of values and ensure it's a map + $values: map.get($utility, values); + @if meta.type-of($values) != "map" { + @if meta.type-of($values) == "list" { + $list: (); + @each $value in $values { + $list: map.merge($list, ($value: $value)); + } + $values: $list; + } @else { + $values: (null: $values); + } + } + + @each $key, $value in $values { + $properties: map.get($utility, property); + $property-map: null; + $custom-class: ""; - // Do not render anything if fluid and non fluid values are the same - $value: if($val == rfs-fluid-value($value), null, $val); + // Check if property is a map (Property-Value Mapping approach) + @if meta.type-of($properties) == "map" { + $property-map: $properties; + @if map.has-key($utility, class) { + $custom-class: map.get($utility, class); } - @else { - $value: rfs-fluid-value($value); + } @else { + // Legacy approach: multiple properties are possible, for example with vertical or horizontal margins or paddings + @if meta.type-of($properties) == "string" { + $properties: list.append((), $properties); + } + // Use custom class if present, otherwise use the first value from the list of properties + @if map.has-key($utility, class) { + $custom-class: map.get($utility, class); + } @else { + $custom-class: list.nth($properties, 1); + } + @if $custom-class == null { + $custom-class: ""; } } - $is-css-var: map-get($utility, css-var); - $is-local-vars: map-get($utility, local-vars); - $is-rtl: map-get($utility, rtl); + // State params to generate state variants + $state: (); + @if map.has-key($utility, state) { + $state: map.get($utility, state); + } - @if $value != null { - @if $is-rtl == false { - /* rtl:begin:remove */ + // Don't add a dash before value key if value key is null (e.g. with shadow class) + $custom-class-modifier: ""; + @if $key { + @if $custom-class == "" { + $custom-class-modifier: $key; + } @else { + $custom-class-modifier: "-" + $key; } + } - @if $is-css-var { - .#{$property-class + $infix + $property-class-modifier} { - --#{$prefix}#{$css-variable-name}: #{$value}; - } + // Build the class name fragment (without prefix or dot) for reuse in state variants + $class-name: ""; + @if $selector-type == "class" { + @if $custom-class != "" { + $class-name: $custom-class + $custom-class-modifier; + } @else if $selector-class != null and $selector-class != "" { + $class-name: $selector-class + $custom-class-modifier; + } @else { + $class-name: $custom-class-modifier; + } + } + + $selector: ""; + @if $selector-type == "class" { + $selector: ".#{$prefix + $class-name}"; + } @else if $selector-type == "attr-starts" { + $selector: "[class^=\"#{$selector-class}\"]"; + } @else if $selector-type == "attr-includes" { + $selector: "[class*=\"#{$selector-class}\"]"; + } + + // Apply child-selector wrapping if present (wraps in :where() for zero specificity) + $child-sel: null; + @if map.has-key($utility, child-selector) { + $child-sel: map.get($utility, child-selector); + } + + $final-selector: $selector; + @if $child-sel { + $final-selector: ":where(#{$selector} #{$child-sel})"; + } - @each $pseudo in $state { - .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} { - --#{$prefix}#{$css-variable-name}: #{$value}; - } + #{$final-selector} { + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); + } + + // Generate state variants (e.g., hover:link-10 instead of link-10-hover) + @if $state != () { + @each $state-variant in $state { + $state-selector: ".#{$prefix}#{$state-variant}\\:#{$class-name}:#{$state-variant}"; + @if $child-sel { + $state-selector: ":where(#{$state-selector} #{$child-sel})"; } - } @else { - .#{$property-class + $infix + $property-class-modifier} { - @each $property in $properties { - @if $is-local-vars { - @each $local-var, $variable in $is-local-vars { - --#{$prefix}#{$local-var}: #{$variable}; - } - } - #{$property}: $value if($enable-important-utilities, !important, null); - } + + #{$state-selector} { + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); } + } + } + } +} - @each $pseudo in $state { - .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} { - @each $property in $properties { - @if $is-local-vars { - @each $local-var, $variable in $is-local-vars { - --#{$prefix}#{$local-var}: #{$variable}; - } - } - #{$property}: $value if($enable-important-utilities, !important, null); - } - } +// Generates all utility classes: base, responsive, print, and dark. +// Extracted so that tests can call this mixin directly with a custom $utilities map +// rather than having to mirror the loop conditions inline. +@mixin generate-utilities-loop($utilities, $breakpoints) { + // Base + responsive (one pass per breakpoint) + @each $breakpoint in map.keys($breakpoints) { + @include bp.media-breakpoint-up($breakpoint, $breakpoints) { + $prefix: bp.breakpoint-prefix($breakpoint, $breakpoints); + + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and (map.get($utility, responsive) or $prefix == "") { + @include generate-utility($utility, $prefix); } } + } + } + + // Print utilities + @media print { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, print) == true { + @include generate-utility($utility, "print\\:"); + } + } + } - @if $is-rtl == false { - /* rtl:end:remove */ + // Dark utilities + @media (prefers-color-scheme: dark) { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, dark) == true { + @include generate-utility($utility, "dark\\:"); } } } diff --git a/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss b/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss index 9dd0ad33..4836b817 100644 --- a/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss +++ b/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss @@ -32,7 +32,7 @@ // Useful for "Skip to main content" links; see https://www.w3.org/WAI/WCAG22/Techniques/general/G1.html @mixin visually-hidden-focusable() { - &:not(:focus):not(:focus-within) { + &:not(:focus, :focus-within) { @include visually-hidden(); } } diff --git a/assets/stylesheets/bootstrap/mixins/index.scss b/assets/stylesheets/bootstrap/mixins/index.scss new file mode 100644 index 00000000..a0201ed3 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/index.scss @@ -0,0 +1,32 @@ +// Toggles +// +// Used in conjunction with global variables to enable certain theme features. + +@forward "tokens"; + +// Deprecate +@forward "deprecate"; + +// Helpers +@forward "color-mode"; +@forward "color-scheme"; +@forward "image"; +@forward "resize"; +@forward "visually-hidden"; +@forward "reset-text"; +@forward "text-truncate"; + +// Utilities +@forward "utilities"; + +// Components +@forward "backdrop"; +@forward "caret"; +@forward "form-validation"; +@forward "mask-icon"; + +// Skins +@forward "border-radius"; +@forward "box-shadow"; +@forward "gradients"; +@forward "transition"; diff --git a/assets/stylesheets/bootstrap/utilities/_api.scss b/assets/stylesheets/bootstrap/utilities/_api.scss index 62e1d398..327573a3 100644 --- a/assets/stylesheets/bootstrap/utilities/_api.scss +++ b/assets/stylesheets/bootstrap/utilities/_api.scss @@ -1,47 +1,7 @@ -// Loop over each breakpoint -@each $breakpoint in map-keys($grid-breakpoints) { +@use "../config" as *; +@use "../mixins/utilities" as *; +@use "../utilities" as *; - // Generate media query if needed - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - // Loop over each utility property - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Only proceed if responsive media queries are enabled or if it's the base media query - @if type-of($utility) == "map" and (map-get($utility, responsive) or $infix == "") { - @include generate-utility($utility, $infix); - } - } - } -} - -// RFS rescaling -@media (min-width: $rfs-mq-value) { - @each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) { - // Loop over each utility property - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Only proceed if responsive media queries are enabled or if it's the base media query - @if type-of($utility) == "map" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == "") { - @include generate-utility($utility, $infix, true); - } - } - } - } -} - - -// Print utilities -@media print { - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Then check if the utility needs print styles - @if type-of($utility) == "map" and map-get($utility, print) == true { - @include generate-utility($utility, "-print"); - } - } +@layer utilities { + @include generate-utilities-loop($utilities, $breakpoints); } diff --git a/assets/stylesheets/bootstrap/vendor/_rfs.scss b/assets/stylesheets/bootstrap/vendor/_rfs.scss deleted file mode 100644 index aa1f82b9..00000000 --- a/assets/stylesheets/bootstrap/vendor/_rfs.scss +++ /dev/null @@ -1,348 +0,0 @@ -// stylelint-disable scss/dimension-no-non-numeric-values - -// SCSS RFS mixin -// -// Automated responsive values for font sizes, paddings, margins and much more -// -// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE) - -// Configuration - -// Base value -$rfs-base-value: 1.25rem !default; -$rfs-unit: rem !default; - -@if $rfs-unit != rem and $rfs-unit != px { - @error "`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`."; -} - -// Breakpoint at where values start decreasing if screen width is smaller -$rfs-breakpoint: 1200px !default; -$rfs-breakpoint-unit: px !default; - -@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem { - @error "`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`."; -} - -// Resize values based on screen height and width -$rfs-two-dimensional: false !default; - -// Factor of decrease -$rfs-factor: 10 !default; - -@if type-of($rfs-factor) != number or $rfs-factor <= 1 { - @error "`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1."; -} - -// Mode. Possibilities: "min-media-query", "max-media-query" -$rfs-mode: min-media-query !default; - -// Generate enable or disable classes. Possibilities: false, "enable" or "disable" -$rfs-class: false !default; - -// 1 rem = $rfs-rem-value px -$rfs-rem-value: 16 !default; - -// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14 -$rfs-safari-iframe-resize-bug-fix: false !default; - -// Disable RFS by setting $enable-rfs to false -$enable-rfs: true !default; - -// Cache $rfs-base-value unit -$rfs-base-value-unit: unit($rfs-base-value); - -@function divide($dividend, $divisor, $precision: 10) { - $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1); - $dividend: abs($dividend); - $divisor: abs($divisor); - @if $dividend == 0 { - @return 0; - } - @if $divisor == 0 { - @error "Cannot divide by 0"; - } - $remainder: $dividend; - $result: 0; - $factor: 10; - @while ($remainder > 0 and $precision >= 0) { - $quotient: 0; - @while ($remainder >= $divisor) { - $remainder: $remainder - $divisor; - $quotient: $quotient + 1; - } - $result: $result * 10 + $quotient; - $factor: $factor * .1; - $remainder: $remainder * 10; - $precision: $precision - 1; - @if ($precision < 0 and $remainder >= $divisor * 5) { - $result: $result + 1; - } - } - $result: $result * $factor * $sign; - $dividend-unit: unit($dividend); - $divisor-unit: unit($divisor); - $unit-map: ( - "px": 1px, - "rem": 1rem, - "em": 1em, - "%": 1% - ); - @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) { - $result: $result * map-get($unit-map, $dividend-unit); - } - @return $result; -} - -// Remove px-unit from $rfs-base-value for calculations -@if $rfs-base-value-unit == px { - $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1); -} -@else if $rfs-base-value-unit == rem { - $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value)); -} - -// Cache $rfs-breakpoint unit to prevent multiple calls -$rfs-breakpoint-unit-cache: unit($rfs-breakpoint); - -// Remove unit from $rfs-breakpoint for calculations -@if $rfs-breakpoint-unit-cache == px { - $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1); -} -@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == "em" { - $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value)); -} - -// Calculate the media query value -$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit}); -$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width); -$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height); - -// Internal mixin used to determine which media query needs to be used -@mixin _rfs-media-query { - @if $rfs-two-dimensional { - @if $rfs-mode == max-media-query { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) { - @content; - } - } - @else { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) { - @content; - } - } - } - @else { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) { - @content; - } - } -} - -// Internal mixin that adds disable classes to the selector if needed. -@mixin _rfs-rule { - @if $rfs-class == disable and $rfs-mode == max-media-query { - // Adding an extra class increases specificity, which prevents the media query to override the property - &, - .disable-rfs &, - &.disable-rfs { - @content; - } - } - @else if $rfs-class == enable and $rfs-mode == min-media-query { - .enable-rfs &, - &.enable-rfs { - @content; - } - } @else { - @content; - } -} - -// Internal mixin that adds enable classes to the selector if needed. -@mixin _rfs-media-query-rule { - - @if $rfs-class == enable { - @if $rfs-mode == min-media-query { - @content; - } - - @include _rfs-media-query () { - .enable-rfs &, - &.enable-rfs { - @content; - } - } - } - @else { - @if $rfs-class == disable and $rfs-mode == min-media-query { - .disable-rfs &, - &.disable-rfs { - @content; - } - } - @include _rfs-media-query () { - @content; - } - } -} - -// Helper function to get the formatted non-responsive value -@function rfs-value($values) { - // Convert to list - $values: if(type-of($values) != list, ($values,), $values); - - $val: ""; - - // Loop over each value and calculate value - @each $value in $values { - @if $value == 0 { - $val: $val + " 0"; - } - @else { - // Cache $value unit - $unit: if(type-of($value) == "number", unit($value), false); - - @if $unit == px { - // Convert to rem if needed - $val: $val + " " + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value); - } - @else if $unit == rem { - // Convert to px if needed - $val: $val + " " + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value); - } @else { - // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value - $val: $val + " " + $value; - } - } - } - - // Remove first space - @return unquote(str-slice($val, 2)); -} - -// Helper function to get the responsive value calculated by RFS -@function rfs-fluid-value($values) { - // Convert to list - $values: if(type-of($values) != list, ($values,), $values); - - $val: ""; - - // Loop over each value and calculate value - @each $value in $values { - @if $value == 0 { - $val: $val + " 0"; - } @else { - // Cache $value unit - $unit: if(type-of($value) == "number", unit($value), false); - - // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value - @if not $unit or $unit != px and $unit != rem { - $val: $val + " " + $value; - } @else { - // Remove unit from $value for calculations - $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value))); - - // Only add the media query if the value is greater than the minimum value - @if abs($value) <= $rfs-base-value or not $enable-rfs { - $val: $val + " " + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px); - } - @else { - // Calculate the minimum value - $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor); - - // Calculate difference between $value and the minimum value - $value-diff: abs($value) - $value-min; - - // Base value formatting - $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px); - - // Use negative value if needed - $min-width: if($value < 0, -$min-width, $min-width); - - // Use `vmin` if two-dimensional is enabled - $variable-unit: if($rfs-two-dimensional, vmin, vw); - - // Calculate the variable width between 0 and $rfs-breakpoint - $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit}; - - // Return the calculated value - $val: $val + " calc(" + $min-width + if($value < 0, " - ", " + ") + $variable-width + ")"; - } - } - } - } - - // Remove first space - @return unquote(str-slice($val, 2)); -} - -// RFS mixin -@mixin rfs($values, $property: font-size) { - @if $values != null { - $val: rfs-value($values); - $fluid-val: rfs-fluid-value($values); - - // Do not print the media query if responsive & non-responsive values are the same - @if $val == $fluid-val { - #{$property}: $val; - } - @else { - @include _rfs-rule () { - #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val); - - // Include safari iframe resize fix if needed - min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null); - } - - @include _rfs-media-query-rule () { - #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val); - } - } - } -} - -// Shorthand helper mixins -@mixin font-size($value) { - @include rfs($value); -} - -@mixin padding($value) { - @include rfs($value, padding); -} - -@mixin padding-top($value) { - @include rfs($value, padding-top); -} - -@mixin padding-right($value) { - @include rfs($value, padding-right); -} - -@mixin padding-bottom($value) { - @include rfs($value, padding-bottom); -} - -@mixin padding-left($value) { - @include rfs($value, padding-left); -} - -@mixin margin($value) { - @include rfs($value, margin); -} - -@mixin margin-top($value) { - @include rfs($value, margin-top); -} - -@mixin margin-right($value) { - @include rfs($value, margin-right); -} - -@mixin margin-bottom($value) { - @include rfs($value, margin-bottom); -} - -@mixin margin-left($value) { - @include rfs($value, margin-left); -} diff --git a/bootstrap.gemspec b/bootstrap.gemspec index b47a63bc..f1b839f6 100644 --- a/bootstrap.gemspec +++ b/bootstrap.gemspec @@ -12,9 +12,12 @@ Gem::Specification.new do |s| s.license = 'MIT' # SassC requires Ruby 2.3.3. Also specify here to make it obvious. + # (Bootstrap 6 stylesheets require a Dart Sass engine to compile, but the gem + # itself stays installable on the same Ruby range as before.) s.required_ruby_version = '>= 2.3.3' - s.add_runtime_dependency 'popper_js', '>= 2.11.8', '< 3' + # Bootstrap 6 uses @floating-ui/dom (vendored in assets/javascripts) instead + # of Popper, so there is no longer a popper_js runtime dependency. s.add_development_dependency 'rake' @@ -22,9 +25,10 @@ Gem::Specification.new do |s| s.add_development_dependency 'minitest', '>= 5.14.4', '< 7' s.add_development_dependency 'minitest-reporters', '~> 1.4.3' s.add_development_dependency 'term-ansicolor' - # Integration testing + # Integration testing (headless browser) s.add_development_dependency 'capybara', '>= 2.6.0' s.add_development_dependency 'cuprite' + s.add_development_dependency 'webrick' # Dummy Rails app dependencies s.add_development_dependency 'railties' s.add_development_dependency 'actionpack', '>= 4.1.5' diff --git a/lib/bootstrap.rb b/lib/bootstrap.rb index d44f7e54..296be8ef 100644 --- a/lib/bootstrap.rb +++ b/lib/bootstrap.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'bootstrap/version' -require 'popper_js' module Bootstrap class << self diff --git a/lib/bootstrap/version.rb b/lib/bootstrap/version.rb index d19fa088..ffcaf946 100644 --- a/lib/bootstrap/version.rb +++ b/lib/bootstrap/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Bootstrap - VERSION = '5.3.8' - BOOTSTRAP_SHA = '25aa8cc0b32f0d1a54be575347e6d84b70b1acd7' + VERSION = '6.0.0.alpha1' + BOOTSTRAP_SHA = '15f17673ebcd82416fc55dbbbefda19dc4f298f8' end diff --git a/tasks/updater.rb b/tasks/updater.rb index 08cc8769..3b385b9a 100644 --- a/tasks/updater.rb +++ b/tasks/updater.rb @@ -18,12 +18,13 @@ class Updater include Js include Scss - def initialize(repo: 'twbs/bootstrap', branch: 'main', save_to: {}, cache_path: 'tmp/bootstrap-cache') + def initialize(repo: 'twbs/bootstrap', branch: 'main', save_to: {}, cache_path: 'tmp/bootstrap-cache', skip_js: false) @logger = Logger.new @repo = repo @branch = branch || 'main' @branch_sha = get_branch_sha @cache_path = cache_path + @skip_js = skip_js @repo_url = "https://github.com/#@repo" @save_to = { js: 'assets/javascripts/bootstrap', @@ -40,11 +41,20 @@ def update_bootstrap puts " twbs cache: #{@cache_path}" puts '-' * 60 - FileUtils.rm_rf('assets') - @save_to.each { |_, v| FileUtils.mkdir_p(v) } - - update_scss_assets - update_javascript_assets + # Bootstrap 6's upstream `v6-dev` still ships stale Bootstrap 5 `dist/js`, + # so `skip_js: true` refreshes only the stylesheets (and version SHA), + # leaving the bundled JavaScript untouched until Bootstrap 6's JS is + # published upstream. + if @skip_js + FileUtils.rm_rf(@save_to[:scss]) + FileUtils.mkdir_p(@save_to[:scss]) + update_scss_assets + else + FileUtils.rm_rf('assets') + @save_to.each { |_, v| FileUtils.mkdir_p(v) } + update_scss_assets + update_javascript_assets + end store_version end diff --git a/tasks/updater/js.rb b/tasks/updater/js.rb index ae352807..f9167349 100644 --- a/tasks/updater/js.rb +++ b/tasks/updater/js.rb @@ -3,59 +3,72 @@ class Updater module Js - INLINED_SRCS = %w[].freeze - def update_javascript_assets log_status 'Updating javascripts...' save_to = @save_to[:js] + # Bootstrap 6 ships ES modules only (no UMD bundle, no `window.bootstrap` + # global). We keep the individual modules and the bundles for importmap + # pinning; Sprockets `//= require` concatenation no longer applies. read_files('js/dist', bootstrap_js_files).each do |name, content| save_file("#{save_to}/#{name}", remove_source_mapping_url(content)) end log_processed "#{bootstrap_js_files * ' '}" - log_status 'Updating javascript manifest' - manifest = "//= require ./bootstrap-global-this-define\n" - bootstrap_js_files.each do |name| - name = name.gsub(/\.js$/, '') - manifest << "//= require ./bootstrap/#{name}\n" - end - manifest << "//= require ./bootstrap-global-this-undefine\n" - dist_js = read_files('dist/js', %w(bootstrap.js bootstrap.min.js)) - { - 'assets/javascripts/bootstrap-global-this-define.js' => <<~JS, - // Set a `globalThis` so that bootstrap components are defined on window.bootstrap instead of window. - window['bootstrap'] = { - "@popperjs/core": window.Popper, - _originalGlobalThis: window['globalThis'] - }; - window['globalThis'] = window['bootstrap']; - JS - 'assets/javascripts/bootstrap-global-this-undefine.js' => <<~JS, - window['globalThis'] = window['bootstrap']._originalGlobalThis; - window['bootstrap']._originalGlobalThis = null; - JS - 'assets/javascripts/bootstrap-sprockets.js' => manifest, - 'assets/javascripts/bootstrap.js' => dist_js['bootstrap.js'], - 'assets/javascripts/bootstrap.min.js' => dist_js['bootstrap.min.js'], - }.each do |path, content| + log_status 'Updating javascript bundles' + # `bootstrap.{js,min.js}` import @floating-ui/dom and vanilla-calendar-pro + # as bare specifiers; the `bootstrap.bundle.{js,min.js}` builds inline those + # dependencies and are fully self-contained (the recommended importmap pin). + dist_files = %w(bootstrap.js bootstrap.min.js bootstrap.bundle.js bootstrap.bundle.min.js) + read_files('dist/js', dist_files).each do |name, content| + path = "assets/javascripts/#{name}" save_file path, remove_source_mapping_url(content) log_processed path end + + vendor_floating_ui + end + + # Bootstrap 6 depends on @floating-ui/dom (replacing Popper). Vendor a + # self-contained ESM bundle so apps can pin it via importmaps without a CDN. + def vendor_floating_ui + version = floating_ui_version + log_status "Vendoring @floating-ui/dom@#{version}" + stub = get_file("https://esm.sh/@floating-ui/dom@#{version}?bundle") + rel = stub[/from\s+"([^"]+)"/, 1] or + raise "Unexpected esm.sh response for @floating-ui/dom@#{version}:\n#{stub}" + bundle = get_file("https://esm.sh#{rel}") + path = 'assets/javascripts/floating-ui.js' + save_file path, bundle + log_processed path + end + + def floating_ui_version + pkg = get_json(file_url 'package.json') + spec = (pkg['dependencies'] || {})['@floating-ui/dom'] || + (pkg['devDependencies'] || {})['@floating-ui/dom'] or + raise 'Could not find @floating-ui/dom in upstream package.json' + spec.sub(/\A\D*/, '') end def bootstrap_js_files @bootstrap_js_files ||= begin - src_files = get_paths_by_type('js/src', /\.js$/) - INLINED_SRCS - puts "src_files: #{src_files.inspect}" + src_files = get_paths_by_type('js/src', /\.js$/) imports = Deps.new - # Get the imports from the ES6 files to order requires correctly. + # Get the imports from the ES modules to order requires correctly. read_files('js/src', src_files).each do |name, content| - file_imports = content.scan(%r{import *(?:[a-zA-Z]*|\{[a-zA-Z ,]*\}) *from '([\w/.-]+)}).flatten(1).map do |f| - Pathname.new(name).dirname.join(f).cleanpath.to_s - end.uniq - imports.add name, *(file_imports - INLINED_SRCS) + file_imports = content.scan(%r{import *(?:[a-zA-Z]*|\{[a-zA-Z ,]*\}) *from '([\w/.-]+)}).flatten(1) + # Only follow relative imports between Bootstrap's own source files; + # skip npm dependencies (e.g. `vanilla-calendar-pro`, `@floating-ui/dom`). + .select { |f| f.start_with?('.') } + .map { |f| Pathname.new(name).dirname.join(f).cleanpath.to_s } + .uniq + imports.add name, *file_imports end - imports.tsort + # Order by the src import graph, but only ship components that are + # actually present in the compiled dist (src/ may contain modules that + # have no standalone dist/ build). + dist_files = get_paths_by_type('js/dist', /\.js$/) + imports.tsort.select { |f| dist_files.include?(f) } end end diff --git a/tasks/updater/network.rb b/tasks/updater/network.rb index 6943bf56..eea26adf 100644 --- a/tasks/updater/network.rb +++ b/tasks/updater/network.rb @@ -84,7 +84,14 @@ def get_branch_sha log cmd result = %x[#{cmd}] raise 'Could not get branch sha!' unless $?.success? && !result.empty? - result.split(/\s+/).first + # `git ls-remote v6-dev` also matches suffixes like + # `refs/heads/mdo/v6-dev`, so pick the exact branch (or tag) ref + # rather than blindly taking the first line. + ref_of = ->(line) { line.split(/\s+/, 2)[1].to_s.strip } + line = result.lines.find { |l| ref_of.call(l) == "refs/heads/#@branch" } || + result.lines.find { |l| ref_of.call(l) == "refs/tags/#@branch" } || + result.lines.first + line.split(/\s+/).first end end end diff --git a/tasks/updater/scss.rb b/tasks/updater/scss.rb index 4af7e968..4c989eda 100644 --- a/tasks/updater/scss.rb +++ b/tasks/updater/scss.rb @@ -11,16 +11,14 @@ def update_scss_assets end log_processed "#{bootstrap_scss_files * ' '}" - log_status 'Updating scss main files' - %w(bootstrap bootstrap-grid bootstrap-reboot bootstrap-utilities).each do |name| - # Compass treats non-partials as targets to copy into the main project, so make them partials. - # Also move them up a level to clearly indicate entry points. - from = "#{save_to}/#{name}.scss" - to = "#{save_to}/../_#{name}.scss" - FileUtils.mv from, to - # As we moved the files, adjust imports accordingly. - File.write to, File.read(to).gsub(/ "/, ' "bootstrap/') - end + log_status 'Updating scss main file' + # Bootstrap 6 exposes a single `bootstrap` entry point. Make it a partial + # and move it up a level to clearly mark it as the entry point, rewriting + # its `@use`/`@forward` paths to account for the move. + from = "#{save_to}/bootstrap.scss" + to = "#{save_to}/../_bootstrap.scss" + FileUtils.mv from, to + File.write to, File.read(to).gsub(/ "/, ' "bootstrap/') end end end diff --git a/test/dummy_rails/app/assets/javascripts/application.js b/test/dummy_rails/app/assets/javascripts/application.js index 8a6aac2a..4b5efa6c 100644 --- a/test/dummy_rails/app/assets/javascripts/application.js +++ b/test/dummy_rails/app/assets/javascripts/application.js @@ -1,8 +1,5 @@ -//= require popper.js -//= require bootstrap-sprockets - -document.addEventListener('DOMContentLoaded', () => { - for (const tooltipTriggerEl of document.querySelectorAll('[data-bs-toggle="tooltip"]')) { - new bootstrap.Tooltip(tooltipTriggerEl) - } -}); +// Bootstrap 6 JavaScript is ES-module only and is loaded through importmaps +// (see the README), not Sprockets `//= require`. This Sprockets-based dummy app +// exercises the Bootstrap 6 *stylesheet* pipeline; the ES-module JavaScript and +// its vendored @floating-ui/dom dependency are smoke-tested separately in +// test/javascript_test.rb. diff --git a/test/dummy_rails/app/assets/stylesheets/application.sass b/test/dummy_rails/app/assets/stylesheets/application.sass index b0d09cef..1f47670e 100644 --- a/test/dummy_rails/app/assets/stylesheets/application.sass +++ b/test/dummy_rails/app/assets/stylesheets/application.sass @@ -1,4 +1,8 @@ -@import 'bootstrap' +// Bootstrap 6 uses the Sass module system (@use), not @import. +@use 'bootstrap' + +// Verify Bootstrap's Sass API is reachable through the gem's load path. +@use 'bootstrap/layout/containers' as containers .test-mixin - +make-container + @include containers.make-container diff --git a/test/javascript_test.rb b/test/javascript_test.rb new file mode 100644 index 00000000..bd871144 --- /dev/null +++ b/test/javascript_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# Smoke-tests the Bootstrap 6 ES-module JavaScript in a real headless browser, +# the same way an importmaps-based app consumes it. Two delivery paths are +# covered: +# +# 1. The self-contained `bootstrap.bundle.min.js` (inlines @floating-ui/dom and +# vanilla-calendar-pro) loaded with NO importmap pins -- the recommended path. +# 2. An individual component module (`bootstrap/tooltip.js`) resolving the bare +# `@floating-ui/dom` specifier to the gem's vendored `floating-ui.js`. +# +# Each path imports a tooltip (positioned via @floating-ui/dom) and asserts the +# component instantiates and that Floating UI actually placed the tooltip. +# Independent of Rails/Sprockets. + +require 'minitest/autorun' +require 'webrick' +require 'ferrum' +require 'fileutils' +require 'tmpdir' + +class JavascriptTest < Minitest::Test + # Resolve the path directly rather than `require 'bootstrap'`: loading the gem + # here would run `Bootstrap.load!` before the dummy Rails app boots and, under + # random test order, prevent the Rails engine from registering its asset paths. + JS_DIR = File.expand_path('../assets/javascripts', __dir__) + + def test_self_contained_bundle_needs_no_pins + # The bundle inlines its dependencies, so no importmap is required at all. + assert_tooltip_works(<<~HTML) + + Button + + + HTML + end + + def test_module_resolves_vendored_floating_ui + # The lean component module resolves the bare @floating-ui/dom specifier to + # the gem's vendored build via an importmap. + assert_tooltip_works(<<~HTML) + + + + Button + + + HTML + end + + private + + # JS that imports a tooltip class, shows a tooltip, and records the outcome on + # the dataset for the Ruby side to read. + def tooltip_probe(module_path, export) + <<~JS + document.body.dataset.status = "loading"; + try { + const Tooltip = (await import("#{module_path}")).#{export}; + const tip = new Tooltip(document.getElementById("btn")); + tip.show(); // positions via @floating-ui/dom computePosition() + const el = document.querySelector(".tooltip"); + document.body.dataset.status = (typeof tip.show === "function" && el) ? "ok" : "fail"; + } catch (e) { + document.body.dataset.status = "error: " + (e && e.message || e); + } + JS + end + + def assert_tooltip_works(html) + docroot = serve_assets(html) + port = start_server(docroot) + browser = new_browser + browser.go_to("http://127.0.0.1:#{port}/index.html") + + status = nil + 60.times do + status = browser.evaluate("document.body.dataset.status") + break if status && status != 'loading' + sleep 0.1 + end + refute_nil status, 'ES module never executed in the browser' + assert_equal 'ok', status, "Bootstrap 6 ESM tooltip failed: #{status}" + + # @floating-ui/dom's computePosition() is async; poll for the inline left/top + # px it writes onto the rendered tooltip element. + style = '' + positioned = false + 30.times do + style = browser.evaluate( + "(document.querySelector('.tooltip') || {getAttribute: () => ''}).getAttribute('style') || ''" + ).to_s + if style =~ /left:\s*[\d.]+px/ && style =~ /top:\s*[\d.]+px/ + positioned = true + break + end + sleep 0.1 + end + assert positioned, "@floating-ui/dom did not position the tooltip (style=#{style.inspect})" + ensure + browser&.quit + @server&.shutdown + end + + def serve_assets(index_html) + docroot = File.join(Dir.tmpdir, "bootstrap-js-test-#{Process.pid}-#{rand(1 << 20)}") + FileUtils.rm_rf(docroot) + FileUtils.mkdir_p(docroot) + FileUtils.cp_r(File.join(JS_DIR, '.'), docroot) + File.write(File.join(docroot, 'index.html'), index_html) + docroot + end + + def start_server(docroot) + @server = WEBrick::HTTPServer.new( + Port: 0, + DocumentRoot: docroot, + Logger: WEBrick::Log.new(File::NULL), + AccessLog: [] + ) + # Ensure JS is served with a module-compatible MIME type. + @server.config[:MimeTypes]['js'] = 'text/javascript' + port = @server.config[:Port] + Thread.new { @server.start } + port + end + + def new_browser + chrome = [ + ENV['CHROMIUM_BIN'], + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium-browser', + '/snap/bin/chromium' + ].compact.find { |p| File.executable?(p) } + + # process_timeout matches the cuprite driver in test_helper.rb: Chrome can + # take well over 30s to first start on loaded CI runners. + opts = { headless: true, process_timeout: 60, timeout: 30 } + opts[:browser_path] = chrome if chrome # otherwise let Ferrum auto-detect (CI) + Ferrum::Browser.new(**opts) + end +end diff --git a/test/rails_test.rb b/test/rails_test.rb index fc67b83d..bd526c29 100644 --- a/test/rails_test.rb +++ b/test/rails_test.rb @@ -2,8 +2,11 @@ class RailsTest < ActionDispatch::IntegrationTest include ::DummyRailsIntegration + include ::SassEngineSupport def test_visit_root + skip_unless_sass_can_compile_bootstrap! + visit root_path # ^ will raise on JS errors @@ -13,6 +16,8 @@ def test_visit_root end def test_precompile + skip_unless_sass_can_compile_bootstrap! + Dummy::Application.load_tasks Rake::Task['assets:precompile'].invoke end diff --git a/test/support/sass_engine_support.rb b/test/support/sass_engine_support.rb new file mode 100644 index 00000000..a31c62ce --- /dev/null +++ b/test/support/sass_engine_support.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Bootstrap 6's stylesheets use the Sass module system (@use) and the CSS +# if()/sass() syntax, which only a recent Dart Sass can compile. libsass (the +# sassc gem) and the older Dart Sass releases that still resolve on Ruby < 3.1 +# fail to parse them, so the stylesheet tests skip on those engines. The probe +# compiles a snippet of the same syntax rather than Bootstrap itself, so a +# genuine Bootstrap regression on a capable engine still fails the tests. +module SassEngineSupport + BOOTSTRAP6_SYNTAX_PROBE = <<~SCSS + @use "sass:math"; + @function probe($a, $b) { + @return if(sass($a > $b): math.div($a, $b); else: math.div($b, $a)); + } + .probe { opacity: probe(2, 1); } + SCSS + + def self.can_compile_bootstrap? + return @can_compile_bootstrap if defined?(@can_compile_bootstrap) + @can_compile_bootstrap = + begin + defined?(SassC::Engine) && + !SassC::Engine.new(BOOTSTRAP6_SYNTAX_PROBE, syntax: :scss).render.nil? + rescue StandardError + false + end + end + + def skip_unless_sass_can_compile_bootstrap! + return if SassEngineSupport.can_compile_bootstrap? + + skip 'Sass engine cannot compile Bootstrap 6 stylesheets (recent Dart Sass required)' + end +end
p,position:"right"}].forEach((({condition:e,position:t})=>{e&&o.push(t);})),Object.assign(a,{top:c<=s-n,bottom:c<=l-n,left:d<=r,right:d<=m-r}),{canShow:a,parentPositions:o}}const handleDay=(e,t,n,a)=>{var o;const l=a.querySelector(`[data-vc-date="${t}"]`),s=null==l?void 0:l.querySelector("[data-vc-date-btn]");if(!l||!s)return;if((null==n?void 0:n.modifier)&&s.classList.add(...n.modifier.trim().split(" ")),!(null==n?void 0:n.html))return;const i=document.createElement("div");i.className=e.styles.datePopup,i.dataset.vcDatePopup="",i.innerHTML=e.sanitizerHTML(n.html),s.ariaExpanded="true",s.ariaLabel=`${s.ariaLabel}, ${null==(o=null==i?void 0:i.textContent)?void 0:o.replace(/^\s+|\s+(?=\s)|\s+$/g,"").replace(/ /g," ")}`,l.appendChild(i),requestAnimationFrame((()=>{if(!i)return;const{canShow:e}=getAvailablePosition(l,i),t=e.bottom?l.offsetHeight:-i.offsetHeight,n=e.left&&!e.right?l.offsetWidth-i.offsetWidth/2:!e.left&&e.right?i.offsetWidth/2:0;Object.assign(i.style,{left:`${n}px`,top:`${t}px`});}));},createDatePopup=(e,t)=>{var n;e.popups&&(null==(n=Object.entries(e.popups))||n.forEach((([n,a])=>handleDay(e,n,a,t))));},getDate=e=>new Date(`${e}T00:00:00`),getDateString=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`,parseDates=e=>e.reduce(((e,t)=>{if(t instanceof Date||"number"==typeof t){const n=t instanceof Date?t:new Date(t);e.push(n.toISOString().substring(0,10));}else t.match(/^(\d{4}-\d{2}-\d{2})$/g)?e.push(t):t.replace(/(\d{4}-\d{2}-\d{2}).*?(\d{4}-\d{2}-\d{2})/g,((t,n,a)=>{const o=getDate(n),l=getDate(a),s=new Date(o.getTime());for(;s<=l;s.setDate(s.getDate()+1))e.push(getDateString(s));return t}));return e}),[]),updateAttribute=(e,t,n,a="")=>{t?e.setAttribute(n,a):e.getAttribute(n)===a&&e.removeAttribute(n);},setDateModifier=(e,t,n,a,o,l,s)=>{var i,r,c,d;const u=getDate(e.context.displayDateMin)>getDate(l)||getDate(e.context.displayDateMax)1&&"multiple-ranged"===e.selectionDatesMode&&(e.context.selectedDates[0]===l&&e.context.selectedDates[e.context.selectedDates.length-1]===l?n.setAttribute("data-vc-date-selected","first-and-last"):e.context.selectedDates[0]===l?n.setAttribute("data-vc-date-selected","first"):e.context.selectedDates[e.context.selectedDates.length-1]===l&&n.setAttribute("data-vc-date-selected","last"),e.context.selectedDates[0]!==l&&e.context.selectedDates[e.context.selectedDates.length-1]!==l&&n.setAttribute("data-vc-date-selected","middle"))):n.hasAttribute("data-vc-date-selected")&&(n.removeAttribute("data-vc-date-selected"),a&&a.removeAttribute("aria-selected")),!e.context.disableDates.includes(l)&&e.enableEdgeDatesOnly&&e.context.selectedDates.length>1&&"multiple-ranged"===e.selectionDatesMode){const t=getDate(e.context.selectedDates[0]),a=getDate(e.context.selectedDates[e.context.selectedDates.length-1]),o=getDate(l);updateAttribute(n,o>t&&onew Date(`${e}T00:00:00.000Z`).toLocaleString(t,n),getWeekNumber=(e,t)=>{const n=getDate(e),a=(n.getDay()-t+7)%7;n.setDate(n.getDate()+4-a);const o=new Date(n.getFullYear(),0,1),l=Math.ceil(((+n-+o)/864e5+1)/7);return {year:n.getFullYear(),week:l}},addWeekNumberForDate=(e,t,n)=>{const a=getWeekNumber(n,e.firstWeekday);a&&(t.dataset.vcDateWeekNumber=String(a.week));},setDaysAsDisabled=(e,t,n)=>{var a,o,l,s,i;const r=null==(a=e.disableWeekdays)?void 0:a.includes(n),c=e.disableAllDates&&!!(null==(o=e.context.enableDates)?void 0:o[0]);!r&&!c||(null==(l=e.context.enableDates)?void 0:l.includes(t))||(null==(s=e.context.disableDates)?void 0:s.includes(t))||(e.context.disableDates.push(t),null==(i=e.context.disableDates)||i.sort(((e,t)=>+new Date(e)-+new Date(t))));},createDate=(e,t,n,a,o,l)=>{const s=getDate(o).getDay(),i="string"==typeof e.locale&&e.locale.length?e.locale:"en",r=document.createElement("div");let c;r.className=e.styles.date,r.dataset.vcDate=o,r.dataset.vcDateMonth=l,r.dataset.vcDateWeekDay=String(s),r.role="gridcell",("current"===l||e.displayDatesOutside)&&(c=document.createElement("button"),c.className=e.styles.dateBtn,c.type="button",c.ariaLabel=getLocaleString(o,i,{dateStyle:"long",timeZone:"UTC"}),c.dataset.vcDateBtn="",c.innerText=String(a),r.appendChild(c)),e.enableWeekNumbers&&addWeekNumberForDate(e,r,o),setDaysAsDisabled(e,o,s),setDateModifier(e,t,r,c,s,o,l),n.addDate(r),e.onCreateDateEls&&e.onCreateDateEls(e,r);},createDatesFromCurrentMonth=(e,t,n,a,o)=>{for(let l=1;l<=n;l++){const n=new Date(a,o,l);createDate(e,a,t,l,getDateString(n),"current");}},createDatesFromNextMonth=(e,t,n,a,o)=>{const l=o+1===12?a+1:a,s=o+1===12?"01":o+2<10?`0${o+2}`:o+2;for(let o=1;o<=n;o++){const n=o<10?`0${o}`:String(o);createDate(e,a,t,o,`${l}-${s}-${n}`,"next");}},createDatesFromPrevMonth=(e,t,n,a,o)=>{let l=new Date(n,a,0).getDate()-(o-1);const s=0===a?n-1:n,i=0===a?12:a<10?`0${a}`:a;for(let a=o;a>0;a--,l++){createDate(e,n,t,l,`${s}-${i}-${l}`,"prev");}},createWeekNumbers=(e,t,n,a,o)=>{if(!e.enableWeekNumbers)return;a.textContent="";const l=document.createElement("b");l.className=e.styles.weekNumbersTitle,l.innerText="#",l.dataset.vcWeekNumbers="title",a.appendChild(l);const s=document.createElement("div");s.className=e.styles.weekNumbersContent,s.dataset.vcWeekNumbers="content",a.appendChild(s);const i=document.createElement("button");i.type="button",i.className=e.styles.weekNumber;const r=o.querySelectorAll("[data-vc-date]"),c=Math.ceil((t+n)/7);for(let t=0;t{const t=new Date(e.context.selectedYear,e.context.selectedMonth,1),n=e.context.mainElement.querySelectorAll('[data-vc="dates"]'),a=e.context.mainElement.querySelectorAll('[data-vc-week="numbers"]');n.forEach(((n,o)=>{e.selectionDatesMode||(n.dataset.vcDatesDisabled=""),n.textContent="";const l=new Date(t);l.setMonth(l.getMonth()+o);const s=l.getMonth(),i=l.getFullYear(),r=(new Date(i,s,1).getDay()-e.firstWeekday+7)%7,c=new Date(i,s+1,0).getDate(),d=r+c,u=Math.ceil(d/7),m=7*u-d,p=[];for(let t=0;t{p[h].appendChild(e),v++,v>=7&&(h++,v=0);}};createDatesFromPrevMonth(e,g,i,s,r),createDatesFromCurrentMonth(e,g,c,i,s),createDatesFromNextMonth(e,g,m,i,s);for(const e of p)n.appendChild(e);createDatePopup(e,n),createWeekNumbers(e,r,c,a[o],n);}));},layoutDefault=e=>`\n \n <#ArrowPrev [month] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [month] />\n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n <#DateRangeTooltip />\n \n \n <#ControlTime />\n`,layoutMonths=e=>`\n \n \n <#Month />\n <#Year />\n \n \n \n \n <#Months />\n \n \n`,layoutMultiple=e=>`\n \n <#ArrowPrev [month] />\n <#ArrowNext [month] />\n \n \n <#Multiple>\n \n \n \n <#Month />\n <#Year />\n \n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n \n \n \n <#/Multiple>\n <#DateRangeTooltip />\n \n <#ControlTime />\n`,layoutYears=e=>`\n \n <#ArrowPrev [year] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [year] />\n \n \n \n <#Years />\n \n \n`,ArrowNext=(e,t)=>``,ArrowPrev=(e,t)=>``,ControlTime=e=>e.selectionTimeMode?``:"",DateRangeTooltip=e=>e.onCreateDateRangeTooltip?``:"",Dates=e=>``,Month=e=>``,Months=e=>``,Week=e=>``,WeekNumbers=e=>e.enableWeekNumbers?``:"",Year=e=>``,Years=e=>``,components={ArrowNext:ArrowNext,ArrowPrev:ArrowPrev,ControlTime:ControlTime,Dates:Dates,DateRangeTooltip:DateRangeTooltip,Month:Month,Months:Months,Week:Week,WeekNumbers:WeekNumbers,Year:Year,Years:Years},getComponent=e=>components[e],parseLayout=(e,t)=>t.replace(/[\n\t]/g,"").replace(/<#(?!\/?Multiple)(.*?)>/g,((t,n)=>{const a=(n.match(/\[(.*?)\]/)||[])[1],o=n.replace(/[/\s\n\t]|\[(.*?)\]/g,""),l=getComponent(o),s=l?l(e,null!=a?a:null):"";return e.sanitizerHTML(s)})).replace(/[\n\t]/g,""),parseMultipleLayout=(e,t)=>t.replace(new RegExp("<#Multiple>(.*?)<#\\/Multiple>","gs"),((t,n)=>{const a=Array(e.context.displayMonthsCount).fill(n).join("");return e.sanitizerHTML(a)})).replace(/[\n\t]/g,""),createLayouts=(e,t)=>{const n={default:layoutDefault,month:layoutMonths,year:layoutYears,multiple:layoutMultiple};if(Object.keys(n).forEach((t=>{const a=t;e.layouts[a].length||(e.layouts[a]=n[a](e));})),e.context.mainElement.className=e.styles.calendar,e.context.mainElement.dataset.vc="calendar",e.context.mainElement.dataset.vcType=e.context.currentType,e.context.mainElement.role="application",e.context.mainElement.tabIndex=0,e.context.mainElement.ariaLabel=e.labels.application,"multiple"!==e.context.currentType){if("multiple"===e.type&&t){const n=e.context.mainElement.querySelector('[data-vc="controls"]'),a=e.context.mainElement.querySelector('[data-vc="grid"]'),o=t.closest('[data-vc="column"]');return n&&n.remove(),a&&(a.dataset.vcGrid="hidden"),o&&(o.dataset.vcColumn=e.context.currentType),void(o&&(o.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))))}e.context.mainElement.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]));}else e.context.mainElement.innerHTML=e.sanitizerHTML(parseMultipleLayout(e,parseLayout(e,e.layouts[e.context.currentType])));},setVisibilityArrows=(e,t,n,a)=>{e.style.visibility=n?"hidden":"",t.style.visibility=a?"hidden":"";},handleDefaultType=(e,t,n)=>{const a=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1))),o=new Date(a.getTime()),l=new Date(a.getTime());o.setMonth(o.getMonth()-e.monthsToSwitch),l.setMonth(l.getMonth()+e.monthsToSwitch);const s=getDate(e.context.dateMin),i=getDate(e.context.dateMax);e.selectionYearsMode||(s.setFullYear(a.getFullYear()),i.setFullYear(a.getFullYear()));const r=!e.selectionMonthsMode||o.getFullYear()i.getFullYear()||l.getFullYear()===i.getFullYear()&&l.getMonth()>i.getMonth()-(e.context.displayMonthsCount-1);setVisibilityArrows(t,n,r,c);},handleYearType=(e,t,n)=>{const a=getDate(e.context.dateMin),o=getDate(e.context.dateMax),l=!!(a.getFullYear()&&e.context.displayYear-7<=a.getFullYear()),s=!!(o.getFullYear()&&e.context.displayYear+7>=o.getFullYear());setVisibilityArrows(t,n,l,s);},visibilityArrows=e=>{if("month"===e.context.currentType)return;const t=e.context.mainElement.querySelector('[data-vc-arrow="prev"]'),n=e.context.mainElement.querySelector('[data-vc-arrow="next"]');if(!t||!n)return;({default:()=>handleDefaultType(e,t,n),year:()=>handleYearType(e,t,n)})["multiple"===e.context.currentType?"default":e.context.currentType]();},visibilityHandler=(e,t,n,a,o)=>{const l=new Date(a.setFullYear(e.context.selectedYear,e.context.selectedMonth+n)).getFullYear(),s=new Date(a.setMonth(e.context.selectedMonth+n)).getMonth(),i=e.context.locale.months.long[s],r=t.closest('[data-vc="column"]');r&&(r.ariaLabel=`${i} ${l}`);const c={month:{id:s,label:i},year:{id:l,label:l}};t.innerText=String(c[o].label),t.dataset[`vc${o.charAt(0).toUpperCase()+o.slice(1)}`]=String(c[o].id),t.ariaLabel=`${e.labels[o]} ${c[o].label}`;const d={month:e.selectionMonthsMode,year:e.selectionYearsMode},u=false===d[o]||"only-arrows"===d[o];u&&(t.tabIndex=-1),t.disabled=u;},visibilityTitle=e=>{const t=e.context.mainElement.querySelectorAll('[data-vc="month"]'),n=e.context.mainElement.querySelectorAll('[data-vc="year"]'),a=new Date(e.context.selectedYear,e.context.selectedMonth,1);[t,n].forEach((t=>null==t?void 0:t.forEach(((t,n)=>visibilityHandler(e,t,n,a,t.dataset.vc)))));},setYearModifier=(e,t,n,a,o)=>{var l;const s={month:"[data-vc-months-month]",year:"[data-vc-years-year]"},i={month:{selected:"data-vc-months-month-selected",aria:"aria-selected",value:"vcMonthsMonth",selectedProperty:"selectedMonth"},year:{selected:"data-vc-years-year-selected",aria:"aria-selected",value:"vcYearsYear",selectedProperty:"selectedYear"}};o&&(null==(l=e.context.mainElement.querySelectorAll(s[n]))||l.forEach((e=>{e.removeAttribute(i[n].selected),e.removeAttribute(i[n].aria);})),setContext(e,i[n].selectedProperty,Number(t.dataset[i[n].value])),visibilityTitle(e),"year"===n&&visibilityArrows(e)),a&&(t.setAttribute(i[n].selected,""),t.setAttribute(i[n].aria,"true"));},getColumnID=(e,t)=>{var n;if("multiple"!==e.type)return {currentValue:null,columnID:0};const a=e.context.mainElement.querySelectorAll('[data-vc="column"]'),o=Array.from(a).findIndex((e=>e.closest(`[data-vc-column="${t}"]`)));return {currentValue:o>=0?Number(null==(n=a[o].querySelector(`[data-vc="${t}"]`))?void 0:n.getAttribute(`data-vc-${t}`)):null,columnID:Math.max(o,0)}},createMonthEl=(e,t,n,a,o,l,s)=>{const i=t.cloneNode(false);return i.className=e.styles.monthsMonth,i.innerText=a,i.ariaLabel=o,i.role="gridcell",i.dataset.vcMonthsMonth=`${s}`,l&&(i.ariaDisabled="true"),l&&(i.tabIndex=-1),i.disabled=l,setYearModifier(e,i,"month",n===s,false),i},createMonths=(e,t)=>{var n,a;const o=null==(n=null==t?void 0:t.closest('[data-vc="header"]'))?void 0:n.querySelector('[data-vc="year"]'),l=o?Number(o.dataset.vcYear):e.context.selectedYear,s=(null==t?void 0:t.dataset.vcMonth)?Number(t.dataset.vcMonth):e.context.selectedMonth;setContext(e,"currentType","month"),createLayouts(e,t),visibilityTitle(e);const i=e.context.mainElement.querySelector('[data-vc="months"]');if(!e.selectionMonthsMode||!i)return;const r=e.monthsToSwitch>1?e.context.locale.months.long.map(((t,n)=>s-e.monthsToSwitch*n)).concat(e.context.locale.months.long.map(((t,n)=>s+e.monthsToSwitch*n))).filter((e=>e>=0&&e<=12)):Array.from(Array(12).keys()),c=document.createElement("button");c.type="button";for(let t=0;t<12;t++){const n=getDate(e.context.dateMin),a=getDate(e.context.dateMax),o=e.context.displayMonthsCount-1,{columnID:d}=getColumnID(e,"month"),u=l<=n.getFullYear()&&t=a.getFullYear()&&t>a.getMonth()-o+d||l>a.getFullYear()||t!==s&&!r.includes(t),m=createMonthEl(e,c,s,e.context.locale.months.short[t],e.context.locale.months.long[t],u,t);i.appendChild(m),e.onCreateMonthEls&&e.onCreateMonthEls(e,m);}null==(a=e.context.mainElement.querySelector("[data-vc-months-month]:not([disabled])"))||a.focus();},TimeInput=(e,t,n,a,o)=>`\n \n \n \n`,TimeRange=(e,t,n,a,o,l,s)=>`\n \n \n \n`,handleActions=(e,t,n,a)=>{(({hour:()=>setContext(e,"selectedHours",n),minute:()=>setContext(e,"selectedMinutes",n)}))[a](),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${e.context.selectedKeeping?` ${e.context.selectedKeeping}`:""}`),e.onChangeTime&&e.onChangeTime(e,t,false),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);},transformTime24=(e,t)=>{var n;return (null==(n={0:{AM:"00",PM:"12"},1:{AM:"01",PM:"13"},2:{AM:"02",PM:"14"},3:{AM:"03",PM:"15"},4:{AM:"04",PM:"16"},5:{AM:"05",PM:"17"},6:{AM:"06",PM:"18"},7:{AM:"07",PM:"19"},8:{AM:"08",PM:"20"},9:{AM:"09",PM:"21"},10:{AM:"10",PM:"22"},11:{AM:"11",PM:"23"},12:{AM:"00",PM:"12"}}[Number(e)])?void 0:n[t])||String(e)},handleClickKeepingTime=(e,t,n,a,o)=>{const l=l=>{const s="AM"===e.context.selectedKeeping?"PM":"AM",i=transformTime24(e.context.selectedHours,s);Number(i)<=a&&Number(i)>=o?(setContext(e,"selectedKeeping",s),n.value=i,handleActions(e,l,e.context.selectedHours,"hour"),t.ariaLabel=`${e.labels.btnKeeping} ${e.context.selectedKeeping}`,t.innerText=e.context.selectedKeeping):e.onChangeTime&&e.onChangeTime(e,l,true);};return t.addEventListener("click",l),()=>{t.removeEventListener("click",l);}},transformTime12=e=>({0:"12",13:"01",14:"02",15:"03",16:"04",17:"05",18:"06",19:"07",20:"08",21:"09",22:"10",23:"11"}[Number(e)]||String(e)),updateInputAndRange=(e,t,n,a)=>{e.value=n,t.value=a;},updateKeepingTime$1=(e,t,n)=>{t&&n&&(setContext(e,"selectedKeeping",n),t.innerText=n);},handleInput$1=(e,t,n,a,o,l,s)=>{const i={hour:(i,r,c)=>{if(!e.selectionTimeMode)return;({12:()=>{if(!e.context.selectedKeeping)return;const d=Number(transformTime24(r,e.context.selectedKeeping));if(!(d<=l&&d>=s))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,true));updateInputAndRange(n,t,transformTime12(r),transformTime24(r,e.context.selectedKeeping)),i>12&&updateKeepingTime$1(e,a,"PM"),handleActions(e,c,transformTime12(r),o);},24:()=>{if(!(i<=l&&i>=s))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,true));updateInputAndRange(n,t,r,r),handleActions(e,c,r,o);}})[e.selectionTimeMode]();},minute:(a,i,r)=>{if(!(a<=l&&a>=s))return n.value=e.context.selectedMinutes,void(e.onChangeTime&&e.onChangeTime(e,r,true));n.value=i,t.value=i,handleActions(e,r,i,o);}},r=e=>{const t=Number(n.value),a=n.value.padStart(2,"0");i[o]&&i[o](t,a,e);};return n.addEventListener("change",r),()=>{n.removeEventListener("change",r);}},updateInputAndTime=(e,t,n,a,o)=>{t.value=o,handleActions(e,n,o,a);},updateKeepingTime=(e,t,n)=>{t&&(setContext(e,"selectedKeeping",n),t.innerText=n);},handleRange=(e,t,n,a,o)=>{const l=l=>{const s=Number(t.value),i=t.value.padStart(2,"0"),r="hour"===o,c=24===e.selectionTimeMode,d=s>0&&s<12;r&&!c&&updateKeepingTime(e,a,0===s||d?"AM":"PM"),updateInputAndTime(e,n,l,o,!r||c||d?i:transformTime12(t.value));};return t.addEventListener("input",l),()=>{t.removeEventListener("input",l);}},handleMouseOver=e=>e.setAttribute("data-vc-input-focus",""),handleMouseOut=e=>e.removeAttribute("data-vc-input-focus"),handleTime=(e,t)=>{const n=t.querySelector('[data-vc-time-range="hour"] input[name="hour"]'),a=t.querySelector('[data-vc-time-range="minute"] input[name="minute"]'),o=t.querySelector('[data-vc-time-input="hour"] input[name="hour"]'),l=t.querySelector('[data-vc-time-input="minute"] input[name="minute"]'),s=t.querySelector('[data-vc-time="keeping"]');if(!(n&&a&&o&&l))return;const i=e=>{e.target===n&&handleMouseOver(o),e.target===a&&handleMouseOver(l);},r=e=>{e.target===n&&handleMouseOut(o),e.target===a&&handleMouseOut(l);};return t.addEventListener("mouseover",i),t.addEventListener("mouseout",r),handleInput$1(e,n,o,s,"hour",e.timeMaxHour,e.timeMinHour),handleInput$1(e,a,l,s,"minute",e.timeMaxMinute,e.timeMinMinute),handleRange(e,n,o,s,"hour"),handleRange(e,a,l,s,"minute"),s&&handleClickKeepingTime(e,s,n,e.timeMaxHour,e.timeMinHour),()=>{t.removeEventListener("mouseover",i),t.removeEventListener("mouseout",r);}},createTime=e=>{const t=e.context.mainElement.querySelector('[data-vc="time"]');if(!e.selectionTimeMode||!t)return;const[n,a]=[e.timeMinHour,e.timeMaxHour],[o,l]=[e.timeMinMinute,e.timeMaxMinute],s=e.context.selectedKeeping?transformTime24(e.context.selectedHours,e.context.selectedKeeping):e.context.selectedHours,i="range"===e.timeControls;var r;t.innerHTML=e.sanitizerHTML(`\n \n ${TimeInput("hour",e.styles.timeHour,e.labels,e.context.selectedHours,i)}\n ${TimeInput("minute",e.styles.timeMinute,e.labels,e.context.selectedMinutes,i)}\n ${12===e.selectionTimeMode?(r=e.context.selectedKeeping,`${r}`):""}\n \n \n ${TimeRange("hour",e.styles.timeRange,e.labels,n,a,e.timeStepHour,s)}\n ${TimeRange("minute",e.styles.timeRange,e.labels,o,l,e.timeStepMinute,e.context.selectedMinutes)}\n \n `),handleTime(e,t);},createWeek=e=>{const t=e.selectedWeekends?[...e.selectedWeekends]:[],n=[...e.context.locale.weekdays.long].reduce(((n,a,o)=>[...n,{id:o,titleShort:e.context.locale.weekdays.short[o],titleLong:a,isWeekend:t.includes(o)}]),[]),a=[...n.slice(e.firstWeekday),...n.slice(0,e.firstWeekday)];e.context.mainElement.querySelectorAll('[data-vc="week"]').forEach((t=>{const n=e.onClickWeekDay?document.createElement("button"):document.createElement("b");e.onClickWeekDay&&(n.type="button"),a.forEach((a=>{const o=n.cloneNode(true);o.innerText=a.titleShort,o.className=e.styles.weekDay,o.role="columnheader",o.ariaLabel=a.titleLong,o.dataset.vcWeekDay=String(a.id),a.isWeekend&&(o.dataset.vcWeekDayOff=""),t.appendChild(o);}));}));},createYearEl=(e,t,n,a,o)=>{const l=t.cloneNode(false);return l.className=e.styles.yearsYear,l.innerText=String(o),l.ariaLabel=String(o),l.role="gridcell",l.dataset.vcYearsYear=`${o}`,a&&(l.ariaDisabled="true"),a&&(l.tabIndex=-1),l.disabled=a,setYearModifier(e,l,"year",n===o,false),l},createYears=(e,t)=>{var n;const a=(null==t?void 0:t.dataset.vcYear)?Number(t.dataset.vcYear):e.context.selectedYear;setContext(e,"currentType","year"),createLayouts(e,t),visibilityTitle(e),visibilityArrows(e);const o=e.context.mainElement.querySelector('[data-vc="years"]');if(!e.selectionYearsMode||!o)return;const l="multiple"!==e.type||e.context.selectedYear===a?0:1,s=document.createElement("button");s.type="button";for(let t=e.context.displayYear-7;tgetDate(e.context.dateMax).getFullYear(),i=createYearEl(e,s,a,n,t);o.appendChild(i),e.onCreateYearEls&&e.onCreateYearEls(e,i);}null==(n=e.context.mainElement.querySelector("[data-vc-years-year]:not([disabled])"))||n.focus();},trackChangesHTMLElement=(e,t,n)=>{new MutationObserver((e=>{for(let a=0;ahaveListener.value=true,check:()=>haveListener.value},setTheme=(e,t)=>e.dataset.vcTheme=t,trackChangesThemeInSystemSettings=(e,t)=>{if(setTheme(e.context.mainElement,t.matches?"dark":"light"),"system"!==e.selectedTheme||haveListener.check())return;const n=e=>{const t=document.querySelectorAll('[data-vc="calendar"]');null==t||t.forEach((t=>setTheme(t,e.matches?"dark":"light")));};t.addEventListener?t.addEventListener("change",n):t.addListener(n),haveListener.set();},detectTheme=(e,t)=>{const n=e.themeAttrDetect.length?document.querySelector(e.themeAttrDetect):null,a=e.themeAttrDetect.replace(/^.*\[(.+)\]/g,((e,t)=>t));if(!n||"system"===n.getAttribute(a))return void trackChangesThemeInSystemSettings(e,t);const o=n.getAttribute(a);o?(setTheme(e.context.mainElement,o),trackChangesHTMLElement(n,a,(()=>{const t=n.getAttribute(a);t&&setTheme(e.context.mainElement,t);}))):trackChangesThemeInSystemSettings(e,t);},handleTheme=e=>{"not all"!==window.matchMedia("(prefers-color-scheme)").media?"system"===e.selectedTheme?detectTheme(e,window.matchMedia("(prefers-color-scheme: dark)")):setTheme(e.context.mainElement,e.selectedTheme):setTheme(e.context.mainElement,"light");},capitalizeFirstLetter=e=>e.charAt(0).toUpperCase()+e.slice(1).replace(/\./,""),getLocaleWeekday=(e,t,n)=>{const a=new Date(`1978-01-0${t+1}T00:00:00.000Z`),o=a.toLocaleString(n,{weekday:"short",timeZone:"UTC"}),l=a.toLocaleString(n,{weekday:"long",timeZone:"UTC"});e.context.locale.weekdays.short.push(capitalizeFirstLetter(o)),e.context.locale.weekdays.long.push(capitalizeFirstLetter(l));},getLocaleMonth=(e,t,n)=>{const a=new Date(`1978-${String(t+1).padStart(2,"0")}-01T00:00:00.000Z`),o=a.toLocaleString(n,{month:"short",timeZone:"UTC"}),l=a.toLocaleString(n,{month:"long",timeZone:"UTC"});e.context.locale.months.short.push(capitalizeFirstLetter(o)),e.context.locale.months.long.push(capitalizeFirstLetter(l));},getLocale=e=>{var t,n,a,o,l,s,i,r;if(!(e.context.locale.weekdays.short[6]&&e.context.locale.weekdays.long[6]&&e.context.locale.months.short[11]&&e.context.locale.months.long[11]))if("string"==typeof e.locale){if("string"==typeof e.locale&&!e.locale.length)throw new Error(errorMessages.notLocale);Array.from({length:7},((t,n)=>getLocaleWeekday(e,n,e.locale))),Array.from({length:12},((t,n)=>getLocaleMonth(e,n,e.locale)));}else {if(!((null==(n=null==(t=e.locale)?void 0:t.weekdays)?void 0:n.short[6])&&(null==(o=null==(a=e.locale)?void 0:a.weekdays)?void 0:o.long[6])&&(null==(s=null==(l=e.locale)?void 0:l.months)?void 0:s.short[11])&&(null==(r=null==(i=e.locale)?void 0:i.months)?void 0:r.long[11])))throw new Error(errorMessages.notLocale);setContext(e,"locale",__spreadValues({},e.locale));}},create=e=>{const t={default:()=>{createWeek(e),createDates(e);},multiple:()=>{createWeek(e),createDates(e);},month:()=>createMonths(e),year:()=>createYears(e)};handleTheme(e),getLocale(e),createLayouts(e),visibilityTitle(e),visibilityArrows(e),createTime(e),t[e.context.currentType]();},handleArrowKeys=e=>{const t=t=>{var n;const a=t.target;if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key)||"button"!==a.localName)return;const o=Array.from(e.context.mainElement.querySelectorAll('[data-vc="calendar"] button')),l=o.indexOf(a);if(-1===l)return;const s=(i=o[l]).hasAttribute("data-vc-date-btn")?7:i.hasAttribute("data-vc-months-month")?4:i.hasAttribute("data-vc-years-year")?5:1;var i;const r=(0, {ArrowUp:()=>Math.max(0,l-s),ArrowDown:()=>Math.min(o.length-1,l+s),ArrowLeft:()=>Math.max(0,l-1),ArrowRight:()=>Math.min(o.length-1,l+1)}[t.key])();null==(n=o[r])||n.focus();};return e.context.mainElement.addEventListener("keydown",t),()=>e.context.mainElement.removeEventListener("keydown",t)},handleMonth=(e,t)=>{const n=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1)));(({prev:()=>n.setMonth(n.getMonth()-e.monthsToSwitch),next:()=>n.setMonth(n.getMonth()+e.monthsToSwitch)}))[t](),setContext(e,"selectedMonth",n.getMonth()),setContext(e,"selectedYear",n.getFullYear()),visibilityTitle(e),visibilityArrows(e),createDates(e);},handleClickArrow=(e,t)=>{const n=t.target.closest("[data-vc-arrow]");if(n){if(["default","multiple"].includes(e.context.currentType))handleMonth(e,n.dataset.vcArrow);else if("year"===e.context.currentType&&void 0!==e.context.displayYear){const a={prev:-15,next:15}[n.dataset.vcArrow];setContext(e,"displayYear",e.context.displayYear+a),createYears(e,t.target);}e.onClickArrow&&e.onClickArrow(e,t);}},resolveToggle=(e,t)=>void 0===t||("function"==typeof t?t(e):t),canToggleSelection=e=>resolveToggle(e,e.enableDateToggle),handleSelectDate=(e,t,n)=>{const a=t.dataset.vcDate,o=t.closest("[data-vc-date][data-vc-date-selected]"),l=canToggleSelection(e);if(o&&!l)return;const s=o?e.context.selectedDates.filter((e=>e!==a)):n?[...e.context.selectedDates,a]:[a];setContext(e,"selectedDates",s);},createDateRangeTooltip=(e,t,n)=>{if(!t)return;if(!n)return t.dataset.vcDateRangeTooltip="hidden",void(t.textContent="");const a=e.context.mainElement.getBoundingClientRect(),o=n.getBoundingClientRect();t.style.left=o.left-a.left+o.width/2+"px",t.style.top=o.bottom-a.top-o.height+"px",t.dataset.vcDateRangeTooltip="visible",t.innerHTML=e.sanitizerHTML(e.onCreateDateRangeTooltip(e,n,t,o,a));},state={self:null,lastDateEl:null,isHovering:false,rangeMin:void 0,rangeMax:void 0,tooltipEl:null,timeoutId:null},addHoverEffect=(e,t,n)=>{var a,o,l;if(!(null==(o=null==(a=state.self)?void 0:a.context)?void 0:o.selectedDates[0]))return;const s=getDateString(e);(null==(l=state.self.context.disableDates)?void 0:l.includes(s))||(state.self.context.mainElement.querySelectorAll(`[data-vc-date="${s}"]`).forEach((e=>e.dataset.vcDateHover="")),t.forEach((e=>e.dataset.vcDateHover="first")),n.forEach((e=>{"first"===e.dataset.vcDateHover?e.dataset.vcDateHover="first-and-last":e.dataset.vcDateHover="last";})));},removeHoverEffect=()=>{var e,t;if(!(null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.mainElement))return;state.self.context.mainElement.querySelectorAll("[data-vc-date-hover]").forEach((e=>e.removeAttribute("data-vc-date-hover")));},handleHoverDatesEvent=e=>{var t,n;if(!e||!(null==(n=null==(t=state.self)?void 0:t.context)?void 0:n.selectedDates[0]))return;if(!e.closest('[data-vc="dates"]'))return state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),void removeHoverEffect();const a=e.closest("[data-vc-date]");if(!a||state.lastDateEl===a)return;state.lastDateEl=a,createDateRangeTooltip(state.self,state.tooltipEl,a),removeHoverEffect();const o=a.dataset.vcDate,l=getDate(state.self.context.selectedDates[0]),s=getDate(o),i=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${state.self.context.selectedDates[0]}"]`),r=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${o}"]`),[c,d]=l{const t=null==e?void 0:e.closest("[data-vc-date-selected]");if(!t&&state.lastDateEl)return state.lastDateEl=null,void createDateRangeTooltip(state.self,state.tooltipEl,null);t&&state.lastDateEl!==t&&(state.lastDateEl=t,createDateRangeTooltip(state.self,state.tooltipEl,t));},optimizedHoverHandler=e=>t=>{const n=t.target;state.isHovering||(state.isHovering=true,requestAnimationFrame((()=>{e(n),state.isHovering=false;})));},optimizedHandleHoverDatesEvent=optimizedHoverHandler(handleHoverDatesEvent),optimizedHandleHoverSelectedDatesRangeEvent=optimizedHoverHandler(handleHoverSelectedDatesRangeEvent),handleCancelSelectionDates=e=>{state.self&&"Escape"===e.key&&(state.lastDateEl=null,setContext(state.self,"selectedDates",[]),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect());},handleMouseLeave=()=>{null!==state.timeoutId&&clearTimeout(state.timeoutId),state.timeoutId=setTimeout((()=>{state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect();}),50);},updateDisabledDates=()=>{var e,t,n,a;if(!(null==(n=null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.selectedDates)?void 0:n[0])||!(null==(a=state.self.context.disableDates)?void 0:a[0]))return;const o=getDate(state.self.context.selectedDates[0]),[l,s]=state.self.context.disableDates.map((e=>getDate(e))).reduce((([e,t],n)=>[o>=n?n:e,o{state.self=e,state.lastDateEl=t,removeHoverEffect(),e.disableDatesGaps&&(state.rangeMin=state.rangeMin?state.rangeMin:e.context.displayDateMin,state.rangeMax=state.rangeMax?state.rangeMax:e.context.displayDateMax),e.onCreateDateRangeTooltip&&(state.tooltipEl=e.context.mainElement.querySelector("[data-vc-date-range-tooltip]"));const n=null==t?void 0:t.dataset.vcDate;if(n){const t=1===e.context.selectedDates.length&&e.context.selectedDates[0].includes(n),a=t&&!canToggleSelection(e)?[n,n]:t&&canToggleSelection(e)?[]:e.context.selectedDates.length>1?[n]:[...e.context.selectedDates,n];setContext(e,"selectedDates",a),e.context.selectedDates.length>1&&e.context.selectedDates.sort(((e,t)=>+new Date(e)-+new Date(t)));}({set:()=>(e.disableDatesGaps&&updateDisabledDates(),createDateRangeTooltip(state.self,state.tooltipEl,t),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.addEventListener("keydown",handleCancelSelectionDates),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates);}),reset:()=>{const[n,a]=[e.context.selectedDates[0],e.context.selectedDates[e.context.selectedDates.length-1]],o=e.context.selectedDates[0]!==e.context.selectedDates[e.context.selectedDates.length-1],l=parseDates([`${n}:${a}`]).filter((t=>!e.context.disableDates.includes(t))),s=o?e.enableEdgeDatesOnly?[n,a]:l:[e.context.selectedDates[0],e.context.selectedDates[0]];if(setContext(e,"selectedDates",s),e.disableDatesGaps&&(setContext(e,"displayDateMin",state.rangeMin),setContext(e,"displayDateMax",state.rangeMax)),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),e.onCreateDateRangeTooltip)return e.context.selectedDates[0]||(state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,null)),e.context.selectedDates[0]&&(state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,t)),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave);}}})[1===e.context.selectedDates.length?"set":"reset"]();},updateDateModifier=e=>{e.context.mainElement.querySelectorAll("[data-vc-date]").forEach((t=>{const n=t.querySelector("[data-vc-date-btn]"),a=t.dataset.vcDate,o=getDate(a).getDay();setDateModifier(e,e.context.selectedYear,t,n,o,a,"current");}));},handleClickDate=(e,t)=>{var n;const a=t.target,o=a.closest("[data-vc-date-btn]");if(!e.selectionDatesMode||!["single","multiple","multiple-ranged"].includes(e.selectionDatesMode)||!o)return;const l=o.closest("[data-vc-date]");(({single:()=>handleSelectDate(e,l,false),multiple:()=>handleSelectDate(e,l,true),"multiple-ranged":()=>handleSelectDateRange(e,l)}))[e.selectionDatesMode](),null==(n=e.context.selectedDates)||n.sort(((e,t)=>+new Date(e)-+new Date(t))),e.onClickDate&&e.onClickDate(e,t),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);const s=a.closest('[data-vc-date-month="prev"]'),i=a.closest('[data-vc-date-month="next"]');({prev:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"prev"):updateDateModifier(e),next:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"next"):updateDateModifier(e),current:()=>updateDateModifier(e)})[s?"prev":i?"next":"current"]();},typeClick=["month","year"],getValue=(e,t,n)=>{const{currentValue:a,columnID:o}=getColumnID(e,t);return "month"===e.context.currentType&&o>=0?n-o:"year"===e.context.currentType&&e.context.selectedYear!==a?n-1:n},handleMultipleYearSelection=(e,t)=>{const n=getValue(e,"year",Number(t.dataset.vcYearsYear)),a=getDate(e.context.dateMin),o=getDate(e.context.dateMax),l=e.context.displayMonthsCount-1,{columnID:s}=getColumnID(e,"year"),i=e.context.selectedMontho.getMonth()-l+s&&n>=o.getFullYear(),c=no.getFullYear(),u=i||c?a.getFullYear():r||d?o.getFullYear():n,m=i||c?a.getMonth():r||d?o.getMonth()-l+s:e.context.selectedMonth;setContext(e,"selectedYear",u),setContext(e,"selectedMonth",m);},handleMultipleMonthSelection=(e,t)=>{const n=t.closest('[data-vc-column="month"]').querySelector('[data-vc="year"]'),a=getValue(e,"month",Number(t.dataset.vcMonthsMonth)),o=Number(n.dataset.vcYear),l=getDate(e.context.dateMin),s=getDate(e.context.dateMax),i=as.getMonth()&&o>=s.getFullYear();setContext(e,"selectedYear",o),setContext(e,"selectedMonth",i?l.getMonth():r?s.getMonth():a);},handleItemClick=(e,t,n,a)=>{var o;({year:()=>{if("multiple"===e.type)return handleMultipleYearSelection(e,a);setContext(e,"selectedYear",Number(a.dataset.vcYearsYear));},month:()=>{if("multiple"===e.type)return handleMultipleMonthSelection(e,a);setContext(e,"selectedMonth",Number(a.dataset.vcMonthsMonth));}})[n]();(({year:()=>{var n;return null==(n=e.onClickYear)?void 0:n.call(e,e,t)},month:()=>{var n;return null==(n=e.onClickMonth)?void 0:n.call(e,e,t)}}))[n](),e.context.currentType!==e.type?(setContext(e,"currentType",e.type),create(e),null==(o=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||o.focus()):setYearModifier(e,a,n,true,true);},handleClickType=(e,t,n)=>{var a;const o=t.target,l=o.closest(`[data-vc="${n}"]`),s={year:()=>createYears(e,o),month:()=>createMonths(e,o)};if(l&&e.onClickTitle&&e.onClickTitle(e,t),l&&e.context.currentType!==n)return s[n]();const i=o.closest(`[data-vc-${n}s-${n}]`);if(i)return handleItemClick(e,t,n,i);const r=o.closest('[data-vc="grid"]'),c=o.closest('[data-vc="column"]');(e.context.currentType===n&&l||"multiple"===e.type&&e.context.currentType===n&&r&&!c)&&(setContext(e,"currentType",e.type),create(e),null==(a=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||a.focus());},handleClickMonthOrYear=(e,t)=>{const n={month:e.selectionMonthsMode,year:e.selectionYearsMode};typeClick.forEach((a=>{n[a]&&t.target&&handleClickType(e,t,a);}));},handleClickWeekNumber=(e,t)=>{if(!e.enableWeekNumbers||!e.onClickWeekNumber)return;const n=t.target.closest("[data-vc-week-number]"),a=e.context.mainElement.querySelectorAll("[data-vc-date-week-number]");if(!n||!a[0])return;const o=Number(n.innerText),l=Number(n.dataset.vcWeekYear),s=Array.from(a).filter((e=>Number(e.dataset.vcDateWeekNumber)===o));e.onClickWeekNumber(e,o,l,s,t);},handleClickWeekDay=(e,t)=>{if(!e.onClickWeekDay)return;const n=t.target.closest("[data-vc-week-day]"),a=t.target.closest('[data-vc="column"]'),o=a?a.querySelectorAll("[data-vc-date-week-day]"):e.context.mainElement.querySelectorAll("[data-vc-date-week-day]");if(!n||!o[0])return;const l=Number(n.dataset.vcWeekDay),s=Array.from(o).filter((e=>Number(e.dataset.vcDateWeekDay)===l));e.onClickWeekDay(e,l,s,t);},handleClick=e=>{const t=t=>{handleClickArrow(e,t),handleClickWeekDay(e,t),handleClickWeekNumber(e,t),handleClickDate(e,t),handleClickMonthOrYear(e,t);};return e.context.mainElement.addEventListener("click",t),()=>e.context.mainElement.removeEventListener("click",t)},initMonthsCount=e=>{if("multiple"===e.type&&(e.displayMonthsCount<=1||e.displayMonthsCount>12))throw new Error(errorMessages.incorrectMonthsCount);if("multiple"!==e.type&&e.displayMonthsCount>1)throw new Error(errorMessages.incorrectMonthsCount);setContext(e,"displayMonthsCount",e.displayMonthsCount?e.displayMonthsCount:"multiple"===e.type?2:1);},getLocalDate=()=>{const e=new Date;return new Date(e.getTime()-6e4*e.getTimezoneOffset()).toISOString().substring(0,10)},resolveDate=(e,t)=>"today"===e?getLocalDate():e instanceof Date||"number"==typeof e||"string"==typeof e?parseDates([e])[0]:t,initRange=e=>{var t,n,a;const o=resolveDate(e.dateMin,e.dateMin),l=resolveDate(e.dateMax,e.dateMax),s=resolveDate(e.displayDateMin,o),i=resolveDate(e.displayDateMax,l);setContext(e,"dateToday",resolveDate(e.dateToday,e.dateToday)),setContext(e,"displayDateMin",s?getDate(o)>=getDate(s)?o:s:o),setContext(e,"displayDateMax",i?getDate(l)<=getDate(i)?l:i:l);const r=e.disableDatesPast&&!e.disableAllDates&&getDate(s)1&&e.context.disableDates.sort(((e,t)=>+new Date(e)-+new Date(t))),setContext(e,"enableDates",e.enableDates[0]?parseDates(e.enableDates):[]),(null==(t=e.context.enableDates)?void 0:t[0])&&(null==(n=e.context.disableDates)?void 0:n[0])&&setContext(e,"disableDates",e.context.disableDates.filter((t=>!e.context.enableDates.includes(t)))),e.context.enableDates.length>1&&e.context.enableDates.sort(((e,t)=>+new Date(e)-+new Date(t))),(null==(a=e.context.enableDates)?void 0:a[0])&&e.disableAllDates&&(setContext(e,"displayDateMin",e.context.enableDates[0]),setContext(e,"displayDateMax",e.context.enableDates[e.context.enableDates.length-1])),setContext(e,"dateMin",e.displayDisabledDates?o:e.context.displayDateMin),setContext(e,"dateMax",e.displayDisabledDates?l:e.context.displayDateMax);},initSelectedDates=e=>{var t;setContext(e,"selectedDates",(null==(t=e.selectedDates)?void 0:t[0])?parseDates(e.selectedDates):[]);},displayClosestValidDate=e=>{const t=t=>{const n=new Date(t);setInitialContext(e,n.getMonth(),n.getFullYear());};if(e.displayDateMin&&"today"!==e.displayDateMin&&(n=e.displayDateMin,a=new Date,new Date(n).getTime()>a.getTime())){const n=e.selectedDates.length&&e.selectedDates[0]?parseDates(e.selectedDates)[0]:e.displayDateMin;return t(getDate(resolveDate(n,e.displayDateMin))),true}var n,a;if(e.displayDateMax&&"today"!==e.displayDateMax&&((e,t)=>new Date(e).getTime(){setContext(e,"selectedMonth",t),setContext(e,"selectedYear",n),setContext(e,"displayYear",n);},initSelectedMonthYear=e=>{var t;if(e.enableJumpToSelectedDate&&(null==(t=e.selectedDates)?void 0:t[0])&&void 0===e.selectedMonth&&void 0===e.selectedYear){const t=getDate(parseDates(e.selectedDates)[0]);return void setInitialContext(e,t.getMonth(),t.getFullYear())}if(displayClosestValidDate(e))return;const n=void 0!==e.selectedMonth&&Number(e.selectedMonth)>=0&&Number(e.selectedMonth)<12,a=void 0!==e.selectedYear&&Number(e.selectedYear)>=0&&Number(e.selectedYear)<=9999;setInitialContext(e,n?Number(e.selectedMonth):getDate(e.context.dateToday).getMonth(),a?Number(e.selectedYear):getDate(e.context.dateToday).getFullYear());},initTime=e=>{var t,n,a;if(!e.selectionTimeMode)return;if(![12,24].includes(e.selectionTimeMode))throw new Error(errorMessages.incorrectTime);const o=12===e.selectionTimeMode,l=o?/^(0[1-9]|1[0-2]):([0-5][0-9]) ?(AM|PM)?$/i:/^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;let[s,i,r]=null!=(a=null==(n=null==(t=e.selectedTime)?void 0:t.match(l))?void 0:n.slice(1))?a:[];s?o&&!r&&(r="AM"):(s=o?transformTime12(String(e.timeMinHour)):String(e.timeMinHour),i=String(e.timeMinMinute),r=o?Number(transformTime12(String(e.timeMinHour)))>=12?"PM":"AM":null),setContext(e,"selectedHours",s.padStart(2,"0")),setContext(e,"selectedMinutes",i.padStart(2,"0")),setContext(e,"selectedKeeping",r),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${r?` ${r}`:""}`);},initAllVariables=e=>{setContext(e,"currentType",e.type),initMonthsCount(e),initRange(e),initSelectedMonthYear(e),initSelectedDates(e),initTime(e);},reset=(e,{year:t,month:n,dates:a,time:o,locale:l},s=true)=>{var i;const r={year:e.selectedYear,month:e.selectedMonth,dates:e.selectedDates,time:e.selectedTime};if(e.selectedYear=t?r.year:e.context.selectedYear,e.selectedMonth=n?r.month:e.context.selectedMonth,e.selectedTime=o?r.time:e.context.selectedTime,e.selectedDates="only-first"===a&&(null==(i=e.context.selectedDates)?void 0:i[0])?[e.context.selectedDates[0]]:true===a?r.dates:e.context.selectedDates,l){setContext(e,"locale",{months:{short:[],long:[]},weekdays:{short:[],long:[]}});}initAllVariables(e),s&&create(e),e.selectedYear=r.year,e.selectedMonth=r.month,e.selectedDates=r.dates,e.selectedTime=r.time,"multiple-ranged"===e.selectionDatesMode&&a&&handleSelectDateRange(e,null);},createToInput=e=>{const t=document.createElement("div");return t.className=e.styles.calendar,t.dataset.vc="calendar",t.dataset.vcInput="",t.dataset.vcCalendarHidden="",setContext(e,"inputModeInit",true),setContext(e,"isShowInInputMode",false),setContext(e,"mainElement",t),document.body.appendChild(e.context.mainElement),reset(e,{year:true,month:true,dates:true,time:true,locale:true}),setTimeout((()=>show(e))),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e)},canOpenOnFocus=e=>resolveToggle(e,e.openOnFocus),handleInput=e=>{setContext(e,"inputElement",e.context.mainElement);const t=()=>{e.context.inputModeInit?setTimeout((()=>show(e))):createToInput(e);};e.context.inputElement.addEventListener("click",t);const n="function"==typeof e.openOnFocus||true===e.openOnFocus,a=()=>{shouldSkipOpenOnFocus(e)?clearSkipOpenOnFocus(e):canOpenOnFocus(e)&&t();};n&&e.context.inputElement.addEventListener("focus",a);const o=t=>{const n="Tab"===t.key&&!t.shiftKey,a=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key);(n||a)&&(t=>{var n;if(!e.context.isShowInInputMode)return false;if(document.activeElement!==e.context.inputElement)return false;const a=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),o=null!=(n=document.createTreeWalker(e.context.mainElement,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>a(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}).nextNode())?n:a(e.context.mainElement)?e.context.mainElement:null;!o||o.tabIndex<0||(t.preventDefault(),o.focus());})(t);};return e.context.inputElement.addEventListener("keydown",o),()=>{e.context.inputElement.removeEventListener("click",t),n&&e.context.inputElement.removeEventListener("focus",a),e.context.inputElement.removeEventListener("keydown",o);}},init=e=>(setContext(e,"originalElement",e.context.mainElement.cloneNode(true)),setContext(e,"isInit",true),e.inputMode?handleInput(e):(initAllVariables(e),create(e),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e))),update=(e,t)=>{if(!e.context.isInit)throw new Error(errorMessages.notInit);reset(e,__spreadValues(__spreadValues({},{year:true,month:true,dates:true,time:true,locale:true}),t),!(e.inputMode&&!e.context.inputModeInit)),e.onUpdate&&e.onUpdate(e);},replaceProperties=(e,t)=>{const n=Object.keys(t);for(let a=0;a{replaceProperties(e,t),e.context.isInit&&update(e,n);};function findBestPickerPosition(e,t){const n="left";if(!t||!e)return n;const{canShow:a,parentPositions:o}=getAvailablePosition(e,t),l=a.left&&a.right;return (l&&a.bottom?"center":l&&a.top?["top","center"]:Array.isArray(o)?["bottom"===o[0]?"top":"bottom",...o.slice(1)]:o)||n}const setPosition=(e,t,n)=>{if(!e)return;const a="auto"===n?findBestPickerPosition(e,t):n,o={top:-t.offsetHeight,bottom:e.offsetHeight,left:0,center:e.offsetWidth/2-t.offsetWidth/2,right:e.offsetWidth-t.offsetWidth},l=Array.isArray(a)?a[0]:"bottom",s=Array.isArray(a)?a[1]:a;t.dataset.vcPosition=l;const{top:i,left:r}=getOffset(e),c=i+o[l];let d=r+o[s];const{vw:u}=getViewportDimensions();if(d+t.clientWidth>u){const e=window.innerWidth-document.body.clientWidth;d=u-t.clientWidth-e;}else d<0&&(d=0);Object.assign(t.style,{left:`${d}px`,top:`${c}px`});},show=e=>{if(e.context.isShowInInputMode)return;if(!e.context.currentType)return void e.context.mainElement.click();setContext(e,"cleanupHandlers",[]),setContext(e,"isShowInInputMode",true),e.inputMode&&restoreTabbing(e.context.mainElement),setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput),e.context.mainElement.removeAttribute("data-vc-calendar-hidden");const t=()=>{setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput);};window.addEventListener("resize",t),e.context.cleanupHandlers.push((()=>window.removeEventListener("resize",t)));const n=t=>{"Escape"===t.key&&hide(e);};document.addEventListener("keydown",n),e.context.cleanupHandlers.push((()=>document.removeEventListener("keydown",n)));const a=t=>{t.target===e.context.inputElement||e.context.mainElement.contains(t.target)||hide(e);};document.addEventListener("click",a,{capture:true}),e.context.cleanupHandlers.push((()=>document.removeEventListener("click",a,{capture:true}))),e.onShow&&e.onShow(e);},labels={application:"Calendar",navigation:"Calendar Navigation",arrowNext:{month:"Next month",year:"Next list of years"},arrowPrev:{month:"Previous month",year:"Previous list of years"},month:"Select month, current selected month:",months:"List of months",year:"Select year, current selected year:",years:"List of years",week:"Days of the week",weekNumber:"Numbers of weeks in a year",dates:"Dates in the current month",selectingTime:"Selecting a time ",inputHour:"Hours",inputMinute:"Minutes",rangeHour:"Slider for selecting hours",rangeMinute:"Slider for selecting minutes",btnKeeping:"Switch AM/PM, current position:"},styles={calendar:"vc",controls:"vc-controls",grid:"vc-grid",column:"vc-column",header:"vc-header",headerContent:"vc-header__content",month:"vc-month",year:"vc-year",arrowPrev:"vc-arrow vc-arrow_prev",arrowNext:"vc-arrow vc-arrow_next",wrapper:"vc-wrapper",content:"vc-content",months:"vc-months",monthsMonth:"vc-months__month",years:"vc-years",yearsYear:"vc-years__year",week:"vc-week",weekDay:"vc-week__day",weekNumbers:"vc-week-numbers",weekNumbersTitle:"vc-week-numbers__title",weekNumbersContent:"vc-week-numbers__content",weekNumber:"vc-week-number",dates:"vc-dates",datesRow:"vc-dates__row",date:"vc-date",dateBtn:"vc-date__btn",datePopup:"vc-date__popup",dateRangeTooltip:"vc-date-range-tooltip",time:"vc-time",timeContent:"vc-time__content",timeHour:"vc-time__hour",timeMinute:"vc-time__minute",timeKeeping:"vc-time__keeping",timeRanges:"vc-time__ranges",timeRange:"vc-time__range"};class OptionsCalendar{constructor(){__publicField(this,"type","default"),__publicField(this,"inputMode",false),__publicField(this,"openOnFocus",true),__publicField(this,"positionToInput","left"),__publicField(this,"firstWeekday",1),__publicField(this,"monthsToSwitch",1),__publicField(this,"themeAttrDetect","html[data-theme]"),__publicField(this,"locale","en"),__publicField(this,"dateToday","today"),__publicField(this,"dateMin","1970-01-01"),__publicField(this,"dateMax","2470-12-31"),__publicField(this,"displayDateMin"),__publicField(this,"displayDateMax"),__publicField(this,"displayDatesOutside",true),__publicField(this,"displayDisabledDates",false),__publicField(this,"displayMonthsCount"),__publicField(this,"disableDates",[]),__publicField(this,"disableAllDates",false),__publicField(this,"disableDatesPast",false),__publicField(this,"disableDatesGaps",false),__publicField(this,"disableWeekdays",[]),__publicField(this,"disableToday",false),__publicField(this,"enableDates",[]),__publicField(this,"enableEdgeDatesOnly",true),__publicField(this,"enableDateToggle",true),__publicField(this,"enableWeekNumbers",false),__publicField(this,"enableMonthChangeOnDayClick",true),__publicField(this,"enableJumpToSelectedDate",false),__publicField(this,"selectionDatesMode","single"),__publicField(this,"selectionMonthsMode",true),__publicField(this,"selectionYearsMode",true),__publicField(this,"selectionTimeMode",false),__publicField(this,"selectedDates",[]),__publicField(this,"selectedMonth"),__publicField(this,"selectedYear"),__publicField(this,"selectedHolidays",[]),__publicField(this,"selectedWeekends",[0,6]),__publicField(this,"selectedTime"),__publicField(this,"selectedTheme","system"),__publicField(this,"timeMinHour",0),__publicField(this,"timeMaxHour",23),__publicField(this,"timeMinMinute",0),__publicField(this,"timeMaxMinute",59),__publicField(this,"timeControls","all"),__publicField(this,"timeStepHour",1),__publicField(this,"timeStepMinute",1),__publicField(this,"sanitizerHTML",(e=>e)),__publicField(this,"onClickDate"),__publicField(this,"onClickWeekDay"),__publicField(this,"onClickWeekNumber"),__publicField(this,"onClickTitle"),__publicField(this,"onClickMonth"),__publicField(this,"onClickYear"),__publicField(this,"onClickArrow"),__publicField(this,"onChangeTime"),__publicField(this,"onChangeToInput"),__publicField(this,"onCreateDateRangeTooltip"),__publicField(this,"onCreateDateEls"),__publicField(this,"onCreateMonthEls"),__publicField(this,"onCreateYearEls"),__publicField(this,"onInit"),__publicField(this,"onUpdate"),__publicField(this,"onDestroy"),__publicField(this,"onShow"),__publicField(this,"onHide"),__publicField(this,"popups",{}),__publicField(this,"labels",__spreadValues({},labels)),__publicField(this,"layouts",{default:"",multiple:"",month:"",year:""}),__publicField(this,"styles",__spreadValues({},styles));}}const _Calendar=class e extends OptionsCalendar{constructor(t,n){var a;super(),__publicField(this,"init",(()=>init(this))),__publicField(this,"update",(e=>update(this,e))),__publicField(this,"destroy",(()=>destroy(this))),__publicField(this,"show",(()=>show(this))),__publicField(this,"hide",(()=>hide(this))),__publicField(this,"set",((e,t)=>set(this,e,t))),__publicField(this,"context"),this.context=__spreadProps(__spreadValues({},this.context),{locale:{months:{short:[],long:[]},weekdays:{short:[],long:[]}}}),setContext(this,"mainElement","string"==typeof t?null!=(a=e.memoizedElements.get(t))?a:this.queryAndMemoize(t):t),n&&replaceProperties(this,n);}queryAndMemoize(t){const n=document.querySelector(t);if(!n)throw new Error(errorMessages.notFoundSelector(t));return e.memoizedElements.set(t,n),n}};__publicField(_Calendar,"memoizedElements",new Map);let Calendar=_Calendar; + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$f = 'datepicker'; +const DATA_KEY$b = 'bs.datepicker'; +const EVENT_KEY$c = `.${DATA_KEY$b}`; +const DATA_API_KEY$7 = '.data-api'; +const EVENT_CHANGE$2 = `change${EVENT_KEY$c}`; +const EVENT_SHOW$4 = `show${EVENT_KEY$c}`; +const EVENT_SHOWN$3 = `shown${EVENT_KEY$c}`; +const EVENT_HIDE$3 = `hide${EVENT_KEY$c}`; +const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$c}`; +const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$c}${DATA_API_KEY$7}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY$c}${DATA_API_KEY$7}`; +const SELECTOR_DATA_TOGGLE$6 = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default$e = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType$e = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default$e; + } + static get DefaultType() { + return DefaultType$e; + } + static get NAME() { + return NAME$f; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN$3); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN$5); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; + } + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; + } + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); + } + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + EventHandler.trigger(this._element, EVENT_CHANGE$2, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; + + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); + } + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); + } + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; + } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$6, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE$6, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$c}${DATA_API_KEY$7}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { + return; + } + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; + } + this.hide(); + }); + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$e = 'dialog'; +const DATA_KEY$a = 'bs.dialog'; +const EVENT_KEY$b = `.${DATA_KEY$a}`; +const DATA_API_KEY$6 = '.data-api'; +const EVENT_SHOW$3 = `show${EVENT_KEY$b}`; +const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$b}`; +const EVENT_CANCEL = `cancel${EVENT_KEY$b}`; +const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$b}${DATA_API_KEY$6}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="dialog"]'; +const Default$d = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType$d = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default$d; + } + static get DefaultType() { + return DefaultType$d; + } + static get NAME() { + return NAME$e; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); + } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$5, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW$3, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$4, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY$b}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN$4, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); + }); + currentInstance.hide(); + } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$d = 'navoverflow'; +const DATA_KEY$9 = 'bs.navoverflow'; +const EVENT_KEY$a = `.${DATA_KEY$9}`; +const EVENT_UPDATE = `update${EVENT_KEY$a}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY$a}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default$c = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType$c = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); + } + + // Getters + static get Default() { + return Default$c; + } + static get DefaultType() { + return DefaultType$c; + } + static get NAME() { + return NAME$d; + } + + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); + } + super.dispose(); + } + + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); + + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); + + // Setup resize observer + this._setupResizeObserver(); + + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; + } + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; + } + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; + } + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + return; + } + const overflowWidth = overflowItem?.offsetWidth || 0; + + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer + + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; + } + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); + } + } + + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); + } + + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); + + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; + } + + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; + } + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); + } + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); + } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); + } + } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$c = 'swipe'; +const EVENT_KEY$9 = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default$b = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType$b = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; + +/** + * Class definition + */ + +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; + } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } + + // Getters + static get Default() { + return Default$b; + } + static get DefaultType() { + return DefaultType$b; + } + static get NAME() { + return NAME$c; + } + + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY$9); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; + } + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; + } + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; + } + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; + } + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); + + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; + } + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe + const direction = absDeltaX / this._deltaX; + this._deltaX = 0; + this._deltaY = 0; + if (!direction) { + return; + } + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; + } + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); + } + } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } + + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$b = 'drawer'; +const DATA_KEY$8 = 'bs.drawer'; +const EVENT_KEY$8 = `.${DATA_KEY$8}`; +const DATA_API_KEY$5 = '.data-api'; +const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; +const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$8}`; +const EVENT_RESIZE$1 = `resize${EVENT_KEY$8}`; +const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; +const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="drawer"]'; +const Default$a = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType$a = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + + // Getters + static get Default() { + return Default$a; + } + static get DefaultType() { + return DefaultType$a; + } + static get NAME() { + return NAME$b; + } + + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } + + // Protected — hook overrides + + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } + + // Private + + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; + } + + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL$1()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); + } + } else if (isRTL$1()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); + } + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$4, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$3, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE$1, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); + } + } +}); +enableDismissTrigger(Drawer); + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$a = 'strength'; +const DATA_KEY$7 = 'bs.strength'; +const EVENT_KEY$7 = `.${DATA_KEY$7}`; +const DATA_API_KEY$4 = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY$7}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default$9 = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType$9 = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); + } + } + + // Getters + static get Default() { + return Default$9; + } + static get DefaultType() { + return DefaultType$9; + } + static get NAME() { + return NAME$a; + } + + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } + + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } + + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } + + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; + + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; + } + + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; + } + if (/[A-Z]/.test(password)) { + score += weights.uppercase; + } + if (/\d/.test(password)) { + score += weights.numbers; + } + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; + } + + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; + } + if (password.length >= 16) { + score += weights.longPassword; + } + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; + } + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); + } + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; + } + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$7}${DATA_API_KEY$4}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$9 = 'otpInput'; +const DATA_KEY$6 = 'bs.otpInput'; +const EVENT_KEY$6 = `.${DATA_KEY$6}`; +const DATA_API_KEY$3 = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY$6}`; +const EVENT_INPUT$1 = `input${EVENT_KEY$6}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$6}${DATA_API_KEY$3}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT$1 = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default$8 = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType$8 = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT$1, this._element); + if (!this._input) { + return; + } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default$8; + } + static get DefaultType() { + return DefaultType$8; + } + static get NAME() { + return NAME$9; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } + + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; + } + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; + } + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); + } + + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); + } + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } + } + } + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); + } + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT$1, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); + } + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$8 = 'chips'; +const DATA_KEY$5 = 'bs.chips'; +const EVENT_KEY$5 = `.${DATA_KEY$5}`; +const DATA_API_KEY$2 = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY$5}`; +const EVENT_REMOVE = `remove${EVENT_KEY$5}`; +const EVENT_CHANGE$1 = `change${EVENT_KEY$5}`; +const EVENT_SELECT = `select${EVENT_KEY$5}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE$2 = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default$7 = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType$7 = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$7; + } + static get DefaultType() { + return DefaultType$7; + } + static get NAME() { + return NAME$8; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; + } + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; + } + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; + } + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; + } + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); + } + if (!chip || !value) { + return false; + } + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; + } + + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; + } + + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); + } + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE$2); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + focus() { + this._input?.focus(); + } + + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } + } + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); + + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); + } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); + + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; + } + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); + } + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); + } + + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { + return; + } + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey + }); + chip.focus(); + } + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); + } + }); + + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; + } + + // No default + } + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; + } + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; + } + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; + } + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; + } + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; + } + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default + } + } + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; + } + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { + return; + } + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; + } + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE$2); + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); + } + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; + } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); + } + } + } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$5}${DATA_API_KEY$2}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list + +const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); + +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); + } + return true; + } + + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; + } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); + } + } + } + return createdDocument.body.innerHTML; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$7 = 'TemplateFactory'; +const Default$6 = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType$6 = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; + +/** + * Class definition + */ + +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } + + // Getters + static get Default() { + return Default$6; + } + static get DefaultType() { + return DefaultType$6; + } + static get NAME() { + return NAME$7; + } + + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); + } + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); + } + return template; + } + + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); + } + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; + } + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; + } + if (isElement$1(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; + } + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; + } + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; + } + templateElement.textContent = element.textContent; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$6 = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE$2 = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW$2 = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE$2 = 'hide'; +const EVENT_HIDDEN$2 = 'hidden'; +const EVENT_SHOW$2 = 'show'; +const EVENT_SHOWN$2 = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK$3 = 'click'; +const EVENT_FOCUSIN$2 = 'focusin'; +const EVENT_FOCUSOUT$1 = 'focusout'; +const EVENT_MOUSEENTER$1 = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN$1 = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL$1() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL$1() ? 'right' : 'left' +}; +const Default$5 = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType$5 = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); + } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); + } + } + + // Getters + static get Default() { + return Default$5; + } + static get DefaultType() { + return DefaultType$5; + } + static get NAME() { + return NAME$6; + } + + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; + } + if (this._isShown()) { + this._leave(); + return; + } + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (!(this._isWithContent() && this._isEnabled)) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; + } + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW$2); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); + if (this._isHovered === false) { + this._leave(); + } + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); + if (hideEvent.defaultPrevented) { + return; + } + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW$2); + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering + + const complete = () => { + if (this._isWithActiveTrigger()) { + return; + } + if (!this._isHovered) { + this._disposeFloating(); + } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); + } + } + + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); + } + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE$2); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); + } + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); + } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } + + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; + } + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; + } + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; + } + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; + } + + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { + const { + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); + } + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); + } + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$3), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER$1) : this.constructor.eventName(EVENT_FOCUSIN$2); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); + } + } + this._hideModalHandler = () => { + if (this._element) { + this.hide(); + } + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; + } + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { + return; + } + + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; + } + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; + } + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); + } + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { + this._isHovered = true; + return; + } + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { + this.show(); + } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; + } + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); + } + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; + } + } + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay + }; + } + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); + } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; + } + } + config.selector = false; + config.trigger = 'manual'; + + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + if (this.tip) { + this.tip.remove(); + this.tip = null; + } + } +} + +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$3); + if (!target) { + return; + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; + +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN$2, SELECTOR_DATA_TOGGLE$3, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER$1, SELECTOR_DATA_TOGGLE$3, initTooltip); + +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$5 = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="popover"]'; +const EVENT_CLICK$2 = 'click'; +const EVENT_FOCUSIN$1 = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default$4 = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType$4 = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; + +/** + * Class definition + */ + +class Popover extends Tooltip { + // Getters + static get Default() { + return Default$4; + } + static get DefaultType() { + return DefaultType$4; + } + static get NAME() { + return NAME$5; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } + + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} + +/** + * Data API implementation - auto-initialize popovers + */ + +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$2); + if (!target) { + return; + } + + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; + +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK$2, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_FOCUSIN$1, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE$2, initPopover); + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$4 = 'range'; +const DATA_KEY$4 = 'bs.range'; +const EVENT_KEY$4 = `.${DATA_KEY$4}`; +const DATA_API_KEY$1 = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY$4}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$4}${DATA_API_KEY$1}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default$3 = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType$3 = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; + } + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); + } + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default$3; + } + static get DefaultType() { + return DefaultType$3; + } + static get NAME() { + return NAME$4; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; + } + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; + } + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); + } + } + if (points.length === 0) { + return; + } + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$3 = 'scrollspy'; +const DATA_KEY$3 = 'bs.scrollspy'; +const EVENT_KEY$3 = `.${DATA_KEY$3}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY$3}`; +const EVENT_CLICK$1 = `click${EVENT_KEY$3}`; +const EVENT_SCROLL = `scroll${EVENT_KEY$3}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY$3}`; +const EVENT_RESIZE = `resize${EVENT_KEY$3}`; +const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$3}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE$1 = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE$1 = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default$2 = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType$2 = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default$2; + } + static get DefaultType() { + return DefaultType$2; + } + static get NAME() { + return NAME$3; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK$1); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); + } + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- + + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); + } else { + this._intersecting.delete(entry.target); + } + } + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; + } + } + + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); + } + if (!active) { + return; + } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); + } + } + + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } + + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; + + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; + } + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } + + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } + + // --- Bottom sentinel ----------------------------------------------------- + + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; + } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } + + // --- Resize (px activation lines only) ----------------------------------- + + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; + } + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; + } + } + _rebuildObserver() { + if (!this._observer) { + return; + } + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + } + + // --- Smooth-scroll settle (hash + focus) --------------------------------- + + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; + } + + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK$1); + EventHandler.on(this._config.target, EVENT_CLICK$1, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { + return; + } + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); + return; + } + + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + } else { + root.scrollTop = height; + } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; + } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; + } + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; + } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; + + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); + } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); + } + section.focus({ + preventScroll: true + }); + } + + // --- Targets / observables ---------------------------------------------- + + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } + + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section + + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); + } + } + + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; + } + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE$1); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE$1)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE$1); + } + return; + } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE$1); + } + } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE$1); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE$1); + } + } +} + +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$2 = 'tab'; +const DATA_KEY$2 = 'bs.tab'; +const EVENT_KEY$2 = `.${DATA_KEY$2}`; +const EVENT_HIDE$1 = `hide${EVENT_KEY$2}`; +const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$2}`; +const EVENT_SHOW$1 = `show${EVENT_KEY$2}`; +const EVENT_SHOWN$1 = `shown${EVENT_KEY$2}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY$2}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY$2}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY$2}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE$1 = 'fade'; +const CLASS_NAME_SHOW$1 = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; + +/** + * Class definition + */ + +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) + } + + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } + + // Getters + static get NAME() { + return NAME$2; + } + + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; + } + + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; + } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } + + // Private + _activate(element, relatedElem) { + if (!element) { + return; + } + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW$1); + return; + } + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; + } + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW$1); + return; + } + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; + } + + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; + } + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); + } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); + } + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); + } + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); + } + if (!isActive) { + child.setAttribute('tabindex', '-1'); + } + this._setAttributeIfNotExists(child, 'role', 'tab'); + + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; + } + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); + } + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; + } + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW$1, open); + } + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); + } + } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } + + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } + + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE$1, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); + +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$1 = 'toast'; +const DATA_KEY$1 = 'bs.toast'; +const EVENT_KEY$1 = `.${DATA_KEY$1}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY$1}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY$1}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY$1}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY$1}`; +const EVENT_HIDE = `hide${EVENT_KEY$1}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY$1}`; +const EVENT_SHOW = `show${EVENT_KEY$1}`; +const EVENT_SHOWN = `shown${EVENT_KEY$1}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType$1 = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default$1 = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); + } + + // Getters + static get Default() { + return Default$1; + } + static get DefaultType() { + return DefaultType$1; + } + static get NAME() { + return NAME$1; + } + + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); + } + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); + } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } + + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; + } + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; + } + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } + } + if (isInteracting) { + this._clearTimeout(); + return; + } + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; + } + this._maybeScheduleHide(); + } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Toast); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; + } + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; + } + + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; + } + this._element.setAttribute(attribute, value); + } +} + +/** + * Data API implementation + */ + +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); + +export { Alert, Button, Carousel, Chips, Collapse, Combobox, Datepicker, Dialog, Drawer, Menu, NavOverflow, OtpInput, Popover, Range, ScrollSpy, Strength, Tab, Toast, Toggler, Tooltip }; diff --git a/assets/javascripts/bootstrap.bundle.min.js b/assets/javascripts/bootstrap.bundle.min.js new file mode 100644 index 00000000..e8dcf8c4 --- /dev/null +++ b/assets/javascripts/bootstrap.bundle.min.js @@ -0,0 +1,8 @@ +/*! + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +const elementMap=new Map,Data={set(e,t,n){elementMap.has(e)||elementMap.set(e,new Map);const s=elementMap.get(e);s.has(t)||0===s.size?s.set(t,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...s.keys()][0]}.`)},get:(e,t)=>elementMap.has(e)&&elementMap.get(e).get(t)||null,getAny:e=>elementMap.has(e)&&elementMap.get(e).values().next().value||null,remove(e,t){if(!elementMap.has(e))return;const n=elementMap.get(e);n.delete(t),0===n.size&&elementMap.delete(e)}},namespaceRegex=/[^.]*(?=\..*)\.|.*/,stripNameRegex=/\..*/,stripUidRegex=/::\d+$/,eventRegistry={};let uidEvent=1;const customEvents={mouseenter:"mouseover",mouseleave:"mouseout"},nativeEvents=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll","scrollend"]);function makeEventUid(e,t){return t&&`${t}::${uidEvent++}`||e.uidEvent||uidEvent++}function getElementEvents(e){const t=makeEventUid(e);return e.uidEvent=t,eventRegistry[t]=eventRegistry[t]||{},eventRegistry[t]}function bootstrapHandler(e,t){return function n(s){return hydrateObj(s,{delegateTarget:e}),n.oneOff&&EventHandler.off(e,s.type,t),t.apply(e,[s])}}function bootstrapDelegationHandler(e,t,n){return function s(i){const o=e.querySelectorAll(t);for(let{target:a}=i;a&&a!==this;a=a.parentNode)for(const l of o)if(l===a)return hydrateObj(i,{delegateTarget:a}),s.oneOff&&EventHandler.off(e,i.type,t,n),n.apply(a,[i])}}function findHandler(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function normalizeParameters(e,t,n){const s="string"==typeof t,i=s?n:t||n;let o=getTypeEvent(e);return nativeEvents.has(o)||(o=e),[s,i,o]}function addHandler(e,t,n,s,i){if("string"!=typeof t||!e)return;let[o,a,l]=normalizeParameters(t,n,s);if(t in customEvents){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};a=e(a)}const r=getElementEvents(e),c=r[l]||(r[l]={}),d=findHandler(c,a,o?n:null);if(d)return void(d.oneOff=d.oneOff&&i);const u=makeEventUid(a,t.replace(namespaceRegex,"")),h=o?bootstrapDelegationHandler(e,n,a):bootstrapHandler(e,a);h.delegationSelector=o?n:null,h.callable=a,h.oneOff=i,h.uidEvent=u,c[u]=h,e.addEventListener(l,h,o)}function removeHandler(e,t,n,s,i){const o=findHandler(t[n],s,i);o&&(e.removeEventListener(n,o,Boolean(i)),delete t[n][o.uidEvent])}function removeNamespacedHandlers(e,t,n,s){const i=t[n]||{};for(const[o,a]of Object.entries(i))o.includes(s)&&removeHandler(e,t,n,a.callable,a.delegationSelector)}function getTypeEvent(e){return e=e.replace(stripNameRegex,""),customEvents[e]||e}const EventHandler={on(e,t,n,s){addHandler(e,t,n,s,!1)},one(e,t,n,s){addHandler(e,t,n,s,!0)},off(e,t,n,s){if("string"!=typeof t||!e)return;const[i,o,a]=normalizeParameters(t,n,s),l=a!==t,r=getElementEvents(e),c=r[a]||{},d=t.startsWith(".");if(void 0===o){if(d)for(const n of Object.keys(r))removeNamespacedHandlers(e,r,n,t.slice(1));for(const[n,s]of Object.entries(c)){const i=n.replace(stripUidRegex,"");l&&!t.includes(i)||removeHandler(e,r,a,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;removeHandler(e,r,a,o,i?n:null)}},trigger(e,t,n){if("string"!=typeof t||!e)return null;const s=hydrateObj(new Event(t,{bubbles:!0,cancelable:!0}),n);return e.dispatchEvent(s),s}};function hydrateObj(e,t={}){for(const[n,s]of Object.entries(t))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get:()=>s})}return e}function normalizeData(e){if("true"===e)return!0;if("false"===e)return!1;if(e===Number(e).toString())return Number(e);if(""===e||"null"===e)return null;if("string"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function normalizeDataKey(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const Manipulator={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${normalizeDataKey(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${normalizeDataKey(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const s of n){let n=s.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=normalizeData(e.dataset[s])}return t},getDataAttribute:(e,t)=>normalizeData(e.getAttribute(`data-bs-${normalizeDataKey(t)}`))},MAX_UID=1e6,MILLISECONDS_MULTIPLIER=1e3,TRANSITION_END="transitionend",parseSelector=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),e),toType=e=>null==e?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),getUID=e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e},getTransitionDurationFromElement=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),i=Number.parseFloat(n);return s||i?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0},triggerTransitionEnd=e=>{e.dispatchEvent(new Event(TRANSITION_END))},isElement$1=e=>!(!e||"object"!=typeof e)&&void 0!==e.nodeType,getElement=e=>isElement$1(e)?e:"string"==typeof e&&e.length>0?document.querySelector(parseSelector(e)):null,isVisible=e=>{if(!isElement$1(e)||0===e.getClientRects().length)return!1;const t="visible"===getComputedStyle(e).getPropertyValue("visibility"),n=e.closest("details:not([open])");if(!n)return t;if(n!==e){const t=e.closest("summary");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},isDisabled=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(void 0!==e.disabled?e.disabled:e.hasAttribute("disabled")&&"false"!==e.getAttribute("disabled")),findShadowRoot=e=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?findShadowRoot(e.parentNode):null},noop=()=>{},reflow=e=>{e.offsetHeight},isRTL$1=()=>"rtl"===document.documentElement.dir,execute=(e,t=[],n=e)=>"function"==typeof e?e.call(...t):n,executeAfterTransition=(e,t,n=!0)=>{if(!n)return void execute(e);const s=getTransitionDurationFromElement(t)+5;let i=!1;const o=({target:n})=>{n===t&&(i=!0,t.removeEventListener(TRANSITION_END,o),execute(e))};t.addEventListener(TRANSITION_END,o),setTimeout(()=>{i||triggerTransitionEnd(t)},s)},getNextActiveElement=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return-1===o?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])};class Config{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=isElement$1(t)?Manipulator.getDataAttribute(t,"config"):{};return{...this.constructor.Default,..."object"==typeof n?n:{},...isElement$1(t)?Manipulator.getDataAttributes(t):{},..."object"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[n,s]of Object.entries(t)){const t=e[n],i=isElement$1(t)?"element":toType(t);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const VERSION="6.0.0-alpha1";class BaseComponent extends Config{constructor(e,t){if(super(),!(e=getElement(e)))return;this._element=e,this._config=this._getConfig(t);const n=Data.get(this._element,this.constructor.DATA_KEY);n&&n.dispose(),Data.set(this._element,this.constructor.DATA_KEY,this)}dispose(){Data.remove(this._element,this.constructor.DATA_KEY),EventHandler.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){executeAfterTransition(()=>{this._element&&e()},t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return Data.get(getElement(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return VERSION}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const getSelector=e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&"#"!==n?n.trim():null}return t?t.split(",").map(e=>parseSelector(e)).join(","):null},SelectorEngine={find:(e,t=document.documentElement)=>[...Element.prototype.querySelectorAll.call(t,e)],findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[...e.children].filter(e=>e.matches(t)),parents(e,t){const n=[];let s=e.parentNode.closest(t);for(;s;)n.push(s),s=s.parentNode.closest(t);return n},closest:(e,t)=>Element.prototype.closest.call(e,t),prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!isDisabled(e)&&isVisible(e))},getSelectorFromElement(e){const t=getSelector(e);return t&&SelectorEngine.findOne(t)?t:null},getElementFromSelector(e){const t=getSelector(e);return t?SelectorEngine.findOne(t):null},getMultipleElementsFromSelector(e){const t=getSelector(e);return t?SelectorEngine.find(t):[]}},enableDismissTrigger=(e,t="hide")=>{const n=`click.dismiss${e.EVENT_KEY}`,s=e.NAME;EventHandler.on(document,n,`[data-bs-dismiss="${s}"]`,function(n){if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),isDisabled(this))return;const i=SelectorEngine.getElementFromSelector(this)||this.closest(`.${s}`);e.getOrCreateInstance(i)[t]()})},eventActionOnPlugin=(e,t,n,s,i=null)=>{eventAction(`${t}.${e.NAME}`,n,t=>{const n=t.targets.filter(Boolean).map(t=>e.getOrCreateInstance(t));"function"==typeof i&&i({...t,instances:n});for(const e of n)e[s]()})},eventAction=(e,t,n)=>{const s=`${t}:not(.disabled):not(:disabled)`;EventHandler.on(document,e,s,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault();const t=SelectorEngine.getSelectorFromElement(this),s=t?SelectorEngine.find(t):[this];n({targets:s,event:e})})},NAME$l="alert",DATA_KEY$h="bs.alert",EVENT_KEY$i=".bs.alert",EVENT_CLOSE="close.bs.alert",EVENT_CLOSED="closed.bs.alert",CLASS_NAME_FADE$4="fade",CLASS_NAME_SHOW$6="show";class Alert extends BaseComponent{static get NAME(){return NAME$l}close(){if(EventHandler.trigger(this._element,EVENT_CLOSE).defaultPrevented)return;this._element.classList.remove("show");const e=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,e)}_destroyElement(){this._element.remove(),EventHandler.trigger(this._element,EVENT_CLOSED),this.dispose()}}enableDismissTrigger(Alert,"close");const NAME$k="button",DATA_KEY$g="bs.button",EVENT_KEY$h=`.${DATA_KEY$g}`,DATA_API_KEY$c=".data-api",CLASS_NAME_ACTIVE$4="active",SELECTOR_DATA_TOGGLE$a='[data-bs-toggle="button"]',EVENT_CLICK_DATA_API$8=`click${EVENT_KEY$h}.data-api`;class Button extends BaseComponent{static get NAME(){return NAME$k}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}}EventHandler.on(document,EVENT_CLICK_DATA_API$8,SELECTOR_DATA_TOGGLE$a,e=>{e.preventDefault();const t=e.target.closest(SELECTOR_DATA_TOGGLE$a);Button.getOrCreateInstance(t).toggle()});const NAME$j="carousel",DATA_KEY$f="bs.carousel",EVENT_KEY$g=`.${DATA_KEY$f}`,DATA_API_KEY$b=".data-api",ARROW_LEFT_KEY$2="ArrowLeft",ARROW_RIGHT_KEY$2="ArrowRight",DIRECTION_LEFT="left",DIRECTION_RIGHT="right",EVENT_SLIDE=`slide${EVENT_KEY$g}`,EVENT_SLID=`slid${EVENT_KEY$g}`,EVENT_KEYDOWN$2=`keydown${EVENT_KEY$g}`,EVENT_MOUSEENTER$2=`mouseenter${EVENT_KEY$g}`,EVENT_MOUSELEAVE$1=`mouseleave${EVENT_KEY$g}`,EVENT_POINTERDOWN$1=`pointerdown${EVENT_KEY$g}`,EVENT_LOAD_DATA_API$3=`load${EVENT_KEY$g}.data-api`,EVENT_CLICK_DATA_API$7=`click${EVENT_KEY$g}.data-api`,CLASS_NAME_CAROUSEL="carousel",CLASS_NAME_ACTIVE$3="active",CLASS_NAME_FADE$3="carousel-fade",CLASS_NAME_CENTER="carousel-center",CLASS_NAME_AUTO="carousel-auto",CLASS_NAME_CLONE="carousel-item-clone",CLASS_NAME_PAUSED="paused",CLASS_NAME_PLAYING="carousel-playing",PROPERTY_INTERVAL="--bs-carousel-interval",SCROLL_DURATION=300,ACTIVE_RATIO_TOLERANCE=.05,SELECTOR_ACTIVE=".active",SELECTOR_ITEM=`.carousel-item:not(.${CLASS_NAME_CLONE})`,SELECTOR_ACTIVE_ITEM=".active"+SELECTOR_ITEM,SELECTOR_INNER$1=".carousel-inner",SELECTOR_INDICATORS=".carousel-indicators",SELECTOR_PLAY_PAUSE=".carousel-control-play-pause",SELECTOR_DATA_SLIDE="[data-bs-slide], [data-bs-slide-to]",SELECTOR_DATA_SLIDE_PREV='[data-bs-slide="prev"]',SELECTOR_DATA_SLIDE_NEXT='[data-bs-slide="next"]',SELECTOR_DATA_AUTOPLAY='[data-bs-autoplay="true"]',KEY_TO_DIRECTION={[ARROW_LEFT_KEY$2]:"right",[ARROW_RIGHT_KEY$2]:"left"},ENDS_STOP="stop",ENDS_WRAP="wrap",ENDS_LOOP="loop",Default$i={autoplay:!1,ends:ENDS_LOOP,interval:5e3,keyboard:!0,pause:"hover"},DefaultType$i={autoplay:"boolean",ends:"string",interval:"number",keyboard:"boolean",pause:"(string|boolean)"},easeInOutCubic=e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2;class Carousel extends BaseComponent{constructor(e,t){super(e,t),this._viewport=SelectorEngine.findOne(SELECTOR_INNER$1,this._element)||this._element,this._indicatorsElement=SelectorEngine.findOne(SELECTOR_INDICATORS,this._element),this._playPauseElement=SelectorEngine.findOne(SELECTOR_PLAY_PAUSE,this._element),this._prevControls=SelectorEngine.find('[data-bs-slide="prev"]',this._element),this._nextControls=SelectorEngine.find('[data-bs-slide="next"]',this._element),this._interval=null,this._observer=null,this._scrollFrame=null,this._looping=!1,this._visibility=new Map,this._playing=this._config.autoplay,this._activeIndex=this._initialActiveIndex(),this._addEventListeners(),this._observeItems(),this._refreshActiveState(),this._playing&&this.cycle(),this._updatePlayPauseControl()}static get Default(){return Default$i}static get DefaultType(){return DefaultType$i}static get NAME(){return NAME$j}next(){this.to(this._navIndex()+1)}nextWhenVisible(){"visible"===document.visibilityState&&isVisible(this._element)&&this.next()}prev(){this.to(this._navIndex()-1)}pause(){this._clearInterval(),this._element.classList.remove("carousel-playing")}cycle(){this._clearInterval(),this._scheduleAutoplay(),this._element.classList.add("carousel-playing")}to(e){if(this._looping)return;const t=this._getItems(),n=Number.parseInt(e,10);if(this._config.ends===ENDS_LOOP&&!this._prefersReducedMotion()&&this._canLoop()){if(n>t.length-1)return void this._loopTransition(!0);if(n<0)return void this._loopTransition(!1)}const s=this._normalizeIndex(n,t.length),i=this._navIndex();null!==s&&s!==i&&(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[s],direction:this._direction(i,s),from:i,to:s}).defaultPrevented||(this._isFade()?this._fadeTo(s):this._scrollToIndex(s)))}dispose(){this._clearInterval(),this._observer&&this._observer.disconnect(),null!==this._scrollFrame&&cancelAnimationFrame(this._scrollFrame);for(const e of SelectorEngine.find(`.${CLASS_NAME_CLONE}`,this._viewport))e.remove();this._viewport.style.scrollSnapType="",EventHandler.off(this._viewport,EVENT_KEY$g),super.dispose()}_configAfterMerge(e){return[ENDS_STOP,ENDS_WRAP,ENDS_LOOP].includes(e.ends)||(e.ends=Default$i.ends),e}_initialActiveIndex(){const e=SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM,this._element),t=e?this._getItems().indexOf(e):0;return Math.max(t,0)}_addEventListeners(){this._config.keyboard&&EventHandler.on(this._element,EVENT_KEYDOWN$2,e=>this._keydown(e)),"hover"===this._config.pause&&(EventHandler.on(this._element,EVENT_MOUSEENTER$2,()=>this.pause()),EventHandler.on(this._element,EVENT_MOUSELEAVE$1,()=>this._maybeEnableCycle())),EventHandler.on(this._viewport,EVENT_POINTERDOWN$1,()=>this._pauseFromInteraction())}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=KEY_TO_DIRECTION[e.key];t&&(e.preventDefault(),this._pauseFromInteraction(),"right"===t?this.prev():this.next())}_observeItems(){if(!this._isFade()&&"undefined"!=typeof IntersectionObserver){this._observer=new IntersectionObserver(e=>this._handleIntersection(e),{root:this._viewport,threshold:[0,.25,.5,.75,1]});for(const e of this._getItems())this._observer.observe(e)}}_handleIntersection(e){if(this._looping)return;for(const t of e)this._visibility.set(t.target,t.isIntersecting?t.intersectionRatio:0);const t=this._getItems().map(e=>this._visibility.get(e)??0),n=Math.max(...t);let s=this._activeIndex;n>0&&(s=t.findIndex(e=>e>=n-.05)),this._setActive(s),this._updateEndControls()}_navIndex(){if(this._isFade()||this._viewport.scrollWidth-this._viewport.clientWidth<=0)return this._activeIndex;let e=this._activeIndex,t=Number.POSITIVE_INFINITY;for(const[n,s]of this._getItems().entries()){const i=Math.abs(this._scrollDelta(s));i{this._viewport.style.scrollSnapType="",this._observer||this._setActive(e),this._updateEndControls()})}_animateScroll(e,t){null!==this._scrollFrame&&(cancelAnimationFrame(this._scrollFrame),this._scrollFrame=null);const n=this._viewport.scrollLeft,s=e-n;if(this._prefersReducedMotion()||"undefined"==typeof requestAnimationFrame)return this._viewport.scrollTo({left:e,behavior:"instant"}),void t();let i=null;const o=a=>{null===i&&(i=a);const l=Math.min((a-i)/300,1);this._viewport.scrollTo({left:n+s*easeInOutCubic(l),behavior:"instant"}),l<1?this._scrollFrame=requestAnimationFrame(o):(this._viewport.scrollTo({left:e,behavior:"instant"}),this._scrollFrame=null,t())};this._scrollFrame=requestAnimationFrame(o)}_scrollDelta(e){const t=this._viewport.getBoundingClientRect(),n=e.getBoundingClientRect();if(this._element.classList.contains("carousel-center"))return n.left+n.width/2-(t.left+t.width/2);const s=Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart)||0;return isRTL$1()?n.right-(t.right-s):n.left-(t.left+s)}_loopTransition(e){const t=this._getItems(),n=t.length-1,s=this._activeIndex,i=e?0:n,o=this._loopDirection(e);if(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[i],direction:o,from:s,to:i}).defaultPrevented)return;this._looping=!0;const a=(e?t[0]:t[n]).cloneNode(!0);a.classList.add(CLASS_NAME_CLONE),a.classList.remove("active"),a.removeAttribute("id");for(const e of SelectorEngine.find("[id]",a))e.removeAttribute("id");a.setAttribute("aria-hidden","true"),a.inert=!0,this._viewport.style.scrollSnapType="none",e?this._viewport.append(a):(this._viewport.prepend(a),this._jumpScroll(this._scrollDelta(t[s]))),this._animateScroll(this._viewport.scrollLeft+this._scrollDelta(a),()=>{a.remove(),this._jumpScroll(this._scrollDelta(t[i])),this._activeIndex=i,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[i],direction:o,from:s,to:i}),this._viewport.style.scrollSnapType="",this._looping=!1})}_loopDirection(e){return isRTL$1()?e?"right":"left":e?"left":"right"}_jumpScroll(e){this._viewport.style.scrollSnapType="none",this._viewport.scrollBy({left:e,top:0,behavior:"instant"})}_fadeTo(e){this._setActive(e)}_setActive(e){const t=this._getItems();if(e===this._activeIndex||!t[e])return;const n=this._activeIndex;this._activeIndex=e,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[e],direction:this._direction(n,e),from:n,to:e})}_refreshActiveState(){const e=this._getItems();for(const[t,n]of e.entries())n.classList.toggle("active",t===this._activeIndex);this._setActiveIndicatorElement(this._activeIndex),this._updateEndControls()}_updateEndControls(){if(this._config.ends!==ENDS_STOP)return;const e=this._viewport,t=e.scrollWidth-e.clientWidth;let n,s;if(t>0){const i=Math.abs(e.scrollLeft);n=i<=1,s=i>=t-1}else{const e=this._getItems().length-1;n=this._activeIndex<=0,s=this._activeIndex>=e}this._setControlsDisabled(this._prevControls,n),this._setControlsDisabled(this._nextControls,s)}_setControlsDisabled(e,t){for(const n of e)t&&n===document.activeElement&&((e===this._prevControls?this._nextControls:this._prevControls)[0]??this._viewport).focus({preventScroll:!0}),n.disabled=t}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=SelectorEngine.findOne(".active",this._indicatorsElement);t&&(t.classList.remove("active"),t.removeAttribute("aria-current"));const n=SelectorEngine.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add("active"),n.setAttribute("aria-current","true"))}_normalizeIndex(e,t){return Number.isNaN(e)||0===t?null:e<0?this._wrapsAround()?t-1:null:e>t-1?this._wrapsAround()?0:null:e}_wrapsAround(){return this._config.ends===ENDS_WRAP||this._config.ends===ENDS_LOOP}_canLoop(){if(this._isFade()||this._getItems().length<2)return!1;const e=getComputedStyle(this._element),t=t=>Number.parseFloat(e.getPropertyValue(t))||0;return 1===(t("--bs-carousel-items")||1)&&0===t("--bs-carousel-items-peek")&&!this._element.classList.contains("carousel-center")&&!this._element.classList.contains("carousel-auto")}_direction(e,t){const n=t>e;return isRTL$1()?n?"right":"left":n?"left":"right"}_scheduleAutoplay(e=this._activeIndex){const t=this._itemInterval(e);this._element.style.setProperty(PROPERTY_INTERVAL,`${t}ms`),this._interval=setTimeout(()=>{const e=this._upcomingIndex();this.nextWhenVisible(),null!==e?this._scheduleAutoplay(e):this.pause()},t)}_upcomingIndex(){return this._normalizeIndex(this._navIndex()+1,this._getItems().length)}_itemInterval(e=this._activeIndex){const t=this._getItems()[e],n=t?Number.parseInt(t.getAttribute("data-bs-interval"),10):Number.NaN;return Number.isNaN(n)?this._config.interval:n}_maybeEnableCycle(){this._playing&&this.cycle()}_pauseFromInteraction(){this._playing=!1,this.pause(),this._updatePlayPauseControl()}_togglePlayPause(){this._playing?this._pauseFromInteraction():(this._playing=!0,this.cycle(),this._updatePlayPauseControl())}_updatePlayPauseControl(){if(!this._playPauseElement)return;this._playPauseElement.classList.toggle("paused",!this._playing);const e=this._playPauseElement.getAttribute(this._playing?"data-bs-pause-label":"data-bs-play-label");e&&this._playPauseElement.setAttribute("aria-label",e)}_isFade(){return this._element.classList.contains("carousel-fade")}_prefersReducedMotion(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}_getItems(){return SelectorEngine.find(SELECTOR_ITEM,this._element)}_clearInterval(){this._interval&&(clearTimeout(this._interval),this._interval=null)}}EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_DATA_SLIDE,function(e){const t=SelectorEngine.getElementFromSelector(this);if(!t||!t.classList.contains("carousel"))return;e.preventDefault();const n=Carousel.getOrCreateInstance(t);n._pauseFromInteraction();const s=this.getAttribute("data-bs-slide-to");s?n.to(s):"next"!==Manipulator.getDataAttribute(this,"slide")?n.prev():n.next()}),EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_PLAY_PAUSE,function(e){const t=SelectorEngine.getElementFromSelector(this);t&&t.classList.contains("carousel")&&(e.preventDefault(),Carousel.getOrCreateInstance(t)._togglePlayPause())}),EventHandler.on(window,EVENT_LOAD_DATA_API$3,()=>{const e=SelectorEngine.find(SELECTOR_DATA_AUTOPLAY);for(const t of e)Carousel.getOrCreateInstance(t)});const NAME$i="collapse",DATA_KEY$e="bs.collapse",EVENT_KEY$f=`.${DATA_KEY$e}`,DATA_API_KEY$a=".data-api",EVENT_SHOW$7=`show${EVENT_KEY$f}`,EVENT_SHOWN$6=`shown${EVENT_KEY$f}`,EVENT_HIDE$6=`hide${EVENT_KEY$f}`,EVENT_HIDDEN$8=`hidden${EVENT_KEY$f}`,EVENT_CLICK_DATA_API$6=`click${EVENT_KEY$f}.data-api`,CLASS_NAME_SHOW$5="show",CLASS_NAME_COLLAPSE="collapse",CLASS_NAME_COLLAPSING="collapsing",CLASS_NAME_COLLAPSED="collapsed",CLASS_NAME_DEEPER_CHILDREN=":scope .collapse .collapse",CLASS_NAME_HORIZONTAL="collapse-horizontal",WIDTH="width",HEIGHT="height",SELECTOR_ACTIVES=".collapse.show, .collapse.collapsing",SELECTOR_DATA_TOGGLE$9='[data-bs-toggle="collapse"]',Default$h={parent:null,toggle:!0},DefaultType$h={parent:"(null|element)",toggle:"boolean"};class Collapse extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=SelectorEngine.find(SELECTOR_DATA_TOGGLE$9);for(const e of n){const t=SelectorEngine.getSelectorFromElement(e),n=SelectorEngine.find(t).filter(e=>e===this._element);null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Default$h}static get DefaultType(){return DefaultType$h}static get NAME(){return NAME$i}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(e=>e!==this._element).map(e=>Collapse.getOrCreateInstance(e,{toggle:!1}))),e.length&&e[0]._isTransitioning)return;if(EventHandler.trigger(this._element,EVENT_SHOW$7).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[t]="",EventHandler.trigger(this._element,EVENT_SHOWN$6)},this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(EventHandler.trigger(this._element,EVENT_HIDE$6).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,reflow(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");for(const e of this._triggerArray){const t=SelectorEngine.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),EventHandler.trigger(this._element,EVENT_HIDDEN$8)},this._element,!0)}_isShown(e=this._element){return e.classList.contains("show")}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=getElement(e.parent),e}_getDimension(){return this._element.classList.contains("collapse-horizontal")?WIDTH:HEIGHT}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9);for(const t of e){const e=SelectorEngine.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN,this._config.parent);return SelectorEngine.find(e,this._config.parent).filter(e=>!t.includes(e))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle("collapsed",!t),n.setAttribute("aria-expanded",t)}}EventHandler.on(document,EVENT_CLICK_DATA_API$6,SELECTOR_DATA_TOGGLE$9,function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of SelectorEngine.getMultipleElementsFromSelector(this))Collapse.getOrCreateInstance(e,{toggle:!1}).toggle()});const min=Math.min,max=Math.max,round=Math.round,floor=Math.floor,createCoords=e=>({x:e,y:e}),oppositeSideMap={left:"right",right:"left",bottom:"top",top:"bottom"};function clamp(e,t,n){return max(e,min(t,n))}function evaluate(e,t){return"function"==typeof e?e(t):e}function getSide(e){return e.split("-")[0]}function getAlignment(e){return e.split("-")[1]}function getOppositeAxis(e){return"x"===e?"y":"x"}function getAxisLength(e){return"y"===e?"height":"width"}function getSideAxis(e){const t=e[0];return"t"===t||"b"===t?"y":"x"}function getAlignmentAxis(e){return getOppositeAxis(getSideAxis(e))}function getAlignmentSides(e,t,n){void 0===n&&(n=!1);const s=getAlignment(e),i=getAlignmentAxis(e),o=getAxisLength(i);let a="x"===i?s===(n?"end":"start")?"right":"left":"start"===s?"bottom":"top";return t.reference[o]>t.floating[o]&&(a=getOppositePlacement(a)),[a,getOppositePlacement(a)]}function getExpandedPlacements(e){const t=getOppositePlacement(e);return[getOppositeAlignmentPlacement(e),t,getOppositeAlignmentPlacement(t)]}function getOppositeAlignmentPlacement(e){return e.includes("start")?e.replace("start","end"):e.replace("end","start")}const lrPlacement=["left","right"],rlPlacement=["right","left"],tbPlacement=["top","bottom"],btPlacement=["bottom","top"];function getSideList(e,t,n){switch(e){case"top":case"bottom":return n?t?rlPlacement:lrPlacement:t?lrPlacement:rlPlacement;case"left":case"right":return t?tbPlacement:btPlacement;default:return[]}}function getOppositeAxisPlacements(e,t,n,s){const i=getAlignment(e);let o=getSideList(getSide(e),"start"===n,s);return i&&(o=o.map(e=>e+"-"+i),t&&(o=o.concat(o.map(getOppositeAlignmentPlacement)))),o}function getOppositePlacement(e){const t=getSide(e);return oppositeSideMap[t]+e.slice(t.length)}function expandPaddingObject(e){return{top:0,right:0,bottom:0,left:0,...e}}function getPaddingObject(e){return"number"!=typeof e?expandPaddingObject(e):{top:e,right:e,bottom:e,left:e}}function rectToClientRect(e){const{x:t,y:n,width:s,height:i}=e;return{width:s,height:i,top:n,left:t,right:t+s,bottom:n+i,x:t,y:n}}function computeCoordsFromPlacement(e,t,n){let{reference:s,floating:i}=e;const o=getSideAxis(t),a=getAlignmentAxis(t),l=getAxisLength(a),r=getSide(t),c="y"===o,d=s.x+s.width/2-i.width/2,u=s.y+s.height/2-i.height/2,h=s[l]/2-i[l]/2;let _;switch(r){case"top":_={x:d,y:s.y-i.height};break;case"bottom":_={x:d,y:s.y+s.height};break;case"right":_={x:s.x+s.width,y:u};break;case"left":_={x:s.x-i.width,y:u};break;default:_={x:s.x,y:s.y}}switch(getAlignment(t)){case"start":_[a]-=h*(n&&c?-1:1);break;case"end":_[a]+=h*(n&&c?-1:1)}return _}async function detectOverflow(e,t){var n;void 0===t&&(t={});const{x:s,y:i,platform:o,rects:a,elements:l,strategy:r}=e,{boundary:c="clippingAncestors",rootBoundary:d="viewport",elementContext:u="floating",altBoundary:h=!1,padding:_=0}=evaluate(t,e),m=getPaddingObject(_),g=l[h?"floating"===u?"reference":"floating":u],p=rectToClientRect(await o.getClippingRect({element:null==(n=await(null==o.isElement?void 0:o.isElement(g)))||n?g:g.contextElement||await(null==o.getDocumentElement?void 0:o.getDocumentElement(l.floating)),boundary:c,rootBoundary:d,strategy:r})),E="floating"===u?{x:s,y:i,width:a.floating.width,height:a.floating.height}:a.reference,f=await(null==o.getOffsetParent?void 0:o.getOffsetParent(l.floating)),v=await(null==o.isElement?void 0:o.isElement(f))&&await(null==o.getScale?void 0:o.getScale(f))||{x:1,y:1},b=rectToClientRect(o.convertOffsetParentRelativeRectToViewportRelativeRect?await o.convertOffsetParentRelativeRectToViewportRelativeRect({elements:l,rect:E,offsetParent:f,strategy:r}):E);return{top:(p.top-b.top+m.top)/v.y,bottom:(b.bottom-p.bottom+m.bottom)/v.y,left:(p.left-b.left+m.left)/v.x,right:(b.right-p.right+m.right)/v.x}}const MAX_RESET_COUNT=50,computePosition$1=async(e,t,n)=>{const{placement:s="bottom",strategy:i="absolute",middleware:o=[],platform:a}=n,l=a.detectOverflow?a:{...a,detectOverflow:detectOverflow},r=await(null==a.isRTL?void 0:a.isRTL(t));let c=await a.getElementRects({reference:e,floating:t,strategy:i}),{x:d,y:u}=computeCoordsFromPlacement(c,s,r),h=s,_=0;const m={};for(let n=0;n({name:"arrow",options:e,async fn(t){const{x:n,y:s,placement:i,rects:o,platform:a,elements:l,middlewareData:r}=t,{element:c,padding:d=0}=evaluate(e,t)||{};if(null==c)return{};const u=getPaddingObject(d),h={x:n,y:s},_=getAlignmentAxis(i),m=getAxisLength(_),g=await a.getDimensions(c),p="y"===_,E=p?"top":"left",f=p?"bottom":"right",v=p?"clientHeight":"clientWidth",b=o.reference[m]+o.reference[_]-h[_]-o.floating[m],T=h[_]-o.reference[_],A=await(null==a.getOffsetParent?void 0:a.getOffsetParent(c));let y=A?A[v]:0;y&&await(null==a.isElement?void 0:a.isElement(A))||(y=l.floating[v]||o.floating[m]);const S=b/2-T/2,D=y/2-g[m]/2-1,C=min(u[E],D),N=min(u[f],D),w=C,x=y-g[m]-N,L=y/2-g[m]/2+S,M=clamp(w,L,x),O=!r.arrow&&null!=getAlignment(i)&&L!==M&&o.reference[m]/2-(Le<=0)){var N,w;const e=((null==(N=o.flip)?void 0:N.index)||0)+1,t=y[e];if(t&&("alignment"!==u||f===getSideAxis(t)||C.every(e=>getSideAxis(e.placement)!==f||e.overflows[0]>0)))return{data:{index:e,overflows:C},reset:{placement:t}};let n=null==(w=C.filter(e=>e.overflows[0]<=0).sort((e,t)=>e.overflows[1]-t.overflows[1])[0])?void 0:w.placement;if(!n)switch(_){case"bestFit":{var x;const e=null==(x=C.filter(e=>{if(A){const t=getSideAxis(e.placement);return t===f||"y"===t}return!0}).map(e=>[e.placement,e.overflows.filter(e=>e>0).reduce((e,t)=>e+t,0)]).sort((e,t)=>e[1]-t[1])[0])?void 0:x[0];e&&(n=e);break}case"initialPlacement":n=l}if(i!==n)return{reset:{placement:n}}}return{}}}},originSides=new Set(["left","top"]);async function convertValueToCoords(e,t){const{placement:n,platform:s,elements:i}=e,o=await(null==s.isRTL?void 0:s.isRTL(i.floating)),a=getSide(n),l=getAlignment(n),r="y"===getSideAxis(n),c=originSides.has(a)?-1:1,d=o&&r?-1:1,u=evaluate(t,e);let{mainAxis:h,crossAxis:_,alignmentAxis:m}="number"==typeof u?{mainAxis:u,crossAxis:0,alignmentAxis:null}:{mainAxis:u.mainAxis||0,crossAxis:u.crossAxis||0,alignmentAxis:u.alignmentAxis};return l&&"number"==typeof m&&(_="end"===l?-1*m:m),r?{x:_*d,y:h*c}:{x:h*c,y:_*d}}const offset$1=function(e){return void 0===e&&(e=0),{name:"offset",options:e,async fn(t){var n,s;const{x:i,y:o,placement:a,middlewareData:l}=t,r=await convertValueToCoords(t,e);return a===(null==(n=l.offset)?void 0:n.placement)&&null!=(s=l.arrow)&&s.alignmentOffset?{}:{x:i+r.x,y:o+r.y,data:{...r,placement:a}}}}},shift$1=function(e){return void 0===e&&(e={}),{name:"shift",options:e,async fn(t){const{x:n,y:s,placement:i,platform:o}=t,{mainAxis:a=!0,crossAxis:l=!1,limiter:r={fn:e=>{let{x:t,y:n}=e;return{x:t,y:n}}},...c}=evaluate(e,t),d={x:n,y:s},u=await o.detectOverflow(t,c),h=getSideAxis(getSide(i)),_=getOppositeAxis(h);let m=d[_],g=d[h];if(a){const e="y"===_?"bottom":"right";m=clamp(m+u["y"===_?"top":"left"],m,m-u[e])}if(l){const e="y"===h?"bottom":"right";g=clamp(g+u["y"===h?"top":"left"],g,g-u[e])}const p=r.fn({...t,[_]:m,[h]:g});return{...p,data:{x:p.x-n,y:p.y-s,enabled:{[_]:a,[h]:l}}}}}};function hasWindow(){return"undefined"!=typeof window}function getNodeName(e){return isNode(e)?(e.nodeName||"").toLowerCase():"#document"}function getWindow(e){var t;return(null==e||null==(t=e.ownerDocument)?void 0:t.defaultView)||window}function getDocumentElement(e){var t;return null==(t=(isNode(e)?e.ownerDocument:e.document)||window.document)?void 0:t.documentElement}function isNode(e){return!!hasWindow()&&(e instanceof Node||e instanceof getWindow(e).Node)}function isElement(e){return!!hasWindow()&&(e instanceof Element||e instanceof getWindow(e).Element)}function isHTMLElement(e){return!!hasWindow()&&(e instanceof HTMLElement||e instanceof getWindow(e).HTMLElement)}function isShadowRoot(e){return!(!hasWindow()||"undefined"==typeof ShadowRoot)&&(e instanceof ShadowRoot||e instanceof getWindow(e).ShadowRoot)}function isOverflowElement(e){const{overflow:t,overflowX:n,overflowY:s,display:i}=getComputedStyle$1(e);return/auto|scroll|overlay|hidden|clip/.test(t+s+n)&&"inline"!==i&&"contents"!==i}function isTableElement(e){return/^(table|td|th)$/.test(getNodeName(e))}function isTopLayer(e){try{if(e.matches(":popover-open"))return!0}catch(e){}try{return e.matches(":modal")}catch(e){return!1}}const willChangeRe=/transform|translate|scale|rotate|perspective|filter/,containRe=/paint|layout|strict|content/,isNotNone=e=>!!e&&"none"!==e;let isWebKitValue;function isContainingBlock(e){const t=isElement(e)?getComputedStyle$1(e):e;return isNotNone(t.transform)||isNotNone(t.translate)||isNotNone(t.scale)||isNotNone(t.rotate)||isNotNone(t.perspective)||!isWebKit()&&(isNotNone(t.backdropFilter)||isNotNone(t.filter))||willChangeRe.test(t.willChange||"")||containRe.test(t.contain||"")}function getContainingBlock(e){let t=getParentNode(e);for(;isHTMLElement(t)&&!isLastTraversableNode(t);){if(isContainingBlock(t))return t;if(isTopLayer(t))return null;t=getParentNode(t)}return null}function isWebKit(){return null==isWebKitValue&&(isWebKitValue="undefined"!=typeof CSS&&CSS.supports&&CSS.supports("-webkit-backdrop-filter","none")),isWebKitValue}function isLastTraversableNode(e){return/^(html|body|#document)$/.test(getNodeName(e))}function getComputedStyle$1(e){return getWindow(e).getComputedStyle(e)}function getNodeScroll(e){return isElement(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function getParentNode(e){if("html"===getNodeName(e))return e;const t=e.assignedSlot||e.parentNode||isShadowRoot(e)&&e.host||getDocumentElement(e);return isShadowRoot(t)?t.host:t}function getNearestOverflowAncestor(e){const t=getParentNode(e);return isLastTraversableNode(t)?e.ownerDocument?e.ownerDocument.body:e.body:isHTMLElement(t)&&isOverflowElement(t)?t:getNearestOverflowAncestor(t)}function getOverflowAncestors(e,t,n){var s;void 0===t&&(t=[]),void 0===n&&(n=!0);const i=getNearestOverflowAncestor(e),o=i===(null==(s=e.ownerDocument)?void 0:s.body),a=getWindow(i);if(o){const e=getFrameElement(a);return t.concat(a,a.visualViewport||[],isOverflowElement(i)?i:[],e&&n?getOverflowAncestors(e):[])}return t.concat(i,getOverflowAncestors(i,[],n))}function getFrameElement(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function getCssDimensions(e){const t=getComputedStyle$1(e);let n=parseFloat(t.width)||0,s=parseFloat(t.height)||0;const i=isHTMLElement(e),o=i?e.offsetWidth:n,a=i?e.offsetHeight:s,l=round(n)!==o||round(s)!==a;return l&&(n=o,s=a),{width:n,height:s,$:l}}function unwrapElement(e){return isElement(e)?e:e.contextElement}function getScale(e){const t=unwrapElement(e);if(!isHTMLElement(t))return createCoords(1);const n=t.getBoundingClientRect(),{width:s,height:i,$:o}=getCssDimensions(t);let a=(o?round(n.width):n.width)/s,l=(o?round(n.height):n.height)/i;return a&&Number.isFinite(a)||(a=1),l&&Number.isFinite(l)||(l=1),{x:a,y:l}}const noOffsets=createCoords(0);function getVisualOffsets(e){const t=getWindow(e);return isWebKit()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:noOffsets}function shouldAddVisualOffsets(e,t,n){return void 0===t&&(t=!1),!(!n||t&&n!==getWindow(e))&&t}function getBoundingClientRect(e,t,n,s){void 0===t&&(t=!1),void 0===n&&(n=!1);const i=e.getBoundingClientRect(),o=unwrapElement(e);let a=createCoords(1);t&&(s?isElement(s)&&(a=getScale(s)):a=getScale(e));const l=shouldAddVisualOffsets(o,n,s)?getVisualOffsets(o):createCoords(0);let r=(i.left+l.x)/a.x,c=(i.top+l.y)/a.y,d=i.width/a.x,u=i.height/a.y;if(o){const e=getWindow(o),t=s&&isElement(s)?getWindow(s):s;let n=e,i=getFrameElement(n);for(;i&&s&&t!==n;){const e=getScale(i),t=i.getBoundingClientRect(),s=getComputedStyle$1(i),o=t.left+(i.clientLeft+parseFloat(s.paddingLeft))*e.x,a=t.top+(i.clientTop+parseFloat(s.paddingTop))*e.y;r*=e.x,c*=e.y,d*=e.x,u*=e.y,r+=o,c+=a,n=getWindow(i),i=getFrameElement(n)}}return rectToClientRect({width:d,height:u,x:r,y:c})}function getWindowScrollBarX(e,t){const n=getNodeScroll(e).scrollLeft;return t?t.left+n:getBoundingClientRect(getDocumentElement(e)).left+n}function getHTMLOffset(e,t){const n=e.getBoundingClientRect();return{x:n.left+t.scrollLeft-getWindowScrollBarX(e,n),y:n.top+t.scrollTop}}function convertOffsetParentRelativeRectToViewportRelativeRect(e){let{elements:t,rect:n,offsetParent:s,strategy:i}=e;const o="fixed"===i,a=getDocumentElement(s),l=!!t&&isTopLayer(t.floating);if(s===a||l&&o)return n;let r={scrollLeft:0,scrollTop:0},c=createCoords(1);const d=createCoords(0),u=isHTMLElement(s);if((u||!u&&!o)&&(("body"!==getNodeName(s)||isOverflowElement(a))&&(r=getNodeScroll(s)),u)){const e=getBoundingClientRect(s);c=getScale(s),d.x=e.x+s.clientLeft,d.y=e.y+s.clientTop}const h=!a||u||o?createCoords(0):getHTMLOffset(a,r);return{width:n.width*c.x,height:n.height*c.y,x:n.x*c.x-r.scrollLeft*c.x+d.x+h.x,y:n.y*c.y-r.scrollTop*c.y+d.y+h.y}}function getClientRects(e){return Array.from(e.getClientRects())}function getDocumentRect(e){const t=getDocumentElement(e),n=getNodeScroll(e),s=e.ownerDocument.body,i=max(t.scrollWidth,t.clientWidth,s.scrollWidth,s.clientWidth),o=max(t.scrollHeight,t.clientHeight,s.scrollHeight,s.clientHeight);let a=-n.scrollLeft+getWindowScrollBarX(e);const l=-n.scrollTop;return"rtl"===getComputedStyle$1(s).direction&&(a+=max(t.clientWidth,s.clientWidth)-i),{width:i,height:o,x:a,y:l}}const SCROLLBAR_MAX=25;function getViewportRect(e,t){const n=getWindow(e),s=getDocumentElement(e),i=n.visualViewport;let o=s.clientWidth,a=s.clientHeight,l=0,r=0;if(i){o=i.width,a=i.height;const e=isWebKit();(!e||e&&"fixed"===t)&&(l=i.offsetLeft,r=i.offsetTop)}const c=getWindowScrollBarX(s);if(c<=0){const e=s.ownerDocument,t=e.body,n=getComputedStyle(t),i="CSS1Compat"===e.compatMode&&parseFloat(n.marginLeft)+parseFloat(n.marginRight)||0,a=Math.abs(s.clientWidth-t.clientWidth-i);a<=25&&(o-=a)}else c<=25&&(o+=c);return{width:o,height:a,x:l,y:r}}function getInnerBoundingClientRect(e,t){const n=getBoundingClientRect(e,!0,"fixed"===t),s=n.top+e.clientTop,i=n.left+e.clientLeft,o=isHTMLElement(e)?getScale(e):createCoords(1);return{width:e.clientWidth*o.x,height:e.clientHeight*o.y,x:i*o.x,y:s*o.y}}function getClientRectFromClippingAncestor(e,t,n){let s;if("viewport"===t)s=getViewportRect(e,n);else if("document"===t)s=getDocumentRect(getDocumentElement(e));else if(isElement(t))s=getInnerBoundingClientRect(t,n);else{const n=getVisualOffsets(e);s={x:t.x-n.x,y:t.y-n.y,width:t.width,height:t.height}}return rectToClientRect(s)}function hasFixedPositionAncestor(e,t){const n=getParentNode(e);return!(n===t||!isElement(n)||isLastTraversableNode(n))&&("fixed"===getComputedStyle$1(n).position||hasFixedPositionAncestor(n,t))}function getClippingElementAncestors(e,t){const n=t.get(e);if(n)return n;let s=getOverflowAncestors(e,[],!1).filter(e=>isElement(e)&&"body"!==getNodeName(e)),i=null;const o="fixed"===getComputedStyle$1(e).position;let a=o?getParentNode(e):e;for(;isElement(a)&&!isLastTraversableNode(a);){const t=getComputedStyle$1(a),n=isContainingBlock(a);n||"fixed"!==t.position||(i=null),(o?!n&&!i:!n&&"static"===t.position&&i&&("absolute"===i.position||"fixed"===i.position)||isOverflowElement(a)&&!n&&hasFixedPositionAncestor(e,a))?s=s.filter(e=>e!==a):i=t,a=getParentNode(a)}return t.set(e,s),s}function getClippingRect(e){let{element:t,boundary:n,rootBoundary:s,strategy:i}=e;const o=[..."clippingAncestors"===n?isTopLayer(t)?[]:getClippingElementAncestors(t,this._c):[].concat(n),s],a=getClientRectFromClippingAncestor(t,o[0],i);let l=a.top,r=a.right,c=a.bottom,d=a.left;for(let e=1;e{a(!1,1e-7)},1e3)}1!==s||rectsAreEqual(c,e.getBoundingClientRect())||a(),g=!1}try{s=new IntersectionObserver(p,{...m,root:i.ownerDocument})}catch(e){s=new IntersectionObserver(p,m)}s.observe(e)}(!0),o}function autoUpdate(e,t,n,s){void 0===s&&(s={});const{ancestorScroll:i=!0,ancestorResize:o=!0,elementResize:a="function"==typeof ResizeObserver,layoutShift:l="function"==typeof IntersectionObserver,animationFrame:r=!1}=s,c=unwrapElement(e),d=i||o?[...c?getOverflowAncestors(c):[],...t?getOverflowAncestors(t):[]]:[];d.forEach(e=>{i&&e.addEventListener("scroll",n,{passive:!0}),o&&e.addEventListener("resize",n)});const u=c&&l?observeMove(c,n):null;let h,_=-1,m=null;a&&(m=new ResizeObserver(e=>{let[s]=e;s&&s.target===c&&m&&t&&(m.unobserve(t),cancelAnimationFrame(_),_=requestAnimationFrame(()=>{var e;null==(e=m)||e.observe(t)})),n()}),c&&!r&&m.observe(c),t&&m.observe(t));let g=r?getBoundingClientRect(e):null;return r&&function t(){const s=getBoundingClientRect(e);g&&!rectsAreEqual(g,s)&&n(),g=s,h=requestAnimationFrame(t)}(),n(),()=>{var e;d.forEach(e=>{i&&e.removeEventListener("scroll",n),o&&e.removeEventListener("resize",n)}),null==u||u(),null==(e=m)||e.disconnect(),m=null,r&&cancelAnimationFrame(h)}}const offset=offset$1,shift=shift$1,flip=flip$1,arrow=arrow$1,computePosition=(e,t,n)=>{const s=new Map,i={platform:platform,...n},o={...i.platform,_c:s};return computePosition$1(e,t,{...i,platform:o})},BREAKPOINTS={sm:576,md:768,lg:1024,xl:1280,"2xl":1536},parseResponsivePlacement=(e,t="bottom")=>{if(!e||!e.includes(":"))return null;const n=e.split(/\s+/),s={xs:t};for(const e of n)if(e.includes(":")){const[t,n]=e.split(":");void 0!==BREAKPOINTS[t]&&(s[t]=n)}else s.xs=e;return s},getResponsivePlacement=(e,t="bottom")=>{if(!e)return t;const n=window.innerWidth;let s=e.xs||t;const i=["sm","md","lg","xl","2xl"];for(const t of i)n>=BREAKPOINTS[t]&&e[t]&&(s=e[t]);return s},createBreakpointListeners=e=>{const t=[];for(const n of Object.keys(BREAKPOINTS)){const s=BREAKPOINTS[n],i=window.matchMedia(`(min-width: ${s}px)`);i.addEventListener("change",e),t.push({mql:i,handler:e})}return t},disposeBreakpointListeners=e=>{for(const{mql:t,handler:n}of e)t.removeEventListener("change",n)},NAME$h="menu",DATA_KEY$d="bs.menu",EVENT_KEY$e=".bs.menu",DATA_API_KEY$9=".data-api",ESCAPE_KEY$2="Escape",TAB_KEY$1="Tab",ARROW_UP_KEY$2="ArrowUp",ARROW_DOWN_KEY$2="ArrowDown",ARROW_LEFT_KEY$1="ArrowLeft",ARROW_RIGHT_KEY$1="ArrowRight",HOME_KEY$2="Home",END_KEY$2="End",ENTER_KEY$1="Enter",SPACE_KEY$1=" ",RIGHT_MOUSE_BUTTON=2,SUBMENU_CLOSE_DELAY=100,EVENT_HIDE$5="hide.bs.menu",EVENT_HIDDEN$7="hidden.bs.menu",EVENT_SHOW$6="show.bs.menu",EVENT_SHOWN$5="shown.bs.menu",EVENT_CLICK_DATA_API$5="click.bs.menu.data-api",EVENT_KEYDOWN_DATA_API="keydown.bs.menu.data-api",EVENT_KEYUP_DATA_API="keyup.bs.menu.data-api",CLASS_NAME_SHOW$4="show",SELECTOR_DATA_TOGGLE$8='[data-bs-toggle="menu"]:not(.disabled):not(:disabled)',SELECTOR_MENU$2=".menu",SELECTOR_SUBMENU=".submenu",SELECTOR_SUBMENU_TOGGLE=".submenu > .menu-item",SELECTOR_NAVBAR_NAV=".navbar-nav",SELECTOR_VISIBLE_ITEMS$1=".menu-item:not(.disabled):not(:disabled)",DEFAULT_PLACEMENT="bottom-start",SUBMENU_PLACEMENT="end-start",resolveLogicalPlacement=e=>isRTL$1()?e.replace(/^start(?=-|$)/,"right").replace(/^end(?=-|$)/,"left"):e.replace(/^start(?=-|$)/,"left").replace(/^end(?=-|$)/,"right"),triangleSign=(e,t,n)=>(e.x-n.x)*(t.y-n.y)-(t.x-n.x)*(e.y-n.y),Default$g={autoClose:!0,boundary:"clippingParents",container:!1,display:"dynamic",offset:[0,2],floatingConfig:null,menu:null,placement:"bottom-start",reference:"toggle",strategy:"absolute",submenuTrigger:"both",submenuDelay:100},DefaultType$g={autoClose:"(boolean|string)",boundary:"(string|element)",container:"(string|element|boolean)",display:"string",offset:"(array|string|function)",floatingConfig:"(null|object|function)",menu:"(null|element)",placement:"string",reference:"(string|element|object)",strategy:"string",submenuTrigger:"string",submenuDelay:"number"};class Menu extends BaseComponent{static _openInstances=new Set;constructor(e,t){super(e,t),this._floatingCleanup=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this._parent=this._element.parentNode,this._openSubmenus=new Map,this._submenuCloseTimeouts=new Map,this._hoverIntentData=null,this._menu=this._config.menu||this._findMenu(),!this._config.menu&&this._menu&&(this._parent=this._findWrapper(this._menu)),this._isSubmenu=this._parent.classList?.contains("submenu"),this._menuOriginalParent=this._menu?.parentNode,this._parseResponsivePlacements(),this._setupSubmenuListeners()}static get Default(){return Default$g}static get DefaultType(){return DefaultType$g}static get NAME(){return"menu"}toggle(){return this._isShown()?this.hide():this.show()}show(){if(isDisabled(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!EventHandler.trigger(this._element,EVENT_SHOW$6,e).defaultPrevented){if(this._moveMenuToContainer(),this._createFloating(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._element.focus({focusVisible:!1}),this._element.setAttribute("aria-expanded","true"),this._menu.classList.add("show"),this._element.classList.add("show"),this._parent&&this._parent.classList.add("show"),Menu._openInstances.add(this),EventHandler.trigger(this._element,EVENT_SHOWN$5,e)}}hide(){if(isDisabled(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._disposeFloating(),this._restoreMenuToOriginalParent(),this._disposeMediaQueryListeners(),this._closeAllSubmenus(),this._clearAllSubmenuTimeouts(),Menu._openInstances.delete(this),super.dispose()}update(){this._floatingCleanup&&this._updateFloatingPosition()}_findMenu(){const e=SelectorEngine.closest(this._element,":has(.menu)");return SelectorEngine.next(this._element,".menu")[0]||SelectorEngine.prev(this._element,".menu")[0]||SelectorEngine.findOne(".menu",e||this._parent)}_findWrapper(e){let t=this._element.parentNode;for(;t instanceof Element&&!t.contains(e);)t=t.parentNode;return t instanceof Element?t:this._element.parentNode}_completeHide(e){if(!EventHandler.trigger(this._element,EVENT_HIDE$5,e).defaultPrevented){if(this._closeAllSubmenus(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._disposeFloating(),this._restoreMenuToOriginalParent(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._parent&&this._parent.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),Manipulator.removeDataAttribute(this._menu,"placement"),Manipulator.removeDataAttribute(this._menu,"display"),Menu._openInstances.delete(this),EventHandler.trigger(this._element,EVENT_HIDDEN$7,e)}}_getConfig(e){if("object"==typeof(e=super._getConfig(e)).reference&&!isElement$1(e.reference)&&"function"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${"menu".toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createFloating(){if("static"===this._config.display)return void Manipulator.setDataAttribute(this._menu,"display","static");let e=this._element;"parent"===this._config.reference?e=this._parent:isElement$1(this._config.reference)?e=getElement(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference),this._updateFloatingPosition(e),this._floatingCleanup=autoUpdate(e,this._menu,()=>this._updateFloatingPosition(e))}async _updateFloatingPosition(e=null){if(!this._menu)return;e||(e="parent"===this._config.reference?this._parent:isElement$1(this._config.reference)?getElement(this._config.reference):"object"==typeof this._config.reference?this._config.reference:this._element);const t=this._getPlacement(),n=this._getFloatingMiddleware(),s=this._getFloatingConfig(t,n);await this._applyFloatingPosition(e,this._menu,s.placement,s.middleware,s.strategy)}_isShown(){return this._menu.classList.contains("show")}_getPlacement(){const e=this._responsivePlacements?getResponsivePlacement(this._responsivePlacements,"bottom-start"):this._config.placement;return resolveLogicalPlacement(e)}_parseResponsivePlacements(){this._responsivePlacements=parseResponsivePlacement(this._config.placement,"bottom-start"),this._responsivePlacements&&this._setupMediaQueryListeners()}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_getFloatingMiddleware(){const e=this._getOffset();return[offset("function"==typeof e?e:{mainAxis:e[1]||0,crossAxis:e[0]||0}),flip({fallbackPlacements:this._getFallbackPlacements()}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})]}_getFallbackPlacements(){return{bottom:["top","bottom-start","bottom-end","top-start","top-end"],"bottom-start":["top-start","bottom-end","top-end"],"bottom-end":["top-end","bottom-start","top-start"],top:["bottom","top-start","top-end","bottom-start","bottom-end"],"top-start":["bottom-start","top-end","bottom-end"],"top-end":["bottom-end","top-start","bottom-start"],right:["left","right-start","right-end","left-start","left-end"],"right-start":["left-start","right-end","left-end","top-start","bottom-start"],"right-end":["left-end","right-start","left-start","top-end","bottom-end"],left:["right","left-start","left-end","right-start","right-end"],"left-start":["right-start","left-end","right-end","top-start","bottom-start"],"left-end":["right-end","left-start","right-start","top-end","bottom-end"]}[this._getPlacement()]||["top","bottom","right","left"]}_getFloatingConfig(e,t){const n={placement:e,middleware:t,strategy:this._config.strategy};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null)}_getContainer(){const{container:e}=this._config;return!1===e?null:!0===e?document.body:getElement(e)}_moveMenuToContainer(){const e=this._getContainer();e&&this._menu&&this._menu.parentNode!==e&&e.append(this._menu)}_restoreMenuToOriginalParent(){this._menuOriginalParent&&this._menu&&this._menu.parentNode!==this._menuOriginalParent&&this._menuOriginalParent.append(this._menu)}async _applyFloatingPosition(e,t,n,s,i="absolute"){if(!t.isConnected)return null;const{x:o,y:a,placement:l}=await computePosition(e,t,{placement:n,middleware:s,strategy:i});return t.isConnected?(Object.assign(t.style,{position:i,left:`${o}px`,top:`${a}px`,margin:"0"}),Manipulator.setDataAttribute(t,"placement",l),l):null}_setupSubmenuListeners(){"hover"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||(EventHandler.on(this._menu,"mouseenter",".submenu > .menu-item",e=>{this._onSubmenuTriggerEnter(e)}),EventHandler.on(this._menu,"mouseleave",".submenu",e=>{this._onSubmenuLeave(e)}),EventHandler.on(this._menu,"mousemove",e=>{this._trackMousePosition(e)})),"click"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||EventHandler.on(this._menu,"click",".submenu > .menu-item",e=>{this._onSubmenuTriggerClick(e)})}_onSubmenuTriggerEnter(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._cancelSubmenuCloseTimeout(s),this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n))}_onSubmenuLeave(e){const t=e.target.closest(".submenu"),n=SelectorEngine.findOne(".menu",t);n&&this._openSubmenus.has(n)&&(this._isMovingTowardSubmenu(e,n)||this._scheduleSubmenuClose(n,t))}_onSubmenuTriggerClick(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;e.preventDefault(),e.stopPropagation();const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._openSubmenus.has(s)?this._closeSubmenu(s,n):(this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n)))}_openSubmenu(e,t,n){if(this._openSubmenus.has(t))return;e.setAttribute("aria-expanded","true"),e.setAttribute("aria-haspopup","true"),t.style.opacity="0",t.classList.add("show"),n.classList.add("show");const s=this._createSubmenuFloating(e,t,n);this._openSubmenus.set(t,s),EventHandler.on(t,"mouseenter",()=>{this._cancelSubmenuCloseTimeout(t)})}_closeSubmenu(e,t){if(!this._openSubmenus.has(e))return;const n=SelectorEngine.find(".submenu .menu.show",e);for(const e of n){const t=e.closest(".submenu");this._closeSubmenu(e,t)}const s=SelectorEngine.findOne(".submenu > .menu-item",t),i=this._openSubmenus.get(e);i&&i(),this._openSubmenus.delete(e),EventHandler.off(e,"mouseenter"),s&&s.setAttribute("aria-expanded","false"),e.classList.remove("show"),t.classList.remove("show"),e.style.opacity=""}_closeAllSubmenus(){for(const[e]of this._openSubmenus){const t=e.closest(".submenu");this._closeSubmenu(e,t)}}_closeSiblingSubmenus(e){const t=e.parentNode,n=SelectorEngine.find(".submenu > .menu.show",t);for(const t of n){const n=t.closest(".submenu");n!==e&&this._closeSubmenu(t,n)}}_createSubmenuFloating(e,t,n){const s=n,i=resolveLogicalPlacement("end-start"),o=[offset({mainAxis:0,crossAxis:-4}),flip({fallbackPlacements:[resolveLogicalPlacement("start-start"),resolveLogicalPlacement("end-end"),resolveLogicalPlacement("start-end")]}),shift({padding:8})],a=()=>this._applyFloatingPosition(s,t,i,o).then(e=>(t.style.opacity="",e));return a(),autoUpdate(s,t,a)}_scheduleSubmenuClose(e,t){this._cancelSubmenuCloseTimeout(e);const n=setTimeout(()=>{this._closeSubmenu(e,t),this._submenuCloseTimeouts.delete(e)},this._config.submenuDelay);this._submenuCloseTimeouts.set(e,n)}_cancelSubmenuCloseTimeout(e){const t=this._submenuCloseTimeouts.get(e);t&&(clearTimeout(t),this._submenuCloseTimeouts.delete(e))}_clearAllSubmenuTimeouts(){for(const e of this._submenuCloseTimeouts.values())clearTimeout(e);this._submenuCloseTimeouts.clear()}_trackMousePosition(e){this._hoverIntentData={x:e.clientX,y:e.clientY,timestamp:Date.now()}}_isMovingTowardSubmenu(e,t){if(!this._hoverIntentData)return!1;const n=t.getBoundingClientRect(),s={x:e.clientX,y:e.clientY},i={x:this._hoverIntentData.x,y:this._hoverIntentData.y},o=isRTL$1()?n.right:n.left,a={x:o,y:n.top},l={x:o,y:n.bottom};return this._pointInTriangle(s,i,a,l)}_pointInTriangle(e,t,n,s){const i=triangleSign(e,t,n),o=triangleSign(e,n,s),a=triangleSign(e,s,t);return!((i<0||o<0||a<0)&&(i>0||o>0||a>0))}_selectMenuItem({key:e,target:t}){const n=t.closest(".menu")||this._menu,s=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,n).filter(e=>isVisible(e));s.length&&getNextActiveElement(s,t,e===ARROW_DOWN_KEY$2,!s.includes(t)).focus()}_handleSubmenuKeydown(e){const{key:t,target:n}=e,s=isRTL$1(),i=s?ARROW_LEFT_KEY$1:ARROW_RIGHT_KEY$1,o=s?ARROW_RIGHT_KEY$1:ARROW_LEFT_KEY$1,a=n.closest(".submenu"),l=a&&n.matches(".submenu > .menu-item");if((t===ENTER_KEY$1||t===SPACE_KEY$1)&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",a);return t&&(this._closeSiblingSubmenus(a),this._openSubmenu(n,t,a),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===i&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",a);return t&&(this._closeSiblingSubmenus(a),this._openSubmenu(n,t,a),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===o){const t=n.closest(".menu"),s=t?.closest(".submenu");if(s){e.preventDefault(),e.stopPropagation();const n=SelectorEngine.findOne(".submenu > .menu-item",s);return this._closeSubmenu(t,s),n&&n.focus(),!0}}if(t===HOME_KEY$2||t===END_KEY$2){e.preventDefault(),e.stopPropagation();const s=n.closest(".menu"),i=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,s).filter(e=>isVisible(e));return i.length&&(t===HOME_KEY$2?i[0]:i.at(-1)).focus(),!0}return!1}static clearMenus(e){if(2!==e.button&&("keyup"!==e.type||"Tab"===e.key))for(const t of Menu._openInstances){if(!1===t._config.autoClose)continue;const n=e.composedPath(),s=n.includes(t._menu);if(n.includes(t._element)||"inside"===t._config.autoClose&&!s||"outside"===t._config.autoClose&&s)continue;const i=e.target.closest?.("form"),o=Boolean(i)&&t._menu.contains(i);if(t._menu.contains(e.target)&&("keyup"===e.type&&"Tab"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)||o))continue;const a={relatedTarget:t._element};"click"===e.type&&(a.clickEvent=e),t._completeHide(a)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName)||e.target.isContentEditable,n="Escape"===e.key,s=[ARROW_UP_KEY$2,ARROW_DOWN_KEY$2].includes(e.key),i=[ARROW_LEFT_KEY$1,ARROW_RIGHT_KEY$1].includes(e.key),o=[HOME_KEY$2,END_KEY$2].includes(e.key),a=[ENTER_KEY$1,SPACE_KEY$1].includes(e.key),l=e.target.matches(".submenu > .menu-item");if(!(s||n||i||o||a&&l))return;if(t&&!n)return;const r=this.matches(SELECTOR_DATA_TOGGLE$8)?this:SelectorEngine.prev(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.next(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8,e.delegateTarget.parentNode);if(!r)return;const c=Menu.getOrCreateInstance(r);if(!(i||o||a&&l)||!c._handleSubmenuKeydown(e)){if(s)return e.preventDefault(),e.stopPropagation(),c.show(),void c._selectMenuItem(e);if(n&&c._isShown()){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".menu"),n=t?.closest(".submenu");if(n&&c._openSubmenus.size>0){const e=SelectorEngine.findOne(".submenu > .menu-item",n);return c._closeSubmenu(t,n),void(e&&e.focus())}c.hide(),r.focus()}}}}EventHandler.on(document,EVENT_KEYDOWN_DATA_API,SELECTOR_DATA_TOGGLE$8,Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_KEYDOWN_DATA_API,".menu",Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_CLICK_DATA_API$5,Menu.clearMenus),EventHandler.on(document,EVENT_KEYUP_DATA_API,Menu.clearMenus),EventHandler.on(document,EVENT_CLICK_DATA_API$5,SELECTOR_DATA_TOGGLE$8,function(e){e.preventDefault(),Menu.getOrCreateInstance(this).toggle()});const NAME$g="combobox",DATA_KEY$c="bs.combobox",EVENT_KEY$d=`.${DATA_KEY$c}`,DATA_API_KEY$8=".data-api",ESCAPE_KEY$1="Escape",TAB_KEY="Tab",ARROW_UP_KEY$1="ArrowUp",ARROW_DOWN_KEY$1="ArrowDown",HOME_KEY$1="Home",END_KEY$1="End",ENTER_KEY="Enter",SPACE_KEY=" ",EVENT_CHANGE$3=`change${EVENT_KEY$d}`,EVENT_SHOW$5=`show${EVENT_KEY$d}`,EVENT_SHOWN$4=`shown${EVENT_KEY$d}`,EVENT_HIDE$4=`hide${EVENT_KEY$d}`,EVENT_HIDDEN$6=`hidden${EVENT_KEY$d}`,EVENT_CLICK_DATA_API$4=`click${EVENT_KEY$d}.data-api`,CLASS_NAME_SHOW$3="show",CLASS_NAME_SELECTED="selected",CLASS_NAME_PLACEHOLDER="combobox-placeholder",SELECTOR_DATA_TOGGLE$7='[data-bs-toggle="combobox"]',SELECTOR_MENU$1=".menu",SELECTOR_MENU_ITEM=".menu-item[data-bs-value]",SELECTOR_VISIBLE_ITEMS=".menu-item[data-bs-value]:not(.disabled):not(:disabled)",SELECTOR_VALUE=".combobox-value",SELECTOR_SEARCH_INPUT=".combobox-search-input",SELECTOR_NO_RESULTS=".combobox-no-results",Default$f={boundary:"clippingParents",multiple:!1,name:null,offset:[0,2],placeholder:"",placement:"bottom-start",search:!1,searchNormalize:!1},DefaultType$f={boundary:"(string|element)",multiple:"boolean",name:"(string|null)",offset:"(array|string|function)",placeholder:"string",placement:"string",search:"boolean",searchNormalize:"boolean"};class Combobox extends BaseComponent{constructor(e,t){super(e,t),this._toggle=this._element,this._menu=SelectorEngine.next(this._toggle,".menu")[0],this._valueDisplay=SelectorEngine.findOne(SELECTOR_VALUE,this._toggle),this._searchInput=SelectorEngine.findOne(SELECTOR_SEARCH_INPUT,this._menu),this._noResults=SelectorEngine.findOne(SELECTOR_NO_RESULTS,this._menu),this._hiddenInput=null,this._menuInstance=null,this._createHiddenInput(),this._createMenuInstance(),this._syncInitialSelection(),this._addEventListeners()}static get Default(){return Default$f}static get DefaultType(){return DefaultType$f}static get NAME(){return NAME$g}toggle(){return this._isShown()?this.hide():this.show()}show(){isDisabled(this._toggle)||this._isShown()||EventHandler.trigger(this._toggle,EVENT_SHOW$5).defaultPrevented||(this._menuInstance.show(),this._searchInput&&(this._searchInput.value="",this._filterItems(""),requestAnimationFrame(()=>this._searchInput.focus())),EventHandler.trigger(this._toggle,EVENT_SHOWN$4))}hide(){this._isShown()&&(EventHandler.trigger(this._toggle,EVENT_HIDE$4).defaultPrevented||(this._menuInstance.hide(),EventHandler.trigger(this._toggle,EVENT_HIDDEN$6)))}dispose(){this._menuInstance&&(this._menuInstance.dispose(),this._menuInstance=null),this._hiddenInput&&(this._hiddenInput.remove(),this._hiddenInput=null),EventHandler.off(this._menu,EVENT_KEY$d),EventHandler.off(this._toggle,EVENT_KEY$d),super.dispose()}_isShown(){return this._menu.classList.contains("show")}_createHiddenInput(){const{name:e}=this._config;e&&(this._hiddenInput=document.createElement("input"),this._hiddenInput.type="hidden",this._hiddenInput.name=e,this._hiddenInput.value="",this._toggle.parentNode.insertBefore(this._hiddenInput,this._toggle))}_createMenuInstance(){this._menuInstance=new Menu(this._toggle,{menu:this._menu,autoClose:!this._config.multiple||"outside",boundary:this._config.boundary,offset:this._config.offset,placement:this._config.placement})}_syncInitialSelection(){this._getSelectedItems().length>0?(this._updateToggleText(),this._updateHiddenInput()):this._showPlaceholder()}_addEventListeners(){EventHandler.on(this._menu,"click",SELECTOR_MENU_ITEM,e=>{const t=e.target.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&(e.preventDefault(),e.stopPropagation(),this._selectItem(t))}),EventHandler.on(this._toggle,"keydown",e=>{this._handleToggleKeydown(e)}),EventHandler.on(this._menu,"keydown",e=>{this._handleMenuKeydown(e)}),this._searchInput&&(EventHandler.on(this._searchInput,"input",()=>{this._filterItems(this._searchInput.value)}),EventHandler.on(this._searchInput,"keydown",e=>{if("ArrowDown"===e.key){e.preventDefault();const t=this._getVisibleItems();t.length>0&&t[0].focus()}"Escape"===e.key&&(this.hide(),this._toggle.focus())}))}_selectItem(e){if(this._config.multiple)e.classList.toggle("selected"),e.setAttribute("aria-selected",e.classList.contains("selected"));else{const t=SelectorEngine.find(".selected",this._menu);for(const e of t)e.classList.remove("selected"),e.setAttribute("aria-selected","false");e.classList.add("selected"),e.setAttribute("aria-selected","true")}this._updateToggleText(),this._updateHiddenInput();const t=this._config.multiple?this._getSelectedItems().map(e=>e.dataset.bsValue):e.dataset.bsValue;EventHandler.trigger(this._toggle,EVENT_CHANGE$3,{value:t,item:e}),this._config.multiple||(this.hide(),this._toggle.focus())}_updateToggleText(){const e=this._getSelectedItems();if(0!==e.length)if(this._valueDisplay.classList.remove("combobox-placeholder"),this._config.multiple&&e.length>1)this._valueDisplay.textContent=`${e.length} selected`;else{const t=e[0],n=SelectorEngine.findOne(".menu-item-content > span:first-child",t);this._valueDisplay.textContent=n?n.textContent:t.textContent.trim()}else this._showPlaceholder()}_showPlaceholder(){const{placeholder:e}=this._config;e&&(this._valueDisplay.textContent=e,this._valueDisplay.classList.add("combobox-placeholder"))}_updateHiddenInput(){if(!this._hiddenInput)return;const e=this._getSelectedItems().map(e=>e.dataset.bsValue);this._hiddenInput.value=this._config.multiple?e.join(","):e[0]||""}_getSelectedItems(){return SelectorEngine.find(".selected",this._menu)}_getVisibleItems(){return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS,this._menu).filter(e=>isVisible(e))}_filterItems(e){const t=this._normalizeText(e.toLowerCase().trim()),n=SelectorEngine.find(SELECTOR_MENU_ITEM,this._menu);let s=0;for(const e of n){const n=this._normalizeText(e.textContent.toLowerCase().trim()),i=!t||n.includes(t);e.style.display=i?"":"none",i&&s++}this._noResults&&this._noResults.classList.toggle("d-none",s>0)}_normalizeText(e){return this._config.searchNormalize?e.normalize("NFD").replace(/[\u0300-\u036F]/g,""):e}_handleToggleKeydown(e){const{key:t}=e;if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault(),this._isShown()||this.show();const n=this._getVisibleItems();return void(n.length>0&&("ArrowDown"===t?n[0]:n.at(-1)).focus())}"Enter"!==t&&" "!==t||this._isShown()||(e.preventDefault(),this.show())}_handleMenuKeydown(e){const{key:t,target:n}=e;if("Escape"===t)return e.preventDefault(),e.stopPropagation(),this.hide(),void this._toggle.focus();if("Tab"===t)return void this.hide();const s=n.matches("input");if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault();const s=this._getVisibleItems();return void(s.length>0&&getNextActiveElement(s,n,"ArrowDown"===t,!s.includes(n)).focus())}if("Home"===t||"End"===t){e.preventDefault();const n=this._getVisibleItems();return void(n.length>0&&("Home"===t?n[0]:n.at(-1)).focus())}if(("Enter"===t||" "===t)&&!s){e.preventDefault();const t=n.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&this._selectItem(t)}}static jQueryInterface(e){return this.each(function(){const t=Combobox.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}})}}EventHandler.on(document,EVENT_CLICK_DATA_API$4,SELECTOR_DATA_TOGGLE$7,function(e){e.preventDefault(),Combobox.getOrCreateInstance(this).toggle()}),EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7))Combobox.getOrCreateInstance(e)}); +/*! name: vanilla-calendar-pro v3.1.0 | url: https://github.com/uvarov-frontend/vanilla-calendar-pro */ +var __defProp=Object.defineProperty,__defProps=Object.defineProperties,__getOwnPropDescs=Object.getOwnPropertyDescriptors,__getOwnPropSymbols=Object.getOwnPropertySymbols,__hasOwnProp=Object.prototype.hasOwnProperty,__propIsEnum=Object.prototype.propertyIsEnumerable,__defNormalProp=(e,t,n)=>t in e?__defProp(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,__spreadValues=(e,t)=>{for(var n in t||(t={}))__hasOwnProp.call(t,n)&&__defNormalProp(e,n,t[n]);if(__getOwnPropSymbols)for(var n of __getOwnPropSymbols(t))__propIsEnum.call(t,n)&&__defNormalProp(e,n,t[n]);return e},__spreadProps=(e,t)=>__defProps(e,__getOwnPropDescs(t)),__publicField=(e,t,n)=>(__defNormalProp(e,"symbol"!=typeof t?t+"":t,n),n);const errorMessages={notFoundSelector:e=>`${e} is not found, check the first argument passed to new Calendar.`,notInit:'The calendar has not been initialized, please initialize it using the "init()" method first.',notLocale:"You specified an incorrect language label or did not specify the required number of values for «locale.weekdays» or «locale.months».",incorrectTime:"The value of the time property can be: false, 12 or 24.",incorrectMonthsCount:"For the «multiple» calendar type, the «displayMonthsCount» parameter can have a value from 2 to 12, and for all others it cannot be greater than 1."},setContext=(e,t,n)=>{e.context[t]=n},destroy=e=>{var t,n,s,i,o;if(!e.context.isInit)throw new Error(errorMessages.notInit);e.inputMode?(null==(t=e.context.mainElement.parentElement)||t.removeChild(e.context.mainElement),null==(s=null==(n=e.context.inputElement)?void 0:n.replaceWith)||s.call(n,e.context.originalElement),setContext(e,"inputElement",void 0)):null==(o=(i=e.context.mainElement).replaceWith)||o.call(i,e.context.originalElement),setContext(e,"mainElement",e.context.originalElement),e.onDestroy&&e.onDestroy(e)},skipOpenOnFocus=new WeakSet,shouldSkipOpenOnFocus=e=>skipOpenOnFocus.has(e),setSkipOpenOnFocus=e=>{skipOpenOnFocus.add(e)},clearSkipOpenOnFocus=e=>{skipOpenOnFocus.delete(e)},PREV_TABINDEX_ATTR="data-vc-prev-tabindex",isFocusable=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),storePrevTabIndex=e=>{if(e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute("tabindex");e.setAttribute(PREV_TABINDEX_ATTR,null!=t?t:"")},restorePrevTabIndex=e=>{if(!e.hasAttribute(PREV_TABINDEX_ATTR))return;const t=e.getAttribute(PREV_TABINDEX_ATTR);""===t||null===t?e.removeAttribute("tabindex"):e.setAttribute("tabindex",t),e.removeAttribute(PREV_TABINDEX_ATTR)},disableTabbing=e=>{isFocusable(e)&&(storePrevTabIndex(e),e.tabIndex=-1);const t=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>isFocusable(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP});for(;t.nextNode();){const e=t.currentNode;storePrevTabIndex(e),e.tabIndex=-1}},restoreTabbing=e=>{restorePrevTabIndex(e),e.querySelectorAll(`[${PREV_TABINDEX_ATTR}]`).forEach(restorePrevTabIndex)},hide=e=>{e.context.isShowInInputMode&&e.context.currentType&&(e.context.mainElement.dataset.vcCalendarHidden="",setContext(e,"isShowInInputMode",!1),e.inputMode&&disableTabbing(e.context.mainElement),e.context.cleanupHandlers[0]&&(e.context.cleanupHandlers.forEach(e=>e()),setContext(e,"cleanupHandlers",[])),e.inputMode&&e.context.inputElement&&e.context.mainElement.contains(document.activeElement)&&(("function"==typeof e.openOnFocus||!0===e.openOnFocus)&&setSkipOpenOnFocus(e),e.context.inputElement.focus()),e.onHide&&e.onHide(e))};function getOffset(e){if(!e||!e.getBoundingClientRect)return{top:0,bottom:0,left:0,right:0};const t=e.getBoundingClientRect(),n=document.documentElement;return{bottom:t.bottom,right:t.right,top:t.top+window.scrollY-n.clientTop,left:t.left+window.scrollX-n.clientLeft}}function getViewportDimensions(){return{vw:Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),vh:Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)}}function getWindowScrollPosition(){return{left:window.scrollX||document.documentElement.scrollLeft||0,top:window.scrollY||document.documentElement.scrollTop||0}}function calculateAvailableSpace(e){const{top:t,left:n}=getWindowScrollPosition(),{top:s,left:i}=getOffset(e),{vh:o,vw:a}=getViewportDimensions(),l=s-t,r=i-n;return{top:l,bottom:o-(l+e.clientHeight),left:r,right:a-(r+e.clientWidth)}}function getAvailablePosition(e,t,n=5){const s={top:!0,bottom:!0,left:!0,right:!0},i=[];if(!t||!e)return{canShow:s,parentPositions:i};const{bottom:o,top:a}=calculateAvailableSpace(e),{top:l,left:r}=getOffset(e),{height:c,width:d}=t.getBoundingClientRect(),{vh:u,vw:h}=getViewportDimensions(),_=h/2,m=u/2;return[{condition:lm,position:"bottom"},{condition:r<_,position:"left"},{condition:r>_,position:"right"}].forEach(({condition:e,position:t})=>{e&&i.push(t)}),Object.assign(s,{top:c<=a-n,bottom:c<=o-n,left:d<=r,right:d<=h-r}),{canShow:s,parentPositions:i}}const handleDay=(e,t,n,s)=>{var i;const o=s.querySelector(`[data-vc-date="${t}"]`),a=null==o?void 0:o.querySelector("[data-vc-date-btn]");if(!o||!a)return;if((null==n?void 0:n.modifier)&&a.classList.add(...n.modifier.trim().split(" ")),!(null==n?void 0:n.html))return;const l=document.createElement("div");l.className=e.styles.datePopup,l.dataset.vcDatePopup="",l.innerHTML=e.sanitizerHTML(n.html),a.ariaExpanded="true",a.ariaLabel=`${a.ariaLabel}, ${null==(i=null==l?void 0:l.textContent)?void 0:i.replace(/^\s+|\s+(?=\s)|\s+$/g,"").replace(/ /g," ")}`,o.appendChild(l),requestAnimationFrame(()=>{if(!l)return;const{canShow:e}=getAvailablePosition(o,l),t=e.bottom?o.offsetHeight:-l.offsetHeight,n=e.left&&!e.right?o.offsetWidth-l.offsetWidth/2:!e.left&&e.right?l.offsetWidth/2:0;Object.assign(l.style,{left:`${n}px`,top:`${t}px`})})},createDatePopup=(e,t)=>{var n;e.popups&&(null==(n=Object.entries(e.popups))||n.forEach(([n,s])=>handleDay(e,n,s,t)))},getDate=e=>new Date(`${e}T00:00:00`),getDateString=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}-${String(e.getDate()).padStart(2,"0")}`,parseDates=e=>e.reduce((e,t)=>{if(t instanceof Date||"number"==typeof t){const n=t instanceof Date?t:new Date(t);e.push(n.toISOString().substring(0,10))}else t.match(/^(\d{4}-\d{2}-\d{2})$/g)?e.push(t):t.replace(/(\d{4}-\d{2}-\d{2}).*?(\d{4}-\d{2}-\d{2})/g,(t,n,s)=>{const i=getDate(n),o=getDate(s),a=new Date(i.getTime());for(;a<=o;a.setDate(a.getDate()+1))e.push(getDateString(a));return t});return e},[]),updateAttribute=(e,t,n,s="")=>{t?e.setAttribute(n,s):e.getAttribute(n)===s&&e.removeAttribute(n)},setDateModifier=(e,t,n,s,i,o,a)=>{var l,r,c,d;const u=getDate(e.context.displayDateMin)>getDate(o)||getDate(e.context.displayDateMax)1&&"multiple-ranged"===e.selectionDatesMode&&(e.context.selectedDates[0]===o&&e.context.selectedDates[e.context.selectedDates.length-1]===o?n.setAttribute("data-vc-date-selected","first-and-last"):e.context.selectedDates[0]===o?n.setAttribute("data-vc-date-selected","first"):e.context.selectedDates[e.context.selectedDates.length-1]===o&&n.setAttribute("data-vc-date-selected","last"),e.context.selectedDates[0]!==o&&e.context.selectedDates[e.context.selectedDates.length-1]!==o&&n.setAttribute("data-vc-date-selected","middle"))):n.hasAttribute("data-vc-date-selected")&&(n.removeAttribute("data-vc-date-selected"),s&&s.removeAttribute("aria-selected")),!e.context.disableDates.includes(o)&&e.enableEdgeDatesOnly&&e.context.selectedDates.length>1&&"multiple-ranged"===e.selectionDatesMode){const t=getDate(e.context.selectedDates[0]),s=getDate(e.context.selectedDates[e.context.selectedDates.length-1]),i=getDate(o);updateAttribute(n,i>t&&inew Date(`${e}T00:00:00.000Z`).toLocaleString(t,n),getWeekNumber=(e,t)=>{const n=getDate(e),s=(n.getDay()-t+7)%7;n.setDate(n.getDate()+4-s);const i=new Date(n.getFullYear(),0,1),o=Math.ceil(((+n-+i)/864e5+1)/7);return{year:n.getFullYear(),week:o}},addWeekNumberForDate=(e,t,n)=>{const s=getWeekNumber(n,e.firstWeekday);s&&(t.dataset.vcDateWeekNumber=String(s.week))},setDaysAsDisabled=(e,t,n)=>{var s,i,o,a,l;const r=null==(s=e.disableWeekdays)?void 0:s.includes(n),c=e.disableAllDates&&!!(null==(i=e.context.enableDates)?void 0:i[0]);!r&&!c||(null==(o=e.context.enableDates)?void 0:o.includes(t))||(null==(a=e.context.disableDates)?void 0:a.includes(t))||(e.context.disableDates.push(t),null==(l=e.context.disableDates)||l.sort((e,t)=>+new Date(e)-+new Date(t)))},createDate=(e,t,n,s,i,o)=>{const a=getDate(i).getDay(),l="string"==typeof e.locale&&e.locale.length?e.locale:"en",r=document.createElement("div");let c;r.className=e.styles.date,r.dataset.vcDate=i,r.dataset.vcDateMonth=o,r.dataset.vcDateWeekDay=String(a),r.role="gridcell",("current"===o||e.displayDatesOutside)&&(c=document.createElement("button"),c.className=e.styles.dateBtn,c.type="button",c.ariaLabel=getLocaleString(i,l,{dateStyle:"long",timeZone:"UTC"}),c.dataset.vcDateBtn="",c.innerText=String(s),r.appendChild(c)),e.enableWeekNumbers&&addWeekNumberForDate(e,r,i),setDaysAsDisabled(e,i,a),setDateModifier(e,t,r,c,a,i,o),n.addDate(r),e.onCreateDateEls&&e.onCreateDateEls(e,r)},createDatesFromCurrentMonth=(e,t,n,s,i)=>{for(let o=1;o<=n;o++){const n=new Date(s,i,o);createDate(e,s,t,o,getDateString(n),"current")}},createDatesFromNextMonth=(e,t,n,s,i)=>{const o=i+1===12?s+1:s,a=i+1===12?"01":i+2<10?`0${i+2}`:i+2;for(let i=1;i<=n;i++){const n=i<10?`0${i}`:String(i);createDate(e,s,t,i,`${o}-${a}-${n}`,"next")}},createDatesFromPrevMonth=(e,t,n,s,i)=>{let o=new Date(n,s,0).getDate()-(i-1);const a=0===s?n-1:n,l=0===s?12:s<10?`0${s}`:s;for(let s=i;s>0;s--,o++)createDate(e,n,t,o,`${a}-${l}-${o}`,"prev")},createWeekNumbers=(e,t,n,s,i)=>{if(!e.enableWeekNumbers)return;s.textContent="";const o=document.createElement("b");o.className=e.styles.weekNumbersTitle,o.innerText="#",o.dataset.vcWeekNumbers="title",s.appendChild(o);const a=document.createElement("div");a.className=e.styles.weekNumbersContent,a.dataset.vcWeekNumbers="content",s.appendChild(a);const l=document.createElement("button");l.type="button",l.className=e.styles.weekNumber;const r=i.querySelectorAll("[data-vc-date]"),c=Math.ceil((t+n)/7);for(let t=0;t{const t=new Date(e.context.selectedYear,e.context.selectedMonth,1),n=e.context.mainElement.querySelectorAll('[data-vc="dates"]'),s=e.context.mainElement.querySelectorAll('[data-vc-week="numbers"]');n.forEach((n,i)=>{e.selectionDatesMode||(n.dataset.vcDatesDisabled=""),n.textContent="";const o=new Date(t);o.setMonth(o.getMonth()+i);const a=o.getMonth(),l=o.getFullYear(),r=(new Date(l,a,1).getDay()-e.firstWeekday+7)%7,c=new Date(l,a+1,0).getDate(),d=r+c,u=Math.ceil(d/7),h=7*u-d,_=[];for(let t=0;t{_[m].appendChild(e),g++,g>=7&&(m++,g=0)}};createDatesFromPrevMonth(e,p,l,a,r),createDatesFromCurrentMonth(e,p,c,l,a),createDatesFromNextMonth(e,p,h,l,a);for(const e of _)n.appendChild(e);createDatePopup(e,n),createWeekNumbers(e,r,c,s[i],n)})},layoutDefault=e=>`\n \n <#ArrowPrev [month] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [month] />\n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n <#DateRangeTooltip />\n \n \n <#ControlTime />\n`,layoutMonths=e=>`\n \n \n <#Month />\n <#Year />\n \n \n \n \n <#Months />\n \n \n`,layoutMultiple=e=>`\n \n <#ArrowPrev [month] />\n <#ArrowNext [month] />\n \n \n <#Multiple>\n \n \n \n <#Month />\n <#Year />\n \n \n \n <#WeekNumbers />\n \n <#Week />\n <#Dates />\n \n \n \n <#/Multiple>\n <#DateRangeTooltip />\n \n <#ControlTime />\n`,layoutYears=e=>`\n \n <#ArrowPrev [year] />\n \n <#Month />\n <#Year />\n \n <#ArrowNext [year] />\n \n \n \n <#Years />\n \n \n`,ArrowNext=(e,t)=>``,ArrowPrev=(e,t)=>``,ControlTime=e=>e.selectionTimeMode?``:"",DateRangeTooltip=e=>e.onCreateDateRangeTooltip?``:"",Dates=e=>``,Month=e=>``,Months=e=>``,Week=e=>``,WeekNumbers=e=>e.enableWeekNumbers?``:"",Year=e=>``,Years=e=>``,components={ArrowNext:ArrowNext,ArrowPrev:ArrowPrev,ControlTime:ControlTime,Dates:Dates,DateRangeTooltip:DateRangeTooltip,Month:Month,Months:Months,Week:Week,WeekNumbers:WeekNumbers,Year:Year,Years:Years},getComponent=e=>components[e],parseLayout=(e,t)=>t.replace(/[\n\t]/g,"").replace(/<#(?!\/?Multiple)(.*?)>/g,(t,n)=>{const s=(n.match(/\[(.*?)\]/)||[])[1],i=n.replace(/[/\s\n\t]|\[(.*?)\]/g,""),o=getComponent(i),a=o?o(e,null!=s?s:null):"";return e.sanitizerHTML(a)}).replace(/[\n\t]/g,""),parseMultipleLayout=(e,t)=>t.replace(new RegExp("<#Multiple>(.*?)<#\\/Multiple>","gs"),(t,n)=>{const s=Array(e.context.displayMonthsCount).fill(n).join("");return e.sanitizerHTML(s)}).replace(/[\n\t]/g,""),createLayouts=(e,t)=>{const n={default:layoutDefault,month:layoutMonths,year:layoutYears,multiple:layoutMultiple};if(Object.keys(n).forEach(t=>{const s=t;e.layouts[s].length||(e.layouts[s]=n[s](e))}),e.context.mainElement.className=e.styles.calendar,e.context.mainElement.dataset.vc="calendar",e.context.mainElement.dataset.vcType=e.context.currentType,e.context.mainElement.role="application",e.context.mainElement.tabIndex=0,e.context.mainElement.ariaLabel=e.labels.application,"multiple"!==e.context.currentType){if("multiple"===e.type&&t){const n=e.context.mainElement.querySelector('[data-vc="controls"]'),s=e.context.mainElement.querySelector('[data-vc="grid"]'),i=t.closest('[data-vc="column"]');return n&&n.remove(),s&&(s.dataset.vcGrid="hidden"),i&&(i.dataset.vcColumn=e.context.currentType),void(i&&(i.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))))}e.context.mainElement.innerHTML=e.sanitizerHTML(parseLayout(e,e.layouts[e.context.currentType]))}else e.context.mainElement.innerHTML=e.sanitizerHTML(parseMultipleLayout(e,parseLayout(e,e.layouts[e.context.currentType])))},setVisibilityArrows=(e,t,n,s)=>{e.style.visibility=n?"hidden":"",t.style.visibility=s?"hidden":""},handleDefaultType=(e,t,n)=>{const s=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1))),i=new Date(s.getTime()),o=new Date(s.getTime());i.setMonth(i.getMonth()-e.monthsToSwitch),o.setMonth(o.getMonth()+e.monthsToSwitch);const a=getDate(e.context.dateMin),l=getDate(e.context.dateMax);e.selectionYearsMode||(a.setFullYear(s.getFullYear()),l.setFullYear(s.getFullYear()));const r=!e.selectionMonthsMode||i.getFullYear()l.getFullYear()||o.getFullYear()===l.getFullYear()&&o.getMonth()>l.getMonth()-(e.context.displayMonthsCount-1);setVisibilityArrows(t,n,r,c)},handleYearType=(e,t,n)=>{const s=getDate(e.context.dateMin),i=getDate(e.context.dateMax),o=!!(s.getFullYear()&&e.context.displayYear-7<=s.getFullYear()),a=!!(i.getFullYear()&&e.context.displayYear+7>=i.getFullYear());setVisibilityArrows(t,n,o,a)},visibilityArrows=e=>{if("month"===e.context.currentType)return;const t=e.context.mainElement.querySelector('[data-vc-arrow="prev"]'),n=e.context.mainElement.querySelector('[data-vc-arrow="next"]');t&&n&&{default:()=>handleDefaultType(e,t,n),year:()=>handleYearType(e,t,n)}["multiple"===e.context.currentType?"default":e.context.currentType]()},visibilityHandler=(e,t,n,s,i)=>{const o=new Date(s.setFullYear(e.context.selectedYear,e.context.selectedMonth+n)).getFullYear(),a=new Date(s.setMonth(e.context.selectedMonth+n)).getMonth(),l=e.context.locale.months.long[a],r=t.closest('[data-vc="column"]');r&&(r.ariaLabel=`${l} ${o}`);const c={month:{id:a,label:l},year:{id:o,label:o}};t.innerText=String(c[i].label),t.dataset[`vc${i.charAt(0).toUpperCase()+i.slice(1)}`]=String(c[i].id),t.ariaLabel=`${e.labels[i]} ${c[i].label}`;const d={month:e.selectionMonthsMode,year:e.selectionYearsMode},u=!1===d[i]||"only-arrows"===d[i];u&&(t.tabIndex=-1),t.disabled=u},visibilityTitle=e=>{const t=e.context.mainElement.querySelectorAll('[data-vc="month"]'),n=e.context.mainElement.querySelectorAll('[data-vc="year"]'),s=new Date(e.context.selectedYear,e.context.selectedMonth,1);[t,n].forEach(t=>null==t?void 0:t.forEach((t,n)=>visibilityHandler(e,t,n,s,t.dataset.vc)))},setYearModifier=(e,t,n,s,i)=>{var o;const a={month:{selected:"data-vc-months-month-selected",aria:"aria-selected",value:"vcMonthsMonth",selectedProperty:"selectedMonth"},year:{selected:"data-vc-years-year-selected",aria:"aria-selected",value:"vcYearsYear",selectedProperty:"selectedYear"}};i&&(null==(o=e.context.mainElement.querySelectorAll({month:"[data-vc-months-month]",year:"[data-vc-years-year]"}[n]))||o.forEach(e=>{e.removeAttribute(a[n].selected),e.removeAttribute(a[n].aria)}),setContext(e,a[n].selectedProperty,Number(t.dataset[a[n].value])),visibilityTitle(e),"year"===n&&visibilityArrows(e)),s&&(t.setAttribute(a[n].selected,""),t.setAttribute(a[n].aria,"true"))},getColumnID=(e,t)=>{var n;if("multiple"!==e.type)return{currentValue:null,columnID:0};const s=e.context.mainElement.querySelectorAll('[data-vc="column"]'),i=Array.from(s).findIndex(e=>e.closest(`[data-vc-column="${t}"]`));return{currentValue:i>=0?Number(null==(n=s[i].querySelector(`[data-vc="${t}"]`))?void 0:n.getAttribute(`data-vc-${t}`)):null,columnID:Math.max(i,0)}},createMonthEl=(e,t,n,s,i,o,a)=>{const l=t.cloneNode(!1);return l.className=e.styles.monthsMonth,l.innerText=s,l.ariaLabel=i,l.role="gridcell",l.dataset.vcMonthsMonth=`${a}`,o&&(l.ariaDisabled="true"),o&&(l.tabIndex=-1),l.disabled=o,setYearModifier(e,l,"month",n===a,!1),l},createMonths=(e,t)=>{var n,s;const i=null==(n=null==t?void 0:t.closest('[data-vc="header"]'))?void 0:n.querySelector('[data-vc="year"]'),o=i?Number(i.dataset.vcYear):e.context.selectedYear,a=(null==t?void 0:t.dataset.vcMonth)?Number(t.dataset.vcMonth):e.context.selectedMonth;setContext(e,"currentType","month"),createLayouts(e,t),visibilityTitle(e);const l=e.context.mainElement.querySelector('[data-vc="months"]');if(!e.selectionMonthsMode||!l)return;const r=e.monthsToSwitch>1?e.context.locale.months.long.map((t,n)=>a-e.monthsToSwitch*n).concat(e.context.locale.months.long.map((t,n)=>a+e.monthsToSwitch*n)).filter(e=>e>=0&&e<=12):Array.from(Array(12).keys()),c=document.createElement("button");c.type="button";for(let t=0;t<12;t++){const n=getDate(e.context.dateMin),s=getDate(e.context.dateMax),i=e.context.displayMonthsCount-1,{columnID:d}=getColumnID(e,"month"),u=o<=n.getFullYear()&&t=s.getFullYear()&&t>s.getMonth()-i+d||o>s.getFullYear()||t!==a&&!r.includes(t),h=createMonthEl(e,c,a,e.context.locale.months.short[t],e.context.locale.months.long[t],u,t);l.appendChild(h),e.onCreateMonthEls&&e.onCreateMonthEls(e,h)}null==(s=e.context.mainElement.querySelector("[data-vc-months-month]:not([disabled])"))||s.focus()},TimeInput=(e,t,n,s,i)=>`\n \n \n \n`,TimeRange=(e,t,n,s,i,o,a)=>`\n \n \n \n`,handleActions=(e,t,n,s)=>{({hour:()=>setContext(e,"selectedHours",n),minute:()=>setContext(e,"selectedMinutes",n)})[s](),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${e.context.selectedKeeping?` ${e.context.selectedKeeping}`:""}`),e.onChangeTime&&e.onChangeTime(e,t,!1),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t)},transformTime24=(e,t)=>{var n;return(null==(n={0:{AM:"00",PM:"12"},1:{AM:"01",PM:"13"},2:{AM:"02",PM:"14"},3:{AM:"03",PM:"15"},4:{AM:"04",PM:"16"},5:{AM:"05",PM:"17"},6:{AM:"06",PM:"18"},7:{AM:"07",PM:"19"},8:{AM:"08",PM:"20"},9:{AM:"09",PM:"21"},10:{AM:"10",PM:"22"},11:{AM:"11",PM:"23"},12:{AM:"00",PM:"12"}}[Number(e)])?void 0:n[t])||String(e)},handleClickKeepingTime=(e,t,n,s,i)=>{const o=o=>{const a="AM"===e.context.selectedKeeping?"PM":"AM",l=transformTime24(e.context.selectedHours,a);Number(l)<=s&&Number(l)>=i?(setContext(e,"selectedKeeping",a),n.value=l,handleActions(e,o,e.context.selectedHours,"hour"),t.ariaLabel=`${e.labels.btnKeeping} ${e.context.selectedKeeping}`,t.innerText=e.context.selectedKeeping):e.onChangeTime&&e.onChangeTime(e,o,!0)};return t.addEventListener("click",o),()=>{t.removeEventListener("click",o)}},transformTime12=e=>({0:"12",13:"01",14:"02",15:"03",16:"04",17:"05",18:"06",19:"07",20:"08",21:"09",22:"10",23:"11"}[Number(e)]||String(e)),updateInputAndRange=(e,t,n,s)=>{e.value=n,t.value=s},updateKeepingTime$1=(e,t,n)=>{t&&n&&(setContext(e,"selectedKeeping",n),t.innerText=n)},handleInput$1=(e,t,n,s,i,o,a)=>{const l={hour:(l,r,c)=>{e.selectionTimeMode&&{12:()=>{if(!e.context.selectedKeeping)return;const d=Number(transformTime24(r,e.context.selectedKeeping));if(!(d<=o&&d>=a))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,!0));updateInputAndRange(n,t,transformTime12(r),transformTime24(r,e.context.selectedKeeping)),l>12&&updateKeepingTime$1(e,s,"PM"),handleActions(e,c,transformTime12(r),i)},24:()=>{if(!(l<=o&&l>=a))return updateInputAndRange(n,t,e.context.selectedHours,e.context.selectedHours),void(e.onChangeTime&&e.onChangeTime(e,c,!0));updateInputAndRange(n,t,r,r),handleActions(e,c,r,i)}}[e.selectionTimeMode]()},minute:(s,l,r)=>{if(!(s<=o&&s>=a))return n.value=e.context.selectedMinutes,void(e.onChangeTime&&e.onChangeTime(e,r,!0));n.value=l,t.value=l,handleActions(e,r,l,i)}},r=e=>{const t=Number(n.value),s=n.value.padStart(2,"0");l[i]&&l[i](t,s,e)};return n.addEventListener("change",r),()=>{n.removeEventListener("change",r)}},updateInputAndTime=(e,t,n,s,i)=>{t.value=i,handleActions(e,n,i,s)},updateKeepingTime=(e,t,n)=>{t&&(setContext(e,"selectedKeeping",n),t.innerText=n)},handleRange=(e,t,n,s,i)=>{const o=o=>{const a=Number(t.value),l=t.value.padStart(2,"0"),r="hour"===i,c=24===e.selectionTimeMode,d=a>0&&a<12;r&&!c&&updateKeepingTime(e,s,0===a||d?"AM":"PM"),updateInputAndTime(e,n,o,i,!r||c||d?l:transformTime12(t.value))};return t.addEventListener("input",o),()=>{t.removeEventListener("input",o)}},handleMouseOver=e=>e.setAttribute("data-vc-input-focus",""),handleMouseOut=e=>e.removeAttribute("data-vc-input-focus"),handleTime=(e,t)=>{const n=t.querySelector('[data-vc-time-range="hour"] input[name="hour"]'),s=t.querySelector('[data-vc-time-range="minute"] input[name="minute"]'),i=t.querySelector('[data-vc-time-input="hour"] input[name="hour"]'),o=t.querySelector('[data-vc-time-input="minute"] input[name="minute"]'),a=t.querySelector('[data-vc-time="keeping"]');if(!(n&&s&&i&&o))return;const l=e=>{e.target===n&&handleMouseOver(i),e.target===s&&handleMouseOver(o)},r=e=>{e.target===n&&handleMouseOut(i),e.target===s&&handleMouseOut(o)};return t.addEventListener("mouseover",l),t.addEventListener("mouseout",r),handleInput$1(e,n,i,a,"hour",e.timeMaxHour,e.timeMinHour),handleInput$1(e,s,o,a,"minute",e.timeMaxMinute,e.timeMinMinute),handleRange(e,n,i,a,"hour"),handleRange(e,s,o,a,"minute"),a&&handleClickKeepingTime(e,a,n,e.timeMaxHour,e.timeMinHour),()=>{t.removeEventListener("mouseover",l),t.removeEventListener("mouseout",r)}},createTime=e=>{const t=e.context.mainElement.querySelector('[data-vc="time"]');if(!e.selectionTimeMode||!t)return;const[n,s]=[e.timeMinHour,e.timeMaxHour],[i,o]=[e.timeMinMinute,e.timeMaxMinute],a=e.context.selectedKeeping?transformTime24(e.context.selectedHours,e.context.selectedKeeping):e.context.selectedHours,l="range"===e.timeControls;var r;t.innerHTML=e.sanitizerHTML(`\n \n ${TimeInput("hour",e.styles.timeHour,e.labels,e.context.selectedHours,l)}\n ${TimeInput("minute",e.styles.timeMinute,e.labels,e.context.selectedMinutes,l)}\n ${12===e.selectionTimeMode?(r=e.context.selectedKeeping,`${r}`):""}\n \n \n ${TimeRange("hour",e.styles.timeRange,e.labels,n,s,e.timeStepHour,a)}\n ${TimeRange("minute",e.styles.timeRange,e.labels,i,o,e.timeStepMinute,e.context.selectedMinutes)}\n \n `),handleTime(e,t)},createWeek=e=>{const t=e.selectedWeekends?[...e.selectedWeekends]:[],n=[...e.context.locale.weekdays.long].reduce((n,s,i)=>[...n,{id:i,titleShort:e.context.locale.weekdays.short[i],titleLong:s,isWeekend:t.includes(i)}],[]),s=[...n.slice(e.firstWeekday),...n.slice(0,e.firstWeekday)];e.context.mainElement.querySelectorAll('[data-vc="week"]').forEach(t=>{const n=e.onClickWeekDay?document.createElement("button"):document.createElement("b");e.onClickWeekDay&&(n.type="button"),s.forEach(s=>{const i=n.cloneNode(!0);i.innerText=s.titleShort,i.className=e.styles.weekDay,i.role="columnheader",i.ariaLabel=s.titleLong,i.dataset.vcWeekDay=String(s.id),s.isWeekend&&(i.dataset.vcWeekDayOff=""),t.appendChild(i)})})},createYearEl=(e,t,n,s,i)=>{const o=t.cloneNode(!1);return o.className=e.styles.yearsYear,o.innerText=String(i),o.ariaLabel=String(i),o.role="gridcell",o.dataset.vcYearsYear=`${i}`,s&&(o.ariaDisabled="true"),s&&(o.tabIndex=-1),o.disabled=s,setYearModifier(e,o,"year",n===i,!1),o},createYears=(e,t)=>{var n;const s=(null==t?void 0:t.dataset.vcYear)?Number(t.dataset.vcYear):e.context.selectedYear;setContext(e,"currentType","year"),createLayouts(e,t),visibilityTitle(e),visibilityArrows(e);const i=e.context.mainElement.querySelector('[data-vc="years"]');if(!e.selectionYearsMode||!i)return;const o="multiple"!==e.type||e.context.selectedYear===s?0:1,a=document.createElement("button");a.type="button";for(let t=e.context.displayYear-7;tgetDate(e.context.dateMax).getFullYear(),l=createYearEl(e,a,s,n,t);i.appendChild(l),e.onCreateYearEls&&e.onCreateYearEls(e,l)}null==(n=e.context.mainElement.querySelector("[data-vc-years-year]:not([disabled])"))||n.focus()},trackChangesHTMLElement=(e,t,n)=>{new MutationObserver(e=>{for(let s=0;shaveListener.value=!0,check:()=>haveListener.value},setTheme=(e,t)=>e.dataset.vcTheme=t,trackChangesThemeInSystemSettings=(e,t)=>{if(setTheme(e.context.mainElement,t.matches?"dark":"light"),"system"!==e.selectedTheme||haveListener.check())return;const n=e=>{const t=document.querySelectorAll('[data-vc="calendar"]');null==t||t.forEach(t=>setTheme(t,e.matches?"dark":"light"))};t.addEventListener?t.addEventListener("change",n):t.addListener(n),haveListener.set()},detectTheme=(e,t)=>{const n=e.themeAttrDetect.length?document.querySelector(e.themeAttrDetect):null,s=e.themeAttrDetect.replace(/^.*\[(.+)\]/g,(e,t)=>t);if(!n||"system"===n.getAttribute(s))return void trackChangesThemeInSystemSettings(e,t);const i=n.getAttribute(s);i?(setTheme(e.context.mainElement,i),trackChangesHTMLElement(n,s,()=>{const t=n.getAttribute(s);t&&setTheme(e.context.mainElement,t)})):trackChangesThemeInSystemSettings(e,t)},handleTheme=e=>{"not all"!==window.matchMedia("(prefers-color-scheme)").media?"system"===e.selectedTheme?detectTheme(e,window.matchMedia("(prefers-color-scheme: dark)")):setTheme(e.context.mainElement,e.selectedTheme):setTheme(e.context.mainElement,"light")},capitalizeFirstLetter=e=>e.charAt(0).toUpperCase()+e.slice(1).replace(/\./,""),getLocaleWeekday=(e,t,n)=>{const s=new Date(`1978-01-0${t+1}T00:00:00.000Z`),i=s.toLocaleString(n,{weekday:"short",timeZone:"UTC"}),o=s.toLocaleString(n,{weekday:"long",timeZone:"UTC"});e.context.locale.weekdays.short.push(capitalizeFirstLetter(i)),e.context.locale.weekdays.long.push(capitalizeFirstLetter(o))},getLocaleMonth=(e,t,n)=>{const s=new Date(`1978-${String(t+1).padStart(2,"0")}-01T00:00:00.000Z`),i=s.toLocaleString(n,{month:"short",timeZone:"UTC"}),o=s.toLocaleString(n,{month:"long",timeZone:"UTC"});e.context.locale.months.short.push(capitalizeFirstLetter(i)),e.context.locale.months.long.push(capitalizeFirstLetter(o))},getLocale=e=>{var t,n,s,i,o,a,l,r;if(!(e.context.locale.weekdays.short[6]&&e.context.locale.weekdays.long[6]&&e.context.locale.months.short[11]&&e.context.locale.months.long[11]))if("string"==typeof e.locale){if("string"==typeof e.locale&&!e.locale.length)throw new Error(errorMessages.notLocale);Array.from({length:7},(t,n)=>getLocaleWeekday(e,n,e.locale)),Array.from({length:12},(t,n)=>getLocaleMonth(e,n,e.locale))}else{if(!((null==(n=null==(t=e.locale)?void 0:t.weekdays)?void 0:n.short[6])&&(null==(i=null==(s=e.locale)?void 0:s.weekdays)?void 0:i.long[6])&&(null==(a=null==(o=e.locale)?void 0:o.months)?void 0:a.short[11])&&(null==(r=null==(l=e.locale)?void 0:l.months)?void 0:r.long[11])))throw new Error(errorMessages.notLocale);setContext(e,"locale",__spreadValues({},e.locale))}},create=e=>{const t={default:()=>{createWeek(e),createDates(e)},multiple:()=>{createWeek(e),createDates(e)},month:()=>createMonths(e),year:()=>createYears(e)};handleTheme(e),getLocale(e),createLayouts(e),visibilityTitle(e),visibilityArrows(e),createTime(e),t[e.context.currentType]()},handleArrowKeys=e=>{const t=t=>{var n;const s=t.target;if(!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key)||"button"!==s.localName)return;const i=Array.from(e.context.mainElement.querySelectorAll('[data-vc="calendar"] button')),o=i.indexOf(s);if(-1===o)return;const a=(l=i[o]).hasAttribute("data-vc-date-btn")?7:l.hasAttribute("data-vc-months-month")?4:l.hasAttribute("data-vc-years-year")?5:1;var l;const r=(0,{ArrowUp:()=>Math.max(0,o-a),ArrowDown:()=>Math.min(i.length-1,o+a),ArrowLeft:()=>Math.max(0,o-1),ArrowRight:()=>Math.min(i.length-1,o+1)}[t.key])();null==(n=i[r])||n.focus()};return e.context.mainElement.addEventListener("keydown",t),()=>e.context.mainElement.removeEventListener("keydown",t)},handleMonth=(e,t)=>{const n=getDate(getDateString(new Date(e.context.selectedYear,e.context.selectedMonth,1)));({prev:()=>n.setMonth(n.getMonth()-e.monthsToSwitch),next:()=>n.setMonth(n.getMonth()+e.monthsToSwitch)})[t](),setContext(e,"selectedMonth",n.getMonth()),setContext(e,"selectedYear",n.getFullYear()),visibilityTitle(e),visibilityArrows(e),createDates(e)},handleClickArrow=(e,t)=>{const n=t.target.closest("[data-vc-arrow]");if(n){if(["default","multiple"].includes(e.context.currentType))handleMonth(e,n.dataset.vcArrow);else if("year"===e.context.currentType&&void 0!==e.context.displayYear){const s={prev:-15,next:15}[n.dataset.vcArrow];setContext(e,"displayYear",e.context.displayYear+s),createYears(e,t.target)}e.onClickArrow&&e.onClickArrow(e,t)}},resolveToggle=(e,t)=>void 0===t||("function"==typeof t?t(e):t),canToggleSelection=e=>resolveToggle(e,e.enableDateToggle),handleSelectDate=(e,t,n)=>{const s=t.dataset.vcDate,i=t.closest("[data-vc-date][data-vc-date-selected]"),o=canToggleSelection(e);if(i&&!o)return;const a=i?e.context.selectedDates.filter(e=>e!==s):n?[...e.context.selectedDates,s]:[s];setContext(e,"selectedDates",a)},createDateRangeTooltip=(e,t,n)=>{if(!t)return;if(!n)return t.dataset.vcDateRangeTooltip="hidden",void(t.textContent="");const s=e.context.mainElement.getBoundingClientRect(),i=n.getBoundingClientRect();t.style.left=i.left-s.left+i.width/2+"px",t.style.top=i.bottom-s.top-i.height+"px",t.dataset.vcDateRangeTooltip="visible",t.innerHTML=e.sanitizerHTML(e.onCreateDateRangeTooltip(e,n,t,i,s))},state={self:null,lastDateEl:null,isHovering:!1,rangeMin:void 0,rangeMax:void 0,tooltipEl:null,timeoutId:null},addHoverEffect=(e,t,n)=>{var s,i,o;if(!(null==(i=null==(s=state.self)?void 0:s.context)?void 0:i.selectedDates[0]))return;const a=getDateString(e);(null==(o=state.self.context.disableDates)?void 0:o.includes(a))||(state.self.context.mainElement.querySelectorAll(`[data-vc-date="${a}"]`).forEach(e=>e.dataset.vcDateHover=""),t.forEach(e=>e.dataset.vcDateHover="first"),n.forEach(e=>{"first"===e.dataset.vcDateHover?e.dataset.vcDateHover="first-and-last":e.dataset.vcDateHover="last"}))},removeHoverEffect=()=>{var e,t;(null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.mainElement)&&state.self.context.mainElement.querySelectorAll("[data-vc-date-hover]").forEach(e=>e.removeAttribute("data-vc-date-hover"))},handleHoverDatesEvent=e=>{var t,n;if(!e||!(null==(n=null==(t=state.self)?void 0:t.context)?void 0:n.selectedDates[0]))return;if(!e.closest('[data-vc="dates"]'))return state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),void removeHoverEffect();const s=e.closest("[data-vc-date]");if(!s||state.lastDateEl===s)return;state.lastDateEl=s,createDateRangeTooltip(state.self,state.tooltipEl,s),removeHoverEffect();const i=s.dataset.vcDate,o=getDate(state.self.context.selectedDates[0]),a=getDate(i),l=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${state.self.context.selectedDates[0]}"]`),r=state.self.context.mainElement.querySelectorAll(`[data-vc-date="${i}"]`),[c,d]=o{const t=null==e?void 0:e.closest("[data-vc-date-selected]");if(!t&&state.lastDateEl)return state.lastDateEl=null,void createDateRangeTooltip(state.self,state.tooltipEl,null);t&&state.lastDateEl!==t&&(state.lastDateEl=t,createDateRangeTooltip(state.self,state.tooltipEl,t))},optimizedHoverHandler=e=>t=>{const n=t.target;state.isHovering||(state.isHovering=!0,requestAnimationFrame(()=>{e(n),state.isHovering=!1}))},optimizedHandleHoverDatesEvent=optimizedHoverHandler(handleHoverDatesEvent),optimizedHandleHoverSelectedDatesRangeEvent=optimizedHoverHandler(handleHoverSelectedDatesRangeEvent),handleCancelSelectionDates=e=>{state.self&&"Escape"===e.key&&(state.lastDateEl=null,setContext(state.self,"selectedDates",[]),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect())},handleMouseLeave=()=>{null!==state.timeoutId&&clearTimeout(state.timeoutId),state.timeoutId=setTimeout(()=>{state.lastDateEl=null,createDateRangeTooltip(state.self,state.tooltipEl,null),removeHoverEffect()},50)},updateDisabledDates=()=>{var e,t,n,s;if(!(null==(n=null==(t=null==(e=state.self)?void 0:e.context)?void 0:t.selectedDates)?void 0:n[0])||!(null==(s=state.self.context.disableDates)?void 0:s[0]))return;const i=getDate(state.self.context.selectedDates[0]),[o,a]=state.self.context.disableDates.map(e=>getDate(e)).reduce(([e,t],n)=>[i>=n?n:e,i{state.self=e,state.lastDateEl=t,removeHoverEffect(),e.disableDatesGaps&&(state.rangeMin=state.rangeMin?state.rangeMin:e.context.displayDateMin,state.rangeMax=state.rangeMax?state.rangeMax:e.context.displayDateMax),e.onCreateDateRangeTooltip&&(state.tooltipEl=e.context.mainElement.querySelector("[data-vc-date-range-tooltip]"));const n=null==t?void 0:t.dataset.vcDate;if(n){const t=1===e.context.selectedDates.length&&e.context.selectedDates[0].includes(n),s=t&&!canToggleSelection(e)?[n,n]:t&&canToggleSelection(e)?[]:e.context.selectedDates.length>1?[n]:[...e.context.selectedDates,n];setContext(e,"selectedDates",s),e.context.selectedDates.length>1&&e.context.selectedDates.sort((e,t)=>+new Date(e)-+new Date(t))}({set:()=>(e.disableDatesGaps&&updateDisabledDates(),createDateRangeTooltip(state.self,state.tooltipEl,t),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.addEventListener("keydown",handleCancelSelectionDates),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates)}),reset:()=>{const[n,s]=[e.context.selectedDates[0],e.context.selectedDates[e.context.selectedDates.length-1]],i=e.context.selectedDates[0]!==e.context.selectedDates[e.context.selectedDates.length-1],o=parseDates([`${n}:${s}`]).filter(t=>!e.context.disableDates.includes(t)),a=i?e.enableEdgeDatesOnly?[n,s]:o:[e.context.selectedDates[0],e.context.selectedDates[0]];if(setContext(e,"selectedDates",a),e.disableDatesGaps&&(setContext(e,"displayDateMin",state.rangeMin),setContext(e,"displayDateMax",state.rangeMax)),state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverDatesEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),state.self.context.mainElement.removeEventListener("keydown",handleCancelSelectionDates),e.onCreateDateRangeTooltip)return e.context.selectedDates[0]||(state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,null)),e.context.selectedDates[0]&&(state.self.context.mainElement.addEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.addEventListener("mouseleave",handleMouseLeave),createDateRangeTooltip(state.self,state.tooltipEl,t)),()=>{state.self.context.mainElement.removeEventListener("mousemove",optimizedHandleHoverSelectedDatesRangeEvent),state.self.context.mainElement.removeEventListener("mouseleave",handleMouseLeave)}}})[1===e.context.selectedDates.length?"set":"reset"]()},updateDateModifier=e=>{e.context.mainElement.querySelectorAll("[data-vc-date]").forEach(t=>{const n=t.querySelector("[data-vc-date-btn]"),s=t.dataset.vcDate,i=getDate(s).getDay();setDateModifier(e,e.context.selectedYear,t,n,i,s,"current")})},handleClickDate=(e,t)=>{var n;const s=t.target,i=s.closest("[data-vc-date-btn]");if(!e.selectionDatesMode||!["single","multiple","multiple-ranged"].includes(e.selectionDatesMode)||!i)return;const o=i.closest("[data-vc-date]");({single:()=>handleSelectDate(e,o,!1),multiple:()=>handleSelectDate(e,o,!0),"multiple-ranged":()=>handleSelectDateRange(e,o)})[e.selectionDatesMode](),null==(n=e.context.selectedDates)||n.sort((e,t)=>+new Date(e)-+new Date(t)),e.onClickDate&&e.onClickDate(e,t),e.inputMode&&e.context.inputElement&&e.context.mainElement&&e.onChangeToInput&&e.onChangeToInput(e,t);const a=s.closest('[data-vc-date-month="prev"]'),l=s.closest('[data-vc-date-month="next"]');({prev:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"prev"):updateDateModifier(e),next:()=>e.enableMonthChangeOnDayClick?handleMonth(e,"next"):updateDateModifier(e),current:()=>updateDateModifier(e)})[a?"prev":l?"next":"current"]()},typeClick=["month","year"],getValue=(e,t,n)=>{const{currentValue:s,columnID:i}=getColumnID(e,t);return"month"===e.context.currentType&&i>=0?n-i:"year"===e.context.currentType&&e.context.selectedYear!==s?n-1:n},handleMultipleYearSelection=(e,t)=>{const n=getValue(e,"year",Number(t.dataset.vcYearsYear)),s=getDate(e.context.dateMin),i=getDate(e.context.dateMax),o=e.context.displayMonthsCount-1,{columnID:a}=getColumnID(e,"year"),l=e.context.selectedMonthi.getMonth()-o+a&&n>=i.getFullYear(),c=ni.getFullYear(),u=l||c?s.getFullYear():r||d?i.getFullYear():n,h=l||c?s.getMonth():r||d?i.getMonth()-o+a:e.context.selectedMonth;setContext(e,"selectedYear",u),setContext(e,"selectedMonth",h)},handleMultipleMonthSelection=(e,t)=>{const n=t.closest('[data-vc-column="month"]').querySelector('[data-vc="year"]'),s=getValue(e,"month",Number(t.dataset.vcMonthsMonth)),i=Number(n.dataset.vcYear),o=getDate(e.context.dateMin),a=getDate(e.context.dateMax),l=sa.getMonth()&&i>=a.getFullYear();setContext(e,"selectedYear",i),setContext(e,"selectedMonth",l?o.getMonth():r?a.getMonth():s)},handleItemClick=(e,t,n,s)=>{var i;({year:()=>{if("multiple"===e.type)return handleMultipleYearSelection(e,s);setContext(e,"selectedYear",Number(s.dataset.vcYearsYear))},month:()=>{if("multiple"===e.type)return handleMultipleMonthSelection(e,s);setContext(e,"selectedMonth",Number(s.dataset.vcMonthsMonth))}})[n](),{year:()=>{var n;return null==(n=e.onClickYear)?void 0:n.call(e,e,t)},month:()=>{var n;return null==(n=e.onClickMonth)?void 0:n.call(e,e,t)}}[n](),e.context.currentType!==e.type?(setContext(e,"currentType",e.type),create(e),null==(i=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||i.focus()):setYearModifier(e,s,n,!0,!0)},handleClickType=(e,t,n)=>{var s;const i=t.target,o=i.closest(`[data-vc="${n}"]`),a={year:()=>createYears(e,i),month:()=>createMonths(e,i)};if(o&&e.onClickTitle&&e.onClickTitle(e,t),o&&e.context.currentType!==n)return a[n]();const l=i.closest(`[data-vc-${n}s-${n}]`);if(l)return handleItemClick(e,t,n,l);const r=i.closest('[data-vc="grid"]'),c=i.closest('[data-vc="column"]');(e.context.currentType===n&&o||"multiple"===e.type&&e.context.currentType===n&&r&&!c)&&(setContext(e,"currentType",e.type),create(e),null==(s=e.context.mainElement.querySelector(`[data-vc="${n}"]`))||s.focus())},handleClickMonthOrYear=(e,t)=>{const n={month:e.selectionMonthsMode,year:e.selectionYearsMode};typeClick.forEach(s=>{n[s]&&t.target&&handleClickType(e,t,s)})},handleClickWeekNumber=(e,t)=>{if(!e.enableWeekNumbers||!e.onClickWeekNumber)return;const n=t.target.closest("[data-vc-week-number]"),s=e.context.mainElement.querySelectorAll("[data-vc-date-week-number]");if(!n||!s[0])return;const i=Number(n.innerText),o=Number(n.dataset.vcWeekYear),a=Array.from(s).filter(e=>Number(e.dataset.vcDateWeekNumber)===i);e.onClickWeekNumber(e,i,o,a,t)},handleClickWeekDay=(e,t)=>{if(!e.onClickWeekDay)return;const n=t.target.closest("[data-vc-week-day]"),s=t.target.closest('[data-vc="column"]'),i=s?s.querySelectorAll("[data-vc-date-week-day]"):e.context.mainElement.querySelectorAll("[data-vc-date-week-day]");if(!n||!i[0])return;const o=Number(n.dataset.vcWeekDay),a=Array.from(i).filter(e=>Number(e.dataset.vcDateWeekDay)===o);e.onClickWeekDay(e,o,a,t)},handleClick=e=>{const t=t=>{handleClickArrow(e,t),handleClickWeekDay(e,t),handleClickWeekNumber(e,t),handleClickDate(e,t),handleClickMonthOrYear(e,t)};return e.context.mainElement.addEventListener("click",t),()=>e.context.mainElement.removeEventListener("click",t)},initMonthsCount=e=>{if("multiple"===e.type&&(e.displayMonthsCount<=1||e.displayMonthsCount>12))throw new Error(errorMessages.incorrectMonthsCount);if("multiple"!==e.type&&e.displayMonthsCount>1)throw new Error(errorMessages.incorrectMonthsCount);setContext(e,"displayMonthsCount",e.displayMonthsCount?e.displayMonthsCount:"multiple"===e.type?2:1)},getLocalDate=()=>{const e=new Date;return new Date(e.getTime()-6e4*e.getTimezoneOffset()).toISOString().substring(0,10)},resolveDate=(e,t)=>"today"===e?getLocalDate():e instanceof Date||"number"==typeof e||"string"==typeof e?parseDates([e])[0]:t,initRange=e=>{var t,n,s;const i=resolveDate(e.dateMin,e.dateMin),o=resolveDate(e.dateMax,e.dateMax),a=resolveDate(e.displayDateMin,i),l=resolveDate(e.displayDateMax,o);setContext(e,"dateToday",resolveDate(e.dateToday,e.dateToday)),setContext(e,"displayDateMin",a?getDate(i)>=getDate(a)?i:a:i),setContext(e,"displayDateMax",l?getDate(o)<=getDate(l)?o:l:o);const r=e.disableDatesPast&&!e.disableAllDates&&getDate(a)1&&e.context.disableDates.sort((e,t)=>+new Date(e)-+new Date(t)),setContext(e,"enableDates",e.enableDates[0]?parseDates(e.enableDates):[]),(null==(t=e.context.enableDates)?void 0:t[0])&&(null==(n=e.context.disableDates)?void 0:n[0])&&setContext(e,"disableDates",e.context.disableDates.filter(t=>!e.context.enableDates.includes(t))),e.context.enableDates.length>1&&e.context.enableDates.sort((e,t)=>+new Date(e)-+new Date(t)),(null==(s=e.context.enableDates)?void 0:s[0])&&e.disableAllDates&&(setContext(e,"displayDateMin",e.context.enableDates[0]),setContext(e,"displayDateMax",e.context.enableDates[e.context.enableDates.length-1])),setContext(e,"dateMin",e.displayDisabledDates?i:e.context.displayDateMin),setContext(e,"dateMax",e.displayDisabledDates?o:e.context.displayDateMax)},initSelectedDates=e=>{var t;setContext(e,"selectedDates",(null==(t=e.selectedDates)?void 0:t[0])?parseDates(e.selectedDates):[])},displayClosestValidDate=e=>{const t=t=>{const n=new Date(t);setInitialContext(e,n.getMonth(),n.getFullYear())};if(e.displayDateMin&&"today"!==e.displayDateMin&&(n=e.displayDateMin,s=new Date,new Date(n).getTime()>s.getTime())){const n=e.selectedDates.length&&e.selectedDates[0]?parseDates(e.selectedDates)[0]:e.displayDateMin;return t(getDate(resolveDate(n,e.displayDateMin))),!0}var n,s;if(e.displayDateMax&&"today"!==e.displayDateMax&&((e,t)=>new Date(e).getTime(){setContext(e,"selectedMonth",t),setContext(e,"selectedYear",n),setContext(e,"displayYear",n)},initSelectedMonthYear=e=>{var t;if(e.enableJumpToSelectedDate&&(null==(t=e.selectedDates)?void 0:t[0])&&void 0===e.selectedMonth&&void 0===e.selectedYear){const t=getDate(parseDates(e.selectedDates)[0]);return void setInitialContext(e,t.getMonth(),t.getFullYear())}if(displayClosestValidDate(e))return;const n=void 0!==e.selectedMonth&&Number(e.selectedMonth)>=0&&Number(e.selectedMonth)<12,s=void 0!==e.selectedYear&&Number(e.selectedYear)>=0&&Number(e.selectedYear)<=9999;setInitialContext(e,n?Number(e.selectedMonth):getDate(e.context.dateToday).getMonth(),s?Number(e.selectedYear):getDate(e.context.dateToday).getFullYear())},initTime=e=>{var t,n,s;if(!e.selectionTimeMode)return;if(![12,24].includes(e.selectionTimeMode))throw new Error(errorMessages.incorrectTime);const i=12===e.selectionTimeMode,o=i?/^(0[1-9]|1[0-2]):([0-5][0-9]) ?(AM|PM)?$/i:/^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;let[a,l,r]=null!=(s=null==(n=null==(t=e.selectedTime)?void 0:t.match(o))?void 0:n.slice(1))?s:[];a?i&&!r&&(r="AM"):(a=i?transformTime12(String(e.timeMinHour)):String(e.timeMinHour),l=String(e.timeMinMinute),r=i?Number(transformTime12(String(e.timeMinHour)))>=12?"PM":"AM":null),setContext(e,"selectedHours",a.padStart(2,"0")),setContext(e,"selectedMinutes",l.padStart(2,"0")),setContext(e,"selectedKeeping",r),setContext(e,"selectedTime",`${e.context.selectedHours}:${e.context.selectedMinutes}${r?` ${r}`:""}`)},initAllVariables=e=>{setContext(e,"currentType",e.type),initMonthsCount(e),initRange(e),initSelectedMonthYear(e),initSelectedDates(e),initTime(e)},reset=(e,{year:t,month:n,dates:s,time:i,locale:o},a=!0)=>{var l;const r={year:e.selectedYear,month:e.selectedMonth,dates:e.selectedDates,time:e.selectedTime};e.selectedYear=t?r.year:e.context.selectedYear,e.selectedMonth=n?r.month:e.context.selectedMonth,e.selectedTime=i?r.time:e.context.selectedTime,e.selectedDates="only-first"===s&&(null==(l=e.context.selectedDates)?void 0:l[0])?[e.context.selectedDates[0]]:!0===s?r.dates:e.context.selectedDates,o&&setContext(e,"locale",{months:{short:[],long:[]},weekdays:{short:[],long:[]}}),initAllVariables(e),a&&create(e),e.selectedYear=r.year,e.selectedMonth=r.month,e.selectedDates=r.dates,e.selectedTime=r.time,"multiple-ranged"===e.selectionDatesMode&&s&&handleSelectDateRange(e,null)},createToInput=e=>{const t=document.createElement("div");return t.className=e.styles.calendar,t.dataset.vc="calendar",t.dataset.vcInput="",t.dataset.vcCalendarHidden="",setContext(e,"inputModeInit",!0),setContext(e,"isShowInInputMode",!1),setContext(e,"mainElement",t),document.body.appendChild(e.context.mainElement),reset(e,{year:!0,month:!0,dates:!0,time:!0,locale:!0}),setTimeout(()=>show(e)),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e)},canOpenOnFocus=e=>resolveToggle(e,e.openOnFocus),handleInput=e=>{setContext(e,"inputElement",e.context.mainElement);const t=()=>{e.context.inputModeInit?setTimeout(()=>show(e)):createToInput(e)};e.context.inputElement.addEventListener("click",t);const n="function"==typeof e.openOnFocus||!0===e.openOnFocus,s=()=>{shouldSkipOpenOnFocus(e)?clearSkipOpenOnFocus(e):canOpenOnFocus(e)&&t()};n&&e.context.inputElement.addEventListener("focus",s);const i=t=>{const n="Tab"===t.key&&!t.shiftKey,s=["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(t.key);(n||s)&&(t=>{var n;if(!e.context.isShowInInputMode)return!1;if(document.activeElement!==e.context.inputElement)return!1;const s=e=>e.tabIndex>=0&&!e.hasAttribute("disabled")&&"true"!==e.getAttribute("aria-disabled"),i=null!=(n=document.createTreeWalker(e.context.mainElement,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>s(e)?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}).nextNode())?n:s(e.context.mainElement)?e.context.mainElement:null;!i||i.tabIndex<0||(t.preventDefault(),i.focus())})(t)};return e.context.inputElement.addEventListener("keydown",i),()=>{e.context.inputElement.removeEventListener("click",t),n&&e.context.inputElement.removeEventListener("focus",s),e.context.inputElement.removeEventListener("keydown",i)}},init=e=>(setContext(e,"originalElement",e.context.mainElement.cloneNode(!0)),setContext(e,"isInit",!0),e.inputMode?handleInput(e):(initAllVariables(e),create(e),e.onInit&&e.onInit(e),handleArrowKeys(e),handleClick(e))),update=(e,t)=>{if(!e.context.isInit)throw new Error(errorMessages.notInit);reset(e,__spreadValues(__spreadValues({},{year:!0,month:!0,dates:!0,time:!0,locale:!0}),t),!(e.inputMode&&!e.context.inputModeInit)),e.onUpdate&&e.onUpdate(e)},replaceProperties=(e,t)=>{const n=Object.keys(t);for(let s=0;s{replaceProperties(e,t),e.context.isInit&&update(e,n)};function findBestPickerPosition(e,t){const n="left";if(!t||!e)return n;const{canShow:s,parentPositions:i}=getAvailablePosition(e,t),o=s.left&&s.right;return(o&&s.bottom?"center":o&&s.top?["top","center"]:Array.isArray(i)?["bottom"===i[0]?"top":"bottom",...i.slice(1)]:i)||n}const setPosition=(e,t,n)=>{if(!e)return;const s="auto"===n?findBestPickerPosition(e,t):n,i={top:-t.offsetHeight,bottom:e.offsetHeight,left:0,center:e.offsetWidth/2-t.offsetWidth/2,right:e.offsetWidth-t.offsetWidth},o=Array.isArray(s)?s[0]:"bottom",a=Array.isArray(s)?s[1]:s;t.dataset.vcPosition=o;const{top:l,left:r}=getOffset(e),c=l+i[o];let d=r+i[a];const{vw:u}=getViewportDimensions();if(d+t.clientWidth>u){const e=window.innerWidth-document.body.clientWidth;d=u-t.clientWidth-e}else d<0&&(d=0);Object.assign(t.style,{left:`${d}px`,top:`${c}px`})},show=e=>{if(e.context.isShowInInputMode)return;if(!e.context.currentType)return void e.context.mainElement.click();setContext(e,"cleanupHandlers",[]),setContext(e,"isShowInInputMode",!0),e.inputMode&&restoreTabbing(e.context.mainElement),setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput),e.context.mainElement.removeAttribute("data-vc-calendar-hidden");const t=()=>{setPosition(e.context.inputElement,e.context.mainElement,e.positionToInput)};window.addEventListener("resize",t),e.context.cleanupHandlers.push(()=>window.removeEventListener("resize",t));const n=t=>{"Escape"===t.key&&hide(e)};document.addEventListener("keydown",n),e.context.cleanupHandlers.push(()=>document.removeEventListener("keydown",n));const s=t=>{t.target===e.context.inputElement||e.context.mainElement.contains(t.target)||hide(e)};document.addEventListener("click",s,{capture:!0}),e.context.cleanupHandlers.push(()=>document.removeEventListener("click",s,{capture:!0})),e.onShow&&e.onShow(e)},labels={application:"Calendar",navigation:"Calendar Navigation",arrowNext:{month:"Next month",year:"Next list of years"},arrowPrev:{month:"Previous month",year:"Previous list of years"},month:"Select month, current selected month:",months:"List of months",year:"Select year, current selected year:",years:"List of years",week:"Days of the week",weekNumber:"Numbers of weeks in a year",dates:"Dates in the current month",selectingTime:"Selecting a time ",inputHour:"Hours",inputMinute:"Minutes",rangeHour:"Slider for selecting hours",rangeMinute:"Slider for selecting minutes",btnKeeping:"Switch AM/PM, current position:"},styles={calendar:"vc",controls:"vc-controls",grid:"vc-grid",column:"vc-column",header:"vc-header",headerContent:"vc-header__content",month:"vc-month",year:"vc-year",arrowPrev:"vc-arrow vc-arrow_prev",arrowNext:"vc-arrow vc-arrow_next",wrapper:"vc-wrapper",content:"vc-content",months:"vc-months",monthsMonth:"vc-months__month",years:"vc-years",yearsYear:"vc-years__year",week:"vc-week",weekDay:"vc-week__day",weekNumbers:"vc-week-numbers",weekNumbersTitle:"vc-week-numbers__title",weekNumbersContent:"vc-week-numbers__content",weekNumber:"vc-week-number",dates:"vc-dates",datesRow:"vc-dates__row",date:"vc-date",dateBtn:"vc-date__btn",datePopup:"vc-date__popup",dateRangeTooltip:"vc-date-range-tooltip",time:"vc-time",timeContent:"vc-time__content",timeHour:"vc-time__hour",timeMinute:"vc-time__minute",timeKeeping:"vc-time__keeping",timeRanges:"vc-time__ranges",timeRange:"vc-time__range"};class OptionsCalendar{constructor(){__publicField(this,"type","default"),__publicField(this,"inputMode",!1),__publicField(this,"openOnFocus",!0),__publicField(this,"positionToInput","left"),__publicField(this,"firstWeekday",1),__publicField(this,"monthsToSwitch",1),__publicField(this,"themeAttrDetect","html[data-theme]"),__publicField(this,"locale","en"),__publicField(this,"dateToday","today"),__publicField(this,"dateMin","1970-01-01"),__publicField(this,"dateMax","2470-12-31"),__publicField(this,"displayDateMin"),__publicField(this,"displayDateMax"),__publicField(this,"displayDatesOutside",!0),__publicField(this,"displayDisabledDates",!1),__publicField(this,"displayMonthsCount"),__publicField(this,"disableDates",[]),__publicField(this,"disableAllDates",!1),__publicField(this,"disableDatesPast",!1),__publicField(this,"disableDatesGaps",!1),__publicField(this,"disableWeekdays",[]),__publicField(this,"disableToday",!1),__publicField(this,"enableDates",[]),__publicField(this,"enableEdgeDatesOnly",!0),__publicField(this,"enableDateToggle",!0),__publicField(this,"enableWeekNumbers",!1),__publicField(this,"enableMonthChangeOnDayClick",!0),__publicField(this,"enableJumpToSelectedDate",!1),__publicField(this,"selectionDatesMode","single"),__publicField(this,"selectionMonthsMode",!0),__publicField(this,"selectionYearsMode",!0),__publicField(this,"selectionTimeMode",!1),__publicField(this,"selectedDates",[]),__publicField(this,"selectedMonth"),__publicField(this,"selectedYear"),__publicField(this,"selectedHolidays",[]),__publicField(this,"selectedWeekends",[0,6]),__publicField(this,"selectedTime"),__publicField(this,"selectedTheme","system"),__publicField(this,"timeMinHour",0),__publicField(this,"timeMaxHour",23),__publicField(this,"timeMinMinute",0),__publicField(this,"timeMaxMinute",59),__publicField(this,"timeControls","all"),__publicField(this,"timeStepHour",1),__publicField(this,"timeStepMinute",1),__publicField(this,"sanitizerHTML",e=>e),__publicField(this,"onClickDate"),__publicField(this,"onClickWeekDay"),__publicField(this,"onClickWeekNumber"),__publicField(this,"onClickTitle"),__publicField(this,"onClickMonth"),__publicField(this,"onClickYear"),__publicField(this,"onClickArrow"),__publicField(this,"onChangeTime"),__publicField(this,"onChangeToInput"),__publicField(this,"onCreateDateRangeTooltip"),__publicField(this,"onCreateDateEls"),__publicField(this,"onCreateMonthEls"),__publicField(this,"onCreateYearEls"),__publicField(this,"onInit"),__publicField(this,"onUpdate"),__publicField(this,"onDestroy"),__publicField(this,"onShow"),__publicField(this,"onHide"),__publicField(this,"popups",{}),__publicField(this,"labels",__spreadValues({},labels)),__publicField(this,"layouts",{default:"",multiple:"",month:"",year:""}),__publicField(this,"styles",__spreadValues({},styles))}}const _Calendar=class e extends OptionsCalendar{constructor(t,n){var s;super(),__publicField(this,"init",()=>init(this)),__publicField(this,"update",e=>update(this,e)),__publicField(this,"destroy",()=>destroy(this)),__publicField(this,"show",()=>show(this)),__publicField(this,"hide",()=>hide(this)),__publicField(this,"set",(e,t)=>set(this,e,t)),__publicField(this,"context"),this.context=__spreadProps(__spreadValues({},this.context),{locale:{months:{short:[],long:[]},weekdays:{short:[],long:[]}}}),setContext(this,"mainElement","string"==typeof t?null!=(s=e.memoizedElements.get(t))?s:this.queryAndMemoize(t):t),n&&replaceProperties(this,n)}queryAndMemoize(t){const n=document.querySelector(t);if(!n)throw new Error(errorMessages.notFoundSelector(t));return e.memoizedElements.set(t,n),n}};__publicField(_Calendar,"memoizedElements",new Map);let Calendar=_Calendar;const NAME$f="datepicker",DATA_KEY$b="bs.datepicker",EVENT_KEY$c=`.${DATA_KEY$b}`,DATA_API_KEY$7=".data-api",EVENT_CHANGE$2=`change${EVENT_KEY$c}`,EVENT_SHOW$4=`show${EVENT_KEY$c}`,EVENT_SHOWN$3=`shown${EVENT_KEY$c}`,EVENT_HIDE$3=`hide${EVENT_KEY$c}`,EVENT_HIDDEN$5=`hidden${EVENT_KEY$c}`,EVENT_CLICK_DATA_API$3=`click${EVENT_KEY$c}.data-api`,EVENT_FOCUSIN_DATA_API=`focusin${EVENT_KEY$c}.data-api`,SELECTOR_DATA_TOGGLE$6='[data-bs-toggle="datepicker"]',HIDE_DELAY=100,Default$e={datepickerTheme:null,dateMin:null,dateMax:null,dateFormat:null,displayElement:null,displayMonthsCount:1,firstWeekday:1,inline:!1,locale:"default",positionElement:null,selectedDates:[],selectionMode:"single",placement:"left",vcpOptions:{}},DefaultType$e={datepickerTheme:"(null|string)",dateMin:"(null|string|number|object)",dateMax:"(null|string|number|object)",dateFormat:"(null|object|function)",displayElement:"(null|string|element|boolean)",displayMonthsCount:"number",firstWeekday:"number",inline:"boolean",locale:"string",positionElement:"(null|string|element)",selectedDates:"array",selectionMode:"string",placement:"string",vcpOptions:"object"};class Datepicker extends BaseComponent{constructor(e,t){super(e,t),this._calendar=null,this._isShown=!1,this._initCalendar()}static get Default(){return Default$e}static get DefaultType(){return DefaultType$e}static get NAME(){return NAME$f}toggle(){if(!this._config.inline)return this._isShown?this.hide():this.show()}show(){this._config.inline||!this._calendar||isDisabled(this._element)||this._isShown||EventHandler.trigger(this._element,EVENT_SHOW$4).defaultPrevented||(this._calendar.show(),this._isShown=!0,EventHandler.trigger(this._element,EVENT_SHOWN$3))}hide(){this._config.inline||this._calendar&&this._isShown&&(EventHandler.trigger(this._element,EVENT_HIDE$3).defaultPrevented||(this._calendar.hide(),this._isShown=!1,EventHandler.trigger(this._element,EVENT_HIDDEN$5)))}dispose(){this._themeObserver&&(this._themeObserver.disconnect(),this._themeObserver=null),this._calendar&&this._calendar.destroy(),this._calendar=null,super.dispose()}getSelectedDates(){const e=this._calendar?.context?.selectedDates;return e?[...e]:[]}setSelectedDates(e){this._calendar&&this._calendar.set({selectedDates:e})}_initCalendar(){this._isInput="INPUT"===this._element.tagName,this._isInline=this._config.inline,this._isInline&&!this._isInput&&(this._boundInput=this._element.querySelector('input[type="hidden"], input[name]')),this._positionElement=this._resolvePositionElement(),this._displayElement=this._resolveDisplayElement();const e=this._buildCalendarOptions();this._calendar=new Calendar(this._positionElement,e),this._calendar.init(),this._setupThemeObserver(),this._isInput&&this._element.value&&this._parseInputValue(),this._updateDisplayWithSelectedDates()}_updateDisplayWithSelectedDates(){const{selectedDates:e}=this._config;if(!e||0===e.length)return;const t=this._formatDateForInput(e);this._isInput&&(this._element.value=t),this._boundInput&&(this._boundInput.value=e.join(",")),this._displayElement&&(this._displayElement.textContent=t)}_resolvePositionElement(){let{positionElement:e}=this._config;if("string"==typeof e&&(e=document.querySelector(e)),!e&&this._isInput&&!this._isInline){const t=this._element.closest(".form-adorn");t&&(e=t)}return e||this._element}_resolveDisplayElement(){const{displayElement:e}=this._config;return"string"==typeof e?document.querySelector(e):!0===e||null===e&&!this._isInput&&!this._isInline?this._element.querySelector("[data-bs-datepicker-display]")||this._element:e}_getThemeAncestor(){return this._element.closest("[data-bs-theme]")}_getEffectiveTheme(){const{datepickerTheme:e}=this._config;if(e)return e;const t=this._getThemeAncestor();return t?.getAttribute("data-bs-theme")||null}_syncThemeAttribute(e){if(!e)return;const t=this._getEffectiveTheme();t?e.setAttribute("data-bs-theme",t):e.removeAttribute("data-bs-theme")}_setupThemeObserver(){const e=this._getThemeAncestor();e&&!this._config.datepickerTheme&&(this._themeObserver=new MutationObserver(()=>{this._syncThemeAttribute(this._calendar?.context?.mainElement)}),this._themeObserver.observe(e,{attributes:!0,attributeFilter:["data-bs-theme"]}))}_buildCalendarOptions(){const e=this._getEffectiveTheme(),t=e&&"auto"!==e?e:"system",n={...this._config.vcpOptions,inputMode:!this._isInline,positionToInput:this._config.placement,firstWeekday:this._config.firstWeekday,locale:this._config.locale,selectionDatesMode:this._config.selectionMode,selectedDates:this._config.selectedDates,displayMonthsCount:this._config.displayMonthsCount,type:this._config.displayMonthsCount>1?"multiple":"default",selectedTheme:t,themeAttrDetect:"[data-bs-theme]",onClickDate:(e,t)=>this._handleDateClick(e,t),onInit:e=>{this._syncThemeAttribute(e.context.mainElement)},onShow:()=>{this._isShown=!0,this._syncThemeAttribute(this._calendar.context.mainElement)},onHide:()=>{this._isShown=!1}};if(this._config.selectedDates.length>0){const e=this._parseDate(this._config.selectedDates[0]);n.selectedMonth=e.getMonth(),n.selectedYear=e.getFullYear()}return this._config.dateMin&&(n.dateMin=this._config.dateMin),this._config.dateMax&&(n.dateMax=this._config.dateMax),n}_handleDateClick(e,t){const n=[...e.context.selectedDates];if(n.length>0){const e=this._formatDateForInput(n);this._isInput&&(this._element.value=e),this._boundInput&&(this._boundInput.value=n.join(",")),this._displayElement&&(this._displayElement.textContent=e)}EventHandler.trigger(this._element,EVENT_CHANGE$2,{dates:n,event:t}),this._maybeHideAfterSelection(n)}_maybeHideAfterSelection(e){this._isInline||("single"===this._config.selectionMode&&e.length>0||"multiple-ranged"===this._config.selectionMode&&e.length>=2)&&setTimeout(()=>this.hide(),100)}_parseDate(e){const[t,n,s]=e.split("-");return new Date(t,n-1,s)}_formatDate(e){const t=this._parseDate(e),n="default"===this._config.locale?void 0:this._config.locale,{dateFormat:s}=this._config;return"function"==typeof s?s(t,n):s&&"object"==typeof s?new Intl.DateTimeFormat(n,s).format(t):t.toLocaleDateString(n)}_formatDateForInput(e){if(0===e.length)return"";if(1===e.length)return this._formatDate(e[0]);const t="multiple-ranged"===this._config.selectionMode?" – ":", ";return e.map(e=>this._formatDate(e)).join(t)}_parseInputValue(){const e=this._element.value.trim();if(!e)return;const t=new Date(e);if(!Number.isNaN(t.getTime())){const e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;this._calendar.set({selectedDates:[e]})}}}EventHandler.on(document,EVENT_CLICK_DATA_API$3,SELECTOR_DATA_TOGGLE$6,function(e){"INPUT"!==this.tagName&&"true"!==this.dataset.bsInline&&(e.preventDefault(),Datepicker.getOrCreateInstance(this).toggle())}),EventHandler.on(document,EVENT_FOCUSIN_DATA_API,SELECTOR_DATA_TOGGLE$6,function(){"INPUT"===this.tagName&&Datepicker.getOrCreateInstance(this).show()}),EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$c}.data-api`,()=>{for(const e of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`))Datepicker.getOrCreateInstance(e)});const CLASS_NAME_OPEN="dialog-open";class DialogBase extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._openedAsModal=!1,this._addDialogListeners()}static get NAME(){return"dialogbase"}toggle(e){return this._element.open?this.hide():this.show(e)}show(e){if(this._element.open||this._isTransitioning)return;if(EventHandler.trigger(this._element,this.constructor.eventName("show"),{relatedTarget:e}).defaultPrevented)return;this._isTransitioning=!0,this._onBeforeShow();const{modal:t,preventBodyScroll:n}=this._getShowOptions();this._showElement({modal:t,preventBodyScroll:n}),this._queueCallback(()=>{this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("shown"),{relatedTarget:e})},this._element,this._isAnimated())}hide(){this._element.open&&!this._isTransitioning&&(EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented||(this._isTransitioning=!0,this._hideElement(),this._queueCallback(()=>{this._element.open&&this._closeAndCleanup(),this._element.classList.remove("hiding"),this._onAfterHide(),this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("hidden"))},this._element,this._isAnimated())))}dispose(){this._element.open&&this._closeAndCleanup(),super.dispose()}_getShowOptions(){return{modal:!0,preventBodyScroll:!0}}_onBeforeShow(){}_onAfterHide(){}_isAnimated(){return!this._element.classList.contains(this._getInstantClassName())}_getInstantClassName(){return"dialog-instant"}_getStaticClassName(){return"dialog-static"}_onCancel(){}_showElement({modal:e=!0,preventBodyScroll:t=!0}={}){this._openedAsModal=e,e?this._element.showModal():this._element.show(),t&&document.documentElement.classList.add("dialog-open")}_hideElement(){this._hideChildComponents(),this._element.classList.add("hiding"),this._shouldDeferClose()||this._closeAndCleanup()}_closeAndCleanup(){this._element.close(),this._openedAsModal=!1,document.querySelector("dialog[open]:modal")||document.documentElement.classList.remove("dialog-open")}_shouldDeferClose(){return!1}_triggerBackdropTransition(){if(EventHandler.trigger(this._element,this.constructor.eventName("hidePrevented")).defaultPrevented)return;const e=this._getStaticClassName();this._element.classList.add(e),this._queueCallback(()=>{this._element.classList.remove(e)},this._element)}_hideChildComponents(){for(const e of SelectorEngine.find('[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]',this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}for(const e of SelectorEngine.find(".toast.show",this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}}_addDialogListeners(){const e=this.constructor.EVENT_KEY;EventHandler.on(this._element,"cancel",e=>{e.preventDefault(),this._config.keyboard?(this._onCancel(),this.hide()):this._triggerBackdropTransition()}),EventHandler.on(this._element,`keydown${e}`,e=>{"Escape"!==e.key||this._openedAsModal||(e.preventDefault(),this._config.keyboard&&(this._onCancel(),this.hide()))}),EventHandler.on(this._element,`click${e}`,e=>{e.target===this._element&&this._openedAsModal&&("static"!==this._config.backdrop?this.hide():this._triggerBackdropTransition())})}}const NAME$e="dialog",DATA_KEY$a="bs.dialog",EVENT_KEY$b=`.${DATA_KEY$a}`,DATA_API_KEY$6=".data-api",EVENT_SHOW$3=`show${EVENT_KEY$b}`,EVENT_HIDDEN$4=`hidden${EVENT_KEY$b}`,EVENT_CANCEL=`cancel${EVENT_KEY$b}`,EVENT_CLICK_DATA_API$2=`click${EVENT_KEY$b}.data-api`,CLASS_NAME_NONMODAL="dialog-nonmodal",CLASS_NAME_INSTANT="dialog-instant",CLASS_NAME_SWAP_IN="dialog-swap-in",SELECTOR_DATA_TOGGLE$5='[data-bs-toggle="dialog"]',Default$d={backdrop:!0,keyboard:!0,modal:!0},DefaultType$d={backdrop:"(boolean|string)",keyboard:"boolean",modal:"boolean"};class Dialog extends DialogBase{static get Default(){return Default$d}static get DefaultType(){return DefaultType$d}static get NAME(){return NAME$e}handleUpdate(){}_getShowOptions(){return{modal:this._config.modal,preventBodyScroll:this._config.modal}}_onBeforeShow(){this._config.modal||this._element.classList.add("dialog-nonmodal")}_onAfterHide(){this._element.classList.remove("dialog-nonmodal")}_shouldDeferClose(){return this._isAnimated()}_onCancel(){EventHandler.trigger(this._element,EVENT_CANCEL)}}EventHandler.on(document,EVENT_CLICK_DATA_API$2,SELECTOR_DATA_TOGGLE$5,function(e){const t=SelectorEngine.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&e.preventDefault(),EventHandler.one(t,EVENT_SHOW$3,e=>{e.defaultPrevented||EventHandler.one(t,EVENT_HIDDEN$4,()=>{isVisible(this)&&this.focus({preventScroll:!0})})});const n=Manipulator.getDataAttributes(this),s=this.closest("dialog[open]");if(s&&s!==t){const e=Dialog.getOrCreateInstance(t,n);t.classList.add("dialog-swap-in"),e.show(this),EventHandler.one(t,`shown${EVENT_KEY$b}`,()=>{t.classList.remove("dialog-swap-in")});const i=Dialog.getInstance(s);return void(i&&(s.classList.add("dialog-instant"),EventHandler.one(s,EVENT_HIDDEN$4,()=>{s.classList.remove("dialog-instant")}),i.hide()))}Dialog.getOrCreateInstance(t,n).toggle(this)}),enableDismissTrigger(Dialog);const NAME$d="navoverflow",DATA_KEY$9="bs.navoverflow",EVENT_KEY$a=`.${DATA_KEY$9}`,EVENT_UPDATE=`update${EVENT_KEY$a}`,EVENT_OVERFLOW=`overflow${EVENT_KEY$a}`,CLASS_NAME_OVERFLOW="nav-overflow",CLASS_NAME_OVERFLOW_MENU="nav-overflow-menu",CLASS_NAME_HIDDEN="d-none",SELECTOR_NAV_ITEM=".nav-item",SELECTOR_NAV_LINK=".nav-link",SELECTOR_OVERFLOW_TOGGLE=".nav-overflow-toggle",SELECTOR_OVERFLOW_MENU=".nav-overflow-menu",SELECTOR_CUSTOM_ICON="[data-bs-overflow-icon]",CLASS_NAME_KEEP="nav-overflow-keep",Default$c={collapseBelow:0,iconPlacement:"start",menuPlacement:"bottom-end",moreText:"More",moreIcon:'',threshold:0},DefaultType$c={collapseBelow:"(number|string)",iconPlacement:"string",menuPlacement:"string",moreText:"string",moreIcon:"string",threshold:"number"};class NavOverflow extends BaseComponent{constructor(e,t){super(e,t),this._items=[],this._overflowItems=[],this._overflowMenu=null,this._overflowToggle=null,this._resizeObserver=null,this._collapseBelow=0,this._isInitialized=!1,this._init()}static get Default(){return Default$c}static get DefaultType(){return DefaultType$c}static get NAME(){return NAME$d}update(){this._calculateOverflow(),EventHandler.trigger(this._element,EVENT_UPDATE)}dispose(){this._resizeObserver&&this._resizeObserver.disconnect(),this._restoreItems(),this._overflowToggle&&this._overflowToggle.parentElement&&this._overflowToggle.parentElement.remove(),super.dispose()}_init(){this._element.classList.add("nav-overflow"),this._items=[...SelectorEngine.find(".nav-item",this._element)];for(const[e,t]of this._items.entries())t.dataset.bsNavOrder=e;this._collapseBelow=this._resolveCollapseBelow(),this._createOverflowMenu(),this._setupResizeObserver(),this._calculateOverflow(),this._isInitialized=!0}_createOverflowMenu(){if(this._overflowToggle=SelectorEngine.findOne(".nav-overflow-toggle",this._element),this._overflowToggle)return void(this._overflowMenu=SelectorEngine.findOne(".nav-overflow-menu",this._element));const e=`${this._resolveIcon()}`,t=`${this._config.moreText}`,n="end"===this._config.iconPlacement?`${t}${e}`:`${e}${t}`,s=document.createElement("li");s.className="nav-item nav-overflow-item",s.innerHTML=`\n \n ${n}\n \n \n `,this._element.append(s),this._overflowToggle=s.querySelector(".nav-overflow-toggle"),this._overflowMenu=s.querySelector(".nav-overflow-menu")}_resolveIcon(){const e=SelectorEngine.findOne(SELECTOR_CUSTOM_ICON,this._element);if(!e)return this._config.moreIcon;const t=e.cloneNode(!0);t.removeAttribute("data-bs-overflow-icon");const n=t.outerHTML;return e.remove(),n}_resolveCollapseBelow(){const e=this._config.collapseBelow;if("number"==typeof e)return e;if("string"==typeof e&&""!==e){const t=getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${e}`);return Number.parseFloat(t)||0}return 0}_setupResizeObserver(){"undefined"!=typeof ResizeObserver?(this._resizeObserver=new ResizeObserver(()=>{this._calculateOverflow()}),this._resizeObserver.observe(this._element)):EventHandler.on(window,"resize",()=>this._calculateOverflow())}_calculateOverflow(){this._restoreItems();const e=this._element.offsetWidth,t=this._overflowToggle?.closest(".nav-item");if(this._collapseBelow>0&&e!e.classList.contains(CLASS_NAME_KEEP));return this._moveToOverflow(e),t&&(e.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),void(e.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:e.length,visibleCount:this._items.length-e.length}))}let n=0;const s=[],i=e-(t?.offsetWidth||0)-this._items.filter(e=>e.classList.contains(CLASS_NAME_KEEP)).reduce((e,t)=>e+t.offsetWidth,0)-10;for(const e of this._items)e.classList.contains(CLASS_NAME_KEEP)||(n+=e.offsetWidth,n>i&&s.push(e));if(this._items.length-s.lengththis._config.threshold){const e=this._items.slice(this._config.threshold).filter(e=>!e.classList.contains(CLASS_NAME_KEEP));s.length=0,s.push(...e)}this._moveToOverflow(s),t&&(s.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),s.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:s.length,visibleCount:this._items.length-s.length})}_moveToOverflow(e){if(this._overflowMenu){this._overflowMenu.innerHTML="",this._overflowItems=[];for(const t of e){const e=SelectorEngine.findOne(".nav-link",t);if(!e)continue;const n=e.cloneNode(!0);n.className="menu-item",e.classList.contains("active")&&n.classList.add("active"),(e.classList.contains("disabled")||e.hasAttribute("disabled"))&&n.classList.add("disabled"),this._overflowMenu.append(n),t.classList.add("d-none"),t.dataset.bsNavOverflow="true",this._overflowItems.push(t)}}}_restoreItems(){for(const e of this._items)e.classList.remove("d-none"),delete e.dataset.bsNavOverflow;this._overflowMenu&&(this._overflowMenu.innerHTML=""),this._overflowItems=[]}}EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find('[data-bs-toggle="nav-overflow"]'))NavOverflow.getOrCreateInstance(e)});const NAME$c="swipe",EVENT_KEY$9=".bs.swipe",EVENT_TOUCHSTART="touchstart.bs.swipe",EVENT_TOUCHMOVE="touchmove.bs.swipe",EVENT_TOUCHEND="touchend.bs.swipe",EVENT_POINTERDOWN="pointerdown.bs.swipe",EVENT_POINTERUP="pointerup.bs.swipe",POINTER_TYPE_TOUCH="touch",POINTER_TYPE_PEN="pen",CLASS_NAME_POINTER_EVENT="pointer-event",SWIPE_THRESHOLD=40,Default$b={endCallback:null,leftCallback:null,rightCallback:null,upCallback:null,downCallback:null},DefaultType$b={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)",upCallback:"(function|null)",downCallback:"(function|null)"};class Swipe extends Config{constructor(e,t){super(),this._element=e,e&&Swipe.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._deltaY=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Default$b}static get DefaultType(){return DefaultType$b}static get NAME(){return NAME$c}dispose(){EventHandler.off(this._element,".bs.swipe")}_start(e){if(!this._supportPointerEvents)return this._deltaX=e.touches[0].clientX,void(this._deltaY=e.touches[0].clientY);this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX,this._deltaY=e.clientY)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX,this._deltaY=e.clientY-this._deltaY),this._handleSwipe(),execute(this._config.endCallback)}_move(e){if(e.touches&&e.touches.length>1)return this._deltaX=0,void(this._deltaY=0);this._deltaX=e.touches[0].clientX-this._deltaX,this._deltaY=e.touches[0].clientY-this._deltaY}_handleSwipe(){const e=Math.abs(this._deltaX),t=Math.abs(this._deltaY);if(t>e&&t>40){const e=this._deltaY>0?"down":"up";return this._deltaX=0,this._deltaY=0,void execute("down"===e?this._config.downCallback:this._config.upCallback)}if(e>40){const t=e/this._deltaX;if(this._deltaX=0,this._deltaY=0,!t)return;return void execute(t>0?this._config.rightCallback:this._config.leftCallback)}this._deltaX=0,this._deltaY=0}_initEvents(){this._supportPointerEvents?(EventHandler.on(this._element,EVENT_POINTERDOWN,e=>this._start(e)),EventHandler.on(this._element,EVENT_POINTERUP,e=>this._end(e)),this._element.classList.add("pointer-event")):(EventHandler.on(this._element,EVENT_TOUCHSTART,e=>this._start(e)),EventHandler.on(this._element,EVENT_TOUCHMOVE,e=>this._move(e)),EventHandler.on(this._element,EVENT_TOUCHEND,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&("pen"===e.pointerType||"touch"===e.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const NAME$b="drawer",DATA_KEY$8="bs.drawer",EVENT_KEY$8=`.${DATA_KEY$8}`,DATA_API_KEY$5=".data-api",EVENT_LOAD_DATA_API$2=`load${EVENT_KEY$8}.data-api`,EVENT_HIDDEN$3=`hidden${EVENT_KEY$8}`,EVENT_RESIZE$1=`resize${EVENT_KEY$8}`,EVENT_CLICK_DATA_API$1=`click${EVENT_KEY$8}.data-api`,SELECTOR_DATA_TOGGLE$4='[data-bs-toggle="drawer"]',Default$a={backdrop:!0,keyboard:!0,scroll:!1},DefaultType$a={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Drawer extends DialogBase{constructor(e,t){super(e,t),this._swipeHelper=null}static get Default(){return Default$a}static get DefaultType(){return DefaultType$a}static get NAME(){return NAME$b}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_getShowOptions(){return{modal:Boolean(this._config.backdrop)||!this._config.scroll,preventBodyScroll:!this._config.scroll}}_onBeforeShow(){this._initSwipe()}_getInstantClassName(){return"drawer-instant"}_getStaticClassName(){return"drawer-static"}_initSwipe(){if(this._swipeHelper||!Swipe.isSupported())return;const e={},t=this._element;t.classList.contains("drawer-bottom")?e.downCallback=()=>this.hide():t.classList.contains("drawer-top")?e.upCallback=()=>this.hide():t.classList.contains("drawer-end")?isRTL$1()?e.leftCallback=()=>this.hide():e.rightCallback=()=>this.hide():isRTL$1()?e.rightCallback=()=>this.hide():e.leftCallback=()=>this.hide(),this._swipeHelper=new Swipe(t,e)}}EventHandler.on(document,EVENT_CLICK_DATA_API$1,SELECTOR_DATA_TOGGLE$4,function(e){const t=SelectorEngine.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this))return;EventHandler.one(t,EVENT_HIDDEN$3,()=>{isVisible(this)&&this.focus({preventScroll:!0})});const n=SelectorEngine.findOne("dialog.drawer[open]");n&&n!==t&&Drawer.getInstance(n).hide(),Drawer.getOrCreateInstance(t).toggle(this)}),EventHandler.on(window,EVENT_LOAD_DATA_API$2,()=>{for(const e of SelectorEngine.find("dialog.drawer[open]"))Drawer.getOrCreateInstance(e).show()}),EventHandler.on(window,EVENT_RESIZE$1,()=>{for(const e of SelectorEngine.find('dialog[open][class*="\\:drawer"]'))"fixed"!==getComputedStyle(e).position&&Drawer.getOrCreateInstance(e).hide()}),enableDismissTrigger(Drawer);const NAME$a="strength",DATA_KEY$7="bs.strength",EVENT_KEY$7=`.${DATA_KEY$7}`,DATA_API_KEY$4=".data-api",EVENT_STRENGTH_CHANGE=`strengthChange${EVENT_KEY$7}`,SELECTOR_DATA_STRENGTH="[data-bs-strength]",STRENGTH_LEVELS=["weak","fair","good","strong"],Default$9={input:null,minLength:8,messages:{weak:"Weak",fair:"Fair",good:"Good",strong:"Strong"},weights:{minLength:1,extraLength:1,lowercase:1,uppercase:1,numbers:1,special:1,multipleSpecial:1,longPassword:1},thresholds:[2,4,6],scorer:null},DefaultType$9={input:"(string|element|null)",minLength:"number",messages:"object",weights:"object",thresholds:"array",scorer:"(function|null)"};class Strength extends BaseComponent{constructor(e,t){super(e,t),this._input=this._getInput(),this._segments=SelectorEngine.find(".strength-segment",this._element),this._textElement=SelectorEngine.findOne(".strength-text",this._element.parentElement),this._currentStrength=null,this._input&&(this._addEventListeners(),this._evaluate())}static get Default(){return Default$9}static get DefaultType(){return DefaultType$9}static get NAME(){return NAME$a}getStrength(){return this._currentStrength}evaluate(){this._evaluate()}_getInput(){if(this._config.input)return"string"==typeof this._config.input?SelectorEngine.findOne(this._config.input):this._config.input;const e=this._element.parentElement;return SelectorEngine.findOne('input[type="password"]',e)}_addEventListeners(){EventHandler.on(this._input,"input",()=>this._evaluate()),EventHandler.on(this._input,"change",()=>this._evaluate())}_evaluate(){const e=this._input.value,t=this._calculateScore(e),n=this._scoreToStrength(t);n!==this._currentStrength&&(this._currentStrength=n,this._updateUI(n,t),EventHandler.trigger(this._element,EVENT_STRENGTH_CHANGE,{strength:n,score:t,password:e.length>0?"***":""}))}_calculateScore(e){if(!e)return 0;if("function"==typeof this._config.scorer)return this._config.scorer(e);const{weights:t}=this._config;let n=0;return e.length>=this._config.minLength&&(n+=t.minLength),e.length>=this._config.minLength+4&&(n+=t.extraLength),/[a-z]/.test(e)&&(n+=t.lowercase),/[A-Z]/.test(e)&&(n+=t.uppercase),/\d/.test(e)&&(n+=t.numbers),/[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.special),/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.multipleSpecial),e.length>=16&&(n+=t.longPassword),n}_scoreToStrength(e){if(0===e)return null;const[t,n,s]=this._config.thresholds;return e<=t?"weak":e<=n?"fair":e<=s?"good":"strong"}_updateUI(e){e?this._element.dataset.bsStrength=e:delete this._element.dataset.bsStrength;const t=e?STRENGTH_LEVELS.indexOf(e):-1;for(const[e,n]of this._segments.entries())e<=t?n.classList.add("active"):n.classList.remove("active");if(this._textElement)if(e&&this._config.messages[e]){this._textElement.textContent=this._config.messages[e],this._textElement.dataset.bsStrength=e;const t={weak:"danger",fair:"warning",good:"info",strong:"success"};this._textElement.style.setProperty("--strength-color",`var(--${t[e]}-text)`)}else this._textElement.textContent="",delete this._textElement.dataset.bsStrength}}EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$7}.data-api`,()=>{for(const e of SelectorEngine.find("[data-bs-strength]"))Strength.getOrCreateInstance(e)});const NAME$9="otpInput",DATA_KEY$6="bs.otpInput",EVENT_KEY$6=`.${DATA_KEY$6}`,DATA_API_KEY$3=".data-api",EVENT_COMPLETE=`complete${EVENT_KEY$6}`,EVENT_INPUT$1=`input${EVENT_KEY$6}`,EVENT_DOMCONTENT_LOADED=`DOMContentLoaded${EVENT_KEY$6}.data-api`,SELECTOR_DATA_OTP="[data-bs-otp]",SELECTOR_INPUT$1="input",SYNC_EVENTS=["blur","keyup","click","select"],CLASS_NAME_INPUT="otp-input",CLASS_NAME_RENDERED="otp-rendered",CLASS_NAME_SLOTS="otp-slots",CLASS_NAME_SLOT="otp-slot",CLASS_NAME_SLOT_FILLED="otp-slot-filled",CLASS_NAME_SLOT_ACTIVE="otp-slot-active",CLASS_NAME_SEPARATOR="otp-separator",MASK_CHARACTER="•",TYPES={numeric:{inputmode:"numeric",pattern:"[0-9]*",filter:/[^0-9]/g},alphanumeric:{inputmode:"text",pattern:"[A-Za-z0-9]*",filter:/[^A-Za-z0-9]/g},alpha:{inputmode:"text",pattern:"[A-Za-z]*",filter:/[^A-Za-z]/g}},Default$8={groups:null,length:null,mask:!1,separator:"·",type:"numeric"},DefaultType$8={groups:"(array|null)",length:"(number|null)",mask:"boolean",separator:"string",type:"string"};class OtpInput extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne("input",this._element),this._input&&(this._type=TYPES[this._config.type]||TYPES.numeric,this._length=this._resolveLength(),this._slots=[],this._setupInput(),this._renderSlots(),this._addEventListeners(),this._render())}static get Default(){return Default$8}static get DefaultType(){return DefaultType$8}static get NAME(){return NAME$9}getValue(){return this._input.value}setValue(e){this._input.value=this._sanitize(String(e)),this._render(),this._checkComplete()}clear(){this._input.value="",this._render(),this._input.focus()}focus(){this._input.focus();const e=this._input.value.length;this._input.setSelectionRange(e,e),this._render()}dispose(){EventHandler.off(this._input,"input",this._onInput),EventHandler.off(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.off(this._input,e,this._onSync);this._slotsContainer?.remove(),this._element.classList.remove("otp-rendered"),super.dispose()}_resolveLength(){if(this._config.length)return this._config.length;const e=Number.parseInt(this._input.getAttribute("maxlength"),10);return Number.isNaN(e)||e<1?6:e}_setupInput(){const e=this._input;"number"!==e.type&&"password"!==e.type||(e.type="text"),e.classList.add("otp-input"),e.setAttribute("maxlength",String(this._length)),e.setAttribute("inputmode",this._type.inputmode),e.setAttribute("pattern",this._type.pattern),e.getAttribute("autocomplete")||e.setAttribute("autocomplete","one-time-code"),e.value&&(e.value=this._sanitize(e.value))}_renderSlots(){const e=document.createElement("div");e.className="otp-slots",e.setAttribute("aria-hidden","true");const{groups:t}=this._config;let n=0,s=0;for(let i=0;i0&&(s++,s===t[n]&&ithis._handleInput(),this._onFocus=()=>this.focus(),this._onSync=()=>this._render(),EventHandler.on(this._input,"input",this._onInput),EventHandler.on(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.on(this._input,e,this._onSync)}_handleInput(){const e=this._sanitize(this._input.value);e!==this._input.value&&(this._input.value=e),this._render(),EventHandler.trigger(this._element,EVENT_INPUT$1,{value:this._input.value}),this._checkComplete()}_sanitize(e){return e.replace(this._type.filter,"").slice(0,this._length)}_render(){const{value:e}=this._input,t=document.activeElement===this._input,n=Math.min(this._input.selectionStart??e.length,this._length-1);for(const[s,i]of this._slots.entries()){const o=e[s]??"";i.textContent=o&&this._config.mask?"•":o,i.classList.toggle("otp-slot-filled",Boolean(o)),i.classList.toggle("otp-slot-active",t&&s===n)}}_checkComplete(){const{value:e}=this._input;e.length===this._length&&EventHandler.trigger(this._element,EVENT_COMPLETE,{value:e})}}EventHandler.on(document,EVENT_DOMCONTENT_LOADED,()=>{for(const e of SelectorEngine.find("[data-bs-otp]"))OtpInput.getOrCreateInstance(e)});const NAME$8="chips",DATA_KEY$5="bs.chips",EVENT_KEY$5=".bs.chips",DATA_API_KEY$2=".data-api",EVENT_ADD="add.bs.chips",EVENT_REMOVE="remove.bs.chips",EVENT_CHANGE$1="change.bs.chips",EVENT_SELECT="select.bs.chips",SELECTOR_DATA_CHIPS="[data-bs-chips]",SELECTOR_GHOST_INPUT=".form-ghost",SELECTOR_CHIP=".chip",SELECTOR_CHIP_DISMISS=".chip-dismiss",CLASS_NAME_CHIP="chip",CLASS_NAME_CHIP_DISMISS="chip-dismiss",CLASS_NAME_ACTIVE$2="active",DEFAULT_DISMISS_ICON='',Default$7={separator:",",allowDuplicates:!1,maxChips:null,placeholder:"",dismissible:!0,dismissIcon:DEFAULT_DISMISS_ICON,createOnBlur:!0},DefaultType$7={separator:"(string|null)",allowDuplicates:"boolean",maxChips:"(number|null)",placeholder:"string",dismissible:"boolean",dismissIcon:"string",createOnBlur:"boolean"};class Chips extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne(".form-ghost",this._element),this._chips=[],this._selectedChips=new Set,this._anchorChip=null,this._input||this._createInput(),this._initializeExistingChips(),this._addEventListeners()}static get Default(){return Default$7}static get DefaultType(){return DefaultType$7}static get NAME(){return NAME$8}add(e){const t=String(e).trim();if(!t)return null;if(!this._config.allowDuplicates&&this._chips.includes(t))return null;if(null!==this._config.maxChips&&this._chips.length>=this._config.maxChips)return null;if(EventHandler.trigger(this._element,EVENT_ADD,{value:t,relatedTarget:this._input}).defaultPrevented)return null;const n=this._createChip(t);return this._element.insertBefore(n,this._input),this._chips.push(t),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),n}remove(e){let t,n;return"string"==typeof e?(n=e,t=this._findChipByValue(n)):(t=e,n=this._getChipValue(t)),!(!t||!n)&&(!EventHandler.trigger(this._element,EVENT_REMOVE,{value:n,chip:t,relatedTarget:this._input}).defaultPrevented&&(this._selectedChips.delete(t),this._anchorChip===t&&(this._anchorChip=null),t.remove(),this._chips=this._chips.filter(e=>e!==n),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),!0))}removeSelected(){const e=[...this._selectedChips];for(const t of e)this.remove(t);this._input?.focus()}getValues(){return[...this._chips]}getSelectedValues(){return[...this._selectedChips].map(e=>this._getChipValue(e))}clear(){const e=SelectorEngine.find(".chip",this._element);for(const t of e)t.remove();this._chips=[],this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:[]})}clearSelection(){for(const e of this._selectedChips)e.classList.remove("active");this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_SELECT,{selected:[]})}selectChip(e,t={}){const{addToSelection:n=!1,rangeSelect:s=!1}=t,i=this._getChipElements();if(i.includes(e)){if(s&&this._anchorChip){const t=i.indexOf(this._anchorChip),s=i.indexOf(e),o=Math.min(t,s),a=Math.max(t,s);n||this.clearSelection();for(let e=o;e<=a;e++)this._selectedChips.add(i[e]),i[e].classList.add("active")}else n?this._selectedChips.has(e)?(this._selectedChips.delete(e),e.classList.remove("active")):(this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e):(this.clearSelection(),this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e);EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}focus(){this._input?.focus()}_getChipElements(){return SelectorEngine.find(".chip",this._element)}_createInput(){const e=document.createElement("input");e.type="text",e.className="form-ghost",this._config.placeholder&&(e.placeholder=this._config.placeholder),this._element.append(e),this._input=e}_initializeExistingChips(){const e=SelectorEngine.find(".chip",this._element);for(const t of e){const e=this._getChipValue(t);e&&(this._chips.push(e),this._setupChip(t))}}_setupChip(e){e.setAttribute("tabindex","0"),this._config.dismissible&&!SelectorEngine.findOne(".chip-dismiss",e)&&e.append(this._createDismissButton())}_createChip(e){const t=document.createElement("span");return t.className="chip",t.dataset.bsChipValue=e,t.append(document.createTextNode(e)),this._setupChip(t),t}_createDismissButton(){const e=document.createElement("button");return e.type="button",e.className="chip-dismiss",e.setAttribute("aria-label","Remove"),e.setAttribute("tabindex","-1"),e.innerHTML=this._config.dismissIcon,e}_findChipByValue(e){return this._getChipElements().find(t=>this._getChipValue(t)===e)}_getChipValue(e){if(e.dataset.bsChipValue)return e.dataset.bsChipValue;const t=e.cloneNode(!0),n=SelectorEngine.findOne(".chip-dismiss",t);return n&&n.remove(),t.textContent?.trim()||""}_addEventListeners(){EventHandler.on(this._input,"keydown",e=>this._handleInputKeydown(e)),EventHandler.on(this._input,"input",e=>this._handleInput(e)),EventHandler.on(this._input,"paste",e=>this._handlePaste(e)),EventHandler.on(this._input,"focus",()=>this.clearSelection()),this._config.createOnBlur&&EventHandler.on(this._input,"blur",e=>{e.relatedTarget?.closest(".chip")||this._createChipFromInput()}),EventHandler.on(this._element,"click",".chip",e=>{if(e.target.closest(".chip-dismiss"))return;const t=e.target.closest(".chip");t&&(e.preventDefault(),this.selectChip(t,{addToSelection:e.metaKey||e.ctrlKey,rangeSelect:e.shiftKey}),t.focus())}),EventHandler.on(this._element,"click",".chip-dismiss",e=>{e.stopPropagation();const t=e.target.closest(".chip");t&&(this.remove(t),this._input?.focus())}),EventHandler.on(this._element,"keydown",".chip",e=>{this._handleChipKeydown(e)}),EventHandler.on(this._element,"click",e=>{e.target===this._element&&(this.clearSelection(),this._input?.focus())})}_handleInputKeydown(e){const{key:t}=e;switch(t){case"Enter":e.preventDefault(),this._createChipFromInput();break;case"Backspace":case"Delete":if(""===this._input.value){e.preventDefault();const t=this._getChipElements();if(t.length>0){const e=t.at(-1);this.selectChip(e),e.focus()}}break;case"ArrowLeft":if(0===this._input.selectionStart&&0===this._input.selectionEnd){e.preventDefault();const t=this._getChipElements();if(t.length>0){const n=t.at(-1);e.shiftKey?this.selectChip(n,{addToSelection:!0}):this.selectChip(n),n.focus()}}break;case"Escape":this._input.value="",this.clearSelection(),this._input.blur()}}_handleChipKeydown(e){const{key:t}=e,n=e.target.closest(".chip");if(!n)return;const s=this._getChipElements(),i=s.indexOf(n);switch(t){case"Backspace":case"Delete":e.preventDefault(),this._handleChipDelete(i,s);break;case"ArrowLeft":e.preventDefault(),this._navigateChip(s,i,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._navigateChip(s,i,1,e.shiftKey);break;case"Home":e.preventDefault(),this._navigateToEdge(s,0,e.shiftKey);break;case"End":case"Escape":e.preventDefault(),this.clearSelection(),this._input?.focus();break;case"a":this._handleSelectAll(e,s)}}_handleChipDelete(e,t){if(0===this._selectedChips.size)return;const n=Math.min(e,t.length-this._selectedChips.size-1);this.removeSelected();const s=this._getChipElements();if(s.length>0){const e=Math.max(0,Math.min(n,s.length-1));s[e].focus(),this.selectChip(s[e])}else this._input?.focus()}_navigateChip(e,t,n,s){const i=t+n;if(n<0&&i>=0){const t=e[i];this.selectChip(t,s?{addToSelection:!0,rangeSelect:!0}:{}),t.focus()}else if(n>0&&i0&&(this.clearSelection(),this._input?.focus())}_navigateToEdge(e,t,n){if(0===e.length)return;const s=e[t];this.selectChip(s,n?{rangeSelect:!0}:{}),s.focus()}_handleSelectAll(e,t){if(e.metaKey||e.ctrlKey){e.preventDefault();for(const e of t)this._selectedChips.add(e),e.classList.add("active");EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}_handleInput(e){const{value:t}=e.target,{separator:n}=this._config;if(n&&t.includes(n)){const e=t.split(n);for(const t of e.slice(0,-1))this.add(t.trim());this._input.value=e.at(-1)}}_handlePaste(e){const{separator:t}=this._config;if(!t)return;const n=(e.clipboardData||window.clipboardData).getData("text");if(n.includes(t)){e.preventDefault();const s=n.split(t);for(const e of s)this.add(e.trim())}}_createChipFromInput(){const e=this._input.value.trim();e&&(this.add(e),this._input.value="")}}EventHandler.on(document,"DOMContentLoaded.bs.chips.data-api",()=>{for(const e of SelectorEngine.find("[data-bs-chips]"))Chips.getOrCreateInstance(e)});const ARIA_ATTRIBUTE_PATTERN=/^aria-[\w-]*$/i,DefaultAllowlist={"*":["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:[]},uriAttributes=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),SAFE_URL_PATTERN=/^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,DATA_URL_PATTERN=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i,allowedAttribute=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!uriAttributes.has(n)||Boolean(SAFE_URL_PATTERN.test(e.nodeValue)||DATA_URL_PATTERN.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))};function sanitizeHtml(e,t,n){if(!e.length)return e;if(n&&"function"==typeof n)return n(e);const s=(new window.DOMParser).parseFromString(e,"text/html"),i=[...s.body.querySelectorAll("*")];for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[...e.attributes],i=[...t["*"]||[],...t[n]||[]];for(const t of s)allowedAttribute(t,i)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const NAME$7="TemplateFactory",Default$6={allowList:DefaultAllowlist,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},DefaultType$6={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},DefaultContentType={entry:"(string|element|function|null)",selector:"(string|element)"};class TemplateFactory extends Config{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Default$6}static get DefaultType(){return DefaultType$6}static get NAME(){return NAME$7}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},DefaultContentType)}_setContent(e,t,n){const s=SelectorEngine.findOne(n,e);s&&((t=this._resolvePossibleFunction(t))?isElement$1(t)?this._putElementInTemplate(getElement(t),s):this._config.html?s.innerHTML=this._maybeSanitize(t):s.textContent=t:s.remove())}_maybeSanitize(e){return this._config.sanitize?sanitizeHtml(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return execute(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML="",void t.append(e);t.textContent=e.textContent}}const NAME$6="tooltip",DISALLOWED_ATTRIBUTES=new Set(["sanitize","allowList","sanitizeFn"]),ESCAPE_KEY="Escape",CLASS_NAME_FADE$2="fade",CLASS_NAME_MODAL="modal",CLASS_NAME_SHOW$2="show",SELECTOR_TOOLTIP_INNER=".tooltip-inner",SELECTOR_MODAL=".modal",SELECTOR_DATA_TOGGLE$3='[data-bs-toggle="tooltip"]',EVENT_MODAL_HIDE="hide.bs.modal",TRIGGER_HOVER="hover",TRIGGER_FOCUS="focus",TRIGGER_CLICK="click",TRIGGER_MANUAL="manual",EVENT_HIDE$2="hide",EVENT_HIDDEN$2="hidden",EVENT_SHOW$2="show",EVENT_SHOWN$2="shown",EVENT_INSERTED="inserted",EVENT_CLICK$3="click",EVENT_FOCUSIN$2="focusin",EVENT_FOCUSOUT$1="focusout",EVENT_MOUSEENTER$1="mouseenter",EVENT_MOUSELEAVE="mouseleave",EVENT_KEYDOWN$1="keydown",AttachmentMap={AUTO:"auto",TOP:"top",RIGHT:isRTL$1()?"left":"right",BOTTOM:"bottom",LEFT:isRTL$1()?"right":"left"},Default$5={allowList:DefaultAllowlist,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",floatingConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},DefaultType$5={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",floatingConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Tooltip extends BaseComponent{constructor(e,t){super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._floatingCleanup=null,this._keydownHandler=null,this._templateFactory=null,this._newContent=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this.tip=null,this._parseResponsivePlacements(),this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Default$5}static get DefaultType(){return DefaultType$5}static get NAME(){return NAME$6}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),this._removeEscapeListener(),EventHandler.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposeFloating(),this._disposeMediaQueryListeners(),super.dispose()}async show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const e=EventHandler.trigger(this._element,this.constructor.eventName("show")),t=(findShadowRoot(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return void(this._isHovered=!1);this._disposeFloating();const n=this._getTipElement();this._element.setAttribute("aria-describedby",n.getAttribute("id"));let{container:s}=this._config;const i=this._element.closest("dialog[open]");if(i&&s===document.body&&(s=i),this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(n),EventHandler.trigger(this._element,this.constructor.eventName("inserted"))),await this._createFloating(n),n.classList.add("show"),this._setEscapeListener(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._queueCallback(()=>{EventHandler.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._removeEscapeListener(),this._getTipElement().classList.remove("show"),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposeFloating(),this._element.removeAttribute("aria-describedby"),EventHandler.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._floatingCleanup&&this.tip&&this._updateFloatingPosition()}_isWithContent(){return Boolean(this._getTitle())||this._hasNewContent()}_hasNewContent(){return Boolean(this._newContent)&&Object.values(this._newContent).some(Boolean)}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();t.classList.remove("fade","show"),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=getUID(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add("fade"),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposeFloating(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new TemplateFactory({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[SELECTOR_TOOLTIP_INNER]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains("fade")}_isShown(){return this.tip&&this.tip.classList.contains("show")}_getPlacement(e){if(this._responsivePlacements){const e=getResponsivePlacement(this._responsivePlacements,"top");return AttachmentMap[e.toUpperCase()]||e}const t=execute(this._config.placement,[this,e,this._element]);return AttachmentMap[t.toUpperCase()]||t}_parseResponsivePlacements(){"string"==typeof this._config.placement?(this._responsivePlacements=parseResponsivePlacement(this._config.placement,"top"),this._responsivePlacements&&this._setupMediaQueryListeners()):this._responsivePlacements=null}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}async _createFloating(e){const t=this._getPlacement(e),n=e.querySelector(`.${this.constructor.NAME}-arrow`);await this._updateFloatingPosition(e,t,n),this._floatingCleanup=autoUpdate(this._element,e,()=>this._updateFloatingPosition(e,null,n))}async _updateFloatingPosition(e=this.tip,t=null,n=null){if(!e)return;t||(t=this._getPlacement(e)),n||(n=e.querySelector(`.${this.constructor.NAME}-arrow`));const s=this._getFloatingMiddleware(n),i=this._getFloatingConfig(t,s),{x:o,y:a,placement:l,middlewareData:r}=await computePosition(this._element,e,i);if(Object.assign(e.style,{position:"absolute",left:`${o}px`,top:`${a}px`}),n&&(n.style.position="absolute"),Manipulator.setDataAttribute(e,"placement",l),n&&r.arrow){const{x:e,y:t}=r.arrow,s=l.startsWith("top")||l.startsWith("bottom");Object.assign(n.style,{left:s&&null!==e?`${e}px`:"",top:s||null===t?"":`${t}px`,right:"",bottom:""})}}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_resolvePossibleFunction(e){return execute(e,[this._element,this._element])}_getFloatingMiddleware(e){const t=this._getOffset(),n=[offset("function"==typeof t?t:{mainAxis:t[1]||0,crossAxis:t[0]||0}),flip({fallbackPlacements:this._config.fallbackPlacements}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})];return e&&n.push(arrow({element:e})),n}_getFloatingConfig(e,t){const n={placement:e,middleware:t};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_setListeners(){const e=this._config.trigger.split(" ");for(const t of e)if("click"===t)EventHandler.on(this._element,this.constructor.eventName("click"),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger.click=!(t._isShown()&&t._activeTrigger.click),t.toggle()});else if("manual"!==t){const e="hover"===t?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),n="hover"===t?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");EventHandler.on(this._element,e,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusin"===e.type?"focus":"hover"]=!0,t._enter()}),EventHandler.on(this._element,n,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusout"===e.type?"focus":"hover"]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},EventHandler.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler)}_setEscapeListener(){this._keydownHandler||(this._keydownHandler=e=>{"Escape"===e.key&&this._isShown()&&this.tip.isConnected&&(e.preventDefault(),e.stopPropagation(),this.hide())},this._element.ownerDocument.addEventListener("keydown",this._keydownHandler,!0))}_removeEscapeListener(){this._keydownHandler&&(this._element.ownerDocument.removeEventListener("keydown",this._keydownHandler,!0),this._keydownHandler=null)}_fixTitle(){const e=this._element.getAttribute("title");e&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=Manipulator.getDataAttributes(this._element);for(const e of Object.keys(t))DISALLOWED_ATTRIBUTES.has(e)&&delete t[e];return e={...t,..."object"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:getElement(e.container),"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),"number"!=typeof e.title&&"boolean"!=typeof e.title||(e.title=e.title.toString()),"number"!=typeof e.content&&"boolean"!=typeof e.content||(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger="manual",e}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null),this.tip&&(this.tip.remove(),this.tip=null)}}const initTooltip=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$3);t&&Tooltip.getOrCreateInstance(t)};EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$3,initTooltip),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$3,initTooltip);const NAME$5="popover",SELECTOR_TITLE=".popover-header",SELECTOR_CONTENT=".popover-body",SELECTOR_DATA_TOGGLE$2='[data-bs-toggle="popover"]',EVENT_CLICK$2="click",EVENT_FOCUSIN$1="focusin",EVENT_MOUSEENTER="mouseenter",Default$4={...Tooltip.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},DefaultType$4={...Tooltip.DefaultType,content:"(null|string|element|function)"};class Popover extends Tooltip{static get Default(){return Default$4}static get DefaultType(){return DefaultType$4}static get NAME(){return NAME$5}_isWithContent(){return Boolean(this._getTitle()||this._getContent())||this._hasNewContent()}_getContentForTemplate(){return{[SELECTOR_TITLE]:this._getTitle(),[SELECTOR_CONTENT]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}}const initPopover=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$2);t&&("click"===e.type&&e.preventDefault(),Popover.getOrCreateInstance(t))};EventHandler.on(document,"click",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$2,initPopover);const NAME$4="range",DATA_KEY$4="bs.range",EVENT_KEY$4=".bs.range",DATA_API_KEY$1=".data-api",EVENT_CHANGED="changed.bs.range",EVENT_DOM_CONTENT_LOADED="DOMContentLoaded.bs.range.data-api",EVENT_INPUT="input",EVENT_CHANGE="change",SELECTOR_RANGE=".form-range",SELECTOR_INPUT=".form-range-input",CLASS_NAME_BUBBLE="form-range-bubble",CLASS_NAME_TICKS="form-range-ticks",CLASS_NAME_TICK="form-range-tick",CLASS_NAME_TICK_LABEL="form-range-tick-label",PROPERTY_FILL="--bs-range-fill",Default$3={bubble:!1,formatter:null},DefaultType$3={bubble:"(boolean|null)",formatter:"(function|null)"};class Range extends BaseComponent{constructor(e,t){super(e,t),this._element&&(this._input=SelectorEngine.findOne(SELECTOR_INPUT,this._element),this._input&&(this._bubble=null,this._bubbleText=null,this._ticks=null,this._updateHandler=()=>this._update(),this._config.bubble&&this._createBubble(),this._createTicks(),this._addEventListeners(),this._update()))}static get Default(){return Default$3}static get DefaultType(){return DefaultType$3}static get NAME(){return NAME$4}update(){this._update()}dispose(){EventHandler.off(this._input,"input",this._updateHandler),EventHandler.off(this._input,"change",this._updateHandler),this._bubble?.remove(),this._ticks?.remove(),super.dispose()}_configAfterMerge(e){return null===e.bubble&&(e.bubble=!0),e}_addEventListeners(){EventHandler.on(this._input,"input",this._updateHandler),EventHandler.on(this._input,"change",this._updateHandler)}_min(){return""===this._input.min?0:Number.parseFloat(this._input.min)}_max(){return""===this._input.max?100:Number.parseFloat(this._input.max)}_value(){return Number.parseFloat(this._input.value)}_ratio(){const e=this._max()-this._min();return e>0?(this._value()-this._min())/e:0}_update(){this._element.style.setProperty(PROPERTY_FILL,`${this._ratio()}`),this._bubbleText&&(this._bubbleText.textContent=this._format(this._value())),EventHandler.trigger(this._input,EVENT_CHANGED,{value:this._value()})}_format(e){return"function"==typeof this._config.formatter?this._config.formatter(e):String(e)}_createBubble(){this._bubble=document.createElement("output"),this._bubble.className=`${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`,this._bubble.setAttribute("aria-hidden","true");const e=document.createElement("div");e.className="tooltip-arrow",this._bubbleText=document.createElement("div"),this._bubbleText.className="tooltip-inner",this._bubble.append(e,this._bubbleText),this._input.insertAdjacentElement("afterend",this._bubble)}_createTicks(){const e=this._input.getAttribute("list"),t=e?document.getElementById(e):null;if(!t)return;const n=this._min(),s=this._max()-n||1,i=[];for(const e of SelectorEngine.find("option",t)){const t=Number.parseFloat(e.value);if(!Number.isNaN(t)){const o=Math.min(Math.max((t-n)/s,0),1);i.push({ratio:o,label:e.label})}}if(0===i.length)return;i.sort((e,t)=>e.ratio-t.ratio),this._ticks=document.createElement("div"),this._ticks.className=CLASS_NAME_TICKS,this._ticks.setAttribute("aria-hidden","true");const o=[0,...i.map(e=>e.ratio),1];this._ticks.style.gridTemplateColumns=o.slice(1).map((e,t)=>e-o[t]+"fr").join(" ");for(const[e,t]of i.entries()){const n=document.createElement("span");if(n.className=CLASS_NAME_TICK,n.style.gridColumnStart=`${e+2}`,t.label){const e=document.createElement("span");e.className=CLASS_NAME_TICK_LABEL,e.textContent=t.label,n.append(e)}this._ticks.append(n)}this._element.append(this._ticks)}}EventHandler.on(document,EVENT_DOM_CONTENT_LOADED,()=>{for(const e of SelectorEngine.find(".form-range"))Range.getOrCreateInstance(e)});const NAME$3="scrollspy",DATA_KEY$3="bs.scrollspy",EVENT_KEY$3=`.${DATA_KEY$3}`,DATA_API_KEY=".data-api",EVENT_ACTIVATE=`activate${EVENT_KEY$3}`,EVENT_CLICK$1=`click${EVENT_KEY$3}`,EVENT_SCROLL=`scroll${EVENT_KEY$3}`,EVENT_SCROLLEND=`scrollend${EVENT_KEY$3}`,EVENT_RESIZE=`resize${EVENT_KEY$3}`,EVENT_LOAD_DATA_API$1=`load${EVENT_KEY$3}.data-api`,CLASS_NAME_MENU_ITEM="menu-item",CLASS_NAME_ACTIVE$1="active",SELECTOR_DATA_SPY='[data-bs-spy="scroll"]',SELECTOR_TARGET_LINKS="[href]",SELECTOR_NAV_LIST_GROUP=".nav, .list-group",SELECTOR_NAV_LINKS=".nav-link",SELECTOR_NAV_ITEMS=".nav-item",SELECTOR_LIST_ITEMS=".list-group-item",SELECTOR_LINK_ITEMS=".nav-link, .nav-item > .nav-link, .list-group-item",SELECTOR_MENU_TOGGLE$1='[data-bs-toggle="menu"]',SCROLL_IDLE_TIMEOUT=100,RESIZE_DEBOUNCE=100,Default$2={rootMargin:null,smoothScroll:!1,target:null,threshold:[0],topMargin:"12%"},DefaultType$2={rootMargin:"(string|null)",smoothScroll:"boolean",target:"element",threshold:"array",topMargin:"string"};class ScrollSpy extends BaseComponent{constructor(e,t){super(e,t),this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map,this._intersecting=new Set,this._activeTarget=null,this._lastActive=null,this._atBottom=!1,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._observer=null,this._sentinel=null,this._sentinelObserver=null,this._pendingNavigation=null,this._settleTimeout=null,this._settleHandler=null,this._scrollIdleHandler=null,this._resizeHandler=null,this._resizeTimeout=null,this.refresh()}static get Default(){return Default$2}static get DefaultType(){return DefaultType$2}static get NAME(){return NAME$3}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e);this._setUpSentinel(),this._maybeAddResizeListener()}dispose(){this._observer?.disconnect(),this._teardownSentinel(),this._disarmSettle(),this._removeResizeListener(),EventHandler.off(this._config.target,EVENT_CLICK$1),super.dispose()}_configAfterMerge(e){return e.target=getElement(e.target)||document.body,"string"==typeof e.threshold&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin??this._getDerivedRootMargin()};return new IntersectionObserver(e=>this._onIntersect(e),e)}_onIntersect(e){for(const t of e)t.isIntersecting?this._intersecting.add(t.target):this._intersecting.delete(t.target);this._computeActive()}_computeActive(){if(!this._element?.isConnected||0===this._sections.length)return;let e=null;if(this._atBottom)e=this._sections.at(-1);else{for(const t of this._sections)this._intersecting.has(t)&&(e=t);e||=this._lastActive??this._sections.at(0)}if(!e)return;this._lastActive=e;const t=this._linkBySection.get(e);t&&this._process(t)}_parseTopMargin(){const e=String(this._config.topMargin);return{value:Number.parseFloat(e)||0,unit:e.endsWith("%")?"%":"px"}}_getDerivedRootMargin(){const{value:e,unit:t}=this._parseTopMargin();let n=e;if("px"===t){const t=this._rootElement?this._rootElement.clientHeight:document.documentElement.clientHeight||window.innerHeight;n=t?e/t*100:12}return`0px 0px -${Math.min(Math.max(100-n,0),100)}% 0px`}_usesPixelMargin(){return!this._config.rootMargin&&"px"===this._parseTopMargin().unit}_setUpSentinel(){if(this._teardownSentinel(),0===this._sections.length)return;const e=document.createElement("div");e.setAttribute("aria-hidden","true"),e.style.cssText="position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;",this._element.append(e),this._sentinel=e,this._sentinelObserver=new IntersectionObserver(e=>this._onSentinel(e),{root:this._rootElement,threshold:[0]}),this._sentinelObserver.observe(e)}_onSentinel(e){const t=e.at(-1);this._atBottom=Boolean(t?.isIntersecting)&&this._isOverflowing(),this._computeActive()}_isOverflowing(){const e=this._rootElement||document.scrollingElement||document.documentElement;return e.scrollHeight>e.clientHeight}_teardownSentinel(){this._sentinelObserver?.disconnect(),this._sentinelObserver=null,this._sentinel?.remove(),this._sentinel=null,this._atBottom=!1}_maybeAddResizeListener(){this._removeResizeListener(),this._usesPixelMargin()&&(this._resizeHandler=()=>{clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(()=>this._rebuildObserver(),100)},EventHandler.on(window,EVENT_RESIZE,this._resizeHandler))}_removeResizeListener(){clearTimeout(this._resizeTimeout),this._resizeTimeout=null,this._resizeHandler&&(EventHandler.off(window,EVENT_RESIZE,this._resizeHandler),this._resizeHandler=null)}_rebuildObserver(){if(this._observer){this._observer.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e)}}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(EventHandler.off(this._config.target,EVENT_CLICK$1),EventHandler.on(this._config.target,EVENT_CLICK$1,"[href]",e=>{const t=e.target.closest("[href]"),n=t&&this._sectionByLink.get(t);if(!n||!this._element)return;e.preventDefault();const s=this._rootElement||window,i=n.offsetTop-this._element.offsetTop,o=this._rootElement?this._rootElement.scrollTop:window.scrollY??window.pageYOffset;if(matchMedia("(prefers-reduced-motion: reduce)").matches||Math.abs(o-i)<=2)return s.scrollTo?s.scrollTo({top:i,behavior:"auto"}):s.scrollTop=i,void this._settleNavigation(t.hash,n);this._pendingNavigation={hash:t.hash,section:n},this._armSettle(),s.scrollTo?s.scrollTo({top:i,behavior:"smooth"}):s.scrollTop=i}))}_armSettle(){this._disarmSettle();const e=this._getSettleTarget();this._settleHandler=()=>this._onSettle(),this._scrollIdleHandler=()=>{clearTimeout(this._settleTimeout),this._settleTimeout=setTimeout(()=>this._onSettle(),100)},EventHandler.on(e,EVENT_SCROLLEND,this._settleHandler),EventHandler.on(e,EVENT_SCROLL,this._scrollIdleHandler)}_disarmSettle(){clearTimeout(this._settleTimeout),this._settleTimeout=null;const e=this._getSettleTarget();this._settleHandler&&(EventHandler.off(e,EVENT_SCROLLEND,this._settleHandler),this._settleHandler=null),this._scrollIdleHandler&&(EventHandler.off(e,EVENT_SCROLL,this._scrollIdleHandler),this._scrollIdleHandler=null)}_getSettleTarget(){return this._rootElement||document}_onSettle(){if(this._disarmSettle(),!this._pendingNavigation)return;const{hash:e,section:t}=this._pendingNavigation;this._settleNavigation(e,t)}_settleNavigation(e,t){this._pendingNavigation=null,window.history?.replaceState&&window.history.replaceState(null,"",e),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.focus({preventScroll:!0})}_initializeTargetsAndObservables(){this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map;const e=SelectorEngine.find("[href]",this._config.target),t=new Set;for(const n of e){if(!n.hash||isDisabled(n))continue;const e=decodeFragment(n.hash.slice(1));if(!e)continue;const s=document.getElementById(e);s&&this._element.contains(s)&&isVisible(s)&&(this._sectionByLink.set(n,s),this._linkBySection.set(s,n),t.has(s)||(t.add(s),this._sections.push(s)))}this._sections.sort((e,t)=>e.getBoundingClientRect().top-t.getBoundingClientRect().top)}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add("active"),this._activateParents(e),EventHandler.trigger(this._element,EVENT_ACTIVATE,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains("menu-item")){const t=e.closest(".menu")?.previousElementSibling;return void(t?.matches(SELECTOR_MENU_TOGGLE$1)&&t.classList.add("active"))}for(const t of SelectorEngine.parents(e,".nav, .list-group"))for(const e of SelectorEngine.prev(t,SELECTOR_LINK_ITEMS))e.classList.add("active")}_clearActiveClass(e){e.classList.remove("active");const t=SelectorEngine.find("[href].active",e);for(const e of t)e.classList.remove("active")}}function decodeFragment(e){try{return decodeURIComponent(e)}catch{return e}}EventHandler.on(window,EVENT_LOAD_DATA_API$1,()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_SPY))ScrollSpy.getOrCreateInstance(e)});const NAME$2="tab",DATA_KEY$2="bs.tab",EVENT_KEY$2=".bs.tab",EVENT_HIDE$1="hide.bs.tab",EVENT_HIDDEN$1="hidden.bs.tab",EVENT_SHOW$1="show.bs.tab",EVENT_SHOWN$1="shown.bs.tab",EVENT_CLICK_DATA_API="click.bs.tab",EVENT_KEYDOWN="keydown.bs.tab",EVENT_LOAD_DATA_API="load.bs.tab",ARROW_LEFT_KEY="ArrowLeft",ARROW_RIGHT_KEY="ArrowRight",ARROW_UP_KEY="ArrowUp",ARROW_DOWN_KEY="ArrowDown",HOME_KEY="Home",END_KEY="End",CLASS_NAME_ACTIVE="active",CLASS_NAME_FADE$1="fade",CLASS_NAME_SHOW$1="show",SELECTOR_MENU_TOGGLE='[data-bs-toggle="menu"]',SELECTOR_MENU=".menu",NOT_SELECTOR_MENU_TOGGLE=`:not(${SELECTOR_MENU_TOGGLE})`,SELECTOR_TAB_PANEL='.list-group, .nav, [role="tablist"]',SELECTOR_OUTER=".nav-item, .list-group-item",SELECTOR_INNER=`.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`,SELECTOR_DATA_TOGGLE$1='[data-bs-toggle="tab"]',SELECTOR_INNER_ELEM=`${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`,SELECTOR_DATA_TOGGLE_ACTIVE='.active[data-bs-toggle="tab"]';class Tab extends BaseComponent{constructor(e){super(e),this._parent=this._element.closest(SELECTOR_TAB_PANEL),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),EventHandler.on(this._element,EVENT_KEYDOWN,e=>this._keydown(e)))}static get NAME(){return"tab"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?EventHandler.trigger(t,EVENT_HIDE$1,{relatedTarget:e}):null;EventHandler.trigger(e,EVENT_SHOW$1,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add("active"),this._activate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleMenu(e,!0),EventHandler.trigger(e,EVENT_SHOWN$1,{relatedTarget:t})):e.classList.add("show")},e,e.classList.contains("fade")))}_deactivate(e,t){e&&(e.classList.remove("active"),e.blur(),this._deactivate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleMenu(e,!1),EventHandler.trigger(e,EVENT_HIDDEN$1,{relatedTarget:t})):e.classList.remove("show")},e,e.classList.contains("fade")))}_keydown(e){if(![ARROW_LEFT_KEY,ARROW_RIGHT_KEY,ARROW_UP_KEY,ARROW_DOWN_KEY,HOME_KEY,END_KEY].includes(e.key))return;if(e.altKey||e.ctrlKey||e.metaKey)return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter(e=>!isDisabled(e));let n;if([HOME_KEY,END_KEY].includes(e.key))n=e.key===HOME_KEY?t[0]:t.at(-1);else{const s=[ARROW_RIGHT_KEY,ARROW_DOWN_KEY].includes(e.key);n=getNextActiveElement(t,e.target,s,!0)}n&&(n.focus({preventScroll:!0}),Tab.getOrCreateInstance(n).show())}_getChildren(){return SelectorEngine.find(SELECTOR_INNER_ELEM,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=SelectorEngine.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleMenu(e,t){const n=this._getOuterElement(e),s=SelectorEngine.findOne(SELECTOR_MENU_TOGGLE,n);if(!s)return;const i=SelectorEngine.findOne(".menu",n);s.classList.toggle("active",t),i&&i.classList.toggle("show",t),s.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains("active")}_getInnerElement(e){return e.matches(SELECTOR_INNER_ELEM)?e:SelectorEngine.findOne(SELECTOR_INNER_ELEM,e)}_getOuterElement(e){return e.closest(SELECTOR_OUTER)||e}}EventHandler.on(document,"click.bs.tab",SELECTOR_DATA_TOGGLE$1,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this)||Tab.getOrCreateInstance(this).show()}),EventHandler.on(window,"load.bs.tab",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE))Tab.getOrCreateInstance(e)});const NAME$1="toast",DATA_KEY$1="bs.toast",EVENT_KEY$1=".bs.toast",EVENT_MOUSEOVER="mouseover.bs.toast",EVENT_MOUSEOUT="mouseout.bs.toast",EVENT_FOCUSIN="focusin.bs.toast",EVENT_FOCUSOUT="focusout.bs.toast",EVENT_HIDE="hide.bs.toast",EVENT_HIDDEN="hidden.bs.toast",EVENT_SHOW="show.bs.toast",EVENT_SHOWN="shown.bs.toast",CLASS_NAME_FADE="fade",CLASS_NAME_HIDE="hide",CLASS_NAME_SHOW="show",CLASS_NAME_SHOWING="showing",DefaultType$1={animation:"boolean",autohide:"boolean",delay:"number"},Default$1={animation:!0,autohide:!0,delay:5e3};class Toast extends BaseComponent{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Default$1}static get DefaultType(){return DefaultType$1}static get NAME(){return NAME$1}show(){EventHandler.trigger(this._element,EVENT_SHOW).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),reflow(this._element),this._element.classList.add("show","showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),EventHandler.trigger(this._element,EVENT_SHOWN),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(EventHandler.trigger(this._element,EVENT_HIDE).defaultPrevented||(this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.add("hide"),this._element.classList.remove("showing","show"),EventHandler.trigger(this._element,EVENT_HIDDEN)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove("show"),super.dispose()}isShown(){return this._element.classList.contains("show")}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":this._hasMouseInteraction=t;break;case"focusin":case"focusout":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){EventHandler.on(this._element,EVENT_MOUSEOVER,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_MOUSEOUT,e=>this._onInteraction(e,!1)),EventHandler.on(this._element,EVENT_FOCUSIN,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_FOCUSOUT,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}}enableDismissTrigger(Toast);const NAME="toggler",DATA_KEY="bs.toggler",EVENT_KEY=`.${DATA_KEY}`,EVENT_TOGGLE=`toggle${EVENT_KEY}`,EVENT_TOGGLED=`toggled${EVENT_KEY}`,EVENT_CLICK="click",SELECTOR_DATA_TOGGLE='[data-bs-toggle="toggler"]',DefaultType={attribute:"string",value:"(string|number|boolean)"},Default={attribute:"class",value:null};class Toggler extends BaseComponent{static get Default(){return Default}static get DefaultType(){return DefaultType}static get NAME(){return NAME}toggle(){EventHandler.trigger(this._element,EVENT_TOGGLE).defaultPrevented||(this._execute(),EventHandler.trigger(this._element,EVENT_TOGGLED))}_execute(){const{attribute:e,value:t}=this._config;"id"!==e&&("class"!==e?this._element.getAttribute(e)!==String(t)?this._element.setAttribute(e,t):this._element.removeAttribute(e):this._element.classList.toggle(t))}}eventActionOnPlugin(Toggler,"click",SELECTOR_DATA_TOGGLE,"toggle");export{Alert,Button,Carousel,Chips,Collapse,Combobox,Datepicker,Dialog,Drawer,Menu,NavOverflow,OtpInput,Popover,Range,ScrollSpy,Strength,Tab,Toast,Toggler,Tooltip}; diff --git a/assets/javascripts/bootstrap.js b/assets/javascripts/bootstrap.js index 59302cc3..3425ed45 100644 --- a/assets/javascripts/bootstrap.js +++ b/assets/javascripts/bootstrap.js @@ -1,4493 +1,7943 @@ /*! - * Bootstrap v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.bootstrap = factory(global.Popper)); -})(this, (function (Popper) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/data.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - /** - * Constants - */ - - const elementMap = new Map(); - const Data = { - set(element, key, instance) { - if (!elementMap.has(element)) { - elementMap.set(element, new Map()); - } - const instanceMap = elementMap.get(element); - - // make it clear we only want one instance per element - // can be removed later when multiple key/instances are fine to be used - if (!instanceMap.has(key) && instanceMap.size !== 0) { - // eslint-disable-next-line no-console - console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); - return; - } - instanceMap.set(key, instance); - }, - get(element, key) { - if (elementMap.has(element)) { - return elementMap.get(element).get(key) || null; - } - return null; - }, - remove(element, key) { - if (!elementMap.has(element)) { - return; - } - const instanceMap = elementMap.get(element); - instanceMap.delete(key); - - // free up element references if there are no instances left for an element - if (instanceMap.size === 0) { - elementMap.delete(element); - } - } - }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/index.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const MAX_UID = 1000000; - const MILLISECONDS_MULTIPLIER = 1000; - const TRANSITION_END = 'transitionend'; - - /** - * Properly escape IDs selectors to handle weird IDs - * @param {string} selector - * @returns {string} - */ - const parseSelector = selector => { - if (selector && window.CSS && window.CSS.escape) { - // document.querySelector needs escaping to handle IDs (html5+) containing for instance / - selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); - } - return selector; - }; - - // Shout-out Angus Croll (https://goo.gl/pxwQGp) - const toType = object => { - if (object === null || object === undefined) { - return `${object}`; - } - return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); - }; - - /** - * Public Util API - */ - - const getUID = prefix => { - do { - prefix += Math.floor(Math.random() * MAX_UID); - } while (document.getElementById(prefix)); - return prefix; - }; - const getTransitionDurationFromElement = element => { - if (!element) { - return 0; - } - - // Get transition-duration of the element - let { - transitionDuration, - transitionDelay - } = window.getComputedStyle(element); - const floatTransitionDuration = Number.parseFloat(transitionDuration); - const floatTransitionDelay = Number.parseFloat(transitionDelay); - - // Return 0 if element or transition duration is not found - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0; - } - - // If multiple durations are defined, take the first - transitionDuration = transitionDuration.split(',')[0]; - transitionDelay = transitionDelay.split(',')[0]; - return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; - }; - const triggerTransitionEnd = element => { - element.dispatchEvent(new Event(TRANSITION_END)); - }; - const isElement = object => { - if (!object || typeof object !== 'object') { - return false; - } - if (typeof object.jquery !== 'undefined') { - object = object[0]; - } - return typeof object.nodeType !== 'undefined'; - }; - const getElement = object => { - // it's a jQuery object or a node element - if (isElement(object)) { - return object.jquery ? object[0] : object; +import { computePosition, autoUpdate, offset, flip, shift, arrow } from '@floating-ui/dom'; +import { Calendar } from 'vanilla-calendar-pro'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const elementMap = new Map(); +const Data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); + + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; } - if (typeof object === 'string' && object.length > 0) { - return document.querySelector(parseSelector(object)); + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; } return null; - }; - const isVisible = element => { - if (!isElement(element) || element.getClientRects().length === 0) { - return false; - } - const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; - // Handle `details` element as its content may falsie appear visible when it is closed - const closedDetails = element.closest('details:not([open])'); - if (!closedDetails) { - return elementIsVisible; - } - if (closedDetails !== element) { - const summary = element.closest('summary'); - if (summary && summary.parentNode !== closedDetails) { - return false; - } - if (summary === null) { - return false; - } - } - return elementIsVisible; - }; - const isDisabled = element => { - if (!element || element.nodeType !== Node.ELEMENT_NODE) { - return true; - } - if (element.classList.contains('disabled')) { - return true; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; } - if (typeof element.disabled !== 'undefined') { - return element.disabled; - } - return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; - }; - const findShadowRoot = element => { - if (!document.documentElement.attachShadow) { - return null; - } - - // Can find the shadow root otherwise it'll return the document - if (typeof element.getRootNode === 'function') { - const root = element.getRootNode(); - return root instanceof ShadowRoot ? root : null; - } - if (element instanceof ShadowRoot) { - return element; + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); - // when we don't find a shadow root - if (!element.parentNode) { - return null; + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); } - return findShadowRoot(element.parentNode); - }; - const noop = () => {}; - - /** - * Trick to restart an element's animation - * - * @param {HTMLElement} element - * @return void - * - * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation - */ - const reflow = element => { - element.offsetHeight; // eslint-disable-line no-unused-expressions - }; - const getjQuery = () => { - if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { - return window.jQuery; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +/** + * Constants + */ + +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll', 'scrollend']); + +/** + * Private methods + */ + +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); } - return null; + return fn.apply(element, [event]); }; - const DOMContentLoadedCallbacks = []; - const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { - // add listener on the first call when the document is in loading state - if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { - for (const callback of DOMContentLoadedCallbacks) { - callback(); - } +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); } - DOMContentLoadedCallbacks.push(callback); - } else { - callback(); - } - }; - const isRTL = () => document.documentElement.dir === 'rtl'; - const defineJQueryPlugin = plugin => { - onDOMContentLoaded(() => { - const $ = getjQuery(); - /* istanbul ignore if */ - if ($) { - const name = plugin.NAME; - const JQUERY_NO_CONFLICT = $.fn[name]; - $.fn[name] = plugin.jQueryInterface; - $.fn[name].Constructor = plugin; - $.fn[name].noConflict = () => { - $.fn[name] = JQUERY_NO_CONFLICT; - return plugin.jQueryInterface; - }; - } - }); - }; - const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { - return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; - }; - const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { - if (!waitForTransition) { - execute(callback); - return; } - const durationPadding = 5; - const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; - let called = false; - const handler = ({ - target - }) => { - if (target !== transitionElement) { - return; - } - called = true; - transitionElement.removeEventListener(TRANSITION_END, handler); - execute(callback); - }; - transitionElement.addEventListener(TRANSITION_END, handler); - setTimeout(() => { - if (!called) { - triggerTransitionEnd(transitionElement); - } - }, emulatedDuration); - }; - - /** - * Return the previous/next element of a list. - * - * @param {array} list The list of elements - * @param activeElement The active element - * @param shouldGetNext Choose to get next or previous element - * @param isCycleAllowed - * @return {Element|elem} The proper element - */ - const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { - const listLength = list.length; - let index = list.indexOf(activeElement); - - // if the element does not exist in the list return an element - // depending on the direction and if cycle is allowed - if (index === -1) { - return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; - } - index += shouldGetNext ? 1 : -1; - if (isCycleAllowed) { - index = (index + listLength) % listLength; - } - return list[Math.max(0, Math.min(index, listLength - 1))]; }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/event-handler.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const namespaceRegex = /[^.]*(?=\..*)\.|.*/; - const stripNameRegex = /\..*/; - const stripUidRegex = /::\d+$/; - const eventRegistry = {}; // Events storage - let uidEvent = 1; - const customEvents = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' - }; - const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); - - /** - * Private methods - */ - - function makeEventUid(element, uid) { - return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; } - function getElementEvents(element) { - const uid = makeEventUid(element); - element.uidEvent = uid; - eventRegistry[uid] = eventRegistry[uid] || {}; - return eventRegistry[uid]; + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; } - function bootstrapHandler(element, fn) { - return function handler(event) { - hydrateObj(event, { - delegateTarget: element - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, fn); - } - return fn.apply(element, [event]); - }; - } - function bootstrapDelegationHandler(element, selector, fn) { - return function handler(event) { - const domElements = element.querySelectorAll(selector); - for (let { - target - } = event; target && target !== this; target = target.parentNode) { - for (const domElement of domElements) { - if (domElement !== target) { - continue; - } - hydrateObj(event, { - delegateTarget: target - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, selector, fn); - } - return fn.apply(target, [event]); + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); } - } + }; }; + callable = wrapFunction(callable); + } + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; } - function findHandler(events, callable, delegationSelector = null) { - return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; } - function normalizeParameters(originalTypeEvent, handler, delegationFunction) { - const isDelegated = typeof handler === 'string'; - // TODO: tooltip passes `false` instead of selector, so we need to check - const callable = isDelegated ? delegationFunction : handler || delegationFunction; - let typeEvent = getTypeEvent(originalTypeEvent); - if (!nativeEvents.has(typeEvent)) { - typeEvent = originalTypeEvent; + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } - return [isDelegated, callable, typeEvent]; } - function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { if (typeof originalTypeEvent !== 'string' || !element) { return; } - let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - - // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position - // this prevents the handler from being dispatched the same way as mouseover or mouseout does - if (originalTypeEvent in customEvents) { - const wrapFunction = fn => { - return function (event) { - if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { - return fn.call(this, event); - } - }; - }; - callable = wrapFunction(callable); - } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; const events = getElementEvents(element); - const handlers = events[typeEvent] || (events[typeEvent] = {}); - const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); - if (previousFunction) { - previousFunction.oneOff = previousFunction.oneOff && oneOff; + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); return; } - const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); - const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); - fn.delegationSelector = isDelegated ? handler : null; - fn.callable = callable; - fn.oneOff = oneOff; - fn.uidEvent = uid; - handlers[uid] = fn; - element.addEventListener(typeEvent, fn, isDelegated); - } - function removeHandler(element, events, typeEvent, handler, delegationSelector) { - const fn = findHandler(events[typeEvent], handler, delegationSelector); - if (!fn) { - return; + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } } - element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); - delete events[typeEvent][fn.uidEvent]; - } - function removeNamespacedHandlers(element, events, typeEvent, namespace) { - const storeElementEvent = events[typeEvent] || {}; - for (const [handlerKey, event] of Object.entries(storeElementEvent)) { - if (handlerKey.includes(namespace)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; } - function getTypeEvent(event) { - // allow to get the native events from namespaced events ('click.bs.button' --> 'click') - event = event.replace(stripNameRegex, ''); - return customEvents[event] || event; - } - const EventHandler = { - on(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, false); - }, - one(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, true); - }, - off(element, originalTypeEvent, handler, delegationFunction) { - if (typeof originalTypeEvent !== 'string' || !element) { - return; - } - const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - const inNamespace = typeEvent !== originalTypeEvent; - const events = getElementEvents(element); - const storeElementEvent = events[typeEvent] || {}; - const isNamespace = originalTypeEvent.startsWith('.'); - if (typeof callable !== 'undefined') { - // Simplest case: handler is passed, remove that listener ONLY. - if (!Object.keys(storeElementEvent).length) { - return; - } - removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); - return; - } - if (isNamespace) { - for (const elementEvent of Object.keys(events)) { - removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); - } - } - for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { - const handlerKey = keyHandlers.replace(stripUidRegex, ''); - if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; } - } - }, - trigger(element, event, args) { - if (typeof event !== 'string' || !element) { - return null; - } - const $ = getjQuery(); - const typeEvent = getTypeEvent(event); - const inNamespace = event !== typeEvent; - let jQueryEvent = null; - let bubbles = true; - let nativeDispatch = true; - let defaultPrevented = false; - if (inNamespace && $) { - jQueryEvent = $.Event(event, args); - $(element).trigger(jQueryEvent); - bubbles = !jQueryEvent.isPropagationStopped(); - nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); - defaultPrevented = jQueryEvent.isDefaultPrevented(); - } - const evt = hydrateObj(new Event(event, { - bubbles, - cancelable: true - }), args); - if (defaultPrevented) { - evt.preventDefault(); - } - if (nativeDispatch) { - element.dispatchEvent(evt); - } - if (evt.defaultPrevented && jQueryEvent) { - jQueryEvent.preventDefault(); - } - return evt; + }); } - }; - function hydrateObj(obj, meta = {}) { - for (const [key, value] of Object.entries(meta)) { - try { - obj[key] = value; - } catch (_unused) { - Object.defineProperty(obj, key, { - configurable: true, - get() { - return value; - } - }); - } + } + return obj; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +function normalizeData(value) { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; } - return obj; + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; } - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/manipulator.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } - function normalizeData(value) { - if (value === 'true') { - return true; - } - if (value === 'false') { + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { return false; } - if (value === Number(value).toString()) { - return Number(value); - } - if (value === '' || value === 'null') { - return null; - } - if (typeof value !== 'string') { - return value; - } - try { - return JSON.parse(decodeURIComponent(value)); - } catch (_unused) { - return value; + if (summary === null) { + return false; } } - function normalizeDataKey(key) { - return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; } - const Manipulator = { - setDataAttribute(element, key, value) { - element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); - }, - removeDataAttribute(element, key) { - element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); - }, - getDataAttributes(element) { - if (!element) { - return {}; - } - const attributes = {}; - const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); - for (const key of bsKeys) { - let pureKey = key.replace(/^bs/, ''); - pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); - attributes[pureKey] = normalizeData(element.dataset[key]); - } - return attributes; - }, - getDataAttribute(element, key) { - return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { + return null; + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const isRTL = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { + return; } + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/config.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Class definition - */ - class Config { - // Getters - static get Default() { - return {}; - } - static get DefaultType() { - return {}; - } - static get NAME() { - throw new Error('You have to implement the static method "NAME", for each component!'); - } - _getConfig(config) { - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - return config; - } - _mergeConfigObj(config, element) { - const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse +/** + * Class definition + */ - return { - ...this.constructor.Default, - ...(typeof jsonConfig === 'object' ? jsonConfig : {}), - ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), - ...(typeof config === 'object' ? config : {}) - }; - } - _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const [property, expectedTypes] of Object.entries(configTypes)) { - const value = config[property]; - const valueType = isElement(value) ? 'element' : toType(value); - if (!new RegExp(expectedTypes).test(valueType)) { - throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); - } +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse + + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); } } } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap base-component.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const VERSION = '5.3.8'; +const VERSION = '6.0.0-alpha1'; - /** - * Class definition - */ +/** + * Class definition + */ - class BaseComponent extends Config { - constructor(element, config) { - super(); - element = getElement(element); - if (!element) { - return; - } - this._element = element; - this._config = this._getConfig(config); - Data.set(this._element, this.constructor.DATA_KEY, this); +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; } + this._element = element; + this._config = this._getConfig(config); - // Public - dispose() { - Data.remove(this._element, this.constructor.DATA_KEY); - EventHandler.off(this._element, this.constructor.EVENT_KEY); - for (const propertyName of Object.getOwnPropertyNames(this)) { - this[propertyName] = null; - } + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); } + Data.set(this._element, this.constructor.DATA_KEY, this); + } - // Private - _queueCallback(callback, element, isAnimated = true) { - executeAfterTransition(callback, element, isAnimated); + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; } - _getConfig(config) { - config = this._mergeConfigObj(config, this._element); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; + } + + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); + + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; } - // Static - static getInstance(element) { - return Data.get(getElement(element), this.DATA_KEY); + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; } - static getOrCreateInstance(element, config = {}) { - return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; + } + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; + } + next = next.nextElementSibling; + } + return []; + }, + focusableChildren(element) { + 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)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); } - static get VERSION() { - return VERSION; + if (isDisabled(this)) { + return; } - static get DATA_KEY() { - return `bs.${this.NAME}`; + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); + + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); } - static get EVENT_KEY() { - return `.${this.DATA_KEY}`; + for (const instance of instances) { + instance[method](); } - static eventName(name) { - return `${name}${this.EVENT_KEY}`; + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event + }); + }); +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$l = 'alert'; +const DATA_KEY$h = 'bs.alert'; +const EVENT_KEY$i = `.${DATA_KEY$h}`; +const EVENT_CLOSE = `close${EVENT_KEY$i}`; +const EVENT_CLOSED = `closed${EVENT_KEY$i}`; +const CLASS_NAME_FADE$4 = 'fade'; +const CLASS_NAME_SHOW$6 = 'show'; + +/** + * Class definition + */ + +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME$l; } - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/selector-engine.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const getSelector = element => { - let selector = element.getAttribute('data-bs-target'); - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href'); + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; + } + this._element.classList.remove(CLASS_NAME_SHOW$6); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$4); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + } - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { - return null; - } + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Alert, 'close'); + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$k = 'button'; +const DATA_KEY$g = 'bs.button'; +const EVENT_KEY$h = `.${DATA_KEY$g}`; +const DATA_API_KEY$c = '.data-api'; +const CLASS_NAME_ACTIVE$4 = 'active'; +const SELECTOR_DATA_TOGGLE$a = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API$8 = `click${EVENT_KEY$h}${DATA_API_KEY$c}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME$k; + } - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}`; - } - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$4)); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$8, SELECTOR_DATA_TOGGLE$a, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE$a); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$j = 'carousel'; +const DATA_KEY$f = 'bs.carousel'; +const EVENT_KEY$g = `.${DATA_KEY$f}`; +const DATA_API_KEY$b = '.data-api'; +const ARROW_LEFT_KEY$2 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$2 = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY$g}`; +const EVENT_SLID = `slid${EVENT_KEY$g}`; +const EVENT_KEYDOWN$2 = `keydown${EVENT_KEY$g}`; +const EVENT_MOUSEENTER$2 = `mouseenter${EVENT_KEY$g}`; +const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$g}`; +const EVENT_POINTERDOWN$1 = `pointerdown${EVENT_KEY$g}`; +const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$g}${DATA_API_KEY$b}`; +const EVENT_CLICK_DATA_API$7 = `click${EVENT_KEY$g}${DATA_API_KEY$b}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE$3 = 'active'; +const CLASS_NAME_FADE$3 = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER$1 = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY$2]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY$2]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default$i = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType$i = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER$1, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); } - return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; - }; - const SelectorEngine = { - find(selector, element = document.documentElement) { - return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); - }, - findOne(selector, element = document.documentElement) { - return Element.prototype.querySelector.call(element, selector); - }, - children(element, selector) { - return [].concat(...element.children).filter(child => child.matches(selector)); - }, - parents(element, selector) { - const parents = []; - let ancestor = element.parentNode.closest(selector); - while (ancestor) { - parents.push(ancestor); - ancestor = ancestor.parentNode.closest(selector); - } - return parents; - }, - prev(element, selector) { - let previous = element.previousElementSibling; - while (previous) { - if (previous.matches(selector)) { - return [previous]; - } - previous = previous.previousElementSibling; - } - return []; - }, - // TODO: this is now unused; remove later along with prev() - next(element, selector) { - let next = element.nextElementSibling; - while (next) { - if (next.matches(selector)) { - return [next]; - } - next = next.nextElementSibling; - } - return []; - }, - focusableChildren(element) { - 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)); - }, - getSelectorFromElement(element) { - const selector = getSelector(element); - if (selector) { - return SelectorEngine.findOne(selector) ? selector : null; - } - return null; - }, - getElementFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.findOne(selector) : null; - }, - getMultipleElementsFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.find(selector) : []; + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default$i; + } + static get DefaultType() { + return DefaultType$i; + } + static get NAME() { + return NAME$j; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); } - }; + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); - /** - * -------------------------------------------------------------------------- - * Bootstrap util/component-functions.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const enableDismissTrigger = (component, method = 'hide') => { - const clickEvent = `click.dismiss${component.EVENT_KEY}`; - const name = component.NAME; - EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; } - if (isDisabled(this)) { + if (rawIndex < 0) { + this._loopTransition(false); return; } - const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); - const instance = component.getOrCreateInstance(target); - - // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method - instance[method](); + } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex }); - }; - - /** - * -------------------------------------------------------------------------- - * Bootstrap alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$f = 'alert'; - const DATA_KEY$a = 'bs.alert'; - const EVENT_KEY$b = `.${DATA_KEY$a}`; - const EVENT_CLOSE = `close${EVENT_KEY$b}`; - const EVENT_CLOSED = `closed${EVENT_KEY$b}`; - const CLASS_NAME_FADE$5 = 'fade'; - const CLASS_NAME_SHOW$8 = 'show'; - - /** - * Class definition - */ - - class Alert extends BaseComponent { - // Getters - static get NAME() { - return NAME$f; + if (slideEvent.defaultPrevented) { + return; } - - // Public - close() { - const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); - if (closeEvent.defaultPrevented) { - return; - } - this._element.classList.remove(CLASS_NAME_SHOW$8); - const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5); - this._queueCallback(() => this._destroyElement(), this._element, isAnimated); + if (this._isFade()) { + this._fadeTo(targetIndex); + return; } - // Private - _destroyElement() { - this._element.remove(); - EventHandler.trigger(this._element, EVENT_CLOSED); - this.dispose(); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Alert.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); - } + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); } - - /** - * Data API implementation - */ - - enableDismissTrigger(Alert, 'close'); - - /** - * jQuery - */ - - defineJQueryPlugin(Alert); - - /** - * -------------------------------------------------------------------------- - * Bootstrap button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$e = 'button'; - const DATA_KEY$9 = 'bs.button'; - const EVENT_KEY$a = `.${DATA_KEY$9}`; - const DATA_API_KEY$6 = '.data-api'; - const CLASS_NAME_ACTIVE$3 = 'active'; - const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="button"]'; - const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`; - - /** - * Class definition - */ - - class Button extends BaseComponent { - // Getters - static get NAME() { - return NAME$e; + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); } - - // Public - toggle() { - // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method - this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3)); + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Button.getOrCreateInstance(this); - if (config === 'toggle') { - data[config](); - } - }); + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => { - event.preventDefault(); - const button = event.target.closest(SELECTOR_DATA_TOGGLE$5); - const data = Button.getOrCreateInstance(button); - data.toggle(); - }); + this._viewport.style.scrollSnapType = ''; - /** - * jQuery - */ - - defineJQueryPlugin(Button); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/swipe.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$d = 'swipe'; - const EVENT_KEY$9 = '.bs.swipe'; - const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; - const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; - const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; - const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; - const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; - const POINTER_TYPE_TOUCH = 'touch'; - const POINTER_TYPE_PEN = 'pen'; - const CLASS_NAME_POINTER_EVENT = 'pointer-event'; - const SWIPE_THRESHOLD = 40; - const Default$c = { - endCallback: null, - leftCallback: null, - rightCallback: null - }; - const DefaultType$c = { - endCallback: '(function|null)', - leftCallback: '(function|null)', - rightCallback: '(function|null)' - }; - - /** - * Class definition - */ - - class Swipe extends Config { - constructor(element, config) { - super(); - this._element = element; - if (!element || !Swipe.isSupported()) { - return; - } - this._config = this._getConfig(config); - this._deltaX = 0; - this._supportPointerEvents = Boolean(window.PointerEvent); - this._initEvents(); - } + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY$g); + super.dispose(); + } - // Getters - static get Default() { - return Default$c; + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default$i.ends; } - static get DefaultType() { - return DefaultType$c; + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN$2, event => this._keydown(event)); } - static get NAME() { - return NAME$d; + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER$2, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); } - // Public - dispose() { - EventHandler.off(this._element, EVENT_KEY$9); + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN$1, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; } - - // Private - _start(event) { - if (!this._supportPointerEvents) { - this._deltaX = event.touches[0].clientX; - return; - } - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX; + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { + this.next(); } } - _end(event) { - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX - this._deltaX; - } - this._handleSwipe(); - execute(this._config.endCallback); + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; } - _move(event) { - this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX; + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); } - _handleSwipe() { - const absDeltaX = Math.abs(this._deltaX); - if (absDeltaX <= SWIPE_THRESHOLD) { - return; - } - const direction = absDeltaX / this._deltaX; - this._deltaX = 0; - if (!direction) { - return; - } - execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; } - _initEvents() { - if (this._supportPointerEvents) { - EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); - EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); - this._element.classList.add(CLASS_NAME_POINTER_EVENT); - } else { - EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); - EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); - EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); - } + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); + } + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; + } + } + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; } - _eventIsPointerPenTouch(event) { - return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); - } - - // Static - static isSupported() { - return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; - } - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$c = 'carousel'; - const DATA_KEY$8 = 'bs.carousel'; - const EVENT_KEY$8 = `.${DATA_KEY$8}`; - const DATA_API_KEY$5 = '.data-api'; - const ARROW_LEFT_KEY$1 = 'ArrowLeft'; - const ARROW_RIGHT_KEY$1 = 'ArrowRight'; - const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - const ORDER_NEXT = 'next'; - const ORDER_PREV = 'prev'; - const DIRECTION_LEFT = 'left'; - const DIRECTION_RIGHT = 'right'; - const EVENT_SLIDE = `slide${EVENT_KEY$8}`; - const EVENT_SLID = `slid${EVENT_KEY$8}`; - const EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`; - const EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`; - const EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`; - const EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`; - const EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; - const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; - const CLASS_NAME_CAROUSEL = 'carousel'; - const CLASS_NAME_ACTIVE$2 = 'active'; - const CLASS_NAME_SLIDE = 'slide'; - const CLASS_NAME_END = 'carousel-item-end'; - const CLASS_NAME_START = 'carousel-item-start'; - const CLASS_NAME_NEXT = 'carousel-item-next'; - const CLASS_NAME_PREV = 'carousel-item-prev'; - const SELECTOR_ACTIVE = '.active'; - const SELECTOR_ITEM = '.carousel-item'; - const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; - const SELECTOR_ITEM_IMG = '.carousel-item img'; - const SELECTOR_INDICATORS = '.carousel-indicators'; - const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; - const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'; - const KEY_TO_DIRECTION = { - [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT, - [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT - }; - const Default$b = { - interval: 5000, - keyboard: true, - pause: 'hover', - ride: false, - touch: true, - wrap: true - }; - const DefaultType$b = { - interval: '(number|boolean)', - // TODO:v6 remove boolean support - keyboard: 'boolean', - pause: '(string|boolean)', - ride: '(boolean|string)', - touch: 'boolean', - wrap: 'boolean' - }; - /** - * Class definition - */ + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); + } + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } - class Carousel extends BaseComponent { - constructor(element, config) { - super(element, config); - this._interval = null; - this._activeElement = null; - this._isSliding = false; - this.touchTimeout = null; - this._swipeHelper = null; - this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); - this._addEventListeners(); - if (this._config.ride === CLASS_NAME_CAROUSEL) { - this.cycle(); - } + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; + } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; + + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; } + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; + } + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; + } - // Getters - static get Default() { - return Default$b; - } - static get DefaultType() { - return DefaultType$b; + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); + } + + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; } - static get NAME() { - return NAME$c; + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE$3); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); + } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); + } + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } - // Public - next() { - this._slide(ORDER_NEXT); + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; } - nextWhenVisible() { - // FIXME TODO use `document.visibilityState` - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && isVisible(this._element)) { - this.next(); - } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE$3, index === this._activeIndex); } - prev() { - this._slide(ORDER_PREV); + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; } - pause() { - if (this._isSliding) { - triggerTransitionEnd(this._element); + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true + }); } - this._clearInterval(); + control.disabled = disabled; } - cycle() { - this._clearInterval(); - this._updateInterval(); - this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval); + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; } - _maybeEnableCycle() { - if (!this._config.ride) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.cycle()); - return; - } - this.cycle(); + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE$3); + active.removeAttribute('aria-current'); } - to(index) { - const items = this._getItems(); - if (index > items.length - 1 || index < 0) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.to(index)); - return; - } - const activeIndex = this._getItemIndex(this._getActive()); - if (activeIndex === index) { - return; - } - const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV; - this._slide(order, items[index]); + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE$3); + newActive.setAttribute('aria-current', 'true'); } - dispose() { - if (this._swipeHelper) { - this._swipeHelper.dispose(); - } - super.dispose(); + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; } - - // Private - _configAfterMerge(config) { - config.defaultInterval = config.interval; - return config; + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; } - _addEventListeners() { - if (this._config.keyboard) { - EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event)); - } - if (this._config.pause === 'hover') { - EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause()); - EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle()); - } - if (this._config.touch && Swipe.isSupported()) { - this._addTouchEventListeners(); - } + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; } - _addTouchEventListeners() { - for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { - EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()); - } - const endCallBack = () => { - if (this._config.pause !== 'hover') { - return; - } + return index; + } - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } - this.pause(); - if (this.touchTimeout) { - clearTimeout(this.touchTimeout); - } - this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval); - }; - const swipeConfig = { - leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)), - rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)), - endCallback: endCallBack - }; - this._swipeHelper = new Swipe(this._element, swipeConfig); - } - _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - const direction = KEY_TO_DIRECTION[event.key]; - if (direction) { - event.preventDefault(); - this._slide(this._directionToOrder(direction)); - } - } - _getItemIndex(element) { - return this._getItems().indexOf(element); - } - _setActiveIndicatorElement(index) { - if (!this._indicatorsElement) { - return; - } - const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); - activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2); - activeIndicator.removeAttribute('aria-current'); - const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); - if (newActiveIndicator) { - newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2); - newActiveIndicator.setAttribute('aria-current', 'true'); - } + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; } - _updateInterval() { - const element = this._activeElement || this._getActive(); - if (!element) { - return; - } - const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10); - this._config.interval = elementInterval || this._config.defaultInterval; + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } - _slide(order, element = null) { - if (this._isSliding) { - return; - } - const activeElement = this._getActive(); - const isNext = order === ORDER_NEXT; - const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap); - if (nextElement === activeElement) { - return; - } - const nextElementIndex = this._getItemIndex(nextElement); - const triggerEvent = eventName => { - return EventHandler.trigger(this._element, eventName, { - relatedTarget: nextElement, - direction: this._orderToDirection(order), - from: this._getItemIndex(activeElement), - to: nextElementIndex - }); - }; - const slideEvent = triggerEvent(EVENT_SLIDE); - if (slideEvent.defaultPrevented) { - return; - } - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - // TODO: change tests that use empty divs to avoid this check + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); return; } - const isCycling = Boolean(this._interval); - this.pause(); - this._isSliding = true; - this._setActiveIndicatorElement(nextElementIndex); - this._activeElement = nextElement; - const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END; - const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV; - nextElement.classList.add(orderClassName); - reflow(nextElement); - activeElement.classList.add(directionalClassName); - nextElement.classList.add(directionalClassName); - const completeCallBack = () => { - nextElement.classList.remove(directionalClassName, orderClassName); - nextElement.classList.add(CLASS_NAME_ACTIVE$2); - activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName); - this._isSliding = false; - triggerEvent(EVENT_SLID); - }; - this._queueCallback(completeCallBack, activeElement, this._isAnimated()); - if (isCycling) { - this.cycle(); - } - } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_SLIDE); - } - _getActive() { - return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + this._scheduleAutoplay(upcoming); + }, interval); + } + + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { + return; } - _getItems() { - return SelectorEngine.find(SELECTOR_ITEM, this._element); + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); + return; } - _clearInterval() { - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { + return; } - _directionToOrder(direction) { - if (isRTL()) { - return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT; - } - return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV; + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); } - _orderToDirection(order) { - if (isRTL()) { - return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE$3); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; } + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Carousel.getOrCreateInstance(this, config); - if (typeof config === 'number') { - data.to(config); - return; - } - if (typeof config === 'string') { - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); + + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API$7, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$i = 'collapse'; +const DATA_KEY$e = 'bs.collapse'; +const EVENT_KEY$f = `.${DATA_KEY$e}`; +const DATA_API_KEY$a = '.data-api'; +const EVENT_SHOW$7 = `show${EVENT_KEY$f}`; +const EVENT_SHOWN$6 = `shown${EVENT_KEY$f}`; +const EVENT_HIDE$6 = `hide${EVENT_KEY$f}`; +const EVENT_HIDDEN$8 = `hidden${EVENT_KEY$f}`; +const EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$f}${DATA_API_KEY$a}`; +const CLASS_NAME_SHOW$5 = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE$9 = '[data-bs-toggle="collapse"]'; +const Default$h = { + parent: null, + toggle: true +}; +const DefaultType$h = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$9); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); } } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$h; + } + static get DefaultType() { + return DefaultType$h; + } + static get NAME() { + return NAME$i; + } - EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); + } + } + show() { + if (this._isTransitioning || this._isShown()) { return; } - event.preventDefault(); - const carousel = Carousel.getOrCreateInstance(target); - const slideIndex = this.getAttribute('data-bs-slide-to'); - if (slideIndex) { - carousel.to(slideIndex); - carousel._maybeEnableCycle(); + let activeChildren = []; + + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); + } + if (activeChildren.length && activeChildren[0]._isTransitioning) { return; } - if (Manipulator.getDataAttribute(this, 'slide') === 'next') { - carousel.next(); - carousel._maybeEnableCycle(); + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$7); + if (startEvent.defaultPrevented) { return; } - carousel.prev(); - carousel._maybeEnableCycle(); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API$3, () => { - const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE); - for (const carousel of carousels) { - Carousel.getOrCreateInstance(carousel); + for (const activeInstance of activeChildren) { + activeInstance.hide(); } - }); - - /** - * jQuery - */ - - defineJQueryPlugin(Carousel); - - /** - * -------------------------------------------------------------------------- - * Bootstrap collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$b = 'collapse'; - const DATA_KEY$7 = 'bs.collapse'; - const EVENT_KEY$7 = `.${DATA_KEY$7}`; - const DATA_API_KEY$4 = '.data-api'; - const EVENT_SHOW$6 = `show${EVENT_KEY$7}`; - const EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`; - const EVENT_HIDE$6 = `hide${EVENT_KEY$7}`; - const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`; - const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`; - const CLASS_NAME_SHOW$7 = 'show'; - const CLASS_NAME_COLLAPSE = 'collapse'; - const CLASS_NAME_COLLAPSING = 'collapsing'; - const CLASS_NAME_COLLAPSED = 'collapsed'; - const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; - const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; - const WIDTH = 'width'; - const HEIGHT = 'height'; - const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; - const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="collapse"]'; - const Default$a = { - parent: null, - toggle: true - }; - const DefaultType$a = { - parent: '(null|element)', - toggle: 'boolean' - }; - - /** - * Class definition - */ - - class Collapse extends BaseComponent { - constructor(element, config) { - super(element, config); + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { this._isTransitioning = false; - this._triggerArray = []; - const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4); - for (const elem of toggleList) { - const selector = SelectorEngine.getSelectorFromElement(elem); - const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); - if (selector !== null && filterElement.length) { - this._triggerArray.push(elem); - } + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN$6); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$5); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); } - this._initializeChildren(); - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN$8); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } + + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW$5); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); + } + } + } + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; + } + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$9, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/floating-ui.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Breakpoints for responsive placement (matches SCSS $breakpoints) + */ +const BREAKPOINTS = { + sm: 576, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536 +}; + +/** + * Parse a placement string that may contain responsive prefixes + * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' } + * + * @param {string} placementString - The placement string to parse + * @param {string} defaultPlacement - The default placement to use for xs/base + * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive + */ +const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => { + // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end") + if (!placementString || !placementString.includes(':')) { + return null; + } + + // Parse the placement string into breakpoint-keyed object + const parts = placementString.split(/\s+/); + const placements = { + xs: defaultPlacement + }; // Default fallback + + for (const part of parts) { + if (part.includes(':')) { + // Responsive placement like "md:top-end" + const [breakpoint, placement] = part.split(':'); + if (BREAKPOINTS[breakpoint] !== undefined) { + placements[breakpoint] = placement; + } + } else { + // Base placement (no prefix = xs/default) + placements.xs = part; + } + } + return placements; +}; + +/** + * Get the active placement for the current viewport width + * + * @param {object} responsivePlacements - Object with breakpoint keys and placement values + * @param {string} defaultPlacement - Fallback placement + * @returns {string} - The active placement for current viewport + */ +const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => { + if (!responsivePlacements) { + return defaultPlacement; + } + + // Get current viewport width + const viewportWidth = window.innerWidth; + + // Find the largest breakpoint that matches + let activePlacement = responsivePlacements.xs || defaultPlacement; + + // Check breakpoints in order (sm, md, lg, xl, 2xl) + const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']; + for (const breakpoint of breakpointOrder) { + const minWidth = BREAKPOINTS[breakpoint]; + if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) { + activePlacement = responsivePlacements[breakpoint]; + } + } + return activePlacement; +}; + +/** + * Create media query listeners for responsive placement changes + * + * @param {Function} callback - Callback to run when breakpoint changes + * @returns {Array} - Array of { mql, handler } objects for cleanup + */ +const createBreakpointListeners = callback => { + const listeners = []; + for (const breakpoint of Object.keys(BREAKPOINTS)) { + const minWidth = BREAKPOINTS[breakpoint]; + const mql = window.matchMedia(`(min-width: ${minWidth}px)`); + mql.addEventListener('change', callback); + listeners.push({ + mql, + handler: callback + }); + } + return listeners; +}; + +/** + * Clean up media query listeners + * + * @param {Array} listeners - Array of { mql, handler } objects + */ +const disposeBreakpointListeners = listeners => { + for (const { + mql, + handler + } of listeners) { + mql.removeEventListener('change', handler); + } +}; + +/** + * -------------------------------------------------------------------------- + * Bootstrap menu.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$h = 'menu'; +const DATA_KEY$d = 'bs.menu'; +const EVENT_KEY$e = `.${DATA_KEY$d}`; +const DATA_API_KEY$9 = '.data-api'; +const ESCAPE_KEY$2 = 'Escape'; +const TAB_KEY$1 = 'Tab'; +const ARROW_UP_KEY$2 = 'ArrowUp'; +const ARROW_DOWN_KEY$2 = 'ArrowDown'; +const ARROW_LEFT_KEY$1 = 'ArrowLeft'; +const ARROW_RIGHT_KEY$1 = 'ArrowRight'; +const HOME_KEY$2 = 'Home'; +const END_KEY$2 = 'End'; +const ENTER_KEY$1 = 'Enter'; +const SPACE_KEY$1 = ' '; +const RIGHT_MOUSE_BUTTON = 2; +const SUBMENU_CLOSE_DELAY = 100; +const EVENT_HIDE$5 = `hide${EVENT_KEY$e}`; +const EVENT_HIDDEN$7 = `hidden${EVENT_KEY$e}`; +const EVENT_SHOW$6 = `show${EVENT_KEY$e}`; +const EVENT_SHOWN$5 = `shown${EVENT_KEY$e}`; +const EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$e}${DATA_API_KEY$9}`; +const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$e}${DATA_API_KEY$9}`; +const CLASS_NAME_SHOW$4 = 'show'; +const SELECTOR_DATA_TOGGLE$8 = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'; +const SELECTOR_MENU$2 = '.menu'; +const SELECTOR_SUBMENU = '.submenu'; +const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'; +const SELECTOR_NAVBAR_NAV = '.navbar-nav'; +const SELECTOR_VISIBLE_ITEMS$1 = '.menu-item:not(.disabled):not(:disabled)'; +const DEFAULT_PLACEMENT = 'bottom-start'; +const SUBMENU_PLACEMENT = 'end-start'; +const resolveLogicalPlacement = placement => { + if (isRTL()) { + return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left'); + } + return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right'); +}; +const triangleSign = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +const Default$g = { + autoClose: true, + boundary: 'clippingParents', + container: false, + display: 'dynamic', + offset: [0, 2], + floatingConfig: null, + menu: null, + placement: DEFAULT_PLACEMENT, + reference: 'toggle', + strategy: 'absolute', + submenuTrigger: 'both', + submenuDelay: SUBMENU_CLOSE_DELAY +}; +const DefaultType$g = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + container: '(string|element|boolean)', + display: 'string', + offset: '(array|string|function)', + floatingConfig: '(null|object|function)', + menu: '(null|element)', + placement: 'string', + reference: '(string|element|object)', + strategy: 'string', + submenuTrigger: 'string', + submenuDelay: 'number' +}; + +/** + * Class definition + */ + +class Menu extends BaseComponent { + static _openInstances = new Set(); + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)'); + } + super(element, config); + this._floatingCleanup = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + this._parent = this._element.parentNode; // menu wrapper + this._openSubmenus = new Map(); + this._submenuCloseTimeouts = new Map(); + this._hoverIntentData = null; + this._menu = this._config.menu || this._findMenu(); + + // When the menu was discovered from the DOM, refine the wrapper to the closest + // ancestor that actually contains it, so the toggle doesn't have to be a direct + // sibling of `.menu` (e.g. when wrapped by web components). The wrapper still + // receives `.show` and acts as the `reference: 'parent'` positioning anchor. + if (!this._config.menu && this._menu) { + this._parent = this._findWrapper(this._menu); + } + this._isSubmenu = this._parent.classList?.contains('submenu'); + this._menuOriginalParent = this._menu?.parentNode; + this._parseResponsivePlacements(); + this._setupSubmenuListeners(); + } + + // Getters + static get Default() { + return Default$g; + } + static get DefaultType() { + return DefaultType$g; + } + static get NAME() { + return NAME$h; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$6, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._moveMenuToContainer(); + this._createFloating(); + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus({ + focusVisible: false + }); + this._element.setAttribute('aria-expanded', 'true'); + this._menu.classList.add(CLASS_NAME_SHOW$4); + this._element.classList.add(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_SHOW$4); + } + Menu._openInstances.add(this); + EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._disposeMediaQueryListeners(); + this._closeAllSubmenus(); + this._clearAllSubmenuTimeouts(); + Menu._openInstances.delete(this); + super.dispose(); + } + update() { + if (this._floatingCleanup) { + this._updateFloatingPosition(); + } + } + + // Private + _findMenu() { + // Fall back to the closest ancestor that contains a menu so the toggle can be + // nested deeper than a direct sibling of `.menu`. + const wrapper = SelectorEngine.closest(this._element, `:has(${SELECTOR_MENU$2})`); + return SelectorEngine.next(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU$2)[0] || SelectorEngine.findOne(SELECTOR_MENU$2, wrapper || this._parent); + } + _findWrapper(menu) { + let wrapper = this._element.parentNode; + while (wrapper instanceof Element && !wrapper.contains(menu)) { + wrapper = wrapper.parentNode; + } + return wrapper instanceof Element ? wrapper : this._element.parentNode; + } + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + this._closeAllSubmenus(); + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._menu.classList.remove(CLASS_NAME_SHOW$4); + this._element.classList.remove(CLASS_NAME_SHOW$4); + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_SHOW$4); + } + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'placement'); + Manipulator.removeDataAttribute(this._menu, 'display'); + Menu._openInstances.delete(this); + EventHandler.trigger(this._element, EVENT_HIDDEN$7, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + throw new TypeError(`${NAME$h.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createFloating() { + if (this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'display', 'static'); + return; + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + this._updateFloatingPosition(referenceElement); + this._floatingCleanup = autoUpdate(referenceElement, this._menu, () => this._updateFloatingPosition(referenceElement)); + } + async _updateFloatingPosition(referenceElement = null) { + if (!this._menu) { + return; + } + if (!referenceElement) { + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } else { + referenceElement = this._element; + } + } + const placement = this._getPlacement(); + const middleware = this._getFloatingMiddleware(); + const floatingConfig = this._getFloatingConfig(placement, middleware); + await this._applyFloatingPosition(referenceElement, this._menu, floatingConfig.placement, floatingConfig.middleware, floatingConfig.strategy); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$4); + } + _getPlacement() { + const placement = this._responsivePlacements ? getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) : this._config.placement; + return resolveLogicalPlacement(placement); + } + _parseResponsivePlacements() { + this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + _getOffset() { + const { + offset: offsetConfig + } = this._config; + if (typeof offsetConfig === 'string') { + return offsetConfig.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offsetConfig === 'function') { + return ({ + placement, + rects + }) => { + const result = offsetConfig({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offsetConfig; + } + _getFloatingMiddleware() { + const offsetValue = this._getOffset(); + const middleware = [offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), flip({ + fallbackPlacements: this._getFallbackPlacements() + }), shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + return middleware; + } + _getFallbackPlacements() { + const placement = this._getPlacement(); + const fallbackMap = { + bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'], + 'bottom-start': ['top-start', 'bottom-end', 'top-end'], + 'bottom-end': ['top-end', 'bottom-start', 'top-start'], + top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'], + 'top-start': ['bottom-start', 'top-end', 'bottom-end'], + 'top-end': ['bottom-end', 'top-start', 'bottom-start'], + right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'], + 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'], + 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'], + left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'], + 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'], + 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end'] + }; + return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware, + strategy: this._config.strategy + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + } + _getContainer() { + const { + container + } = this._config; + if (container === false) { + return null; + } + return container === true ? document.body : getElement(container); + } + _moveMenuToContainer() { + const container = this._getContainer(); + if (!container || !this._menu) { + return; + } + if (this._menu.parentNode !== container) { + container.append(this._menu); + } + } + _restoreMenuToOriginalParent() { + if (!this._menuOriginalParent || !this._menu) { + return; + } + if (this._menu.parentNode !== this._menuOriginalParent) { + this._menuOriginalParent.append(this._menu); + } + } + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { + if (!floating.isConnected) { + return null; + } + const { + x, + y, + placement: finalPlacement + } = await computePosition(reference, floating, { + placement, + middleware, + strategy + }); + if (!floating.isConnected) { + return null; + } + Object.assign(floating.style, { + position: strategy, + left: `${x}px`, + top: `${y}px`, + margin: '0' + }); + Manipulator.setDataAttribute(floating, 'placement', finalPlacement); + return finalPlacement; + } + + // ------------------------------------------------------------------------- + // Submenu handling + // ------------------------------------------------------------------------- + + _setupSubmenuListeners() { + if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerEnter(event); + }); + EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => { + this._onSubmenuLeave(event); + }); + EventHandler.on(this._menu, 'mousemove', event => { + this._trackMousePosition(event); + }); + } + if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerClick(event); + }); + } + } + _onSubmenuTriggerEnter(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + this._cancelSubmenuCloseTimeout(submenu); + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + _onSubmenuLeave(event) { + const submenuWrapper = event.target.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu || !this._openSubmenus.has(submenu)) { + return; + } + if (this._isMovingTowardSubmenu(event, submenu)) { + return; + } + this._scheduleSubmenuClose(submenu, submenuWrapper); + } + _onSubmenuTriggerClick(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + event.preventDefault(); + event.stopPropagation(); + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (!submenu) { + return; + } + if (this._openSubmenus.has(submenu)) { + this._closeSubmenu(submenu, submenuWrapper); + } else { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + } + _openSubmenu(trigger, submenu, submenuWrapper) { + if (this._openSubmenus.has(submenu)) { + return; + } + trigger.setAttribute('aria-expanded', 'true'); + trigger.setAttribute('aria-haspopup', 'true'); + + // Keep the submenu transparent until Floating UI applies the first position, so + // it doesn't flash at its CSS fallback position (top: 0, over the parent menu) + // before being moved into place. `opacity` (unlike `visibility`/`display`) keeps + // the submenu measurable for flip/shift and focusable for keyboard navigation. + submenu.style.opacity = '0'; + submenu.classList.add(CLASS_NAME_SHOW$4); + submenuWrapper.classList.add(CLASS_NAME_SHOW$4); + const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper); + this._openSubmenus.set(submenu, cleanup); + EventHandler.on(submenu, 'mouseenter', () => { + this._cancelSubmenuCloseTimeout(submenu); + }); + } + _closeSubmenu(submenu, submenuWrapper) { + if (!this._openSubmenus.has(submenu)) { + return; + } + const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, submenu); + for (const nested of nestedSubmenus) { + const nestedWrapper = nested.closest(SELECTOR_SUBMENU); + this._closeSubmenu(nested, nestedWrapper); + } + const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper); + const cleanup = this._openSubmenus.get(submenu); + if (cleanup) { + cleanup(); + } + this._openSubmenus.delete(submenu); + EventHandler.off(submenu, 'mouseenter'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + submenu.classList.remove(CLASS_NAME_SHOW$4); + submenuWrapper.classList.remove(CLASS_NAME_SHOW$4); + + // Keep the Floating UI position styles in place while the submenu fades out. + // Clearing them here would let the submenu snap back to its CSS fallback + // (`top: 0`, over the parent menu) for the duration of the close transition, + // causing it to flash over the parent. They get recomputed on the next open + // (and the opacity gate in `_openSubmenu` hides any stale position until then). + submenu.style.opacity = ''; + } + _closeAllSubmenus() { + for (const [submenu] of this._openSubmenus) { + const submenuWrapper = submenu.closest(SELECTOR_SUBMENU); + this._closeSubmenu(submenu, submenuWrapper); + } + } + _closeSiblingSubmenus(currentSubmenuWrapper) { + const parent = currentSubmenuWrapper.parentNode; + const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU$2}.${CLASS_NAME_SHOW$4}`, parent); + for (const siblingMenu of siblingSubmenus) { + const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU); + if (siblingWrapper !== currentSubmenuWrapper) { + this._closeSubmenu(siblingMenu, siblingWrapper); + } + } + } + _createSubmenuFloating(trigger, submenu, submenuWrapper) { + const referenceElement = submenuWrapper; + const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT); + const middleware = [offset({ + mainAxis: 0, + crossAxis: -4 + }), flip({ + fallbackPlacements: [resolveLogicalPlacement('start-start'), resolveLogicalPlacement('end-end'), resolveLogicalPlacement('start-end')] + }), shift({ + padding: 8 + })]; + const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware).then(finalPlacement => { + // Reveal the submenu now that it has been positioned (see `_openSubmenu`); + // clearing the inline opacity lets the CSS fade-in transition take over. + submenu.style.opacity = ''; + return finalPlacement; + }); + updatePosition(); + return autoUpdate(referenceElement, submenu, updatePosition); + } + _scheduleSubmenuClose(submenu, submenuWrapper) { + this._cancelSubmenuCloseTimeout(submenu); + const timeoutId = setTimeout(() => { + this._closeSubmenu(submenu, submenuWrapper); + this._submenuCloseTimeouts.delete(submenu); + }, this._config.submenuDelay); + this._submenuCloseTimeouts.set(submenu, timeoutId); + } + _cancelSubmenuCloseTimeout(submenu) { + const timeoutId = this._submenuCloseTimeouts.get(submenu); + if (timeoutId) { + clearTimeout(timeoutId); + this._submenuCloseTimeouts.delete(submenu); + } + } + _clearAllSubmenuTimeouts() { + for (const timeoutId of this._submenuCloseTimeouts.values()) { + clearTimeout(timeoutId); + } + this._submenuCloseTimeouts.clear(); + } + + // ------------------------------------------------------------------------- + // Hover intent / Safe triangle + // ------------------------------------------------------------------------- + + _trackMousePosition(event) { + this._hoverIntentData = { + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + }; + } + _isMovingTowardSubmenu(event, submenu) { + if (!this._hoverIntentData) { + return false; + } + const submenuRect = submenu.getBoundingClientRect(); + const currentPos = { + x: event.clientX, + y: event.clientY + }; + const lastPos = { + x: this._hoverIntentData.x, + y: this._hoverIntentData.y + }; + const isRtl = isRTL(); + const targetX = isRtl ? submenuRect.right : submenuRect.left; + const topCorner = { + x: targetX, + y: submenuRect.top + }; + const bottomCorner = { + x: targetX, + y: submenuRect.bottom + }; + return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner); + } + _pointInTriangle(point, v1, v2, v3) { + const d1 = triangleSign(point, v1, v2); + const d2 = triangleSign(point, v2, v3); + const d3 = triangleSign(point, v3, v1); + const hasNeg = d1 < 0 || d2 < 0 || d3 < 0; + const hasPos = d1 > 0 || d2 > 0 || d3 > 0; + return !(hasNeg && hasPos); + } + + // ------------------------------------------------------------------------- + // Keyboard navigation + // ------------------------------------------------------------------------- + + _selectMenuItem({ + key, + target + }) { + const currentMenu = target.closest(SELECTOR_MENU$2) || this._menu; + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$2, !items.includes(target)).focus(); + } + _handleSubmenuKeydown(event) { + const { + key, + target + } = event; + const isRtl = isRTL(); + const enterKey = isRtl ? ARROW_LEFT_KEY$1 : ARROW_RIGHT_KEY$1; + const exitKey = isRtl ? ARROW_RIGHT_KEY$1 : ARROW_LEFT_KEY$1; + const submenuWrapper = target.closest(SELECTOR_SUBMENU); + const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE); + if ((key === ENTER_KEY$1 || key === SPACE_KEY$1) && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === enterKey && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU$2, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === exitKey) { + const currentMenu = target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper) { + event.preventDefault(); + event.stopPropagation(); + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + this._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return true; + } + } + if (key === HOME_KEY$2 || key === END_KEY$2) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = target.closest(SELECTOR_MENU$2); + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`, currentMenu).filter(element => isVisible(element)); + if (items.length) { + const targetItem = key === HOME_KEY$2 ? items[0] : items.at(-1); + targetItem.focus(); + } + return true; + } + return false; + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { + return; + } + for (const instance of Menu._openInstances) { + if (instance._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(instance._menu); + if (composedPath.includes(instance._element) || instance._config.autoClose === 'inside' && !isMenuTarget || instance._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Don't auto-close when interacting with a form inside the menu — clicks + // on a form's labels, buttons, etc. (not just inputs) should keep it open. + const formAncestor = event.target.closest?.('form'); + const isInsideMenuForm = Boolean(formAncestor) && instance._menu.contains(formAncestor); + if (instance._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName) || isInsideMenuForm)) { + continue; + } + const relatedTarget = { + relatedTarget: instance._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + instance._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the + // menu doesn't hijack their arrow keys. + const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable; + const isEscapeEvent = event.key === ESCAPE_KEY$2; + const isUpOrDownEvent = [ARROW_UP_KEY$2, ARROW_DOWN_KEY$2].includes(event.key); + const isLeftOrRightEvent = [ARROW_LEFT_KEY$1, ARROW_RIGHT_KEY$1].includes(event.key); + const isHomeOrEndEvent = [HOME_KEY$2, END_KEY$2].includes(event.key); + const isEnterOrSpaceEvent = [ENTER_KEY$1, SPACE_KEY$1].includes(event.key); + const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE); + if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent && !(isEnterOrSpaceEvent && isSubmenuTrigger)) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$8) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$8)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8, event.delegateTarget.parentNode); + if (!getToggleButton) { + return; + } + const instance = Menu.getOrCreateInstance(getToggleButton); + if ((isLeftOrRightEvent || isHomeOrEndEvent || isEnterOrSpaceEvent && isSubmenuTrigger) && instance._handleSubmenuKeydown(event)) { + return; + } + if (isUpOrDownEvent) { + event.preventDefault(); + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (isEscapeEvent && instance._isShown()) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = event.target.closest(SELECTOR_MENU$2); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper && instance._openSubmenus.size > 0) { + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + instance._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return; + } + instance.hide(); + getToggleButton.focus(); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$8, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU$2, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, Menu.clearMenus); +EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_TOGGLE$8, function (event) { + event.preventDefault(); + Menu.getOrCreateInstance(this).toggle(); +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap combobox.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$g = 'combobox'; +const DATA_KEY$c = 'bs.combobox'; +const EVENT_KEY$d = `.${DATA_KEY$c}`; +const DATA_API_KEY$8 = '.data-api'; +const ESCAPE_KEY$1 = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY$1 = 'ArrowUp'; +const ARROW_DOWN_KEY$1 = 'ArrowDown'; +const HOME_KEY$1 = 'Home'; +const END_KEY$1 = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const EVENT_CHANGE$3 = `change${EVENT_KEY$d}`; +const EVENT_SHOW$5 = `show${EVENT_KEY$d}`; +const EVENT_SHOWN$4 = `shown${EVENT_KEY$d}`; +const EVENT_HIDE$4 = `hide${EVENT_KEY$d}`; +const EVENT_HIDDEN$6 = `hidden${EVENT_KEY$d}`; +const EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$d}${DATA_API_KEY$8}`; +const CLASS_NAME_SHOW$3 = 'show'; +const CLASS_NAME_SELECTED = 'selected'; +const CLASS_NAME_PLACEHOLDER = 'combobox-placeholder'; +const SELECTOR_DATA_TOGGLE$7 = '[data-bs-toggle="combobox"]'; +const SELECTOR_MENU$1 = '.menu'; +const SELECTOR_MENU_ITEM = '.menu-item[data-bs-value]'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item[data-bs-value]:not(.disabled):not(:disabled)'; +const SELECTOR_VALUE = '.combobox-value'; +const SELECTOR_SEARCH_INPUT = '.combobox-search-input'; +const SELECTOR_NO_RESULTS = '.combobox-no-results'; +const Default$f = { + boundary: 'clippingParents', + multiple: false, + name: null, + offset: [0, 2], + placeholder: '', + placement: 'bottom-start', + search: false, + searchNormalize: false +}; +const DefaultType$f = { + boundary: '(string|element)', + multiple: 'boolean', + name: '(string|null)', + offset: '(array|string|function)', + placeholder: 'string', + placement: 'string', + search: 'boolean', + searchNormalize: 'boolean' +}; + +/** + * Class definition + */ + +class Combobox extends BaseComponent { + constructor(element, config) { + super(element, config); + this._toggle = this._element; + this._menu = SelectorEngine.next(this._toggle, SELECTOR_MENU$1)[0]; + this._valueDisplay = SelectorEngine.findOne(SELECTOR_VALUE, this._toggle); + this._searchInput = SelectorEngine.findOne(SELECTOR_SEARCH_INPUT, this._menu); + this._noResults = SelectorEngine.findOne(SELECTOR_NO_RESULTS, this._menu); + this._hiddenInput = null; + this._menuInstance = null; + this._createHiddenInput(); + this._createMenuInstance(); + this._syncInitialSelection(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$f; + } + static get DefaultType() { + return DefaultType$f; + } + static get NAME() { + return NAME$g; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._toggle) || this._isShown()) { + return; + } + const showEvent = EventHandler.trigger(this._toggle, EVENT_SHOW$5); + if (showEvent.defaultPrevented) { + return; + } + this._menuInstance.show(); + if (this._searchInput) { + this._searchInput.value = ''; + this._filterItems(''); + requestAnimationFrame(() => this._searchInput.focus()); + } + EventHandler.trigger(this._toggle, EVENT_SHOWN$4); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._toggle, EVENT_HIDE$4); + if (hideEvent.defaultPrevented) { + return; + } + this._menuInstance.hide(); + EventHandler.trigger(this._toggle, EVENT_HIDDEN$6); + } + dispose() { + if (this._menuInstance) { + this._menuInstance.dispose(); + this._menuInstance = null; + } + if (this._hiddenInput) { + this._hiddenInput.remove(); + this._hiddenInput = null; + } + EventHandler.off(this._menu, EVENT_KEY$d); + EventHandler.off(this._toggle, EVENT_KEY$d); + super.dispose(); + } + + // Private + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW$3); + } + _createHiddenInput() { + const { + name + } = this._config; + if (!name) { + return; + } + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = name; + this._hiddenInput.value = ''; + this._toggle.parentNode.insertBefore(this._hiddenInput, this._toggle); + } + _createMenuInstance() { + this._menuInstance = new Menu(this._toggle, { + menu: this._menu, + autoClose: this._config.multiple ? 'outside' : true, + boundary: this._config.boundary, + offset: this._config.offset, + placement: this._config.placement + }); + } + _syncInitialSelection() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length > 0) { + this._updateToggleText(); + this._updateHiddenInput(); + } else { + this._showPlaceholder(); + } + } + _addEventListeners() { + EventHandler.on(this._menu, 'click', SELECTOR_MENU_ITEM, event => { + const item = event.target.closest(SELECTOR_MENU_ITEM); + if (!item || isDisabled(item)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this._selectItem(item); + }); + EventHandler.on(this._toggle, 'keydown', event => { + this._handleToggleKeydown(event); + }); + EventHandler.on(this._menu, 'keydown', event => { + this._handleMenuKeydown(event); + }); + if (this._searchInput) { + EventHandler.on(this._searchInput, 'input', () => { + this._filterItems(this._searchInput.value); + }); + EventHandler.on(this._searchInput, 'keydown', event => { + if (event.key === ARROW_DOWN_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + items[0].focus(); + } + } + if (event.key === ESCAPE_KEY$1) { + this.hide(); + this._toggle.focus(); + } + }); + } + } + _selectItem(item) { + if (this._config.multiple) { + item.classList.toggle(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', item.classList.contains(CLASS_NAME_SELECTED)); + } else { + const previouslySelected = SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + for (const prev of previouslySelected) { + prev.classList.remove(CLASS_NAME_SELECTED); + prev.setAttribute('aria-selected', 'false'); + } + item.classList.add(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', 'true'); + } + this._updateToggleText(); + this._updateHiddenInput(); + const value = this._config.multiple ? this._getSelectedItems().map(el => el.dataset.bsValue) : item.dataset.bsValue; + EventHandler.trigger(this._toggle, EVENT_CHANGE$3, { + value, + item + }); + if (!this._config.multiple) { + this.hide(); + this._toggle.focus(); + } + } + _updateToggleText() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length === 0) { + this._showPlaceholder(); + return; + } + this._valueDisplay.classList.remove(CLASS_NAME_PLACEHOLDER); + if (this._config.multiple && selectedItems.length > 1) { + this._valueDisplay.textContent = `${selectedItems.length} selected`; + } else { + const item = selectedItems[0]; + const label = SelectorEngine.findOne('.menu-item-content > span:first-child', item); + this._valueDisplay.textContent = label ? label.textContent : item.textContent.trim(); + } + } + _showPlaceholder() { + const { + placeholder + } = this._config; + if (placeholder) { + this._valueDisplay.textContent = placeholder; + this._valueDisplay.classList.add(CLASS_NAME_PLACEHOLDER); + } + } + _updateHiddenInput() { + if (!this._hiddenInput) { + return; + } + const selectedItems = this._getSelectedItems(); + const values = selectedItems.map(el => el.dataset.bsValue); + this._hiddenInput.value = this._config.multiple ? values.join(',') : values[0] || ''; + } + _getSelectedItems() { + return SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + } + _getVisibleItems() { + return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(item => isVisible(item)); + } + _filterItems(query) { + const normalizedQuery = this._normalizeText(query.toLowerCase().trim()); + const items = SelectorEngine.find(SELECTOR_MENU_ITEM, this._menu); + let visibleCount = 0; + for (const item of items) { + const text = this._normalizeText(item.textContent.toLowerCase().trim()); + const matches = !normalizedQuery || text.includes(normalizedQuery); + item.style.display = matches ? '' : 'none'; + if (matches) { + visibleCount++; + } + } + if (this._noResults) { + this._noResults.classList.toggle('d-none', visibleCount > 0); + } + } + _normalizeText(text) { + if (this._config.searchNormalize) { + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + } + return text; + } + _handleToggleKeydown(event) { + const { + key + } = event; + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + if (!this._isShown()) { + this.show(); } - if (this._config.toggle) { - this.toggle(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const target = key === ARROW_DOWN_KEY$1 ? items[0] : items.at(-1); + target.focus(); } + return; } - - // Getters - static get Default() { - return Default$a; + if ((key === ENTER_KEY || key === SPACE_KEY) && !this._isShown()) { + event.preventDefault(); + this.show(); } - static get DefaultType() { - return DefaultType$a; + } + _handleMenuKeydown(event) { + const { + key, + target + } = event; + if (key === ESCAPE_KEY$1) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + this._toggle.focus(); + return; } - static get NAME() { - return NAME$b; + if (key === TAB_KEY) { + this.hide(); + return; } - - // Public - toggle() { - if (this._isShown()) { - this.hide(); - } else { - this.show(); + const isInput = target.matches('input'); + if (key === ARROW_DOWN_KEY$1 || key === ARROW_UP_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); } + return; } - show() { - if (this._isTransitioning || this._isShown()) { - return; + if (key === HOME_KEY$1 || key === END_KEY$1) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const targetItem = key === HOME_KEY$1 ? items[0] : items.at(-1); + targetItem.focus(); } - let activeChildren = []; - - // find active children - if (this._config.parent) { - activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { - toggle: false - })); + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !isInput) { + event.preventDefault(); + const item = target.closest(SELECTOR_MENU_ITEM); + if (item && !isDisabled(item)) { + this._selectItem(item); } - if (activeChildren.length && activeChildren[0]._isTransitioning) { + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Combobox.getOrCreateInstance(this, config); + if (typeof config !== 'string') { return; } - const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6); - if (startEvent.defaultPrevented) { - return; + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$7, function (event) { + event.preventDefault(); + Combobox.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const toggle of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7)) { + Combobox.getOrCreateInstance(toggle); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$f = 'datepicker'; +const DATA_KEY$b = 'bs.datepicker'; +const EVENT_KEY$c = `.${DATA_KEY$b}`; +const DATA_API_KEY$7 = '.data-api'; +const EVENT_CHANGE$2 = `change${EVENT_KEY$c}`; +const EVENT_SHOW$4 = `show${EVENT_KEY$c}`; +const EVENT_SHOWN$3 = `shown${EVENT_KEY$c}`; +const EVENT_HIDE$3 = `hide${EVENT_KEY$c}`; +const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$c}`; +const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$c}${DATA_API_KEY$7}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY$c}${DATA_API_KEY$7}`; +const SELECTOR_DATA_TOGGLE$6 = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default$e = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType$e = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default$e; + } + static get DefaultType() { + return DefaultType$e; + } + static get NAME() { + return NAME$f; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN$3); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN$5); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; } - for (const activeInstance of activeChildren) { - activeInstance.hide(); + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; } - const dimension = this._getDimension(); - this._element.classList.remove(CLASS_NAME_COLLAPSE); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.style[dimension] = 0; - this._addAriaAndCollapsedClass(this._triggerArray, true); - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); - this._element.style[dimension] = ''; - EventHandler.trigger(this._element, EVENT_SHOWN$6); - }; - const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - const scrollSize = `scroll${capitalizedDimension}`; - this._queueCallback(complete, this._element, true); - this._element.style[dimension] = `${this._element[scrollSize]}px`; + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); } - hide() { - if (this._isTransitioning || !this._isShown()) { - return; + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; } - const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6); - if (startEvent.defaultPrevented) { - return; + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); } - const dimension = this._getDimension(); - this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; - reflow(this._element); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7); - for (const trigger of this._triggerArray) { - const element = SelectorEngine.getElementFromSelector(trigger); - if (element && !this._isShown(element)) { - this._addAriaAndCollapsedClass([trigger], false); - } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; } - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE); - EventHandler.trigger(this._element, EVENT_HIDDEN$6); - }; - this._element.style[dimension] = ''; - this._queueCallback(complete, this._element, true); } + EventHandler.trigger(this._element, EVENT_CHANGE$2, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; - // Private - _isShown(element = this._element) { - return element.classList.contains(CLASS_NAME_SHOW$7); + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); } - _configAfterMerge(config) { - config.toggle = Boolean(config.toggle); // Coerce string values - config.parent = getElement(config.parent); - return config; + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); } - _getDimension() { - return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; } - _initializeChildren() { - if (!this._config.parent) { - return; - } - const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4); - for (const element of children) { - const selected = SelectorEngine.getElementFromSelector(element); - if (selected) { - this._addAriaAndCollapsedClass([element], this._isShown(selected)); - } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$6, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE$6, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$c}${DATA_API_KEY$7}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); } } - _getFirstLevelChildren(selector) { - const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); - // remove children if greater depth - return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } } - _addAriaAndCollapsedClass(triggerArray, isOpen) { - if (!triggerArray.length) { + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { return; } - for (const element of triggerArray) { - element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); - element.setAttribute('aria-expanded', isOpen); + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; } + this.hide(); + }); + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$e = 'dialog'; +const DATA_KEY$a = 'bs.dialog'; +const EVENT_KEY$b = `.${DATA_KEY$a}`; +const DATA_API_KEY$6 = '.data-api'; +const EVENT_SHOW$3 = `show${EVENT_KEY$b}`; +const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$b}`; +const EVENT_CANCEL = `cancel${EVENT_KEY$b}`; +const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$b}${DATA_API_KEY$6}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle="dialog"]'; +const Default$d = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType$d = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default$d; + } + static get DefaultType() { + return DefaultType$d; + } + static get NAME() { + return NAME$e; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ - // Static - static jQueryInterface(config) { - const _config = {}; - if (typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false; +EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$5, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW$3, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$4, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); } - return this.each(function () { - const data = Collapse.getOrCreateInstance(this, _config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY$b}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN$4, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); }); + currentInstance.hide(); } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$d = 'navoverflow'; +const DATA_KEY$9 = 'bs.navoverflow'; +const EVENT_KEY$a = `.${DATA_KEY$9}`; +const EVENT_UPDATE = `update${EVENT_KEY$a}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY$a}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default$c = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType$c = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$c; + } + static get DefaultType() { + return DefaultType$c; + } + static get NAME() { + return NAME$d; + } - EventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { - event.preventDefault(); + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); } - for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { - Collapse.getOrCreateInstance(element, { - toggle: false - }).toggle(); + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); } - }); + super.dispose(); + } - /** - * jQuery - */ - - defineJQueryPlugin(Collapse); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$a = 'dropdown'; - const DATA_KEY$6 = 'bs.dropdown'; - const EVENT_KEY$6 = `.${DATA_KEY$6}`; - const DATA_API_KEY$3 = '.data-api'; - const ESCAPE_KEY$2 = 'Escape'; - const TAB_KEY$1 = 'Tab'; - const ARROW_UP_KEY$1 = 'ArrowUp'; - const ARROW_DOWN_KEY$1 = 'ArrowDown'; - const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button - - const EVENT_HIDE$5 = `hide${EVENT_KEY$6}`; - const EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`; - const EVENT_SHOW$5 = `show${EVENT_KEY$6}`; - const EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`; - const EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`; - const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`; - const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`; - const CLASS_NAME_SHOW$6 = 'show'; - const CLASS_NAME_DROPUP = 'dropup'; - const CLASS_NAME_DROPEND = 'dropend'; - const CLASS_NAME_DROPSTART = 'dropstart'; - const CLASS_NAME_DROPUP_CENTER = 'dropup-center'; - const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'; - const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'; - const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`; - const SELECTOR_MENU = '.dropdown-menu'; - const SELECTOR_NAVBAR = '.navbar'; - const SELECTOR_NAVBAR_NAV = '.navbar-nav'; - const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; - const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'; - const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'; - const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'; - const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'; - const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'; - const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'; - const PLACEMENT_TOPCENTER = 'top'; - const PLACEMENT_BOTTOMCENTER = 'bottom'; - const Default$9 = { - autoClose: true, - boundary: 'clippingParents', - display: 'dynamic', - offset: [0, 2], - popperConfig: null, - reference: 'toggle' - }; - const DefaultType$9 = { - autoClose: '(boolean|string)', - boundary: '(string|element)', - display: 'string', - offset: '(array|string|function)', - popperConfig: '(null|object|function)', - reference: '(string|element|object)' - }; + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); - /** - * Class definition - */ + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); - class Dropdown extends BaseComponent { - constructor(element, config) { - super(element, config); - this._popper = null; - this._parent = this._element.parentNode; // dropdown wrapper - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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(); - } + // Setup resize observer + this._setupResizeObserver(); - // Getters - static get Default() { - return Default$9; + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; } - static get DefaultType() { - return DefaultType$9; + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; } - static get NAME() { - return NAME$a; + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; } - - // Public - toggle() { - return this._isShown() ? this.hide() : this.show(); + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; } - show() { - if (isDisabled(this._element) || this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget); - if (showEvent.defaultPrevented) { - return; - } - this._createPopper(); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', noop); + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); } } - this._element.focus(); - this._element.setAttribute('aria-expanded', true); - this._menu.classList.add(CLASS_NAME_SHOW$6); - this._element.classList.add(CLASS_NAME_SHOW$6); - EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget); - } - hide() { - if (isDisabled(this._element) || !this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - this._completeHide(relatedTarget); - } - dispose() { - if (this._popper) { - this._popper.destroy(); - } - super.dispose(); - } - update() { - this._inNavbar = this._detectNavbar(); - if (this._popper) { - this._popper.update(); + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); } + return; } + const overflowWidth = overflowItem?.offsetWidth || 0; - // Private - _completeHide(relatedTarget) { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget); - if (hideEvent.defaultPrevented) { - return; - } + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', noop); - } - } - if (this._popper) { - this._popper.destroy(); - } - this._menu.classList.remove(CLASS_NAME_SHOW$6); - this._element.classList.remove(CLASS_NAME_SHOW$6); - this._element.setAttribute('aria-expanded', 'false'); - Manipulator.removeDataAttribute(this._menu, 'popper'); - EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget); - } - _getConfig(config) { - config = super._getConfig(config); - if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { - // Popper virtual elements require a getBoundingClientRect method - throw new TypeError(`${NAME$a.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); - } - return config; - } - _createPopper() { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)'); + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; } - let referenceElement = this._element; - if (this._config.reference === 'parent') { - referenceElement = this._parent; - } else if (isElement(this._config.reference)) { - referenceElement = getElement(this._config.reference); - } else if (typeof this._config.reference === 'object') { - referenceElement = this._config.reference; + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); } - const popperConfig = this._getPopperConfig(); - this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig); - } - _isShown() { - return this._menu.classList.contains(CLASS_NAME_SHOW$6); } - _getPlacement() { - const parentDropdown = this._parent; - if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { - return PLACEMENT_RIGHT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) { - return PLACEMENT_LEFT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) { - return PLACEMENT_TOPCENTER; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) { - return PLACEMENT_BOTTOMCENTER; - } - // We need to trim the value because custom properties can also include spaces - const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'; - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) { - return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP; - } - return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM; - } - _detectNavbar() { - return this._element.closest(SELECTOR_NAVBAR) !== null; + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _getPopperConfig() { - const defaultBsPopperConfig = { - placement: this._getPlacement(), - modifiers: [{ - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }] - }; - // Disable Popper if we have a static display or Dropdown is in Navbar - if (this._inNavbar || this._config.display === 'static') { - Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove - defaultBsPopperConfig.modifiers = [{ - name: 'applyStyles', - enabled: false - }]; - } - return { - ...defaultBsPopperConfig, - ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; - } - _selectMenuItem({ - key, - target - }) { - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element)); - if (!items.length) { - return; - } + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); - // if target isn't included in items (e.g. when expanding the dropdown) - // allow cycling to get the last item in case key equals ARROW_UP_KEY - getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus(); + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Dropdown.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length }); } - static clearMenus(event) { - if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) { - return; - } - const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN); - for (const toggle of openToggles) { - const context = Dropdown.getInstance(toggle); - if (!context || context._config.autoClose === false) { - continue; - } - 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) { - continue; - } - - // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu - if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) { - continue; - } - const relatedTarget = { - relatedTarget: context._element - }; - if (event.type === 'click') { - relatedTarget.clickEvent = event; - } - context._completeHide(relatedTarget); - } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; } - static dataApiKeydownHandler(event) { - // If not an UP | DOWN | ESCAPE key => not a dropdown command - // If input/textarea && if key is other than ESCAPE => not a dropdown command - const isInput = /input|textarea/i.test(event.target.tagName); - const isEscapeEvent = event.key === ESCAPE_KEY$2; - const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key); - if (!isUpOrDownEvent && !isEscapeEvent) { - return; - } - if (isInput && !isEscapeEvent) { - return; + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; } - event.preventDefault(); - - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode); - const instance = Dropdown.getOrCreateInstance(getToggleButton); - if (isUpOrDownEvent) { - event.stopPropagation(); - instance.show(); - instance._selectMenuItem(event); - return; + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); } - if (instance._isShown()) { - // else is escape and we check if it is shown - event.stopPropagation(); - instance.hide(); - getToggleButton.focus(); + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); } } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus); - EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) { - event.preventDefault(); - Dropdown.getOrCreateInstance(this).toggle(); - }); - - /** - * jQuery - */ - - defineJQueryPlugin(Dropdown); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/backdrop.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$9 = 'backdrop'; - const CLASS_NAME_FADE$4 = 'fade'; - const CLASS_NAME_SHOW$5 = 'show'; - const EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`; - const Default$8 = { - className: 'modal-backdrop', - clickCallback: null, - isAnimated: false, - isVisible: true, - // if false, we use the backdrop helper without adding any element to the dom - rootElement: 'body' // give the choice to place backdrop under different elements - }; - const DefaultType$8 = { - className: 'string', - clickCallback: '(function|null)', - isAnimated: 'boolean', - isVisible: 'boolean', - rootElement: '(element|string)' - }; - - /** - * Class definition - */ +/** + * Data API implementation + */ - class Backdrop extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isAppended = false; - this._element = null; +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$c = 'swipe'; +const EVENT_KEY$9 = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default$b = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType$b = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; + +/** + * Class definition + */ + +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } - // Getters - static get Default() { - return Default$8; - } - static get DefaultType() { - return DefaultType$8; - } - static get NAME() { - return NAME$9; - } + // Getters + static get Default() { + return Default$b; + } + static get DefaultType() { + return DefaultType$b; + } + static get NAME() { + return NAME$c; + } - // Public - show(callback) { - if (!this._config.isVisible) { - execute(callback); - return; - } - this._append(); - const element = this._getElement(); - if (this._config.isAnimated) { - reflow(element); - } - element.classList.add(CLASS_NAME_SHOW$5); - this._emulateAnimation(() => { - execute(callback); - }); + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY$9); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; } - hide(callback) { - if (!this._config.isVisible) { - execute(callback); - return; - } - this._getElement().classList.remove(CLASS_NAME_SHOW$5); - this._emulateAnimation(() => { - this.dispose(); - execute(callback); - }); + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; } - dispose() { - if (!this._isAppended) { - return; - } - EventHandler.off(this._element, EVENT_MOUSEDOWN); - this._element.remove(); - this._isAppended = false; + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; } - - // Private - _getElement() { - if (!this._element) { - const backdrop = document.createElement('div'); - backdrop.className = this._config.className; - if (this._config.isAnimated) { - backdrop.classList.add(CLASS_NAME_FADE$4); - } - this._element = backdrop; - } - return this._element; + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; } - _configAfterMerge(config) { - // use getElement() with the default "body" to get a fresh Element on each instantiation - config.rootElement = getElement(config.rootElement); - return config; + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); + + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; } - _append() { - if (this._isAppended) { + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe + const direction = absDeltaX / this._deltaX; + this._deltaX = 0; + this._deltaY = 0; + if (!direction) { return; } - const element = this._getElement(); - this._config.rootElement.append(element); - EventHandler.on(element, EVENT_MOUSEDOWN, () => { - execute(this._config.clickCallback); - }); - this._isAppended = true; + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; } - _emulateAnimation(callback) { - executeAfterTransition(callback, this._getElement(), this._config.isAnimated); + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); } } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } - /** - * -------------------------------------------------------------------------- - * Bootstrap util/focustrap.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$b = 'drawer'; +const DATA_KEY$8 = 'bs.drawer'; +const EVENT_KEY$8 = `.${DATA_KEY$8}`; +const DATA_API_KEY$5 = '.data-api'; +const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`; +const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$8}`; +const EVENT_RESIZE$1 = `resize${EVENT_KEY$8}`; +const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`; +const SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle="drawer"]'; +const Default$a = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType$a = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + // Getters + static get Default() { + return Default$a; + } + static get DefaultType() { + return DefaultType$a; + } + static get NAME() { + return NAME$b; + } - /** - * Constants - */ + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } - const NAME$8 = 'focustrap'; - const DATA_KEY$5 = 'bs.focustrap'; - const EVENT_KEY$5 = `.${DATA_KEY$5}`; - const EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`; - const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`; - const TAB_KEY = 'Tab'; - const TAB_NAV_FORWARD = 'forward'; - const TAB_NAV_BACKWARD = 'backward'; - const Default$7 = { - autofocus: true, - trapElement: null // The element to trap focus inside of - }; - const DefaultType$7 = { - autofocus: 'boolean', - trapElement: 'element' - }; + // Protected — hook overrides - /** - * Class definition - */ + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } - class FocusTrap extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isActive = false; - this._lastTabNavDirection = null; - } + // Private - // Getters - static get Default() { - return Default$7; - } - static get DefaultType() { - return DefaultType$7; - } - static get NAME() { - return NAME$8; + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; } - // Public - activate() { - if (this._isActive) { - return; - } - if (this._config.autofocus) { - this._config.trapElement.focus(); + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); } - EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop - EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event)); - EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)); - this._isActive = true; + } else if (isRTL()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); } - deactivate() { - if (!this._isActive) { - return; - } - this._isActive = false; - EventHandler.off(document, EVENT_KEY$5); + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$4, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN$3, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); } + }); - // Private - _handleFocusin(event) { - const { - trapElement - } = this._config; - if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { - return; - } - const elements = SelectorEngine.focusableChildren(trapElement); - if (elements.length === 0) { - trapElement.focus(); - } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { - elements[elements.length - 1].focus(); - } else { - elements[0].focus(); - } + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE$1, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); } - _handleKeydown(event) { - if (event.key !== TAB_KEY) { - return; - } - this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD; + } +}); +enableDismissTrigger(Drawer); + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$a = 'strength'; +const DATA_KEY$7 = 'bs.strength'; +const EVENT_KEY$7 = `.${DATA_KEY$7}`; +const DATA_API_KEY$4 = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY$7}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default$9 = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType$9 = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); } } - /** - * -------------------------------------------------------------------------- - * Bootstrap util/scrollBar.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Getters + static get Default() { + return Default$9; + } + static get DefaultType() { + return DefaultType$9; + } + static get NAME() { + return NAME$a; + } + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } - /** - * Constants - */ + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } - const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; - const SELECTOR_STICKY_CONTENT = '.sticky-top'; - const PROPERTY_PADDING = 'padding-right'; - const PROPERTY_MARGIN = 'margin-right'; + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } - /** - * Class definition - */ + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; - class ScrollBarHelper { - constructor() { - this._element = document.body; + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; } - // Public - getWidth() { - // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes - const documentWidth = document.documentElement.clientWidth; - return Math.abs(window.innerWidth - documentWidth); + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; } - hide() { - const width = this.getWidth(); - this._disableOverFlow(); - // give padding to element to balance the hidden scrollbar width - this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth - this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width); + if (/[A-Z]/.test(password)) { + score += weights.uppercase; } - reset() { - this._resetElementAttributes(this._element, 'overflow'); - this._resetElementAttributes(this._element, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN); + if (/\d/.test(password)) { + score += weights.numbers; } - isOverflowing() { - return this.getWidth() > 0; + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; } - // Private - _disableOverFlow() { - this._saveInitialAttribute(this._element, 'overflow'); - this._element.style.overflow = 'hidden'; - } - _setElementAttributes(selector, styleProperty, callback) { - const scrollbarWidth = this.getWidth(); - const manipulationCallBack = element => { - if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { - return; - } - this._saveInitialAttribute(element, styleProperty); - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty); - element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`); - }; - this._applyManipulationCallback(selector, manipulationCallBack); + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; } - _saveInitialAttribute(element, styleProperty) { - const actualValue = element.style.getPropertyValue(styleProperty); - if (actualValue) { - Manipulator.setDataAttribute(element, styleProperty, actualValue); - } + if (password.length >= 16) { + score += weights.longPassword; } - _resetElementAttributes(selector, styleProperty) { - const manipulationCallBack = element => { - const value = Manipulator.getDataAttribute(element, styleProperty); - // We only want to remove the property if the value is `null`; the value can also be zero - if (value === null) { - element.style.removeProperty(styleProperty); - return; - } - Manipulator.removeDataAttribute(element, styleProperty); - element.style.setProperty(styleProperty, value); - }; - this._applyManipulationCallback(selector, manipulationCallBack); + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; } - _applyManipulationCallback(selector, callBack) { - if (isElement(selector)) { - callBack(selector); - return; + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); } - for (const sel of SelectorEngine.find(selector, this._element)) { - callBack(sel); + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; } } } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$7 = 'modal'; - const DATA_KEY$4 = 'bs.modal'; - const EVENT_KEY$4 = `.${DATA_KEY$4}`; - const DATA_API_KEY$2 = '.data-api'; - const ESCAPE_KEY$1 = 'Escape'; - const EVENT_HIDE$4 = `hide${EVENT_KEY$4}`; - const EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`; - const EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`; - const EVENT_SHOW$4 = `show${EVENT_KEY$4}`; - const EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`; - const EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`; - const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`; - const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`; - const EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`; - const EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`; - const CLASS_NAME_OPEN = 'modal-open'; - const CLASS_NAME_FADE$3 = 'fade'; - const CLASS_NAME_SHOW$4 = 'show'; - const CLASS_NAME_STATIC = 'modal-static'; - const OPEN_SELECTOR$1 = '.modal.show'; - const SELECTOR_DIALOG = '.modal-dialog'; - const SELECTOR_MODAL_BODY = '.modal-body'; - const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="modal"]'; - const Default$6 = { - backdrop: true, - focus: true, - keyboard: true - }; - const DefaultType$6 = { - backdrop: '(boolean|string)', - focus: 'boolean', - keyboard: 'boolean' - }; +/** + * Data API implementation + */ - /** - * Class definition - */ - - class Modal extends BaseComponent { - constructor(element, config) { - super(element, config); - this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element); - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._isShown = false; - this._isTransitioning = false; - this._scrollBar = new ScrollBarHelper(); - this._addEventListeners(); +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$7}${DATA_API_KEY$4}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$9 = 'otpInput'; +const DATA_KEY$6 = 'bs.otpInput'; +const EVENT_KEY$6 = `.${DATA_KEY$6}`; +const DATA_API_KEY$3 = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY$6}`; +const EVENT_INPUT$1 = `input${EVENT_KEY$6}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$6}${DATA_API_KEY$3}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT$1 = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default$8 = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType$8 = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT$1, this._element); + if (!this._input) { + return; } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default$8; + } + static get DefaultType() { + return DefaultType$8; + } + static get NAME() { + return NAME$9; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } - // Getters - static get Default() { - return Default$6; + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; } - static get DefaultType() { - return DefaultType$6; + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; } - static get NAME() { - return NAME$7; + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); } - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); } - show(relatedTarget) { - if (this._isShown || this._isTransitioning) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._isTransitioning = true; - this._scrollBar.hide(); - document.body.classList.add(CLASS_NAME_OPEN); - this._adjustDialog(); - this._backdrop.show(() => this._showElement(relatedTarget)); - } - hide() { - if (!this._isShown || this._isTransitioning) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4); - if (hideEvent.defaultPrevented) { - return; + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } } - this._isShown = false; - this._isTransitioning = true; - this._focustrap.deactivate(); - this._element.classList.remove(CLASS_NAME_SHOW$4); - this._queueCallback(() => this._hideModal(), this._element, this._isAnimated()); } - dispose() { - EventHandler.off(window, EVENT_KEY$4); - EventHandler.off(this._dialog, EVENT_KEY$4); - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); } - handleUpdate() { - this._adjustDialog(); + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT$1, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); } - - // Private - _initializeBackDrop() { - return new Backdrop({ - isVisible: Boolean(this._config.backdrop), - // 'static' option will be translated to true, and booleans will keep their value, - isAnimated: this._isAnimated() + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value }); } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$8 = 'chips'; +const DATA_KEY$5 = 'bs.chips'; +const EVENT_KEY$5 = `.${DATA_KEY$5}`; +const DATA_API_KEY$2 = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY$5}`; +const EVENT_REMOVE = `remove${EVENT_KEY$5}`; +const EVENT_CHANGE$1 = `change${EVENT_KEY$5}`; +const EVENT_SELECT = `select${EVENT_KEY$5}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE$2 = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default$7 = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType$7 = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default$7; + } + static get DefaultType() { + return DefaultType$7; + } + static get NAME() { + return NAME$8; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; } - _showElement(relatedTarget) { - // try to append dynamic modal - if (!document.body.contains(this._element)) { - document.body.append(this._element); - } - this._element.style.display = 'block'; - this._element.removeAttribute('aria-hidden'); - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.scrollTop = 0; - const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog); - if (modalBody) { - modalBody.scrollTop = 0; - } - reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW$4); - const transitionComplete = () => { - if (this._config.focus) { - this._focustrap.activate(); - } - this._isTransitioning = false; - EventHandler.trigger(this._element, EVENT_SHOWN$4, { - relatedTarget - }); - }; - this._queueCallback(transitionComplete, this._dialog, this._isAnimated()); + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => { - if (event.key !== ESCAPE_KEY$1) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - this._triggerBackdropTransition(); - }); - EventHandler.on(window, EVENT_RESIZE$1, () => { - if (this._isShown && !this._isTransitioning) { - this._adjustDialog(); - } - }); - EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => { - // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks - EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => { - if (this._element !== event.target || this._element !== event2.target) { - return; - } - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition(); - return; - } - if (this._config.backdrop) { - this.hide(); - } - }); - }); + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; } - _hideModal() { - this._element.style.display = 'none'; - this._element.setAttribute('aria-hidden', true); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - this._isTransitioning = false; - this._backdrop.hide(() => { - document.body.classList.remove(CLASS_NAME_OPEN); - this._resetAdjustments(); - this._scrollBar.reset(); - EventHandler.trigger(this._element, EVENT_HIDDEN$4); - }); + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_FADE$3); + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); } - _triggerBackdropTransition() { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1); - if (hideEvent.defaultPrevented) { - return; - } - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const initialOverflowY = this._element.style.overflowY; - // return if the following background transition hasn't yet completed - if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { - return; - } - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden'; - } - this._element.classList.add(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.classList.remove(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.style.overflowY = initialOverflowY; - }, this._dialog); - }, this._dialog); - this._element.focus(); - } - - /** - * The following methods are used to handle overflowing modals - */ - - _adjustDialog() { - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const scrollbarWidth = this._scrollBar.getWidth(); - const isBodyOverflowing = scrollbarWidth > 0; - if (isBodyOverflowing && !isModalOverflowing) { - const property = isRTL() ? 'paddingLeft' : 'paddingRight'; - this._element.style[property] = `${scrollbarWidth}px`; - } - if (!isBodyOverflowing && isModalOverflowing) { - const property = isRTL() ? 'paddingRight' : 'paddingLeft'; - this._element.style[property] = `${scrollbarWidth}px`; - } + if (!chip || !value) { + return false; } - _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; } - // Static - static jQueryInterface(config, relatedTarget) { - return this.each(function () { - const data = Modal.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](relatedTarget); - }); + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; } - } - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); } - EventHandler.one(target, EVENT_SHOW$4, showEvent => { - if (showEvent.defaultPrevented) { - // only register focus restorer if modal will actually get shown - return; + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE$1, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE$2); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE$2); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; } - EventHandler.one(target, EVENT_HIDDEN$4, () => { - if (isVisible(this)) { - this.focus(); - } - }); + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE$2); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() }); + } + focus() { + this._input?.focus(); + } - // avoid conflict when clicking modal toggler while another one is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1); - if (alreadyOpen) { - Modal.getInstance(alreadyOpen).hide(); + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } } - const data = Modal.getOrCreateInstance(target); - data.toggle(this); - }); - enableDismissTrigger(Modal); - - /** - * jQuery - */ - - defineJQueryPlugin(Modal); - - /** - * -------------------------------------------------------------------------- - * Bootstrap offcanvas.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$6 = 'offcanvas'; - const DATA_KEY$3 = 'bs.offcanvas'; - const EVENT_KEY$3 = `.${DATA_KEY$3}`; - const DATA_API_KEY$1 = '.data-api'; - const EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`; - const ESCAPE_KEY = 'Escape'; - const CLASS_NAME_SHOW$3 = 'show'; - const CLASS_NAME_SHOWING$1 = 'showing'; - const CLASS_NAME_HIDING = 'hiding'; - const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'; - const OPEN_SELECTOR = '.offcanvas.show'; - const EVENT_SHOW$3 = `show${EVENT_KEY$3}`; - const EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`; - const EVENT_HIDE$3 = `hide${EVENT_KEY$3}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`; - const EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`; - const EVENT_RESIZE = `resize${EVENT_KEY$3}`; - const EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`; - const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="offcanvas"]'; - const Default$5 = { - backdrop: true, - keyboard: true, - scroll: false - }; - const DefaultType$5 = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - scroll: 'boolean' - }; - - /** - * Class definition - */ + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); - class Offcanvas extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isShown = false; - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._addEventListeners(); + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); - // Getters - static get Default() { - return Default$5; + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; } - static get DefaultType() { - return DefaultType$5; + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); } - static get NAME() { - return NAME$6; + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); } - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, { - relatedTarget - }); - if (showEvent.defaultPrevented) { + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { return; } - this._isShown = true; - this._backdrop.show(); - if (!this._config.scroll) { - new ScrollBarHelper().hide(); - } - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.classList.add(CLASS_NAME_SHOWING$1); - const completeCallBack = () => { - if (!this._config.scroll || this._config.backdrop) { - this._focustrap.activate(); - } - this._element.classList.add(CLASS_NAME_SHOW$3); - this._element.classList.remove(CLASS_NAME_SHOWING$1); - EventHandler.trigger(this._element, EVENT_SHOWN$3, { - relatedTarget + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey }); - }; - this._queueCallback(completeCallBack, this._element, true); - } - hide() { - if (!this._isShown) { - return; + chip.focus(); } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3); - if (hideEvent.defaultPrevented) { - return; + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); } - this._focustrap.deactivate(); - this._element.blur(); - this._isShown = false; - this._element.classList.add(CLASS_NAME_HIDING); - this._backdrop.hide(); - const completeCallback = () => { - this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - if (!this._config.scroll) { - new ScrollBarHelper().reset(); - } - EventHandler.trigger(this._element, EVENT_HIDDEN$3); - }; - this._queueCallback(completeCallback, this._element, true); - } - dispose() { - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } + }); - // Private - _initializeBackDrop() { - const clickCallback = () => { - if (this._config.backdrop === 'static') { - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - return; + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; } - this.hide(); - }; - // 'static' option will be translated to true, and booleans will keep their value - const isVisible = Boolean(this._config.backdrop); - return new Backdrop({ - className: CLASS_NAME_BACKDROP, - isVisible, - isAnimated: true, - rootElement: this._element.parentNode, - clickCallback: isVisible ? clickCallback : null - }); + // No default } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; } - if (this._config.keyboard) { - this.hide(); - return; + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; } - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - }); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Offcanvas.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; } - data[config](this); - }); + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default } } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; } - if (isDisabled(this)) { + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { return; } - EventHandler.one(target, EVENT_HIDDEN$3, () => { - // focus on trigger when it is closed - if (isVisible(this)) { - this.focus(); - } - }); - - // avoid conflict when clicking a toggler of an offcanvas, while another is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen && alreadyOpen !== target) { - Offcanvas.getInstance(alreadyOpen).hide(); + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; } - const data = Offcanvas.getOrCreateInstance(target); - data.toggle(this); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API$2, () => { - for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { - Offcanvas.getOrCreateInstance(selector).show(); + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE$2); } - }); - EventHandler.on(window, EVENT_RESIZE, () => { - for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) { - if (getComputedStyle(element).position !== 'fixed') { - Offcanvas.getOrCreateInstance(element).hide(); - } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); } - }); - enableDismissTrigger(Offcanvas); - - /** - * jQuery - */ - - defineJQueryPlugin(Offcanvas); - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - // js-docs-start allow-list - const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; - const DefaultAllowlist = { - // Global attributes allowed on any supplied element below. - '*': ['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: [] - }; - // js-docs-end allow-list - - const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); - - /** - * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation - * contexts. - * - * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 - */ - const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - const allowedAttribute = (attribute, allowedAttributeList) => { - const attributeName = attribute.nodeName.toLowerCase(); - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)); - } - return true; + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; } - - // Check if a regular expression validates the attribute. - return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); - }; - function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { - if (!unsafeHtml.length) { - return unsafeHtml; - } - if (sanitizeFunction && typeof sanitizeFunction === 'function') { - return sanitizeFunction(unsafeHtml); - } - const domParser = new window.DOMParser(); - const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); - const elements = [].concat(...createdDocument.body.querySelectorAll('*')); - for (const element of elements) { - const elementName = element.nodeName.toLowerCase(); - if (!Object.keys(allowList).includes(elementName)) { - element.remove(); - continue; - } - const attributeList = [].concat(...element.attributes); - const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []); - for (const attribute of attributeList) { - if (!allowedAttribute(attribute, allowedAttributes)) { - element.removeAttribute(attribute.nodeName); - } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); } } - return createdDocument.body.innerHTML; } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} - /** - * -------------------------------------------------------------------------- - * Bootstrap util/template-factory.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Data API implementation + */ +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY$5}${DATA_API_KEY$2}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list + +const uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']); + +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; + +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); + } + return true; + } - /** - * Constants - */ + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; + } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); + } + } + } + return createdDocument.body.innerHTML; +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$7 = 'TemplateFactory'; +const Default$6 = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType$6 = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; + +/** + * Class definition + */ + +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } - const NAME$5 = 'TemplateFactory'; - const Default$4 = { - allowList: DefaultAllowlist, - content: {}, - // { selector : text , selector2 : text2 , } - extraClass: '', - html: false, - sanitize: true, - sanitizeFn: null, - template: '' - }; - const DefaultType$4 = { - allowList: 'object', - content: 'object', - extraClass: '(string|function)', - html: 'boolean', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - template: 'string' - }; - const DefaultContentType = { - entry: '(string|element|function|null)', - selector: '(string|element)' - }; + // Getters + static get Default() { + return Default$6; + } + static get DefaultType() { + return DefaultType$6; + } + static get NAME() { + return NAME$7; + } - /** - * Class definition - */ + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); + } + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); + } + return template; + } - class TemplateFactory extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); } - - // Getters - static get Default() { - return Default$4; + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; + } + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; + } + if (isElement(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; } - static get DefaultType() { - return DefaultType$4; + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; + } + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; } - static get NAME() { - return NAME$5; + templateElement.textContent = element.textContent; + } +} + +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$6 = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE$2 = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW$2 = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE$2 = 'hide'; +const EVENT_HIDDEN$2 = 'hidden'; +const EVENT_SHOW$2 = 'show'; +const EVENT_SHOWN$2 = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK$3 = 'click'; +const EVENT_FOCUSIN$2 = 'focusin'; +const EVENT_FOCUSOUT$1 = 'focusout'; +const EVENT_MOUSEENTER$1 = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN$1 = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL() ? 'right' : 'left' +}; +const Default$5 = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType$5 = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); + } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); } + } + + // Getters + static get Default() { + return Default$5; + } + static get DefaultType() { + return DefaultType$5; + } + static get NAME() { + return NAME$6; + } - // Public - getContent() { - return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; } - hasContent() { - return this.getContent().length > 0; + if (this._isShown()) { + this._leave(); + return; } - changeContent(content) { - this._checkContent(content); - this._config.content = { - ...this._config.content, - ...content - }; - return this; + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); } - toHtml() { - const templateWrapper = document.createElement('div'); - templateWrapper.innerHTML = this._maybeSanitize(this._config.template); - for (const [selector, text] of Object.entries(this._config.content)) { - this._setContent(templateWrapper, text, selector); - } - const template = templateWrapper.children[0]; - const extraClass = this._resolvePossibleFunction(this._config.extraClass); - if (extraClass) { - template.classList.add(...extraClass.split(' ')); - } - return template; + if (!(this._isWithContent() && this._isEnabled)) { + return; } - - // Private - _typeCheckConfig(config) { - super._typeCheckConfig(config); - this._checkContent(config.content); - } - _checkContent(arg) { - for (const [selector, content] of Object.entries(arg)) { - super._typeCheckConfig({ - selector, - entry: content - }, DefaultContentType); - } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; } - _setContent(template, content, selector) { - const templateElement = SelectorEngine.findOne(selector, template); - if (!templateElement) { - return; - } - content = this._resolvePossibleFunction(content); - if (!content) { - templateElement.remove(); - return; - } - if (isElement(content)) { - this._putElementInTemplate(getElement(content), templateElement); - return; - } - if (this._config.html) { - templateElement.innerHTML = this._maybeSanitize(content); - return; + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW$2); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); + if (this._isHovered === false) { + this._leave(); } - templateElement.textContent = content; - } - _maybeSanitize(arg) { - return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; } - _resolvePossibleFunction(arg) { - return execute(arg, [undefined, this]); + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); + if (hideEvent.defaultPrevented) { + return; } - _putElementInTemplate(element, templateElement) { - if (this._config.html) { - templateElement.innerHTML = ''; - templateElement.append(element); - return; - } - templateElement.textContent = element.textContent; - } - } - - /** - * -------------------------------------------------------------------------- - * Bootstrap tooltip.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$4 = 'tooltip'; - const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); - const CLASS_NAME_FADE$2 = 'fade'; - const CLASS_NAME_MODAL = 'modal'; - const CLASS_NAME_SHOW$2 = 'show'; - const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; - const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; - const EVENT_MODAL_HIDE = 'hide.bs.modal'; - const TRIGGER_HOVER = 'hover'; - const TRIGGER_FOCUS = 'focus'; - const TRIGGER_CLICK = 'click'; - const TRIGGER_MANUAL = 'manual'; - const EVENT_HIDE$2 = 'hide'; - const EVENT_HIDDEN$2 = 'hidden'; - const EVENT_SHOW$2 = 'show'; - const EVENT_SHOWN$2 = 'shown'; - const EVENT_INSERTED = 'inserted'; - const EVENT_CLICK$1 = 'click'; - const EVENT_FOCUSIN$1 = 'focusin'; - const EVENT_FOCUSOUT$1 = 'focusout'; - const EVENT_MOUSEENTER = 'mouseenter'; - const EVENT_MOUSELEAVE = 'mouseleave'; - const AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: isRTL() ? 'left' : 'right', - BOTTOM: 'bottom', - LEFT: isRTL() ? 'right' : 'left' - }; - const Default$3 = { - allowList: DefaultAllowlist, - animation: true, - boundary: 'clippingParents', - container: false, - customClass: '', - delay: 0, - fallbackPlacements: ['top', 'right', 'bottom', 'left'], - html: false, - offset: [0, 6], - placement: 'top', - popperConfig: null, - sanitize: true, - sanitizeFn: null, - selector: false, - template: '' + '' + '' + '', - title: '', - trigger: 'hover focus' - }; - const DefaultType$3 = { - allowList: 'object', - animation: 'boolean', - boundary: '(string|element)', - container: '(string|element|boolean)', - customClass: '(string|function)', - delay: '(number|object)', - fallbackPlacements: 'array', - html: 'boolean', - offset: '(array|string|function)', - placement: '(string|function)', - popperConfig: '(null|object|function)', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - selector: '(string|boolean)', - template: 'string', - title: '(string|element|function)', - trigger: 'string' - }; + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW$2); - /** - * Class definition - */ + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering - class Tooltip extends BaseComponent { - constructor(element, config) { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)'); + const complete = () => { + if (this._isWithActiveTrigger()) { + return; } - super(element, config); - - // Private - this._isEnabled = true; - this._timeout = 0; - this._isHovered = null; - this._activeTrigger = {}; - this._popper = null; - this._templateFactory = null; - this._newContent = null; - - // Protected - this.tip = null; - this._setListeners(); - if (!this._config.selector) { - this._fixTitle(); + if (!this._isHovered) { + this._disposeFloating(); } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); } + } - // Getters - static get Default() { - return Default$3; + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); } - static get DefaultType() { - return DefaultType$3; + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE$2); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); } - static get NAME() { - return NAME$4; + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } - // Public - enable() { - this._isEnabled = true; + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; } - disable() { - this._isEnabled = false; + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; } - toggleEnabled() { - this._isEnabled = !this._isEnabled; + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); } - toggle() { - if (!this._isEnabled) { - return; - } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { if (this._isShown()) { - this._leave(); - return; + this._updateFloatingPosition(); } - this._enter(); + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; } - dispose() { - clearTimeout(this._timeout); - EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); - if (this._element.getAttribute('data-bs-original-title')) { - this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); - } - this._disposePopper(); - super.dispose(); + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; } - show() { - if (this._element.style.display === 'none') { - throw new Error('Please use show on visible elements'); - } - if (!(this._isWithContent() && this._isEnabled)) { - return; - } - const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2)); - const shadowRoot = findShadowRoot(this._element); - const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); - if (showEvent.defaultPrevented || !isInTheDom) { - return; - } - // TODO: v6 remove this or make it optional - this._disposePopper(); - const tip = this._getTipElement(); - this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { const { - container - } = this._config; - if (!this._element.ownerDocument.documentElement.contains(this.tip)) { - container.append(tip); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); - } - this._popper = this._createPopper(tip); - tip.classList.add(CLASS_NAME_SHOW$2); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', noop); - } - } - const complete = () => { - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2)); - if (this._isHovered === false) { - this._leave(); - } - this._isHovered = false; + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); + } + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - hide() { - if (!this._isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2)); - if (hideEvent.defaultPrevented) { - return; - } - const tip = this._getTipElement(); - tip.classList.remove(CLASS_NAME_SHOW$2); - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', noop); - } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); + } + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$3), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER$1) : this.constructor.eventName(EVENT_FOCUSIN$2); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); } - this._activeTrigger[TRIGGER_CLICK] = false; - this._activeTrigger[TRIGGER_FOCUS] = false; - this._activeTrigger[TRIGGER_HOVER] = false; - this._isHovered = null; // it is a trick to support manual triggering - - const complete = () => { - if (this._isWithActiveTrigger()) { - return; - } - if (!this._isHovered) { - this._disposePopper(); - } - this._element.removeAttribute('aria-describedby'); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2)); - }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - update() { - if (this._popper) { - this._popper.update(); + this._hideModalHandler = () => { + if (this._element) { + this.hide(); } + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; } + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { + return; + } - // Protected - _isWithContent() { - return Boolean(this._getTitle()); + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; } - _getTipElement() { - if (!this.tip) { - this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); - } - return this.tip; + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN$1, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; } - _createTipElement(content) { - const tip = this._getTemplateFactory(content).toHtml(); - - // TODO: remove this check in v6 - if (!tip) { - return null; - } - tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2); - // TODO: v6 the following can be achieved with CSS only - tip.classList.add(`bs-${this.constructor.NAME}-auto`); - const tipId = getUID(this.constructor.NAME).toString(); - tip.setAttribute('id', tipId); - if (this._isAnimated()) { - tip.classList.add(CLASS_NAME_FADE$2); - } - return tip; + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); } - setContent(content) { - this._newContent = content; - if (this._isShown()) { - this._disposePopper(); + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { + this._isHovered = true; + return; + } + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { this.show(); } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; } - _getTemplateFactory(content) { - if (this._templateFactory) { - this._templateFactory.changeContent(content); - } else { - this._templateFactory = new TemplateFactory({ - ...this._config, - // the `content` var has to be after `this._config` - // to override config.content in case of popover - content, - extraClass: this._resolvePossibleFunction(this._config.customClass) - }); + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); + } + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; } - return this._templateFactory; } - _getContentForTemplate() { - return { - [SELECTOR_TOOLTIP_INNER]: this._getTitle() + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay }; } - _getTitle() { - return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; + } + } + config.selector = false; + config.trigger = 'manual'; - // Private - _initializeOnDelegatedTarget(event) { - return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; } - _isAnimated() { - return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2); + if (this.tip) { + this.tip.remove(); + this.tip = null; } - _isShown() { - return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2); + } +} + +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$3); + if (!target) { + return; + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; + +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN$2, SELECTOR_DATA_TOGGLE$3, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER$1, SELECTOR_DATA_TOGGLE$3, initTooltip); + +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$5 = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle="popover"]'; +const EVENT_CLICK$2 = 'click'; +const EVENT_FOCUSIN$1 = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default$4 = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType$4 = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; + +/** + * Class definition + */ + +class Popover extends Tooltip { + // Getters + static get Default() { + return Default$4; + } + static get DefaultType() { + return DefaultType$4; + } + static get NAME() { + return NAME$5; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } + + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} + +/** + * Data API implementation - auto-initialize popovers + */ + +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE$2); + if (!target) { + return; + } + + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } + + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; + +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK$2, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_FOCUSIN$1, SELECTOR_DATA_TOGGLE$2, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE$2, initPopover); + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$4 = 'range'; +const DATA_KEY$4 = 'bs.range'; +const EVENT_KEY$4 = `.${DATA_KEY$4}`; +const DATA_API_KEY$1 = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY$4}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY$4}${DATA_API_KEY$1}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default$3 = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType$3 = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; } - _createPopper(tip) { - const placement = execute(this._config.placement, [this, tip, this._element]); - const attachment = AttachmentMap[placement.toUpperCase()]; - return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment)); + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _resolvePossibleFunction(arg) { - return execute(arg, [this._element, this._element]); - } - _getPopperConfig(attachment) { - const defaultBsPopperConfig = { - placement: attachment, - modifiers: [{ - name: 'flip', - options: { - fallbackPlacements: this._config.fallbackPlacements - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }, { - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'arrow', - options: { - element: `.${this.constructor.NAME}-arrow` - } - }, { - name: 'preSetPlacement', - enabled: true, - phase: 'beforeMain', - fn: data => { - // Pre-set Popper's placement attribute in order to read the arrow sizes properly. - // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement - this._getTipElement().setAttribute('data-popper-placement', data.state.placement); - } - }] - }; - return { - ...defaultBsPopperConfig, - ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); } - _setListeners() { - const triggers = this._config.trigger.split(' '); - for (const trigger of triggers) { - if (trigger === 'click') { - EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); - context.toggle(); - }); - } else if (trigger !== TRIGGER_MANUAL) { - const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1); - const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1); - EventHandler.on(this._element, eventIn, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; - context._enter(); - }); - EventHandler.on(this._element, eventOut, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); - context._leave(); - }); - } - } - this._hideModalHandler = () => { - if (this._element) { - this.hide(); - } - }; - EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default$3; + } + static get DefaultType() { + return DefaultType$3; + } + static get NAME() { + return NAME$4; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; } - _fixTitle() { - const title = this._element.getAttribute('title'); - if (!title) { - return; - } - if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { - this._element.setAttribute('aria-label', title); - } - this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility - this._element.removeAttribute('title'); + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; } - _enter() { - if (this._isShown() || this._isHovered) { - this._isHovered = true; - return; + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); } - this._isHovered = true; - this._setTimeout(() => { - if (this._isHovered) { - this.show(); - } - }, this._config.delay.show); } - _leave() { - if (this._isWithActiveTrigger()) { - return; - } - this._isHovered = false; - this._setTimeout(() => { - if (!this._isHovered) { - this.hide(); - } - }, this._config.delay.hide); + if (points.length === 0) { + return; } - _setTimeout(handler, timeout) { - clearTimeout(this._timeout); - this._timeout = setTimeout(handler, timeout); + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$3 = 'scrollspy'; +const DATA_KEY$3 = 'bs.scrollspy'; +const EVENT_KEY$3 = `.${DATA_KEY$3}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY$3}`; +const EVENT_CLICK$1 = `click${EVENT_KEY$3}`; +const EVENT_SCROLL = `scroll${EVENT_KEY$3}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY$3}`; +const EVENT_RESIZE = `resize${EVENT_KEY$3}`; +const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$3}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE$1 = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE$1 = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default$2 = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType$2 = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default$2; + } + static get DefaultType() { + return DefaultType$2; + } + static get NAME() { + return NAME$3; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } - _isWithActiveTrigger() { - return Object.values(this._activeTrigger).includes(true); + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK$1); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); } - _getConfig(config) { - const dataAttributes = Manipulator.getDataAttributes(this._element); - for (const dataAttribute of Object.keys(dataAttributes)) { - if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { - delete dataAttributes[dataAttribute]; - } - } - config = { - ...dataAttributes, - ...(typeof config === 'object' && config ? config : {}) - }; - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - config.container = config.container === false ? document.body : getElement(config.container); - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - if (typeof config.content === 'number') { - config.content = config.content.toString(); + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- + + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); + } else { + this._intersecting.delete(entry.target); } - return config; } - _getDelegateConfig() { - const config = {}; - for (const [key, value] of Object.entries(this._config)) { - if (this.constructor.Default[key] !== value) { - config[key] = value; + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; } } - config.selector = false; - config.trigger = 'manual'; - - // In the future can be replaced with: - // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) - // `Object.fromEntries(keysWithDifferentValues)` - return config; - } - _disposePopper() { - if (this._popper) { - this._popper.destroy(); - this._popper = null; - } - if (this.tip) { - this.tip.remove(); - this.tip = null; - } - } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tooltip.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); + } + if (!active) { + return; + } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); } } - /** - * jQuery - */ - - defineJQueryPlugin(Tooltip); - - /** - * -------------------------------------------------------------------------- - * Bootstrap popover.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; + + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; + } + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } - /** - * Constants - */ + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } - const NAME$3 = 'popover'; - const SELECTOR_TITLE = '.popover-header'; - const SELECTOR_CONTENT = '.popover-body'; - const Default$2 = { - ...Tooltip.Default, - content: '', - offset: [0, 8], - placement: 'right', - template: '' + '' + '' + '' + '', - trigger: 'click' - }; - const DefaultType$2 = { - ...Tooltip.DefaultType, - content: '(null|string|element|function)' - }; + // --- Bottom sentinel ----------------------------------------------------- - /** - * Class definition - */ + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; + } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } - class Popover extends Tooltip { - // Getters - static get Default() { - return Default$2; - } - static get DefaultType() { - return DefaultType$2; - } - static get NAME() { - return NAME$3; - } + // --- Resize (px activation lines only) ----------------------------------- - // Overrides - _isWithContent() { - return this._getTitle() || this._getContent(); + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; } - - // Private - _getContentForTemplate() { - return { - [SELECTOR_TITLE]: this._getTitle(), - [SELECTOR_CONTENT]: this._getContent() - }; + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; } - _getContent() { - return this._resolvePossibleFunction(this._config.content); + } + _rebuildObserver() { + if (!this._observer) { + return; } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Popover.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } } - /** - * jQuery - */ - - defineJQueryPlugin(Popover); - - /** - * -------------------------------------------------------------------------- - * Bootstrap scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$2 = 'scrollspy'; - const DATA_KEY$2 = 'bs.scrollspy'; - const EVENT_KEY$2 = `.${DATA_KEY$2}`; - const DATA_API_KEY = '.data-api'; - const EVENT_ACTIVATE = `activate${EVENT_KEY$2}`; - const EVENT_CLICK = `click${EVENT_KEY$2}`; - const EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`; - const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; - const CLASS_NAME_ACTIVE$1 = 'active'; - const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; - const SELECTOR_TARGET_LINKS = '[href]'; - const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; - const SELECTOR_NAV_LINKS = '.nav-link'; - const SELECTOR_NAV_ITEMS = '.nav-item'; - const SELECTOR_LIST_ITEMS = '.list-group-item'; - const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; - const SELECTOR_DROPDOWN = '.dropdown'; - const SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle'; - const Default$1 = { - offset: null, - // TODO: v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: '0px 0px -25%', - smoothScroll: false, - target: null, - threshold: [0.1, 0.5, 1] - }; - const DefaultType$1 = { - offset: '(number|null)', - // TODO v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: 'string', - smoothScroll: 'boolean', - target: 'element', - threshold: 'array' - }; + // --- Smooth-scroll settle (hash + focus) --------------------------------- - /** - * Class definition - */ - - class ScrollSpy extends BaseComponent { - constructor(element, config) { - super(element, config); - - // this._element is the observablesContainer and config.target the menu links wrapper - this._targetLinks = new Map(); - this._observableSections = new Map(); - this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; - this._activeTarget = null; - this._observer = null; - this._previousScrollData = { - visibleEntryTop: 0, - parentScrollTop: 0 - }; - this.refresh(); // initialize + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; } - // Getters - static get Default() { - return Default$1; - } - static get DefaultType() { - return DefaultType$1; - } - static get NAME() { - return NAME$2; - } + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK$1); + EventHandler.on(this._config.target, EVENT_CLICK$1, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { + return; + } + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); + return; + } - // Public - refresh() { - this._initializeTargetsAndObservables(); - this._maybeEnableSmoothScroll(); - if (this._observer) { - this._observer.disconnect(); + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); } else { - this._observer = this._getNewObserver(); - } - for (const section of this._observableSections.values()) { - this._observer.observe(section); + root.scrollTop = height; } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; + } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; } - dispose() { - this._observer.disconnect(); - super.dispose(); + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; - // Private - _configAfterMerge(config) { - // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case - config.target = getElement(config.target) || document.body; - - // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only - config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin; - if (typeof config.threshold === 'string') { - config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); - } - return config; + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); } - _maybeEnableSmoothScroll() { - if (!this._config.smoothScroll) { - return; - } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); + } + section.focus({ + preventScroll: true + }); + } - // unregister any previous listeners - EventHandler.off(this._config.target, EVENT_CLICK); - EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { - const observableSection = this._observableSections.get(event.target.hash); - if (observableSection) { - event.preventDefault(); - const root = this._rootElement || window; - const height = observableSection.offsetTop - this._element.offsetTop; - if (root.scrollTo) { - root.scrollTo({ - top: height, - behavior: 'smooth' - }); - return; - } + // --- Targets / observables ---------------------------------------------- - // Chrome 60 doesn't support `scrollTo` - root.scrollTop = height; - } - }); - } - _getNewObserver() { - const options = { - root: this._rootElement, - threshold: this._config.threshold, - rootMargin: this._config.rootMargin - }; - return new IntersectionObserver(entries => this._observerCallback(entries), options); - } + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } - // The logic of selection - _observerCallback(entries) { - const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); - const activate = entry => { - this._previousScrollData.visibleEntryTop = entry.target.offsetTop; - this._process(targetElement(entry)); - }; - const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; - const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; - this._previousScrollData.parentScrollTop = parentScrollTop; - for (const entry of entries) { - if (!entry.isIntersecting) { - this._activeTarget = null; - this._clearActiveClass(targetElement(entry)); - continue; - } - const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; - // if we are scrolling down, pick the bigger offsetTop - if (userScrollsDown && entryIsLowerThanPrevious) { - activate(entry); - // if parent isn't scrolled, let's keep the first visible item, breaking the iteration - if (!parentScrollTop) { - return; - } - continue; - } + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section - // if we are scrolling up, pick the smallest offsetTop - if (!userScrollsDown && !entryIsLowerThanPrevious) { - activate(entry); - } + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); } } - _initializeTargetsAndObservables() { - this._targetLinks = new Map(); - this._observableSections = new Map(); - const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); - for (const anchor of targetLinks) { - // ensure that the anchor has an id and is not disabled - if (!anchor.hash || isDisabled(anchor)) { - continue; - } - const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element); - // ensure that the observableSection exists & is visible - if (isVisible(observableSection)) { - this._targetLinks.set(decodeURI(anchor.hash), anchor); - this._observableSections.set(anchor.hash, observableSection); - } - } + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; } - _process(target) { - if (this._activeTarget === target) { - return; + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE$1); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE$1)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE$1); } - this._clearActiveClass(this._config.target); - this._activeTarget = target; - target.classList.add(CLASS_NAME_ACTIVE$1); - this._activateParents(target); - EventHandler.trigger(this._element, EVENT_ACTIVATE, { - relatedTarget: target - }); + return; } - _activateParents(target) { - // Activate dropdown parents - if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { - SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1); - return; - } - for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { - // Set triggered links parents as active - // With both and markup a parent is the previous sibling of any nav ancestor - for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { - item.classList.add(CLASS_NAME_ACTIVE$1); - } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE$1); } } - _clearActiveClass(parent) { - parent.classList.remove(CLASS_NAME_ACTIVE$1); - const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); - for (const node of activeNodes) { - node.classList.remove(CLASS_NAME_ACTIVE$1); - } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE$1); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE$1}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE$1); } + } +} + +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = ScrollSpy.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); +/** + * Data API implementation + */ + +EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$2 = 'tab'; +const DATA_KEY$2 = 'bs.tab'; +const EVENT_KEY$2 = `.${DATA_KEY$2}`; +const EVENT_HIDE$1 = `hide${EVENT_KEY$2}`; +const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$2}`; +const EVENT_SHOW$1 = `show${EVENT_KEY$2}`; +const EVENT_SHOWN$1 = `shown${EVENT_KEY$2}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY$2}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY$2}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY$2}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE$1 = 'fade'; +const CLASS_NAME_SHOW$1 = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; + +/** + * Class definition + */ + +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) } + + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); } - /** - * Data API implementation - */ + // Getters + static get NAME() { + return NAME$2; + } - EventHandler.on(window, EVENT_LOAD_DATA_API$1, () => { - for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { - ScrollSpy.getOrCreateInstance(spy); + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; } - }); - - /** - * jQuery - */ - - defineJQueryPlugin(ScrollSpy); - - /** - * -------------------------------------------------------------------------- - * Bootstrap tab.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME$1 = 'tab'; - const DATA_KEY$1 = 'bs.tab'; - const EVENT_KEY$1 = `.${DATA_KEY$1}`; - const EVENT_HIDE$1 = `hide${EVENT_KEY$1}`; - const EVENT_HIDDEN$1 = `hidden${EVENT_KEY$1}`; - const EVENT_SHOW$1 = `show${EVENT_KEY$1}`; - const EVENT_SHOWN$1 = `shown${EVENT_KEY$1}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY$1}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY$1}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY$1}`; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const HOME_KEY = 'Home'; - const END_KEY = 'End'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_FADE$1 = 'fade'; - const CLASS_NAME_SHOW$1 = 'show'; - const CLASS_DROPDOWN = 'dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; - const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; - const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; - const SELECTOR_OUTER = '.nav-item, .list-group-item'; - const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6 - const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; - const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`; - - /** - * Class definition - */ - - class Tab extends BaseComponent { - constructor(element) { - super(element); - this._parent = this._element.closest(SELECTOR_TAB_PANEL); - if (!this._parent) { - return; - // TODO: should throw exception in v6 - // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) - } - // Set up initial aria attributes - this._setInitialAttributes(this._parent, this._getChildren()); - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } - // Getters - static get NAME() { - return NAME$1; + // Private + _activate(element, relatedElem) { + if (!element) { + return; } + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section - // Public - show() { - // Shows this elem and deactivate the active sibling if exists - const innerElem = this._element; - if (this._elemIsActive(innerElem)) { + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW$1); return; } - - // Search for active tab on same parent to deactivate it - const active = this._getActiveElem(); - const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE$1, { - relatedTarget: innerElem - }) : null; - const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW$1, { - relatedTarget: active + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN$1, { + relatedTarget: relatedElem }); - if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { - return; - } - this._deactivate(active, innerElem); - this._activate(innerElem, active); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; } + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - // Private - _activate(element, relatedElem) { - if (!element) { + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW$1); return; } - element.classList.add(CLASS_NAME_ACTIVE); - this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN$1, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; + } - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.add(CLASS_NAME_SHOW$1); - return; - } - element.removeAttribute('tabindex'); - element.setAttribute('aria-selected', true); - this._toggleDropDown(element, true); - EventHandler.trigger(element, EVENT_SHOWN$1, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; } - _deactivate(element, relatedElem) { - if (!element) { - return; - } - element.classList.remove(CLASS_NAME_ACTIVE); - element.blur(); - this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.remove(CLASS_NAME_SHOW$1); - return; - } - element.setAttribute('aria-selected', false); - element.setAttribute('tabindex', '-1'); - this._toggleDropDown(element, false); - EventHandler.trigger(element, EVENT_HIDDEN$1, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE$1)); + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); } - _keydown(event) { - if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { - return; - } - event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page - event.preventDefault(); - const children = this._getChildren().filter(element => !isDisabled(element)); - let nextActiveElement; - if ([HOME_KEY, END_KEY].includes(event.key)) { - nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]; - } else { - const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); - nextActiveElement = getNextActiveElement(children, event.target, isNext, true); - } - if (nextActiveElement) { - nextActiveElement.focus({ - preventScroll: true - }); - Tab.getOrCreateInstance(nextActiveElement).show(); - } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); } - _getChildren() { - // collection of inner elements - return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); } - _getActiveElem() { - return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); } - _setInitialAttributes(parent, children) { - this._setAttributeIfNotExists(parent, 'role', 'tablist'); - for (const child of children) { - this._setInitialAttributesOnChild(child); - } + if (!isActive) { + child.setAttribute('tabindex', '-1'); } - _setInitialAttributesOnChild(child) { - child = this._getInnerElement(child); - const isActive = this._elemIsActive(child); - const outerElem = this._getOuterElement(child); - child.setAttribute('aria-selected', isActive); - if (outerElem !== child) { - this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); - } - if (!isActive) { - child.setAttribute('tabindex', '-1'); - } - this._setAttributeIfNotExists(child, 'role', 'tab'); + this._setAttributeIfNotExists(child, 'role', 'tab'); - // set attributes to the related panel too - this._setInitialAttributesOnTargetPanel(child); + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; } - _setInitialAttributesOnTargetPanel(child) { - const target = SelectorEngine.getElementFromSelector(child); - if (!target) { - return; - } - this._setAttributeIfNotExists(target, 'role', 'tabpanel'); - if (child.id) { - this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); - } + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); } - _toggleDropDown(element, open) { - const outerElem = this._getOuterElement(element); - if (!outerElem.classList.contains(CLASS_DROPDOWN)) { - return; - } - const toggle = (selector, className) => { - const element = SelectorEngine.findOne(selector, outerElem); - if (element) { - element.classList.toggle(className, open); - } - }; - toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE); - toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW$1); - outerElem.setAttribute('aria-expanded', open); + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; } - _setAttributeIfNotExists(element, attribute, value) { - if (!element.hasAttribute(attribute)) { - element.setAttribute(attribute, value); - } + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW$1, open); } - _elemIsActive(elem) { - return elem.classList.contains(CLASS_NAME_ACTIVE); + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); } + } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } - // Try to get the inner element (usually the .nav-link) - _getInnerElement(elem) { - return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); - } + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } - // Try to get the outer element (usually the .nav-item) - _getOuterElement(elem) { - return elem.closest(SELECTOR_OUTER) || elem; - } + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tab.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE$1, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); + +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME$1 = 'toast'; +const DATA_KEY$1 = 'bs.toast'; +const EVENT_KEY$1 = `.${DATA_KEY$1}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY$1}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY$1}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY$1}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY$1}`; +const EVENT_HIDE = `hide${EVENT_KEY$1}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY$1}`; +const EVENT_SHOW = `show${EVENT_KEY$1}`; +const EVENT_SHOWN = `shown${EVENT_KEY$1}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType$1 = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default$1 = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); } - /** - * Data API implementation - */ + // Getters + static get Default() { + return Default$1; + } + static get DefaultType() { + return DefaultType$1; + } + static get NAME() { + return NAME$1; + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (isDisabled(this)) { + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { return; } - Tab.getOrCreateInstance(this).show(); - }); - - /** - * Initialize on focus - */ - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { - Tab.getOrCreateInstance(element); - } - }); - /** - * jQuery - */ - - defineJQueryPlugin(Tab); - - /** - * -------------------------------------------------------------------------- - * Bootstrap toast.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'toast'; - const DATA_KEY = 'bs.toast'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; - const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const DefaultType = { - animation: 'boolean', - autohide: 'boolean', - delay: 'number' - }; - const Default = { - animation: true, - autohide: true, - delay: 5000 - }; - - /** - * Class definition - */ - - class Toast extends BaseComponent { - constructor(element, config) { - super(element, config); - this._timeout = null; - this._hasMouseInteraction = false; - this._hasKeyboardInteraction = false; - this._setListeners(); + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); } - - // Getters - static get Default() { - return Default; + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; } - static get DefaultType() { - return DefaultType; + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; } - static get NAME() { - return NAME; + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } - // Public - show() { - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (showEvent.defaultPrevented) { - return; - } - this._clearTimeout(); - if (this._config.animation) { - this._element.classList.add(CLASS_NAME_FADE); - } - const complete = () => { - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN); - this._maybeScheduleHide(); - }; - this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated - reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; } - hide() { - if (!this.isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - const complete = () => { - this._element.classList.add(CLASS_NAME_HIDE); // @deprecated - this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.classList.add(CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; + } + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } } - dispose() { + if (isInteracting) { this._clearTimeout(); - if (this.isShown()) { - this._element.classList.remove(CLASS_NAME_SHOW); - } - super.dispose(); + return; } - isShown() { - return this._element.classList.contains(CLASS_NAME_SHOW); + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; } + this._maybeScheduleHide(); + } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} + +/** + * Data API implementation + */ + +enableDismissTrigger(Toast); + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Private - _maybeScheduleHide() { - if (!this._config.autohide) { - return; - } - if (this._hasMouseInteraction || this._hasKeyboardInteraction) { - return; - } - this._timeout = setTimeout(() => { - this.hide(); - }, this._config.delay); - } - _onInteraction(event, isInteracting) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - { - this._hasMouseInteraction = isInteracting; - break; - } - case 'focusin': - case 'focusout': - { - this._hasKeyboardInteraction = isInteracting; - break; - } - } - if (isInteracting) { - this._clearTimeout(); - return; - } - const nextElement = event.relatedTarget; - if (this._element === nextElement || this._element.contains(nextElement)) { - return; - } - this._maybeScheduleHide(); - } - _setListeners() { - EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); - EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; } - _clearTimeout() { - clearTimeout(this._timeout); - this._timeout = null; + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Toast.getOrCreateInstance(this, config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - } - }); + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; } + this._element.setAttribute(attribute, value); } +} - /** - * Data API implementation - */ - - enableDismissTrigger(Toast); - - /** - * jQuery - */ - - defineJQueryPlugin(Toast); - - /** - * -------------------------------------------------------------------------- - * Bootstrap index.umd.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const index_umd = { - Alert, - Button, - Carousel, - Collapse, - Dropdown, - Modal, - Offcanvas, - Popover, - ScrollSpy, - Tab, - Toast, - Tooltip - }; +/** + * Data API implementation + */ - return index_umd; +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); -})); +export { Alert, Button, Carousel, Chips, Collapse, Combobox, Datepicker, Dialog, Drawer, Menu, NavOverflow, OtpInput, Popover, Range, ScrollSpy, Strength, Tab, Toast, Toggler, Tooltip }; diff --git a/assets/javascripts/bootstrap.min.js b/assets/javascripts/bootstrap.min.js index 8c0e1727..0609a811 100644 --- a/assets/javascripts/bootstrap.min.js +++ b/assets/javascripts/bootstrap.min.js @@ -1,6 +1,6 @@ /*! - * Bootstrap v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),a=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),l=t=>{t.dispatchEvent(new Event(o))},c=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),h=t=>c(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,d=t=>{if(!c(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},u=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),_=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?_(t.parentNode):null},g=()=>{},f=t=>{t.offsetHeight},m=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,p=[],b=()=>"rtl"===document.documentElement.dir,v=t=>{var e;e=()=>{const e=m();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of p)t()}),p.push(e)):e()},y=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,w=(t,e,i=!0)=>{if(!i)return void y(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),y(t))};e.addEventListener(o,r),setTimeout(()=>{n||l(e)},s)},A=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},E=/[^.]*(?=\..*)\.|.*/,C=/\..*/,T=/::\d+$/,k={};let $=1;const S={mouseenter:"mouseover",mouseleave:"mouseout"},L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${$++}`||t.uidEvent||$++}function I(t){const e=O(t);return t.uidEvent=e,k[e]=k[e]||{},k[e]}function D(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function N(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=j(t);return L.has(o)||(o=t),[s,n,o]}function P(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=N(e,i,s);if(e in S){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=I(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=O(r,e.replace(E,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return z(n,{delegateTarget:r}),s.oneOff&&F.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return z(s,{delegateTarget:t}),i.oneOff&&F.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function x(t,e,i,s,n){const o=D(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function M(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&x(t,e,i,r.callable,r.delegationSelector)}function j(t){return t=t.replace(C,""),S[t]||t}const F={on(t,e,i,s){P(t,e,i,s,!1)},one(t,e,i,s){P(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=N(e,i,s),a=r!==e,l=I(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))M(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(T,"");a&&!e.includes(n)||x(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;x(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=m();let n=null,o=!0,r=!0,a=!1;e!==j(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=z(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function z(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function H(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function B(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const q={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${B(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${B(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=H(t.dataset[s])}return e},getDataAttribute:(t,e)=>H(t.getAttribute(`data-bs-${B(e)}`))};class W{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=c(e)?q.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...c(e)?q.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,s]of Object.entries(e)){const e=t[i],n=c(e)?"element":a(e);if(!new RegExp(s).test(n))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${n}" but expected type "${s}".`)}}}class R extends W{constructor(t,e){super(),(t=h(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),F.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){w(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(h(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.8"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const K=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map(t=>r(t)).join(","):null},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!u(t)&&d(t))},getSelectorFromElement(t){const e=K(t);return e&&V.findOne(e)?e:null},getElementFromSelector(t){const e=K(t);return e?V.findOne(e):null},getMultipleElementsFromSelector(t){const e=K(t);return e?V.find(e):[]}},Q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;F.on(document,i,`[data-bs-dismiss="${s}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),u(this))return;const n=V.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()})},X=".bs.alert",Y=`close${X}`,U=`closed${X}`;class G extends R{static get NAME(){return"alert"}close(){if(F.trigger(this._element,Y).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),F.trigger(this._element,U),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=G.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}Q(G,"close"),v(G);const J='[data-bs-toggle="button"]';class Z extends R{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=Z.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}F.on(document,"click.bs.button.data-api",J,t=>{t.preventDefault();const e=t.target.closest(J);Z.getOrCreateInstance(e).toggle()}),v(Z);const tt=".bs.swipe",et=`touchstart${tt}`,it=`touchmove${tt}`,st=`touchend${tt}`,nt=`pointerdown${tt}`,ot=`pointerup${tt}`,rt={endCallback:null,leftCallback:null,rightCallback:null},at={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class lt extends W{constructor(t,e){super(),this._element=t,t&<.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return rt}static get DefaultType(){return at}static get NAME(){return"swipe"}dispose(){F.off(this._element,tt)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),y(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&y(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(F.on(this._element,nt,t=>this._start(t)),F.on(this._element,ot,t=>this._end(t)),this._element.classList.add("pointer-event")):(F.on(this._element,et,t=>this._start(t)),F.on(this._element,it,t=>this._move(t)),F.on(this._element,st,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ct=".bs.carousel",ht=".data-api",dt="ArrowLeft",ut="ArrowRight",_t="next",gt="prev",ft="left",mt="right",pt=`slide${ct}`,bt=`slid${ct}`,vt=`keydown${ct}`,yt=`mouseenter${ct}`,wt=`mouseleave${ct}`,At=`dragstart${ct}`,Et=`load${ct}${ht}`,Ct=`click${ct}${ht}`,Tt="carousel",kt="active",$t=".active",St=".carousel-item",Lt=$t+St,Ot={[dt]:mt,[ut]:ft},It={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Dt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Nt extends R{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Tt&&this.cycle()}static get Default(){return It}static get DefaultType(){return Dt}static get NAME(){return"carousel"}next(){this._slide(_t)}nextWhenVisible(){!document.hidden&&d(this._element)&&this.next()}prev(){this._slide(gt)}pause(){this._isSliding&&l(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?F.one(this._element,bt,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void F.one(this._element,bt,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?_t:gt;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&F.on(this._element,vt,t=>this._keydown(t)),"hover"===this._config.pause&&(F.on(this._element,yt,()=>this.pause()),F.on(this._element,wt,()=>this._maybeEnableCycle())),this._config.touch&<.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of V.find(".carousel-item img",this._element))F.on(t,At,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(ft)),rightCallback:()=>this._slide(this._directionToOrder(mt)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new lt(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Ot[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=V.findOne($t,this._indicatorsElement);e.classList.remove(kt),e.removeAttribute("aria-current");const i=V.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(kt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===_t,n=e||A(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>F.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(pt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),f(n),i.classList.add(l),n.classList.add(l),this._queueCallback(()=>{n.classList.remove(l,c),n.classList.add(kt),i.classList.remove(kt,c,l),this._isSliding=!1,r(bt)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return V.findOne(Lt,this._element)}_getItems(){return V.find(St,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return b()?t===ft?gt:_t:t===ft?_t:gt}_orderToDirection(t){return b()?t===gt?ft:mt:t===gt?mt:ft}static jQueryInterface(t){return this.each(function(){const e=Nt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}F.on(document,Ct,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=V.getElementFromSelector(this);if(!e||!e.classList.contains(Tt))return;t.preventDefault();const i=Nt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===q.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),F.on(window,Et,()=>{const t=V.find('[data-bs-ride="carousel"]');for(const e of t)Nt.getOrCreateInstance(e)}),v(Nt);const Pt=".bs.collapse",xt=`show${Pt}`,Mt=`shown${Pt}`,jt=`hide${Pt}`,Ft=`hidden${Pt}`,zt=`click${Pt}.data-api`,Ht="show",Bt="collapse",qt="collapsing",Wt=`:scope .${Bt} .${Bt}`,Rt='[data-bs-toggle="collapse"]',Kt={parent:null,toggle:!0},Vt={parent:"(null|element)",toggle:"boolean"};class Qt extends R{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=V.find(Rt);for(const t of i){const e=V.getSelectorFromElement(t),i=V.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Kt}static get DefaultType(){return Vt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>Qt.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(F.trigger(this._element,xt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Bt),this._element.classList.add(qt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(qt),this._element.classList.add(Bt,Ht),this._element.style[e]="",F.trigger(this._element,Mt)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(F.trigger(this._element,jt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,f(this._element),this._element.classList.add(qt),this._element.classList.remove(Bt,Ht);for(const t of this._triggerArray){const e=V.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(qt),this._element.classList.add(Bt),F.trigger(this._element,Ft)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(Ht)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=h(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Rt);for(const e of t){const t=V.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=V.find(Wt,this._config.parent);return V.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=Qt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}F.on(document,zt,Rt,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of V.getMultipleElementsFromSelector(this))Qt.getOrCreateInstance(t,{toggle:!1}).toggle()}),v(Qt);const Xt="dropdown",Yt=".bs.dropdown",Ut=".data-api",Gt="ArrowUp",Jt="ArrowDown",Zt=`hide${Yt}`,te=`hidden${Yt}`,ee=`show${Yt}`,ie=`shown${Yt}`,se=`click${Yt}${Ut}`,ne=`keydown${Yt}${Ut}`,oe=`keyup${Yt}${Ut}`,re="show",ae='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',le=`${ae}.${re}`,ce=".dropdown-menu",he=b()?"top-end":"top-start",de=b()?"top-start":"top-end",ue=b()?"bottom-end":"bottom-start",_e=b()?"bottom-start":"bottom-end",ge=b()?"left-start":"right-start",fe=b()?"right-start":"left-start",me={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},pe={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class be extends R{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=V.next(this._element,ce)[0]||V.prev(this._element,ce)[0]||V.findOne(ce,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return me}static get DefaultType(){return pe}static get NAME(){return Xt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(u(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!F.trigger(this._element,ee,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))F.on(t,"mouseover",g);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(re),this._element.classList.add(re),F.trigger(this._element,ie,t)}}hide(){if(u(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!F.trigger(this._element,Zt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.off(t,"mouseover",g);this._popper&&this._popper.destroy(),this._menu.classList.remove(re),this._element.classList.remove(re),this._element.setAttribute("aria-expanded","false"),q.removeDataAttribute(this._menu,"popper"),F.trigger(this._element,te,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!c(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Xt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:c(this._config.reference)?t=h(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(re)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return ge;if(t.classList.contains("dropstart"))return fe;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?de:he:e?_e:ue}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(q.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...y(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>d(t));i.length&&A(i,e,t===Jt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=be.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=V.find(le);for(const i of e){const e=be.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Gt,Jt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(ae)?this:V.prev(this,ae)[0]||V.next(this,ae)[0]||V.findOne(ae,t.delegateTarget.parentNode),o=be.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}F.on(document,ne,ae,be.dataApiKeydownHandler),F.on(document,ne,ce,be.dataApiKeydownHandler),F.on(document,se,be.clearMenus),F.on(document,oe,be.clearMenus),F.on(document,se,ae,function(t){t.preventDefault(),be.getOrCreateInstance(this).toggle()}),v(be);const ve="backdrop",ye="show",we=`mousedown.bs.${ve}`,Ae={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ee={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ce extends W{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Ae}static get DefaultType(){return Ee}static get NAME(){return ve}show(t){if(!this._config.isVisible)return void y(t);this._append();const e=this._getElement();this._config.isAnimated&&f(e),e.classList.add(ye),this._emulateAnimation(()=>{y(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(ye),this._emulateAnimation(()=>{this.dispose(),y(t)})):y(t)}dispose(){this._isAppended&&(F.off(this._element,we),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=h(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),F.on(t,we,()=>{y(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){w(t,this._getElement(),this._config.isAnimated)}}const Te=".bs.focustrap",ke=`focusin${Te}`,$e=`keydown.tab${Te}`,Se="backward",Le={autofocus:!0,trapElement:null},Oe={autofocus:"boolean",trapElement:"element"};class Ie extends W{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Le}static get DefaultType(){return Oe}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),F.off(document,Te),F.on(document,ke,t=>this._handleFocusin(t)),F.on(document,$e,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,F.off(document,Te))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=V.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Se?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Se:"forward")}}const De=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ne=".sticky-top",Pe="padding-right",xe="margin-right";class Me{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Pe,e=>e+t),this._setElementAttributes(De,Pe,e=>e+t),this._setElementAttributes(Ne,xe,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Pe),this._resetElementAttributes(De,Pe),this._resetElementAttributes(Ne,xe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&q.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=q.getDataAttribute(t,e);null!==i?(q.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(c(t))e(t);else for(const i of V.find(t,this._element))e(i)}}const je=".bs.modal",Fe=`hide${je}`,ze=`hidePrevented${je}`,He=`hidden${je}`,Be=`show${je}`,qe=`shown${je}`,We=`resize${je}`,Re=`click.dismiss${je}`,Ke=`mousedown.dismiss${je}`,Ve=`keydown.dismiss${je}`,Qe=`click${je}.data-api`,Xe="modal-open",Ye="show",Ue="modal-static",Ge={backdrop:!0,focus:!0,keyboard:!0},Je={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ze extends R{constructor(t,e){super(t,e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Me,this._addEventListeners()}static get Default(){return Ge}static get DefaultType(){return Je}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||F.trigger(this._element,Be,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Xe),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(F.trigger(this._element,Fe).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Ye),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){F.off(window,je),F.off(this._dialog,je),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ce({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ie({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=V.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),f(this._element),this._element.classList.add(Ye),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,F.trigger(this._element,qe,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){F.on(this._element,Ve,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),F.on(window,We,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),F.on(this._element,Ke,t=>{F.one(this._element,Re,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Xe),this._resetAdjustments(),this._scrollBar.reset(),F.trigger(this._element,He)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(F.trigger(this._element,ze).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ue)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ue),this._queueCallback(()=>{this._element.classList.remove(Ue),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=b()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=b()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Ze.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}F.on(document,Qe,'[data-bs-toggle="modal"]',function(t){const e=V.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),F.one(e,Be,t=>{t.defaultPrevented||F.one(e,He,()=>{d(this)&&this.focus()})});const i=V.findOne(".modal.show");i&&Ze.getInstance(i).hide(),Ze.getOrCreateInstance(e).toggle(this)}),Q(Ze),v(Ze);const ti=".bs.offcanvas",ei=".data-api",ii=`load${ti}${ei}`,si="show",ni="showing",oi="hiding",ri=".offcanvas.show",ai=`show${ti}`,li=`shown${ti}`,ci=`hide${ti}`,hi=`hidePrevented${ti}`,di=`hidden${ti}`,ui=`resize${ti}`,_i=`click${ti}${ei}`,gi=`keydown.dismiss${ti}`,fi={backdrop:!0,keyboard:!0,scroll:!1},mi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class pi extends R{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return fi}static get DefaultType(){return mi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||F.trigger(this._element,ai,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Me).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(ni),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(si),this._element.classList.remove(ni),F.trigger(this._element,li,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(F.trigger(this._element,ci).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(oi),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(si,oi),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Me).reset(),F.trigger(this._element,di)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ce({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():F.trigger(this._element,hi)}:null})}_initializeFocusTrap(){return new Ie({trapElement:this._element})}_addEventListeners(){F.on(this._element,gi,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():F.trigger(this._element,hi))})}static jQueryInterface(t){return this.each(function(){const e=pi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}F.on(document,_i,'[data-bs-toggle="offcanvas"]',function(t){const e=V.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this))return;F.one(e,di,()=>{d(this)&&this.focus()});const i=V.findOne(ri);i&&i!==e&&pi.getInstance(i).hide(),pi.getOrCreateInstance(e).toggle(this)}),F.on(window,ii,()=>{for(const t of V.find(ri))pi.getOrCreateInstance(t).show()}),F.on(window,ui,()=>{for(const t of V.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&pi.getOrCreateInstance(t).hide()}),Q(pi),v(pi);const bi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],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:[]},vi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),yi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,wi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!vi.has(i)||Boolean(yi.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},Ai={allowList:bi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},Ei={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ci={entry:"(string|element|function|null)",selector:"(string|element)"};class Ti extends W{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Ai}static get DefaultType(){return Ei}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ci)}_setContent(t,e,i){const s=V.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?c(e)?this._putElementInTemplate(h(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)wi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return y(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const ki=new Set(["sanitize","allowList","sanitizeFn"]),$i="fade",Si="show",Li=".tooltip-inner",Oi=".modal",Ii="hide.bs.modal",Di="hover",Ni="focus",Pi="click",xi={AUTO:"auto",TOP:"top",RIGHT:b()?"left":"right",BOTTOM:"bottom",LEFT:b()?"right":"left"},Mi={allowList:bi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ji={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Fi extends R{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Mi}static get DefaultType(){return ji}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),F.off(this._element.closest(Oi),Ii,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=F.trigger(this._element,this.constructor.eventName("show")),e=(_(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),F.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(Si),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.on(t,"mouseover",g);this._queueCallback(()=>{F.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!F.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(Si),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))F.off(t,"mouseover",g);this._activeTrigger[Pi]=!1,this._activeTrigger[Ni]=!1,this._activeTrigger[Di]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),F.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove($i,Si),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add($i),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ti({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Li]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains($i)}_isShown(){return this.tip&&this.tip.classList.contains(Si)}_createPopper(t){const e=y(this._config.placement,[this,t,this._element]),s=xi[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return y(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...y(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)F.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[Pi]=!(e._isShown()&&e._activeTrigger[Pi]),e.toggle()});else if("manual"!==e){const t=e===Di?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Di?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");F.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Ni:Di]=!0,e._enter()}),F.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Ni:Di]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},F.on(this._element.closest(Oi),Ii,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=q.getDataAttributes(this._element);for(const t of Object.keys(e))ki.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:h(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}v(Fi);const zi=".popover-header",Hi=".popover-body",Bi={...Fi.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},qi={...Fi.DefaultType,content:"(null|string|element|function)"};class Wi extends Fi{static get Default(){return Bi}static get DefaultType(){return qi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[zi]:this._getTitle(),[Hi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=Wi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}v(Wi);const Ri=".bs.scrollspy",Ki=`activate${Ri}`,Vi=`click${Ri}`,Qi=`load${Ri}.data-api`,Xi="active",Yi="[href]",Ui=".nav-link",Gi=`${Ui}, .nav-item > ${Ui}, .list-group-item`,Ji={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Zi={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class ts extends R{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ji}static get DefaultType(){return Zi}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=h(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(F.off(this._config.target,Vi),F.on(this._config.target,Vi,Yi,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=V.find(Yi,this._config.target);for(const e of t){if(!e.hash||u(e))continue;const t=V.findOne(decodeURI(e.hash),this._element);d(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Xi),this._activateParents(t),F.trigger(this._element,Ki,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))V.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Xi);else for(const e of V.parents(t,".nav, .list-group"))for(const t of V.prev(e,Gi))t.classList.add(Xi)}_clearActiveClass(t){t.classList.remove(Xi);const e=V.find(`${Yi}.${Xi}`,t);for(const t of e)t.classList.remove(Xi)}static jQueryInterface(t){return this.each(function(){const e=ts.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}F.on(window,Qi,()=>{for(const t of V.find('[data-bs-spy="scroll"]'))ts.getOrCreateInstance(t)}),v(ts);const es=".bs.tab",is=`hide${es}`,ss=`hidden${es}`,ns=`show${es}`,os=`shown${es}`,rs=`click${es}`,as=`keydown${es}`,ls=`load${es}`,cs="ArrowLeft",hs="ArrowRight",ds="ArrowUp",us="ArrowDown",_s="Home",gs="End",fs="active",ms="fade",ps="show",bs=".dropdown-toggle",vs=`:not(${bs})`,ys='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',ws=`.nav-link${vs}, .list-group-item${vs}, [role="tab"]${vs}, ${ys}`,As=`.${fs}[data-bs-toggle="tab"], .${fs}[data-bs-toggle="pill"], .${fs}[data-bs-toggle="list"]`;class Es extends R{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),F.on(this._element,as,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?F.trigger(e,is,{relatedTarget:t}):null;F.trigger(t,ns,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(fs),this._activate(V.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),F.trigger(t,os,{relatedTarget:e})):t.classList.add(ps)},t,t.classList.contains(ms)))}_deactivate(t,e){t&&(t.classList.remove(fs),t.blur(),this._deactivate(V.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),F.trigger(t,ss,{relatedTarget:e})):t.classList.remove(ps)},t,t.classList.contains(ms)))}_keydown(t){if(![cs,hs,ds,us,_s,gs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!u(t));let i;if([_s,gs].includes(t.key))i=e[t.key===_s?0:e.length-1];else{const s=[hs,us].includes(t.key);i=A(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),Es.getOrCreateInstance(i).show())}_getChildren(){return V.find(ws,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=V.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=V.findOne(t,i);n&&n.classList.toggle(s,e)};s(bs,fs),s(".dropdown-menu",ps),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(fs)}_getInnerElement(t){return t.matches(ws)?t:V.findOne(ws,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=Es.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}F.on(document,rs,ys,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),u(this)||Es.getOrCreateInstance(this).show()}),F.on(window,ls,()=>{for(const t of V.find(As))Es.getOrCreateInstance(t)}),v(Es);const Cs=".bs.toast",Ts=`mouseover${Cs}`,ks=`mouseout${Cs}`,$s=`focusin${Cs}`,Ss=`focusout${Cs}`,Ls=`hide${Cs}`,Os=`hidden${Cs}`,Is=`show${Cs}`,Ds=`shown${Cs}`,Ns="hide",Ps="show",xs="showing",Ms={animation:"boolean",autohide:"boolean",delay:"number"},js={animation:!0,autohide:!0,delay:5e3};class Fs extends R{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return js}static get DefaultType(){return Ms}static get NAME(){return"toast"}show(){F.trigger(this._element,Is).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ns),f(this._element),this._element.classList.add(Ps,xs),this._queueCallback(()=>{this._element.classList.remove(xs),F.trigger(this._element,Ds),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(F.trigger(this._element,Ls).defaultPrevented||(this._element.classList.add(xs),this._queueCallback(()=>{this._element.classList.add(Ns),this._element.classList.remove(xs,Ps),F.trigger(this._element,Os)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ps),super.dispose()}isShown(){return this._element.classList.contains(Ps)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){F.on(this._element,Ts,t=>this._onInteraction(t,!0)),F.on(this._element,ks,t=>this._onInteraction(t,!1)),F.on(this._element,$s,t=>this._onInteraction(t,!0)),F.on(this._element,Ss,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=Fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}return Q(Fs),v(Fs),{Alert:G,Button:Z,Carousel:Nt,Collapse:Qt,Dropdown:be,Modal:Ze,Offcanvas:pi,Popover:Wi,ScrollSpy:ts,Tab:Es,Toast:Fs,Tooltip:Fi}}); +import{computePosition,autoUpdate,offset,flip,shift,arrow}from"@floating-ui/dom";import{Calendar}from"vanilla-calendar-pro";const elementMap=new Map,Data={set(e,t,n){elementMap.has(e)||elementMap.set(e,new Map);const s=elementMap.get(e);s.has(t)||0===s.size?s.set(t,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...s.keys()][0]}.`)},get:(e,t)=>elementMap.has(e)&&elementMap.get(e).get(t)||null,getAny:e=>elementMap.has(e)&&elementMap.get(e).values().next().value||null,remove(e,t){if(!elementMap.has(e))return;const n=elementMap.get(e);n.delete(t),0===n.size&&elementMap.delete(e)}},namespaceRegex=/[^.]*(?=\..*)\.|.*/,stripNameRegex=/\..*/,stripUidRegex=/::\d+$/,eventRegistry={};let uidEvent=1;const customEvents={mouseenter:"mouseover",mouseleave:"mouseout"},nativeEvents=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll","scrollend"]);function makeEventUid(e,t){return t&&`${t}::${uidEvent++}`||e.uidEvent||uidEvent++}function getElementEvents(e){const t=makeEventUid(e);return e.uidEvent=t,eventRegistry[t]=eventRegistry[t]||{},eventRegistry[t]}function bootstrapHandler(e,t){return function n(s){return hydrateObj(s,{delegateTarget:e}),n.oneOff&&EventHandler.off(e,s.type,t),t.apply(e,[s])}}function bootstrapDelegationHandler(e,t,n){return function s(i){const o=e.querySelectorAll(t);for(let{target:r}=i;r&&r!==this;r=r.parentNode)for(const l of o)if(l===r)return hydrateObj(i,{delegateTarget:r}),s.oneOff&&EventHandler.off(e,i.type,t,n),n.apply(r,[i])}}function findHandler(e,t,n=null){return Object.values(e).find(e=>e.callable===t&&e.delegationSelector===n)}function normalizeParameters(e,t,n){const s="string"==typeof t,i=s?n:t||n;let o=getTypeEvent(e);return nativeEvents.has(o)||(o=e),[s,i,o]}function addHandler(e,t,n,s,i){if("string"!=typeof t||!e)return;let[o,r,l]=normalizeParameters(t,n,s);if(t in customEvents){const e=e=>function(t){if(!t.relatedTarget||t.relatedTarget!==t.delegateTarget&&!t.delegateTarget.contains(t.relatedTarget))return e.call(this,t)};r=e(r)}const a=getElementEvents(e),c=a[l]||(a[l]={}),_=findHandler(c,r,o?n:null);if(_)return void(_.oneOff=_.oneOff&&i);const h=makeEventUid(r,t.replace(namespaceRegex,"")),u=o?bootstrapDelegationHandler(e,n,r):bootstrapHandler(e,r);u.delegationSelector=o?n:null,u.callable=r,u.oneOff=i,u.uidEvent=h,c[h]=u,e.addEventListener(l,u,o)}function removeHandler(e,t,n,s,i){const o=findHandler(t[n],s,i);o&&(e.removeEventListener(n,o,Boolean(i)),delete t[n][o.uidEvent])}function removeNamespacedHandlers(e,t,n,s){const i=t[n]||{};for(const[o,r]of Object.entries(i))o.includes(s)&&removeHandler(e,t,n,r.callable,r.delegationSelector)}function getTypeEvent(e){return e=e.replace(stripNameRegex,""),customEvents[e]||e}const EventHandler={on(e,t,n,s){addHandler(e,t,n,s,!1)},one(e,t,n,s){addHandler(e,t,n,s,!0)},off(e,t,n,s){if("string"!=typeof t||!e)return;const[i,o,r]=normalizeParameters(t,n,s),l=r!==t,a=getElementEvents(e),c=a[r]||{},_=t.startsWith(".");if(void 0===o){if(_)for(const n of Object.keys(a))removeNamespacedHandlers(e,a,n,t.slice(1));for(const[n,s]of Object.entries(c)){const i=n.replace(stripUidRegex,"");l&&!t.includes(i)||removeHandler(e,a,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;removeHandler(e,a,r,o,i?n:null)}},trigger(e,t,n){if("string"!=typeof t||!e)return null;const s=hydrateObj(new Event(t,{bubbles:!0,cancelable:!0}),n);return e.dispatchEvent(s),s}};function hydrateObj(e,t={}){for(const[n,s]of Object.entries(t))try{e[n]=s}catch{Object.defineProperty(e,n,{configurable:!0,get:()=>s})}return e}function normalizeData(e){if("true"===e)return!0;if("false"===e)return!1;if(e===Number(e).toString())return Number(e);if(""===e||"null"===e)return null;if("string"!=typeof e)return e;try{return JSON.parse(decodeURIComponent(e))}catch{return e}}function normalizeDataKey(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}const Manipulator={setDataAttribute(e,t,n){e.setAttribute(`data-bs-${normalizeDataKey(t)}`,n)},removeDataAttribute(e,t){e.removeAttribute(`data-bs-${normalizeDataKey(t)}`)},getDataAttributes(e){if(!e)return{};const t={},n=Object.keys(e.dataset).filter(e=>e.startsWith("bs")&&!e.startsWith("bsConfig"));for(const s of n){let n=s.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1),t[n]=normalizeData(e.dataset[s])}return t},getDataAttribute:(e,t)=>normalizeData(e.getAttribute(`data-bs-${normalizeDataKey(t)}`))},MAX_UID=1e6,MILLISECONDS_MULTIPLIER=1e3,TRANSITION_END="transitionend",parseSelector=e=>(e&&window.CSS&&window.CSS.escape&&(e=e.replace(/#([^\s"#']+)/g,(e,t)=>`#${CSS.escape(t)}`)),e),toType=e=>null==e?`${e}`:Object.prototype.toString.call(e).match(/\s([a-z]+)/i)[1].toLowerCase(),getUID=e=>{do{e+=Math.floor(1e6*Math.random())}while(document.getElementById(e));return e},getTransitionDurationFromElement=e=>{if(!e)return 0;let{transitionDuration:t,transitionDelay:n}=window.getComputedStyle(e);const s=Number.parseFloat(t),i=Number.parseFloat(n);return s||i?(t=t.split(",")[0],n=n.split(",")[0],1e3*(Number.parseFloat(t)+Number.parseFloat(n))):0},triggerTransitionEnd=e=>{e.dispatchEvent(new Event(TRANSITION_END))},isElement=e=>!(!e||"object"!=typeof e)&&void 0!==e.nodeType,getElement=e=>isElement(e)?e:"string"==typeof e&&e.length>0?document.querySelector(parseSelector(e)):null,isVisible=e=>{if(!isElement(e)||0===e.getClientRects().length)return!1;const t="visible"===getComputedStyle(e).getPropertyValue("visibility"),n=e.closest("details:not([open])");if(!n)return t;if(n!==e){const t=e.closest("summary");if(t&&t.parentNode!==n)return!1;if(null===t)return!1}return t},isDisabled=e=>!e||e.nodeType!==Node.ELEMENT_NODE||!!e.classList.contains("disabled")||(void 0!==e.disabled?e.disabled:e.hasAttribute("disabled")&&"false"!==e.getAttribute("disabled")),findShadowRoot=e=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof e.getRootNode){const t=e.getRootNode();return t instanceof ShadowRoot?t:null}return e instanceof ShadowRoot?e:e.parentNode?findShadowRoot(e.parentNode):null},noop=()=>{},reflow=e=>{e.offsetHeight},isRTL=()=>"rtl"===document.documentElement.dir,execute=(e,t=[],n=e)=>"function"==typeof e?e.call(...t):n,executeAfterTransition=(e,t,n=!0)=>{if(!n)return void execute(e);const s=getTransitionDurationFromElement(t)+5;let i=!1;const o=({target:n})=>{n===t&&(i=!0,t.removeEventListener(TRANSITION_END,o),execute(e))};t.addEventListener(TRANSITION_END,o),setTimeout(()=>{i||triggerTransitionEnd(t)},s)},getNextActiveElement=(e,t,n,s)=>{const i=e.length;let o=e.indexOf(t);return-1===o?!n&&s?e[i-1]:e[0]:(o+=n?1:-1,s&&(o=(o+i)%i),e[Math.max(0,Math.min(o,i-1))])};class Config{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(e){return e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e}_mergeConfigObj(e,t){const n=isElement(t)?Manipulator.getDataAttribute(t,"config"):{};return{...this.constructor.Default,..."object"==typeof n?n:{},...isElement(t)?Manipulator.getDataAttributes(t):{},..."object"==typeof e?e:{}}}_typeCheckConfig(e,t=this.constructor.DefaultType){for(const[n,s]of Object.entries(t)){const t=e[n],i=isElement(t)?"element":toType(t);if(!new RegExp(s).test(i))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${i}" but expected type "${s}".`)}}}const VERSION="6.0.0-alpha1";class BaseComponent extends Config{constructor(e,t){if(super(),!(e=getElement(e)))return;this._element=e,this._config=this._getConfig(t);const n=Data.get(this._element,this.constructor.DATA_KEY);n&&n.dispose(),Data.set(this._element,this.constructor.DATA_KEY,this)}dispose(){Data.remove(this._element,this.constructor.DATA_KEY),EventHandler.off(this._element,this.constructor.EVENT_KEY);for(const e of Object.getOwnPropertyNames(this))this[e]=null}_queueCallback(e,t,n=!0){executeAfterTransition(()=>{this._element&&e()},t,n)}_getConfig(e){return e=this._mergeConfigObj(e,this._element),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}static getInstance(e){return Data.get(getElement(e),this.DATA_KEY)}static getOrCreateInstance(e,t={}){return this.getInstance(e)||new this(e,"object"==typeof t?t:null)}static get VERSION(){return VERSION}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(e){return`${e}${this.EVENT_KEY}`}}const getSelector=e=>{let t=e.getAttribute("data-bs-target");if(!t||"#"===t){let n=e.getAttribute("href");if(!n||!n.includes("#")&&!n.startsWith("."))return null;n.includes("#")&&!n.startsWith("#")&&(n=`#${n.split("#")[1]}`),t=n&&"#"!==n?n.trim():null}return t?t.split(",").map(e=>parseSelector(e)).join(","):null},SelectorEngine={find:(e,t=document.documentElement)=>[...Element.prototype.querySelectorAll.call(t,e)],findOne:(e,t=document.documentElement)=>Element.prototype.querySelector.call(t,e),children:(e,t)=>[...e.children].filter(e=>e.matches(t)),parents(e,t){const n=[];let s=e.parentNode.closest(t);for(;s;)n.push(s),s=s.parentNode.closest(t);return n},closest:(e,t)=>Element.prototype.closest.call(e,t),prev(e,t){let n=e.previousElementSibling;for(;n;){if(n.matches(t))return[n];n=n.previousElementSibling}return[]},next(e,t){let n=e.nextElementSibling;for(;n;){if(n.matches(t))return[n];n=n.nextElementSibling}return[]},focusableChildren(e){const t=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(e=>`${e}:not([tabindex^="-"])`).join(",");return this.find(t,e).filter(e=>!isDisabled(e)&&isVisible(e))},getSelectorFromElement(e){const t=getSelector(e);return t&&SelectorEngine.findOne(t)?t:null},getElementFromSelector(e){const t=getSelector(e);return t?SelectorEngine.findOne(t):null},getMultipleElementsFromSelector(e){const t=getSelector(e);return t?SelectorEngine.find(t):[]}},enableDismissTrigger=(e,t="hide")=>{const n=`click.dismiss${e.EVENT_KEY}`,s=e.NAME;EventHandler.on(document,n,`[data-bs-dismiss="${s}"]`,function(n){if(["A","AREA"].includes(this.tagName)&&n.preventDefault(),isDisabled(this))return;const i=SelectorEngine.getElementFromSelector(this)||this.closest(`.${s}`);e.getOrCreateInstance(i)[t]()})},eventActionOnPlugin=(e,t,n,s,i=null)=>{eventAction(`${t}.${e.NAME}`,n,t=>{const n=t.targets.filter(Boolean).map(t=>e.getOrCreateInstance(t));"function"==typeof i&&i({...t,instances:n});for(const e of n)e[s]()})},eventAction=(e,t,n)=>{const s=`${t}:not(.disabled):not(:disabled)`;EventHandler.on(document,e,s,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault();const t=SelectorEngine.getSelectorFromElement(this),s=t?SelectorEngine.find(t):[this];n({targets:s,event:e})})},NAME$l="alert",DATA_KEY$h="bs.alert",EVENT_KEY$i=".bs.alert",EVENT_CLOSE="close.bs.alert",EVENT_CLOSED="closed.bs.alert",CLASS_NAME_FADE$4="fade",CLASS_NAME_SHOW$6="show";class Alert extends BaseComponent{static get NAME(){return NAME$l}close(){if(EventHandler.trigger(this._element,EVENT_CLOSE).defaultPrevented)return;this._element.classList.remove("show");const e=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,e)}_destroyElement(){this._element.remove(),EventHandler.trigger(this._element,EVENT_CLOSED),this.dispose()}}enableDismissTrigger(Alert,"close");const NAME$k="button",DATA_KEY$g="bs.button",EVENT_KEY$h=`.${DATA_KEY$g}`,DATA_API_KEY$c=".data-api",CLASS_NAME_ACTIVE$4="active",SELECTOR_DATA_TOGGLE$a='[data-bs-toggle="button"]',EVENT_CLICK_DATA_API$8=`click${EVENT_KEY$h}.data-api`;class Button extends BaseComponent{static get NAME(){return NAME$k}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}}EventHandler.on(document,EVENT_CLICK_DATA_API$8,SELECTOR_DATA_TOGGLE$a,e=>{e.preventDefault();const t=e.target.closest(SELECTOR_DATA_TOGGLE$a);Button.getOrCreateInstance(t).toggle()});const NAME$j="carousel",DATA_KEY$f="bs.carousel",EVENT_KEY$g=`.${DATA_KEY$f}`,DATA_API_KEY$b=".data-api",ARROW_LEFT_KEY$2="ArrowLeft",ARROW_RIGHT_KEY$2="ArrowRight",DIRECTION_LEFT="left",DIRECTION_RIGHT="right",EVENT_SLIDE=`slide${EVENT_KEY$g}`,EVENT_SLID=`slid${EVENT_KEY$g}`,EVENT_KEYDOWN$2=`keydown${EVENT_KEY$g}`,EVENT_MOUSEENTER$2=`mouseenter${EVENT_KEY$g}`,EVENT_MOUSELEAVE$1=`mouseleave${EVENT_KEY$g}`,EVENT_POINTERDOWN$1=`pointerdown${EVENT_KEY$g}`,EVENT_LOAD_DATA_API$3=`load${EVENT_KEY$g}.data-api`,EVENT_CLICK_DATA_API$7=`click${EVENT_KEY$g}.data-api`,CLASS_NAME_CAROUSEL="carousel",CLASS_NAME_ACTIVE$3="active",CLASS_NAME_FADE$3="carousel-fade",CLASS_NAME_CENTER="carousel-center",CLASS_NAME_AUTO="carousel-auto",CLASS_NAME_CLONE="carousel-item-clone",CLASS_NAME_PAUSED="paused",CLASS_NAME_PLAYING="carousel-playing",PROPERTY_INTERVAL="--bs-carousel-interval",SCROLL_DURATION=300,ACTIVE_RATIO_TOLERANCE=.05,SELECTOR_ACTIVE=".active",SELECTOR_ITEM=`.carousel-item:not(.${CLASS_NAME_CLONE})`,SELECTOR_ACTIVE_ITEM=".active"+SELECTOR_ITEM,SELECTOR_INNER$1=".carousel-inner",SELECTOR_INDICATORS=".carousel-indicators",SELECTOR_PLAY_PAUSE=".carousel-control-play-pause",SELECTOR_DATA_SLIDE="[data-bs-slide], [data-bs-slide-to]",SELECTOR_DATA_SLIDE_PREV='[data-bs-slide="prev"]',SELECTOR_DATA_SLIDE_NEXT='[data-bs-slide="next"]',SELECTOR_DATA_AUTOPLAY='[data-bs-autoplay="true"]',KEY_TO_DIRECTION={[ARROW_LEFT_KEY$2]:"right",[ARROW_RIGHT_KEY$2]:"left"},ENDS_STOP="stop",ENDS_WRAP="wrap",ENDS_LOOP="loop",Default$i={autoplay:!1,ends:ENDS_LOOP,interval:5e3,keyboard:!0,pause:"hover"},DefaultType$i={autoplay:"boolean",ends:"string",interval:"number",keyboard:"boolean",pause:"(string|boolean)"},easeInOutCubic=e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2;class Carousel extends BaseComponent{constructor(e,t){super(e,t),this._viewport=SelectorEngine.findOne(SELECTOR_INNER$1,this._element)||this._element,this._indicatorsElement=SelectorEngine.findOne(SELECTOR_INDICATORS,this._element),this._playPauseElement=SelectorEngine.findOne(SELECTOR_PLAY_PAUSE,this._element),this._prevControls=SelectorEngine.find('[data-bs-slide="prev"]',this._element),this._nextControls=SelectorEngine.find('[data-bs-slide="next"]',this._element),this._interval=null,this._observer=null,this._scrollFrame=null,this._looping=!1,this._visibility=new Map,this._playing=this._config.autoplay,this._activeIndex=this._initialActiveIndex(),this._addEventListeners(),this._observeItems(),this._refreshActiveState(),this._playing&&this.cycle(),this._updatePlayPauseControl()}static get Default(){return Default$i}static get DefaultType(){return DefaultType$i}static get NAME(){return NAME$j}next(){this.to(this._navIndex()+1)}nextWhenVisible(){"visible"===document.visibilityState&&isVisible(this._element)&&this.next()}prev(){this.to(this._navIndex()-1)}pause(){this._clearInterval(),this._element.classList.remove("carousel-playing")}cycle(){this._clearInterval(),this._scheduleAutoplay(),this._element.classList.add("carousel-playing")}to(e){if(this._looping)return;const t=this._getItems(),n=Number.parseInt(e,10);if(this._config.ends===ENDS_LOOP&&!this._prefersReducedMotion()&&this._canLoop()){if(n>t.length-1)return void this._loopTransition(!0);if(n<0)return void this._loopTransition(!1)}const s=this._normalizeIndex(n,t.length),i=this._navIndex();null!==s&&s!==i&&(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[s],direction:this._direction(i,s),from:i,to:s}).defaultPrevented||(this._isFade()?this._fadeTo(s):this._scrollToIndex(s)))}dispose(){this._clearInterval(),this._observer&&this._observer.disconnect(),null!==this._scrollFrame&&cancelAnimationFrame(this._scrollFrame);for(const e of SelectorEngine.find(`.${CLASS_NAME_CLONE}`,this._viewport))e.remove();this._viewport.style.scrollSnapType="",EventHandler.off(this._viewport,EVENT_KEY$g),super.dispose()}_configAfterMerge(e){return[ENDS_STOP,ENDS_WRAP,ENDS_LOOP].includes(e.ends)||(e.ends=Default$i.ends),e}_initialActiveIndex(){const e=SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM,this._element),t=e?this._getItems().indexOf(e):0;return Math.max(t,0)}_addEventListeners(){this._config.keyboard&&EventHandler.on(this._element,EVENT_KEYDOWN$2,e=>this._keydown(e)),"hover"===this._config.pause&&(EventHandler.on(this._element,EVENT_MOUSEENTER$2,()=>this.pause()),EventHandler.on(this._element,EVENT_MOUSELEAVE$1,()=>this._maybeEnableCycle())),EventHandler.on(this._viewport,EVENT_POINTERDOWN$1,()=>this._pauseFromInteraction())}_keydown(e){if(/input|textarea/i.test(e.target.tagName))return;const t=KEY_TO_DIRECTION[e.key];t&&(e.preventDefault(),this._pauseFromInteraction(),"right"===t?this.prev():this.next())}_observeItems(){if(!this._isFade()&&"undefined"!=typeof IntersectionObserver){this._observer=new IntersectionObserver(e=>this._handleIntersection(e),{root:this._viewport,threshold:[0,.25,.5,.75,1]});for(const e of this._getItems())this._observer.observe(e)}}_handleIntersection(e){if(this._looping)return;for(const t of e)this._visibility.set(t.target,t.isIntersecting?t.intersectionRatio:0);const t=this._getItems().map(e=>this._visibility.get(e)??0),n=Math.max(...t);let s=this._activeIndex;n>0&&(s=t.findIndex(e=>e>=n-.05)),this._setActive(s),this._updateEndControls()}_navIndex(){if(this._isFade()||this._viewport.scrollWidth-this._viewport.clientWidth<=0)return this._activeIndex;let e=this._activeIndex,t=Number.POSITIVE_INFINITY;for(const[n,s]of this._getItems().entries()){const i=Math.abs(this._scrollDelta(s));i{this._viewport.style.scrollSnapType="",this._observer||this._setActive(e),this._updateEndControls()})}_animateScroll(e,t){null!==this._scrollFrame&&(cancelAnimationFrame(this._scrollFrame),this._scrollFrame=null);const n=this._viewport.scrollLeft,s=e-n;if(this._prefersReducedMotion()||"undefined"==typeof requestAnimationFrame)return this._viewport.scrollTo({left:e,behavior:"instant"}),void t();let i=null;const o=r=>{null===i&&(i=r);const l=Math.min((r-i)/300,1);this._viewport.scrollTo({left:n+s*easeInOutCubic(l),behavior:"instant"}),l<1?this._scrollFrame=requestAnimationFrame(o):(this._viewport.scrollTo({left:e,behavior:"instant"}),this._scrollFrame=null,t())};this._scrollFrame=requestAnimationFrame(o)}_scrollDelta(e){const t=this._viewport.getBoundingClientRect(),n=e.getBoundingClientRect();if(this._element.classList.contains("carousel-center"))return n.left+n.width/2-(t.left+t.width/2);const s=Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart)||0;return isRTL()?n.right-(t.right-s):n.left-(t.left+s)}_loopTransition(e){const t=this._getItems(),n=t.length-1,s=this._activeIndex,i=e?0:n,o=this._loopDirection(e);if(EventHandler.trigger(this._element,EVENT_SLIDE,{relatedTarget:t[i],direction:o,from:s,to:i}).defaultPrevented)return;this._looping=!0;const r=(e?t[0]:t[n]).cloneNode(!0);r.classList.add(CLASS_NAME_CLONE),r.classList.remove("active"),r.removeAttribute("id");for(const e of SelectorEngine.find("[id]",r))e.removeAttribute("id");r.setAttribute("aria-hidden","true"),r.inert=!0,this._viewport.style.scrollSnapType="none",e?this._viewport.append(r):(this._viewport.prepend(r),this._jumpScroll(this._scrollDelta(t[s]))),this._animateScroll(this._viewport.scrollLeft+this._scrollDelta(r),()=>{r.remove(),this._jumpScroll(this._scrollDelta(t[i])),this._activeIndex=i,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[i],direction:o,from:s,to:i}),this._viewport.style.scrollSnapType="",this._looping=!1})}_loopDirection(e){return isRTL()?e?"right":"left":e?"left":"right"}_jumpScroll(e){this._viewport.style.scrollSnapType="none",this._viewport.scrollBy({left:e,top:0,behavior:"instant"})}_fadeTo(e){this._setActive(e)}_setActive(e){const t=this._getItems();if(e===this._activeIndex||!t[e])return;const n=this._activeIndex;this._activeIndex=e,this._refreshActiveState(),EventHandler.trigger(this._element,EVENT_SLID,{relatedTarget:t[e],direction:this._direction(n,e),from:n,to:e})}_refreshActiveState(){const e=this._getItems();for(const[t,n]of e.entries())n.classList.toggle("active",t===this._activeIndex);this._setActiveIndicatorElement(this._activeIndex),this._updateEndControls()}_updateEndControls(){if(this._config.ends!==ENDS_STOP)return;const e=this._viewport,t=e.scrollWidth-e.clientWidth;let n,s;if(t>0){const i=Math.abs(e.scrollLeft);n=i<=1,s=i>=t-1}else{const e=this._getItems().length-1;n=this._activeIndex<=0,s=this._activeIndex>=e}this._setControlsDisabled(this._prevControls,n),this._setControlsDisabled(this._nextControls,s)}_setControlsDisabled(e,t){for(const n of e)t&&n===document.activeElement&&((e===this._prevControls?this._nextControls:this._prevControls)[0]??this._viewport).focus({preventScroll:!0}),n.disabled=t}_setActiveIndicatorElement(e){if(!this._indicatorsElement)return;const t=SelectorEngine.findOne(".active",this._indicatorsElement);t&&(t.classList.remove("active"),t.removeAttribute("aria-current"));const n=SelectorEngine.findOne(`[data-bs-slide-to="${e}"]`,this._indicatorsElement);n&&(n.classList.add("active"),n.setAttribute("aria-current","true"))}_normalizeIndex(e,t){return Number.isNaN(e)||0===t?null:e<0?this._wrapsAround()?t-1:null:e>t-1?this._wrapsAround()?0:null:e}_wrapsAround(){return this._config.ends===ENDS_WRAP||this._config.ends===ENDS_LOOP}_canLoop(){if(this._isFade()||this._getItems().length<2)return!1;const e=getComputedStyle(this._element),t=t=>Number.parseFloat(e.getPropertyValue(t))||0;return 1===(t("--bs-carousel-items")||1)&&0===t("--bs-carousel-items-peek")&&!this._element.classList.contains("carousel-center")&&!this._element.classList.contains("carousel-auto")}_direction(e,t){const n=t>e;return isRTL()?n?"right":"left":n?"left":"right"}_scheduleAutoplay(e=this._activeIndex){const t=this._itemInterval(e);this._element.style.setProperty(PROPERTY_INTERVAL,`${t}ms`),this._interval=setTimeout(()=>{const e=this._upcomingIndex();this.nextWhenVisible(),null!==e?this._scheduleAutoplay(e):this.pause()},t)}_upcomingIndex(){return this._normalizeIndex(this._navIndex()+1,this._getItems().length)}_itemInterval(e=this._activeIndex){const t=this._getItems()[e],n=t?Number.parseInt(t.getAttribute("data-bs-interval"),10):Number.NaN;return Number.isNaN(n)?this._config.interval:n}_maybeEnableCycle(){this._playing&&this.cycle()}_pauseFromInteraction(){this._playing=!1,this.pause(),this._updatePlayPauseControl()}_togglePlayPause(){this._playing?this._pauseFromInteraction():(this._playing=!0,this.cycle(),this._updatePlayPauseControl())}_updatePlayPauseControl(){if(!this._playPauseElement)return;this._playPauseElement.classList.toggle("paused",!this._playing);const e=this._playPauseElement.getAttribute(this._playing?"data-bs-pause-label":"data-bs-play-label");e&&this._playPauseElement.setAttribute("aria-label",e)}_isFade(){return this._element.classList.contains("carousel-fade")}_prefersReducedMotion(){return"undefined"!=typeof window&&"function"==typeof window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches}_getItems(){return SelectorEngine.find(SELECTOR_ITEM,this._element)}_clearInterval(){this._interval&&(clearTimeout(this._interval),this._interval=null)}}EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_DATA_SLIDE,function(e){const t=SelectorEngine.getElementFromSelector(this);if(!t||!t.classList.contains("carousel"))return;e.preventDefault();const n=Carousel.getOrCreateInstance(t);n._pauseFromInteraction();const s=this.getAttribute("data-bs-slide-to");s?n.to(s):"next"!==Manipulator.getDataAttribute(this,"slide")?n.prev():n.next()}),EventHandler.on(document,EVENT_CLICK_DATA_API$7,SELECTOR_PLAY_PAUSE,function(e){const t=SelectorEngine.getElementFromSelector(this);t&&t.classList.contains("carousel")&&(e.preventDefault(),Carousel.getOrCreateInstance(t)._togglePlayPause())}),EventHandler.on(window,EVENT_LOAD_DATA_API$3,()=>{const e=SelectorEngine.find(SELECTOR_DATA_AUTOPLAY);for(const t of e)Carousel.getOrCreateInstance(t)});const NAME$i="collapse",DATA_KEY$e="bs.collapse",EVENT_KEY$f=`.${DATA_KEY$e}`,DATA_API_KEY$a=".data-api",EVENT_SHOW$7=`show${EVENT_KEY$f}`,EVENT_SHOWN$6=`shown${EVENT_KEY$f}`,EVENT_HIDE$6=`hide${EVENT_KEY$f}`,EVENT_HIDDEN$8=`hidden${EVENT_KEY$f}`,EVENT_CLICK_DATA_API$6=`click${EVENT_KEY$f}.data-api`,CLASS_NAME_SHOW$5="show",CLASS_NAME_COLLAPSE="collapse",CLASS_NAME_COLLAPSING="collapsing",CLASS_NAME_COLLAPSED="collapsed",CLASS_NAME_DEEPER_CHILDREN=":scope .collapse .collapse",CLASS_NAME_HORIZONTAL="collapse-horizontal",WIDTH="width",HEIGHT="height",SELECTOR_ACTIVES=".collapse.show, .collapse.collapsing",SELECTOR_DATA_TOGGLE$9='[data-bs-toggle="collapse"]',Default$h={parent:null,toggle:!0},DefaultType$h={parent:"(null|element)",toggle:"boolean"};class Collapse extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._triggerArray=[];const n=SelectorEngine.find(SELECTOR_DATA_TOGGLE$9);for(const e of n){const t=SelectorEngine.getSelectorFromElement(e),n=SelectorEngine.find(t).filter(e=>e===this._element);null!==t&&n.length&&this._triggerArray.push(e)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Default$h}static get DefaultType(){return DefaultType$h}static get NAME(){return NAME$i}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let e=[];if(this._config.parent&&(e=this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(e=>e!==this._element).map(e=>Collapse.getOrCreateInstance(e,{toggle:!1}))),e.length&&e[0]._isTransitioning)return;if(EventHandler.trigger(this._element,EVENT_SHOW$7).defaultPrevented)return;for(const t of e)t.hide();const t=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[t]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n=`scroll${t[0].toUpperCase()+t.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[t]="",EventHandler.trigger(this._element,EVENT_SHOWN$6)},this._element,!0),this._element.style[t]=`${this._element[n]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(EventHandler.trigger(this._element,EVENT_HIDE$6).defaultPrevented)return;const e=this._getDimension();this._element.style[e]=`${this._element.getBoundingClientRect()[e]}px`,reflow(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");for(const e of this._triggerArray){const t=SelectorEngine.getElementFromSelector(e);t&&!this._isShown(t)&&this._addAriaAndCollapsedClass([e],!1)}this._isTransitioning=!0,this._element.style[e]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),EventHandler.trigger(this._element,EVENT_HIDDEN$8)},this._element,!0)}_isShown(e=this._element){return e.classList.contains("show")}_configAfterMerge(e){return e.toggle=Boolean(e.toggle),e.parent=getElement(e.parent),e}_getDimension(){return this._element.classList.contains("collapse-horizontal")?WIDTH:HEIGHT}_initializeChildren(){if(!this._config.parent)return;const e=this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$9);for(const t of e){const e=SelectorEngine.getElementFromSelector(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}}_getFirstLevelChildren(e){const t=SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN,this._config.parent);return SelectorEngine.find(e,this._config.parent).filter(e=>!t.includes(e))}_addAriaAndCollapsedClass(e,t){if(e.length)for(const n of e)n.classList.toggle("collapsed",!t),n.setAttribute("aria-expanded",t)}}EventHandler.on(document,EVENT_CLICK_DATA_API$6,SELECTOR_DATA_TOGGLE$9,function(e){("A"===e.target.tagName||e.delegateTarget&&"A"===e.delegateTarget.tagName)&&e.preventDefault();for(const e of SelectorEngine.getMultipleElementsFromSelector(this))Collapse.getOrCreateInstance(e,{toggle:!1}).toggle()});const BREAKPOINTS={sm:576,md:768,lg:1024,xl:1280,"2xl":1536},parseResponsivePlacement=(e,t="bottom")=>{if(!e||!e.includes(":"))return null;const n=e.split(/\s+/),s={xs:t};for(const e of n)if(e.includes(":")){const[t,n]=e.split(":");void 0!==BREAKPOINTS[t]&&(s[t]=n)}else s.xs=e;return s},getResponsivePlacement=(e,t="bottom")=>{if(!e)return t;const n=window.innerWidth;let s=e.xs||t;const i=["sm","md","lg","xl","2xl"];for(const t of i)n>=BREAKPOINTS[t]&&e[t]&&(s=e[t]);return s},createBreakpointListeners=e=>{const t=[];for(const n of Object.keys(BREAKPOINTS)){const s=BREAKPOINTS[n],i=window.matchMedia(`(min-width: ${s}px)`);i.addEventListener("change",e),t.push({mql:i,handler:e})}return t},disposeBreakpointListeners=e=>{for(const{mql:t,handler:n}of e)t.removeEventListener("change",n)},NAME$h="menu",DATA_KEY$d="bs.menu",EVENT_KEY$e=".bs.menu",DATA_API_KEY$9=".data-api",ESCAPE_KEY$2="Escape",TAB_KEY$1="Tab",ARROW_UP_KEY$2="ArrowUp",ARROW_DOWN_KEY$2="ArrowDown",ARROW_LEFT_KEY$1="ArrowLeft",ARROW_RIGHT_KEY$1="ArrowRight",HOME_KEY$2="Home",END_KEY$2="End",ENTER_KEY$1="Enter",SPACE_KEY$1=" ",RIGHT_MOUSE_BUTTON=2,SUBMENU_CLOSE_DELAY=100,EVENT_HIDE$5="hide.bs.menu",EVENT_HIDDEN$7="hidden.bs.menu",EVENT_SHOW$6="show.bs.menu",EVENT_SHOWN$5="shown.bs.menu",EVENT_CLICK_DATA_API$5="click.bs.menu.data-api",EVENT_KEYDOWN_DATA_API="keydown.bs.menu.data-api",EVENT_KEYUP_DATA_API="keyup.bs.menu.data-api",CLASS_NAME_SHOW$4="show",SELECTOR_DATA_TOGGLE$8='[data-bs-toggle="menu"]:not(.disabled):not(:disabled)',SELECTOR_MENU$2=".menu",SELECTOR_SUBMENU=".submenu",SELECTOR_SUBMENU_TOGGLE=".submenu > .menu-item",SELECTOR_NAVBAR_NAV=".navbar-nav",SELECTOR_VISIBLE_ITEMS$1=".menu-item:not(.disabled):not(:disabled)",DEFAULT_PLACEMENT="bottom-start",SUBMENU_PLACEMENT="end-start",resolveLogicalPlacement=e=>isRTL()?e.replace(/^start(?=-|$)/,"right").replace(/^end(?=-|$)/,"left"):e.replace(/^start(?=-|$)/,"left").replace(/^end(?=-|$)/,"right"),triangleSign=(e,t,n)=>(e.x-n.x)*(t.y-n.y)-(t.x-n.x)*(e.y-n.y),Default$g={autoClose:!0,boundary:"clippingParents",container:!1,display:"dynamic",offset:[0,2],floatingConfig:null,menu:null,placement:"bottom-start",reference:"toggle",strategy:"absolute",submenuTrigger:"both",submenuDelay:100},DefaultType$g={autoClose:"(boolean|string)",boundary:"(string|element)",container:"(string|element|boolean)",display:"string",offset:"(array|string|function)",floatingConfig:"(null|object|function)",menu:"(null|element)",placement:"string",reference:"(string|element|object)",strategy:"string",submenuTrigger:"string",submenuDelay:"number"};class Menu extends BaseComponent{static _openInstances=new Set;constructor(e,t){if(void 0===computePosition)throw new TypeError("Bootstrap's menus require Floating UI (https://floating-ui.com)");super(e,t),this._floatingCleanup=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this._parent=this._element.parentNode,this._openSubmenus=new Map,this._submenuCloseTimeouts=new Map,this._hoverIntentData=null,this._menu=this._config.menu||this._findMenu(),!this._config.menu&&this._menu&&(this._parent=this._findWrapper(this._menu)),this._isSubmenu=this._parent.classList?.contains("submenu"),this._menuOriginalParent=this._menu?.parentNode,this._parseResponsivePlacements(),this._setupSubmenuListeners()}static get Default(){return Default$g}static get DefaultType(){return DefaultType$g}static get NAME(){return"menu"}toggle(){return this._isShown()?this.hide():this.show()}show(){if(isDisabled(this._element)||this._isShown())return;const e={relatedTarget:this._element};if(!EventHandler.trigger(this._element,EVENT_SHOW$6,e).defaultPrevented){if(this._moveMenuToContainer(),this._createFloating(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._element.focus({focusVisible:!1}),this._element.setAttribute("aria-expanded","true"),this._menu.classList.add("show"),this._element.classList.add("show"),this._parent&&this._parent.classList.add("show"),Menu._openInstances.add(this),EventHandler.trigger(this._element,EVENT_SHOWN$5,e)}}hide(){if(isDisabled(this._element)||!this._isShown())return;const e={relatedTarget:this._element};this._completeHide(e)}dispose(){this._disposeFloating(),this._restoreMenuToOriginalParent(),this._disposeMediaQueryListeners(),this._closeAllSubmenus(),this._clearAllSubmenuTimeouts(),Menu._openInstances.delete(this),super.dispose()}update(){this._floatingCleanup&&this._updateFloatingPosition()}_findMenu(){const e=SelectorEngine.closest(this._element,":has(.menu)");return SelectorEngine.next(this._element,".menu")[0]||SelectorEngine.prev(this._element,".menu")[0]||SelectorEngine.findOne(".menu",e||this._parent)}_findWrapper(e){let t=this._element.parentNode;for(;t instanceof Element&&!t.contains(e);)t=t.parentNode;return t instanceof Element?t:this._element.parentNode}_completeHide(e){if(!EventHandler.trigger(this._element,EVENT_HIDE$5,e).defaultPrevented){if(this._closeAllSubmenus(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._disposeFloating(),this._restoreMenuToOriginalParent(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._parent&&this._parent.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),Manipulator.removeDataAttribute(this._menu,"placement"),Manipulator.removeDataAttribute(this._menu,"display"),Menu._openInstances.delete(this),EventHandler.trigger(this._element,EVENT_HIDDEN$7,e)}}_getConfig(e){if("object"==typeof(e=super._getConfig(e)).reference&&!isElement(e.reference)&&"function"!=typeof e.reference.getBoundingClientRect)throw new TypeError(`${"menu".toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return e}_createFloating(){if("static"===this._config.display)return void Manipulator.setDataAttribute(this._menu,"display","static");let e=this._element;"parent"===this._config.reference?e=this._parent:isElement(this._config.reference)?e=getElement(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference),this._updateFloatingPosition(e),this._floatingCleanup=autoUpdate(e,this._menu,()=>this._updateFloatingPosition(e))}async _updateFloatingPosition(e=null){if(!this._menu)return;e||(e="parent"===this._config.reference?this._parent:isElement(this._config.reference)?getElement(this._config.reference):"object"==typeof this._config.reference?this._config.reference:this._element);const t=this._getPlacement(),n=this._getFloatingMiddleware(),s=this._getFloatingConfig(t,n);await this._applyFloatingPosition(e,this._menu,s.placement,s.middleware,s.strategy)}_isShown(){return this._menu.classList.contains("show")}_getPlacement(){const e=this._responsivePlacements?getResponsivePlacement(this._responsivePlacements,"bottom-start"):this._config.placement;return resolveLogicalPlacement(e)}_parseResponsivePlacements(){this._responsivePlacements=parseResponsivePlacement(this._config.placement,"bottom-start"),this._responsivePlacements&&this._setupMediaQueryListeners()}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_getFloatingMiddleware(){const e=this._getOffset();return[offset("function"==typeof e?e:{mainAxis:e[1]||0,crossAxis:e[0]||0}),flip({fallbackPlacements:this._getFallbackPlacements()}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})]}_getFallbackPlacements(){return{bottom:["top","bottom-start","bottom-end","top-start","top-end"],"bottom-start":["top-start","bottom-end","top-end"],"bottom-end":["top-end","bottom-start","top-start"],top:["bottom","top-start","top-end","bottom-start","bottom-end"],"top-start":["bottom-start","top-end","bottom-end"],"top-end":["bottom-end","top-start","bottom-start"],right:["left","right-start","right-end","left-start","left-end"],"right-start":["left-start","right-end","left-end","top-start","bottom-start"],"right-end":["left-end","right-start","left-start","top-end","bottom-end"],left:["right","left-start","left-end","right-start","right-end"],"left-start":["right-start","left-end","right-end","top-start","bottom-start"],"left-end":["right-end","left-start","right-start","top-end","bottom-end"]}[this._getPlacement()]||["top","bottom","right","left"]}_getFloatingConfig(e,t){const n={placement:e,middleware:t,strategy:this._config.strategy};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null)}_getContainer(){const{container:e}=this._config;return!1===e?null:!0===e?document.body:getElement(e)}_moveMenuToContainer(){const e=this._getContainer();e&&this._menu&&this._menu.parentNode!==e&&e.append(this._menu)}_restoreMenuToOriginalParent(){this._menuOriginalParent&&this._menu&&this._menu.parentNode!==this._menuOriginalParent&&this._menuOriginalParent.append(this._menu)}async _applyFloatingPosition(e,t,n,s,i="absolute"){if(!t.isConnected)return null;const{x:o,y:r,placement:l}=await computePosition(e,t,{placement:n,middleware:s,strategy:i});return t.isConnected?(Object.assign(t.style,{position:i,left:`${o}px`,top:`${r}px`,margin:"0"}),Manipulator.setDataAttribute(t,"placement",l),l):null}_setupSubmenuListeners(){"hover"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||(EventHandler.on(this._menu,"mouseenter",".submenu > .menu-item",e=>{this._onSubmenuTriggerEnter(e)}),EventHandler.on(this._menu,"mouseleave",".submenu",e=>{this._onSubmenuLeave(e)}),EventHandler.on(this._menu,"mousemove",e=>{this._trackMousePosition(e)})),"click"!==this._config.submenuTrigger&&"both"!==this._config.submenuTrigger||EventHandler.on(this._menu,"click",".submenu > .menu-item",e=>{this._onSubmenuTriggerClick(e)})}_onSubmenuTriggerEnter(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._cancelSubmenuCloseTimeout(s),this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n))}_onSubmenuLeave(e){const t=e.target.closest(".submenu"),n=SelectorEngine.findOne(".menu",t);n&&this._openSubmenus.has(n)&&(this._isMovingTowardSubmenu(e,n)||this._scheduleSubmenuClose(n,t))}_onSubmenuTriggerClick(e){const t=e.target.closest(".submenu > .menu-item");if(!t)return;e.preventDefault(),e.stopPropagation();const n=t.closest(".submenu"),s=SelectorEngine.findOne(".menu",n);s&&(this._openSubmenus.has(s)?this._closeSubmenu(s,n):(this._closeSiblingSubmenus(n),this._openSubmenu(t,s,n)))}_openSubmenu(e,t,n){if(this._openSubmenus.has(t))return;e.setAttribute("aria-expanded","true"),e.setAttribute("aria-haspopup","true"),t.style.opacity="0",t.classList.add("show"),n.classList.add("show");const s=this._createSubmenuFloating(e,t,n);this._openSubmenus.set(t,s),EventHandler.on(t,"mouseenter",()=>{this._cancelSubmenuCloseTimeout(t)})}_closeSubmenu(e,t){if(!this._openSubmenus.has(e))return;const n=SelectorEngine.find(".submenu .menu.show",e);for(const e of n){const t=e.closest(".submenu");this._closeSubmenu(e,t)}const s=SelectorEngine.findOne(".submenu > .menu-item",t),i=this._openSubmenus.get(e);i&&i(),this._openSubmenus.delete(e),EventHandler.off(e,"mouseenter"),s&&s.setAttribute("aria-expanded","false"),e.classList.remove("show"),t.classList.remove("show"),e.style.opacity=""}_closeAllSubmenus(){for(const[e]of this._openSubmenus){const t=e.closest(".submenu");this._closeSubmenu(e,t)}}_closeSiblingSubmenus(e){const t=e.parentNode,n=SelectorEngine.find(".submenu > .menu.show",t);for(const t of n){const n=t.closest(".submenu");n!==e&&this._closeSubmenu(t,n)}}_createSubmenuFloating(e,t,n){const s=n,i=resolveLogicalPlacement("end-start"),o=[offset({mainAxis:0,crossAxis:-4}),flip({fallbackPlacements:[resolveLogicalPlacement("start-start"),resolveLogicalPlacement("end-end"),resolveLogicalPlacement("start-end")]}),shift({padding:8})],r=()=>this._applyFloatingPosition(s,t,i,o).then(e=>(t.style.opacity="",e));return r(),autoUpdate(s,t,r)}_scheduleSubmenuClose(e,t){this._cancelSubmenuCloseTimeout(e);const n=setTimeout(()=>{this._closeSubmenu(e,t),this._submenuCloseTimeouts.delete(e)},this._config.submenuDelay);this._submenuCloseTimeouts.set(e,n)}_cancelSubmenuCloseTimeout(e){const t=this._submenuCloseTimeouts.get(e);t&&(clearTimeout(t),this._submenuCloseTimeouts.delete(e))}_clearAllSubmenuTimeouts(){for(const e of this._submenuCloseTimeouts.values())clearTimeout(e);this._submenuCloseTimeouts.clear()}_trackMousePosition(e){this._hoverIntentData={x:e.clientX,y:e.clientY,timestamp:Date.now()}}_isMovingTowardSubmenu(e,t){if(!this._hoverIntentData)return!1;const n=t.getBoundingClientRect(),s={x:e.clientX,y:e.clientY},i={x:this._hoverIntentData.x,y:this._hoverIntentData.y},o=isRTL()?n.right:n.left,r={x:o,y:n.top},l={x:o,y:n.bottom};return this._pointInTriangle(s,i,r,l)}_pointInTriangle(e,t,n,s){const i=triangleSign(e,t,n),o=triangleSign(e,n,s),r=triangleSign(e,s,t);return!((i<0||o<0||r<0)&&(i>0||o>0||r>0))}_selectMenuItem({key:e,target:t}){const n=t.closest(".menu")||this._menu,s=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,n).filter(e=>isVisible(e));s.length&&getNextActiveElement(s,t,e===ARROW_DOWN_KEY$2,!s.includes(t)).focus()}_handleSubmenuKeydown(e){const{key:t,target:n}=e,s=isRTL(),i=s?ARROW_LEFT_KEY$1:ARROW_RIGHT_KEY$1,o=s?ARROW_RIGHT_KEY$1:ARROW_LEFT_KEY$1,r=n.closest(".submenu"),l=r&&n.matches(".submenu > .menu-item");if((t===ENTER_KEY$1||t===SPACE_KEY$1)&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",r);return t&&(this._closeSiblingSubmenus(r),this._openSubmenu(n,t,r),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===i&&l){e.preventDefault(),e.stopPropagation();const t=SelectorEngine.findOne(".menu",r);return t&&(this._closeSiblingSubmenus(r),this._openSubmenu(n,t,r),requestAnimationFrame(()=>{const e=SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS$1,t);e&&e.focus()})),!0}if(t===o){const t=n.closest(".menu"),s=t?.closest(".submenu");if(s){e.preventDefault(),e.stopPropagation();const n=SelectorEngine.findOne(".submenu > .menu-item",s);return this._closeSubmenu(t,s),n&&n.focus(),!0}}if(t===HOME_KEY$2||t===END_KEY$2){e.preventDefault(),e.stopPropagation();const s=n.closest(".menu"),i=SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS$1}`,s).filter(e=>isVisible(e));return i.length&&(t===HOME_KEY$2?i[0]:i.at(-1)).focus(),!0}return!1}static clearMenus(e){if(2!==e.button&&("keyup"!==e.type||"Tab"===e.key))for(const t of Menu._openInstances){if(!1===t._config.autoClose)continue;const n=e.composedPath(),s=n.includes(t._menu);if(n.includes(t._element)||"inside"===t._config.autoClose&&!s||"outside"===t._config.autoClose&&s)continue;const i=e.target.closest?.("form"),o=Boolean(i)&&t._menu.contains(i);if(t._menu.contains(e.target)&&("keyup"===e.type&&"Tab"===e.key||/input|select|option|textarea|form/i.test(e.target.tagName)||o))continue;const r={relatedTarget:t._element};"click"===e.type&&(r.clickEvent=e),t._completeHide(r)}}static dataApiKeydownHandler(e){const t=/input|textarea/i.test(e.target.tagName)||e.target.isContentEditable,n="Escape"===e.key,s=[ARROW_UP_KEY$2,ARROW_DOWN_KEY$2].includes(e.key),i=[ARROW_LEFT_KEY$1,ARROW_RIGHT_KEY$1].includes(e.key),o=[HOME_KEY$2,END_KEY$2].includes(e.key),r=[ENTER_KEY$1,SPACE_KEY$1].includes(e.key),l=e.target.matches(".submenu > .menu-item");if(!(s||n||i||o||r&&l))return;if(t&&!n)return;const a=this.matches(SELECTOR_DATA_TOGGLE$8)?this:SelectorEngine.prev(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.next(this,SELECTOR_DATA_TOGGLE$8)[0]||SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$8,e.delegateTarget.parentNode);if(!a)return;const c=Menu.getOrCreateInstance(a);if(!(i||o||r&&l)||!c._handleSubmenuKeydown(e)){if(s)return e.preventDefault(),e.stopPropagation(),c.show(),void c._selectMenuItem(e);if(n&&c._isShown()){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".menu"),n=t?.closest(".submenu");if(n&&c._openSubmenus.size>0){const e=SelectorEngine.findOne(".submenu > .menu-item",n);return c._closeSubmenu(t,n),void(e&&e.focus())}c.hide(),a.focus()}}}}EventHandler.on(document,EVENT_KEYDOWN_DATA_API,SELECTOR_DATA_TOGGLE$8,Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_KEYDOWN_DATA_API,".menu",Menu.dataApiKeydownHandler),EventHandler.on(document,EVENT_CLICK_DATA_API$5,Menu.clearMenus),EventHandler.on(document,EVENT_KEYUP_DATA_API,Menu.clearMenus),EventHandler.on(document,EVENT_CLICK_DATA_API$5,SELECTOR_DATA_TOGGLE$8,function(e){e.preventDefault(),Menu.getOrCreateInstance(this).toggle()});const NAME$g="combobox",DATA_KEY$c="bs.combobox",EVENT_KEY$d=`.${DATA_KEY$c}`,DATA_API_KEY$8=".data-api",ESCAPE_KEY$1="Escape",TAB_KEY="Tab",ARROW_UP_KEY$1="ArrowUp",ARROW_DOWN_KEY$1="ArrowDown",HOME_KEY$1="Home",END_KEY$1="End",ENTER_KEY="Enter",SPACE_KEY=" ",EVENT_CHANGE$3=`change${EVENT_KEY$d}`,EVENT_SHOW$5=`show${EVENT_KEY$d}`,EVENT_SHOWN$4=`shown${EVENT_KEY$d}`,EVENT_HIDE$4=`hide${EVENT_KEY$d}`,EVENT_HIDDEN$6=`hidden${EVENT_KEY$d}`,EVENT_CLICK_DATA_API$4=`click${EVENT_KEY$d}.data-api`,CLASS_NAME_SHOW$3="show",CLASS_NAME_SELECTED="selected",CLASS_NAME_PLACEHOLDER="combobox-placeholder",SELECTOR_DATA_TOGGLE$7='[data-bs-toggle="combobox"]',SELECTOR_MENU$1=".menu",SELECTOR_MENU_ITEM=".menu-item[data-bs-value]",SELECTOR_VISIBLE_ITEMS=".menu-item[data-bs-value]:not(.disabled):not(:disabled)",SELECTOR_VALUE=".combobox-value",SELECTOR_SEARCH_INPUT=".combobox-search-input",SELECTOR_NO_RESULTS=".combobox-no-results",Default$f={boundary:"clippingParents",multiple:!1,name:null,offset:[0,2],placeholder:"",placement:"bottom-start",search:!1,searchNormalize:!1},DefaultType$f={boundary:"(string|element)",multiple:"boolean",name:"(string|null)",offset:"(array|string|function)",placeholder:"string",placement:"string",search:"boolean",searchNormalize:"boolean"};class Combobox extends BaseComponent{constructor(e,t){super(e,t),this._toggle=this._element,this._menu=SelectorEngine.next(this._toggle,".menu")[0],this._valueDisplay=SelectorEngine.findOne(SELECTOR_VALUE,this._toggle),this._searchInput=SelectorEngine.findOne(SELECTOR_SEARCH_INPUT,this._menu),this._noResults=SelectorEngine.findOne(SELECTOR_NO_RESULTS,this._menu),this._hiddenInput=null,this._menuInstance=null,this._createHiddenInput(),this._createMenuInstance(),this._syncInitialSelection(),this._addEventListeners()}static get Default(){return Default$f}static get DefaultType(){return DefaultType$f}static get NAME(){return NAME$g}toggle(){return this._isShown()?this.hide():this.show()}show(){isDisabled(this._toggle)||this._isShown()||EventHandler.trigger(this._toggle,EVENT_SHOW$5).defaultPrevented||(this._menuInstance.show(),this._searchInput&&(this._searchInput.value="",this._filterItems(""),requestAnimationFrame(()=>this._searchInput.focus())),EventHandler.trigger(this._toggle,EVENT_SHOWN$4))}hide(){this._isShown()&&(EventHandler.trigger(this._toggle,EVENT_HIDE$4).defaultPrevented||(this._menuInstance.hide(),EventHandler.trigger(this._toggle,EVENT_HIDDEN$6)))}dispose(){this._menuInstance&&(this._menuInstance.dispose(),this._menuInstance=null),this._hiddenInput&&(this._hiddenInput.remove(),this._hiddenInput=null),EventHandler.off(this._menu,EVENT_KEY$d),EventHandler.off(this._toggle,EVENT_KEY$d),super.dispose()}_isShown(){return this._menu.classList.contains("show")}_createHiddenInput(){const{name:e}=this._config;e&&(this._hiddenInput=document.createElement("input"),this._hiddenInput.type="hidden",this._hiddenInput.name=e,this._hiddenInput.value="",this._toggle.parentNode.insertBefore(this._hiddenInput,this._toggle))}_createMenuInstance(){this._menuInstance=new Menu(this._toggle,{menu:this._menu,autoClose:!this._config.multiple||"outside",boundary:this._config.boundary,offset:this._config.offset,placement:this._config.placement})}_syncInitialSelection(){this._getSelectedItems().length>0?(this._updateToggleText(),this._updateHiddenInput()):this._showPlaceholder()}_addEventListeners(){EventHandler.on(this._menu,"click",SELECTOR_MENU_ITEM,e=>{const t=e.target.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&(e.preventDefault(),e.stopPropagation(),this._selectItem(t))}),EventHandler.on(this._toggle,"keydown",e=>{this._handleToggleKeydown(e)}),EventHandler.on(this._menu,"keydown",e=>{this._handleMenuKeydown(e)}),this._searchInput&&(EventHandler.on(this._searchInput,"input",()=>{this._filterItems(this._searchInput.value)}),EventHandler.on(this._searchInput,"keydown",e=>{if("ArrowDown"===e.key){e.preventDefault();const t=this._getVisibleItems();t.length>0&&t[0].focus()}"Escape"===e.key&&(this.hide(),this._toggle.focus())}))}_selectItem(e){if(this._config.multiple)e.classList.toggle("selected"),e.setAttribute("aria-selected",e.classList.contains("selected"));else{const t=SelectorEngine.find(".selected",this._menu);for(const e of t)e.classList.remove("selected"),e.setAttribute("aria-selected","false");e.classList.add("selected"),e.setAttribute("aria-selected","true")}this._updateToggleText(),this._updateHiddenInput();const t=this._config.multiple?this._getSelectedItems().map(e=>e.dataset.bsValue):e.dataset.bsValue;EventHandler.trigger(this._toggle,EVENT_CHANGE$3,{value:t,item:e}),this._config.multiple||(this.hide(),this._toggle.focus())}_updateToggleText(){const e=this._getSelectedItems();if(0!==e.length)if(this._valueDisplay.classList.remove("combobox-placeholder"),this._config.multiple&&e.length>1)this._valueDisplay.textContent=`${e.length} selected`;else{const t=e[0],n=SelectorEngine.findOne(".menu-item-content > span:first-child",t);this._valueDisplay.textContent=n?n.textContent:t.textContent.trim()}else this._showPlaceholder()}_showPlaceholder(){const{placeholder:e}=this._config;e&&(this._valueDisplay.textContent=e,this._valueDisplay.classList.add("combobox-placeholder"))}_updateHiddenInput(){if(!this._hiddenInput)return;const e=this._getSelectedItems().map(e=>e.dataset.bsValue);this._hiddenInput.value=this._config.multiple?e.join(","):e[0]||""}_getSelectedItems(){return SelectorEngine.find(".selected",this._menu)}_getVisibleItems(){return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS,this._menu).filter(e=>isVisible(e))}_filterItems(e){const t=this._normalizeText(e.toLowerCase().trim()),n=SelectorEngine.find(SELECTOR_MENU_ITEM,this._menu);let s=0;for(const e of n){const n=this._normalizeText(e.textContent.toLowerCase().trim()),i=!t||n.includes(t);e.style.display=i?"":"none",i&&s++}this._noResults&&this._noResults.classList.toggle("d-none",s>0)}_normalizeText(e){return this._config.searchNormalize?e.normalize("NFD").replace(/[\u0300-\u036F]/g,""):e}_handleToggleKeydown(e){const{key:t}=e;if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault(),this._isShown()||this.show();const n=this._getVisibleItems();return void(n.length>0&&("ArrowDown"===t?n[0]:n.at(-1)).focus())}"Enter"!==t&&" "!==t||this._isShown()||(e.preventDefault(),this.show())}_handleMenuKeydown(e){const{key:t,target:n}=e;if("Escape"===t)return e.preventDefault(),e.stopPropagation(),this.hide(),void this._toggle.focus();if("Tab"===t)return void this.hide();const s=n.matches("input");if("ArrowDown"===t||"ArrowUp"===t){e.preventDefault();const s=this._getVisibleItems();return void(s.length>0&&getNextActiveElement(s,n,"ArrowDown"===t,!s.includes(n)).focus())}if("Home"===t||"End"===t){e.preventDefault();const n=this._getVisibleItems();return void(n.length>0&&("Home"===t?n[0]:n.at(-1)).focus())}if(("Enter"===t||" "===t)&&!s){e.preventDefault();const t=n.closest(SELECTOR_MENU_ITEM);t&&!isDisabled(t)&&this._selectItem(t)}}static jQueryInterface(e){return this.each(function(){const t=Combobox.getOrCreateInstance(this,e);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}})}}EventHandler.on(document,EVENT_CLICK_DATA_API$4,SELECTOR_DATA_TOGGLE$7,function(e){e.preventDefault(),Combobox.getOrCreateInstance(this).toggle()}),EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE$7))Combobox.getOrCreateInstance(e)});const NAME$f="datepicker",DATA_KEY$b="bs.datepicker",EVENT_KEY$c=`.${DATA_KEY$b}`,DATA_API_KEY$7=".data-api",EVENT_CHANGE$2=`change${EVENT_KEY$c}`,EVENT_SHOW$4=`show${EVENT_KEY$c}`,EVENT_SHOWN$3=`shown${EVENT_KEY$c}`,EVENT_HIDE$3=`hide${EVENT_KEY$c}`,EVENT_HIDDEN$5=`hidden${EVENT_KEY$c}`,EVENT_CLICK_DATA_API$3=`click${EVENT_KEY$c}.data-api`,EVENT_FOCUSIN_DATA_API=`focusin${EVENT_KEY$c}.data-api`,SELECTOR_DATA_TOGGLE$6='[data-bs-toggle="datepicker"]',HIDE_DELAY=100,Default$e={datepickerTheme:null,dateMin:null,dateMax:null,dateFormat:null,displayElement:null,displayMonthsCount:1,firstWeekday:1,inline:!1,locale:"default",positionElement:null,selectedDates:[],selectionMode:"single",placement:"left",vcpOptions:{}},DefaultType$e={datepickerTheme:"(null|string)",dateMin:"(null|string|number|object)",dateMax:"(null|string|number|object)",dateFormat:"(null|object|function)",displayElement:"(null|string|element|boolean)",displayMonthsCount:"number",firstWeekday:"number",inline:"boolean",locale:"string",positionElement:"(null|string|element)",selectedDates:"array",selectionMode:"string",placement:"string",vcpOptions:"object"};class Datepicker extends BaseComponent{constructor(e,t){super(e,t),this._calendar=null,this._isShown=!1,this._initCalendar()}static get Default(){return Default$e}static get DefaultType(){return DefaultType$e}static get NAME(){return NAME$f}toggle(){if(!this._config.inline)return this._isShown?this.hide():this.show()}show(){this._config.inline||!this._calendar||isDisabled(this._element)||this._isShown||EventHandler.trigger(this._element,EVENT_SHOW$4).defaultPrevented||(this._calendar.show(),this._isShown=!0,EventHandler.trigger(this._element,EVENT_SHOWN$3))}hide(){this._config.inline||this._calendar&&this._isShown&&(EventHandler.trigger(this._element,EVENT_HIDE$3).defaultPrevented||(this._calendar.hide(),this._isShown=!1,EventHandler.trigger(this._element,EVENT_HIDDEN$5)))}dispose(){this._themeObserver&&(this._themeObserver.disconnect(),this._themeObserver=null),this._calendar&&this._calendar.destroy(),this._calendar=null,super.dispose()}getSelectedDates(){const e=this._calendar?.context?.selectedDates;return e?[...e]:[]}setSelectedDates(e){this._calendar&&this._calendar.set({selectedDates:e})}_initCalendar(){this._isInput="INPUT"===this._element.tagName,this._isInline=this._config.inline,this._isInline&&!this._isInput&&(this._boundInput=this._element.querySelector('input[type="hidden"], input[name]')),this._positionElement=this._resolvePositionElement(),this._displayElement=this._resolveDisplayElement();const e=this._buildCalendarOptions();this._calendar=new Calendar(this._positionElement,e),this._calendar.init(),this._setupThemeObserver(),this._isInput&&this._element.value&&this._parseInputValue(),this._updateDisplayWithSelectedDates()}_updateDisplayWithSelectedDates(){const{selectedDates:e}=this._config;if(!e||0===e.length)return;const t=this._formatDateForInput(e);this._isInput&&(this._element.value=t),this._boundInput&&(this._boundInput.value=e.join(",")),this._displayElement&&(this._displayElement.textContent=t)}_resolvePositionElement(){let{positionElement:e}=this._config;if("string"==typeof e&&(e=document.querySelector(e)),!e&&this._isInput&&!this._isInline){const t=this._element.closest(".form-adorn");t&&(e=t)}return e||this._element}_resolveDisplayElement(){const{displayElement:e}=this._config;return"string"==typeof e?document.querySelector(e):!0===e||null===e&&!this._isInput&&!this._isInline?this._element.querySelector("[data-bs-datepicker-display]")||this._element:e}_getThemeAncestor(){return this._element.closest("[data-bs-theme]")}_getEffectiveTheme(){const{datepickerTheme:e}=this._config;if(e)return e;const t=this._getThemeAncestor();return t?.getAttribute("data-bs-theme")||null}_syncThemeAttribute(e){if(!e)return;const t=this._getEffectiveTheme();t?e.setAttribute("data-bs-theme",t):e.removeAttribute("data-bs-theme")}_setupThemeObserver(){const e=this._getThemeAncestor();e&&!this._config.datepickerTheme&&(this._themeObserver=new MutationObserver(()=>{this._syncThemeAttribute(this._calendar?.context?.mainElement)}),this._themeObserver.observe(e,{attributes:!0,attributeFilter:["data-bs-theme"]}))}_buildCalendarOptions(){const e=this._getEffectiveTheme(),t=e&&"auto"!==e?e:"system",n={...this._config.vcpOptions,inputMode:!this._isInline,positionToInput:this._config.placement,firstWeekday:this._config.firstWeekday,locale:this._config.locale,selectionDatesMode:this._config.selectionMode,selectedDates:this._config.selectedDates,displayMonthsCount:this._config.displayMonthsCount,type:this._config.displayMonthsCount>1?"multiple":"default",selectedTheme:t,themeAttrDetect:"[data-bs-theme]",onClickDate:(e,t)=>this._handleDateClick(e,t),onInit:e=>{this._syncThemeAttribute(e.context.mainElement)},onShow:()=>{this._isShown=!0,this._syncThemeAttribute(this._calendar.context.mainElement)},onHide:()=>{this._isShown=!1}};if(this._config.selectedDates.length>0){const e=this._parseDate(this._config.selectedDates[0]);n.selectedMonth=e.getMonth(),n.selectedYear=e.getFullYear()}return this._config.dateMin&&(n.dateMin=this._config.dateMin),this._config.dateMax&&(n.dateMax=this._config.dateMax),n}_handleDateClick(e,t){const n=[...e.context.selectedDates];if(n.length>0){const e=this._formatDateForInput(n);this._isInput&&(this._element.value=e),this._boundInput&&(this._boundInput.value=n.join(",")),this._displayElement&&(this._displayElement.textContent=e)}EventHandler.trigger(this._element,EVENT_CHANGE$2,{dates:n,event:t}),this._maybeHideAfterSelection(n)}_maybeHideAfterSelection(e){this._isInline||("single"===this._config.selectionMode&&e.length>0||"multiple-ranged"===this._config.selectionMode&&e.length>=2)&&setTimeout(()=>this.hide(),100)}_parseDate(e){const[t,n,s]=e.split("-");return new Date(t,n-1,s)}_formatDate(e){const t=this._parseDate(e),n="default"===this._config.locale?void 0:this._config.locale,{dateFormat:s}=this._config;return"function"==typeof s?s(t,n):s&&"object"==typeof s?new Intl.DateTimeFormat(n,s).format(t):t.toLocaleDateString(n)}_formatDateForInput(e){if(0===e.length)return"";if(1===e.length)return this._formatDate(e[0]);const t="multiple-ranged"===this._config.selectionMode?" – ":", ";return e.map(e=>this._formatDate(e)).join(t)}_parseInputValue(){const e=this._element.value.trim();if(!e)return;const t=new Date(e);if(!Number.isNaN(t.getTime())){const e=`${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,"0")}-${String(t.getDate()).padStart(2,"0")}`;this._calendar.set({selectedDates:[e]})}}}EventHandler.on(document,EVENT_CLICK_DATA_API$3,SELECTOR_DATA_TOGGLE$6,function(e){"INPUT"!==this.tagName&&"true"!==this.dataset.bsInline&&(e.preventDefault(),Datepicker.getOrCreateInstance(this).toggle())}),EventHandler.on(document,EVENT_FOCUSIN_DATA_API,SELECTOR_DATA_TOGGLE$6,function(){"INPUT"===this.tagName&&Datepicker.getOrCreateInstance(this).show()}),EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$c}.data-api`,()=>{for(const e of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE$6}[data-bs-inline="true"]`))Datepicker.getOrCreateInstance(e)});const CLASS_NAME_OPEN="dialog-open";class DialogBase extends BaseComponent{constructor(e,t){super(e,t),this._isTransitioning=!1,this._openedAsModal=!1,this._addDialogListeners()}static get NAME(){return"dialogbase"}toggle(e){return this._element.open?this.hide():this.show(e)}show(e){if(this._element.open||this._isTransitioning)return;if(EventHandler.trigger(this._element,this.constructor.eventName("show"),{relatedTarget:e}).defaultPrevented)return;this._isTransitioning=!0,this._onBeforeShow();const{modal:t,preventBodyScroll:n}=this._getShowOptions();this._showElement({modal:t,preventBodyScroll:n}),this._queueCallback(()=>{this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("shown"),{relatedTarget:e})},this._element,this._isAnimated())}hide(){this._element.open&&!this._isTransitioning&&(EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented||(this._isTransitioning=!0,this._hideElement(),this._queueCallback(()=>{this._element.open&&this._closeAndCleanup(),this._element.classList.remove("hiding"),this._onAfterHide(),this._isTransitioning=!1,EventHandler.trigger(this._element,this.constructor.eventName("hidden"))},this._element,this._isAnimated())))}dispose(){this._element.open&&this._closeAndCleanup(),super.dispose()}_getShowOptions(){return{modal:!0,preventBodyScroll:!0}}_onBeforeShow(){}_onAfterHide(){}_isAnimated(){return!this._element.classList.contains(this._getInstantClassName())}_getInstantClassName(){return"dialog-instant"}_getStaticClassName(){return"dialog-static"}_onCancel(){}_showElement({modal:e=!0,preventBodyScroll:t=!0}={}){this._openedAsModal=e,e?this._element.showModal():this._element.show(),t&&document.documentElement.classList.add("dialog-open")}_hideElement(){this._hideChildComponents(),this._element.classList.add("hiding"),this._shouldDeferClose()||this._closeAndCleanup()}_closeAndCleanup(){this._element.close(),this._openedAsModal=!1,document.querySelector("dialog[open]:modal")||document.documentElement.classList.remove("dialog-open")}_shouldDeferClose(){return!1}_triggerBackdropTransition(){if(EventHandler.trigger(this._element,this.constructor.eventName("hidePrevented")).defaultPrevented)return;const e=this._getStaticClassName();this._element.classList.add(e),this._queueCallback(()=>{this._element.classList.remove(e)},this._element)}_hideChildComponents(){for(const e of SelectorEngine.find('[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]',this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}for(const e of SelectorEngine.find(".toast.show",this._element)){const t=Data.getAny(e);t&&"function"==typeof t.hide&&t.hide()}}_addDialogListeners(){const e=this.constructor.EVENT_KEY;EventHandler.on(this._element,"cancel",e=>{e.preventDefault(),this._config.keyboard?(this._onCancel(),this.hide()):this._triggerBackdropTransition()}),EventHandler.on(this._element,`keydown${e}`,e=>{"Escape"!==e.key||this._openedAsModal||(e.preventDefault(),this._config.keyboard&&(this._onCancel(),this.hide()))}),EventHandler.on(this._element,`click${e}`,e=>{e.target===this._element&&this._openedAsModal&&("static"!==this._config.backdrop?this.hide():this._triggerBackdropTransition())})}}const NAME$e="dialog",DATA_KEY$a="bs.dialog",EVENT_KEY$b=`.${DATA_KEY$a}`,DATA_API_KEY$6=".data-api",EVENT_SHOW$3=`show${EVENT_KEY$b}`,EVENT_HIDDEN$4=`hidden${EVENT_KEY$b}`,EVENT_CANCEL=`cancel${EVENT_KEY$b}`,EVENT_CLICK_DATA_API$2=`click${EVENT_KEY$b}.data-api`,CLASS_NAME_NONMODAL="dialog-nonmodal",CLASS_NAME_INSTANT="dialog-instant",CLASS_NAME_SWAP_IN="dialog-swap-in",SELECTOR_DATA_TOGGLE$5='[data-bs-toggle="dialog"]',Default$d={backdrop:!0,keyboard:!0,modal:!0},DefaultType$d={backdrop:"(boolean|string)",keyboard:"boolean",modal:"boolean"};class Dialog extends DialogBase{static get Default(){return Default$d}static get DefaultType(){return DefaultType$d}static get NAME(){return NAME$e}handleUpdate(){}_getShowOptions(){return{modal:this._config.modal,preventBodyScroll:this._config.modal}}_onBeforeShow(){this._config.modal||this._element.classList.add("dialog-nonmodal")}_onAfterHide(){this._element.classList.remove("dialog-nonmodal")}_shouldDeferClose(){return this._isAnimated()}_onCancel(){EventHandler.trigger(this._element,EVENT_CANCEL)}}EventHandler.on(document,EVENT_CLICK_DATA_API$2,SELECTOR_DATA_TOGGLE$5,function(e){const t=SelectorEngine.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&e.preventDefault(),EventHandler.one(t,EVENT_SHOW$3,e=>{e.defaultPrevented||EventHandler.one(t,EVENT_HIDDEN$4,()=>{isVisible(this)&&this.focus({preventScroll:!0})})});const n=Manipulator.getDataAttributes(this),s=this.closest("dialog[open]");if(s&&s!==t){const e=Dialog.getOrCreateInstance(t,n);t.classList.add("dialog-swap-in"),e.show(this),EventHandler.one(t,`shown${EVENT_KEY$b}`,()=>{t.classList.remove("dialog-swap-in")});const i=Dialog.getInstance(s);return void(i&&(s.classList.add("dialog-instant"),EventHandler.one(s,EVENT_HIDDEN$4,()=>{s.classList.remove("dialog-instant")}),i.hide()))}Dialog.getOrCreateInstance(t,n).toggle(this)}),enableDismissTrigger(Dialog);const NAME$d="navoverflow",DATA_KEY$9="bs.navoverflow",EVENT_KEY$a=`.${DATA_KEY$9}`,EVENT_UPDATE=`update${EVENT_KEY$a}`,EVENT_OVERFLOW=`overflow${EVENT_KEY$a}`,CLASS_NAME_OVERFLOW="nav-overflow",CLASS_NAME_OVERFLOW_MENU="nav-overflow-menu",CLASS_NAME_HIDDEN="d-none",SELECTOR_NAV_ITEM=".nav-item",SELECTOR_NAV_LINK=".nav-link",SELECTOR_OVERFLOW_TOGGLE=".nav-overflow-toggle",SELECTOR_OVERFLOW_MENU=".nav-overflow-menu",SELECTOR_CUSTOM_ICON="[data-bs-overflow-icon]",CLASS_NAME_KEEP="nav-overflow-keep",Default$c={collapseBelow:0,iconPlacement:"start",menuPlacement:"bottom-end",moreText:"More",moreIcon:'',threshold:0},DefaultType$c={collapseBelow:"(number|string)",iconPlacement:"string",menuPlacement:"string",moreText:"string",moreIcon:"string",threshold:"number"};class NavOverflow extends BaseComponent{constructor(e,t){super(e,t),this._items=[],this._overflowItems=[],this._overflowMenu=null,this._overflowToggle=null,this._resizeObserver=null,this._collapseBelow=0,this._isInitialized=!1,this._init()}static get Default(){return Default$c}static get DefaultType(){return DefaultType$c}static get NAME(){return NAME$d}update(){this._calculateOverflow(),EventHandler.trigger(this._element,EVENT_UPDATE)}dispose(){this._resizeObserver&&this._resizeObserver.disconnect(),this._restoreItems(),this._overflowToggle&&this._overflowToggle.parentElement&&this._overflowToggle.parentElement.remove(),super.dispose()}_init(){this._element.classList.add("nav-overflow"),this._items=[...SelectorEngine.find(".nav-item",this._element)];for(const[e,t]of this._items.entries())t.dataset.bsNavOrder=e;this._collapseBelow=this._resolveCollapseBelow(),this._createOverflowMenu(),this._setupResizeObserver(),this._calculateOverflow(),this._isInitialized=!0}_createOverflowMenu(){if(this._overflowToggle=SelectorEngine.findOne(".nav-overflow-toggle",this._element),this._overflowToggle)return void(this._overflowMenu=SelectorEngine.findOne(".nav-overflow-menu",this._element));const e=`${this._resolveIcon()}`,t=`${this._config.moreText}`,n="end"===this._config.iconPlacement?`${t}${e}`:`${e}${t}`,s=document.createElement("li");s.className="nav-item nav-overflow-item",s.innerHTML=`\n \n ${n}\n \n \n `,this._element.append(s),this._overflowToggle=s.querySelector(".nav-overflow-toggle"),this._overflowMenu=s.querySelector(".nav-overflow-menu")}_resolveIcon(){const e=SelectorEngine.findOne(SELECTOR_CUSTOM_ICON,this._element);if(!e)return this._config.moreIcon;const t=e.cloneNode(!0);t.removeAttribute("data-bs-overflow-icon");const n=t.outerHTML;return e.remove(),n}_resolveCollapseBelow(){const e=this._config.collapseBelow;if("number"==typeof e)return e;if("string"==typeof e&&""!==e){const t=getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${e}`);return Number.parseFloat(t)||0}return 0}_setupResizeObserver(){"undefined"!=typeof ResizeObserver?(this._resizeObserver=new ResizeObserver(()=>{this._calculateOverflow()}),this._resizeObserver.observe(this._element)):EventHandler.on(window,"resize",()=>this._calculateOverflow())}_calculateOverflow(){this._restoreItems();const e=this._element.offsetWidth,t=this._overflowToggle?.closest(".nav-item");if(this._collapseBelow>0&&e!e.classList.contains(CLASS_NAME_KEEP));return this._moveToOverflow(e),t&&(e.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),void(e.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:e.length,visibleCount:this._items.length-e.length}))}let n=0;const s=[],i=e-(t?.offsetWidth||0)-this._items.filter(e=>e.classList.contains(CLASS_NAME_KEEP)).reduce((e,t)=>e+t.offsetWidth,0)-10;for(const e of this._items)e.classList.contains(CLASS_NAME_KEEP)||(n+=e.offsetWidth,n>i&&s.push(e));if(this._items.length-s.lengththis._config.threshold){const e=this._items.slice(this._config.threshold).filter(e=>!e.classList.contains(CLASS_NAME_KEEP));s.length=0,s.push(...e)}this._moveToOverflow(s),t&&(s.length>0?t.classList.remove("d-none"):t.classList.add("d-none")),s.length>0&&EventHandler.trigger(this._element,EVENT_OVERFLOW,{overflowCount:s.length,visibleCount:this._items.length-s.length})}_moveToOverflow(e){if(this._overflowMenu){this._overflowMenu.innerHTML="",this._overflowItems=[];for(const t of e){const e=SelectorEngine.findOne(".nav-link",t);if(!e)continue;const n=e.cloneNode(!0);n.className="menu-item",e.classList.contains("active")&&n.classList.add("active"),(e.classList.contains("disabled")||e.hasAttribute("disabled"))&&n.classList.add("disabled"),this._overflowMenu.append(n),t.classList.add("d-none"),t.dataset.bsNavOverflow="true",this._overflowItems.push(t)}}}_restoreItems(){for(const e of this._items)e.classList.remove("d-none"),delete e.dataset.bsNavOverflow;this._overflowMenu&&(this._overflowMenu.innerHTML=""),this._overflowItems=[]}}EventHandler.on(document,"DOMContentLoaded",()=>{for(const e of SelectorEngine.find('[data-bs-toggle="nav-overflow"]'))NavOverflow.getOrCreateInstance(e)});const NAME$c="swipe",EVENT_KEY$9=".bs.swipe",EVENT_TOUCHSTART="touchstart.bs.swipe",EVENT_TOUCHMOVE="touchmove.bs.swipe",EVENT_TOUCHEND="touchend.bs.swipe",EVENT_POINTERDOWN="pointerdown.bs.swipe",EVENT_POINTERUP="pointerup.bs.swipe",POINTER_TYPE_TOUCH="touch",POINTER_TYPE_PEN="pen",CLASS_NAME_POINTER_EVENT="pointer-event",SWIPE_THRESHOLD=40,Default$b={endCallback:null,leftCallback:null,rightCallback:null,upCallback:null,downCallback:null},DefaultType$b={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)",upCallback:"(function|null)",downCallback:"(function|null)"};class Swipe extends Config{constructor(e,t){super(),this._element=e,e&&Swipe.isSupported()&&(this._config=this._getConfig(t),this._deltaX=0,this._deltaY=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Default$b}static get DefaultType(){return DefaultType$b}static get NAME(){return NAME$c}dispose(){EventHandler.off(this._element,".bs.swipe")}_start(e){if(!this._supportPointerEvents)return this._deltaX=e.touches[0].clientX,void(this._deltaY=e.touches[0].clientY);this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX,this._deltaY=e.clientY)}_end(e){this._eventIsPointerPenTouch(e)&&(this._deltaX=e.clientX-this._deltaX,this._deltaY=e.clientY-this._deltaY),this._handleSwipe(),execute(this._config.endCallback)}_move(e){if(e.touches&&e.touches.length>1)return this._deltaX=0,void(this._deltaY=0);this._deltaX=e.touches[0].clientX-this._deltaX,this._deltaY=e.touches[0].clientY-this._deltaY}_handleSwipe(){const e=Math.abs(this._deltaX),t=Math.abs(this._deltaY);if(t>e&&t>40){const e=this._deltaY>0?"down":"up";return this._deltaX=0,this._deltaY=0,void execute("down"===e?this._config.downCallback:this._config.upCallback)}if(e>40){const t=e/this._deltaX;if(this._deltaX=0,this._deltaY=0,!t)return;return void execute(t>0?this._config.rightCallback:this._config.leftCallback)}this._deltaX=0,this._deltaY=0}_initEvents(){this._supportPointerEvents?(EventHandler.on(this._element,EVENT_POINTERDOWN,e=>this._start(e)),EventHandler.on(this._element,EVENT_POINTERUP,e=>this._end(e)),this._element.classList.add("pointer-event")):(EventHandler.on(this._element,EVENT_TOUCHSTART,e=>this._start(e)),EventHandler.on(this._element,EVENT_TOUCHMOVE,e=>this._move(e)),EventHandler.on(this._element,EVENT_TOUCHEND,e=>this._end(e)))}_eventIsPointerPenTouch(e){return this._supportPointerEvents&&("pen"===e.pointerType||"touch"===e.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const NAME$b="drawer",DATA_KEY$8="bs.drawer",EVENT_KEY$8=`.${DATA_KEY$8}`,DATA_API_KEY$5=".data-api",EVENT_LOAD_DATA_API$2=`load${EVENT_KEY$8}.data-api`,EVENT_HIDDEN$3=`hidden${EVENT_KEY$8}`,EVENT_RESIZE$1=`resize${EVENT_KEY$8}`,EVENT_CLICK_DATA_API$1=`click${EVENT_KEY$8}.data-api`,SELECTOR_DATA_TOGGLE$4='[data-bs-toggle="drawer"]',Default$a={backdrop:!0,keyboard:!0,scroll:!1},DefaultType$a={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Drawer extends DialogBase{constructor(e,t){super(e,t),this._swipeHelper=null}static get Default(){return Default$a}static get DefaultType(){return DefaultType$a}static get NAME(){return NAME$b}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_getShowOptions(){return{modal:Boolean(this._config.backdrop)||!this._config.scroll,preventBodyScroll:!this._config.scroll}}_onBeforeShow(){this._initSwipe()}_getInstantClassName(){return"drawer-instant"}_getStaticClassName(){return"drawer-static"}_initSwipe(){if(this._swipeHelper||!Swipe.isSupported())return;const e={},t=this._element;t.classList.contains("drawer-bottom")?e.downCallback=()=>this.hide():t.classList.contains("drawer-top")?e.upCallback=()=>this.hide():t.classList.contains("drawer-end")?isRTL()?e.leftCallback=()=>this.hide():e.rightCallback=()=>this.hide():isRTL()?e.rightCallback=()=>this.hide():e.leftCallback=()=>this.hide(),this._swipeHelper=new Swipe(t,e)}}EventHandler.on(document,EVENT_CLICK_DATA_API$1,SELECTOR_DATA_TOGGLE$4,function(e){const t=SelectorEngine.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this))return;EventHandler.one(t,EVENT_HIDDEN$3,()=>{isVisible(this)&&this.focus({preventScroll:!0})});const n=SelectorEngine.findOne("dialog.drawer[open]");n&&n!==t&&Drawer.getInstance(n).hide(),Drawer.getOrCreateInstance(t).toggle(this)}),EventHandler.on(window,EVENT_LOAD_DATA_API$2,()=>{for(const e of SelectorEngine.find("dialog.drawer[open]"))Drawer.getOrCreateInstance(e).show()}),EventHandler.on(window,EVENT_RESIZE$1,()=>{for(const e of SelectorEngine.find('dialog[open][class*="\\:drawer"]'))"fixed"!==getComputedStyle(e).position&&Drawer.getOrCreateInstance(e).hide()}),enableDismissTrigger(Drawer);const NAME$a="strength",DATA_KEY$7="bs.strength",EVENT_KEY$7=`.${DATA_KEY$7}`,DATA_API_KEY$4=".data-api",EVENT_STRENGTH_CHANGE=`strengthChange${EVENT_KEY$7}`,SELECTOR_DATA_STRENGTH="[data-bs-strength]",STRENGTH_LEVELS=["weak","fair","good","strong"],Default$9={input:null,minLength:8,messages:{weak:"Weak",fair:"Fair",good:"Good",strong:"Strong"},weights:{minLength:1,extraLength:1,lowercase:1,uppercase:1,numbers:1,special:1,multipleSpecial:1,longPassword:1},thresholds:[2,4,6],scorer:null},DefaultType$9={input:"(string|element|null)",minLength:"number",messages:"object",weights:"object",thresholds:"array",scorer:"(function|null)"};class Strength extends BaseComponent{constructor(e,t){super(e,t),this._input=this._getInput(),this._segments=SelectorEngine.find(".strength-segment",this._element),this._textElement=SelectorEngine.findOne(".strength-text",this._element.parentElement),this._currentStrength=null,this._input&&(this._addEventListeners(),this._evaluate())}static get Default(){return Default$9}static get DefaultType(){return DefaultType$9}static get NAME(){return NAME$a}getStrength(){return this._currentStrength}evaluate(){this._evaluate()}_getInput(){if(this._config.input)return"string"==typeof this._config.input?SelectorEngine.findOne(this._config.input):this._config.input;const e=this._element.parentElement;return SelectorEngine.findOne('input[type="password"]',e)}_addEventListeners(){EventHandler.on(this._input,"input",()=>this._evaluate()),EventHandler.on(this._input,"change",()=>this._evaluate())}_evaluate(){const e=this._input.value,t=this._calculateScore(e),n=this._scoreToStrength(t);n!==this._currentStrength&&(this._currentStrength=n,this._updateUI(n,t),EventHandler.trigger(this._element,EVENT_STRENGTH_CHANGE,{strength:n,score:t,password:e.length>0?"***":""}))}_calculateScore(e){if(!e)return 0;if("function"==typeof this._config.scorer)return this._config.scorer(e);const{weights:t}=this._config;let n=0;return e.length>=this._config.minLength&&(n+=t.minLength),e.length>=this._config.minLength+4&&(n+=t.extraLength),/[a-z]/.test(e)&&(n+=t.lowercase),/[A-Z]/.test(e)&&(n+=t.uppercase),/\d/.test(e)&&(n+=t.numbers),/[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.special),/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(e)&&(n+=t.multipleSpecial),e.length>=16&&(n+=t.longPassword),n}_scoreToStrength(e){if(0===e)return null;const[t,n,s]=this._config.thresholds;return e<=t?"weak":e<=n?"fair":e<=s?"good":"strong"}_updateUI(e){e?this._element.dataset.bsStrength=e:delete this._element.dataset.bsStrength;const t=e?STRENGTH_LEVELS.indexOf(e):-1;for(const[e,n]of this._segments.entries())e<=t?n.classList.add("active"):n.classList.remove("active");if(this._textElement)if(e&&this._config.messages[e]){this._textElement.textContent=this._config.messages[e],this._textElement.dataset.bsStrength=e;const t={weak:"danger",fair:"warning",good:"info",strong:"success"};this._textElement.style.setProperty("--strength-color",`var(--${t[e]}-text)`)}else this._textElement.textContent="",delete this._textElement.dataset.bsStrength}}EventHandler.on(document,`DOMContentLoaded${EVENT_KEY$7}.data-api`,()=>{for(const e of SelectorEngine.find("[data-bs-strength]"))Strength.getOrCreateInstance(e)});const NAME$9="otpInput",DATA_KEY$6="bs.otpInput",EVENT_KEY$6=`.${DATA_KEY$6}`,DATA_API_KEY$3=".data-api",EVENT_COMPLETE=`complete${EVENT_KEY$6}`,EVENT_INPUT$1=`input${EVENT_KEY$6}`,EVENT_DOMCONTENT_LOADED=`DOMContentLoaded${EVENT_KEY$6}.data-api`,SELECTOR_DATA_OTP="[data-bs-otp]",SELECTOR_INPUT$1="input",SYNC_EVENTS=["blur","keyup","click","select"],CLASS_NAME_INPUT="otp-input",CLASS_NAME_RENDERED="otp-rendered",CLASS_NAME_SLOTS="otp-slots",CLASS_NAME_SLOT="otp-slot",CLASS_NAME_SLOT_FILLED="otp-slot-filled",CLASS_NAME_SLOT_ACTIVE="otp-slot-active",CLASS_NAME_SEPARATOR="otp-separator",MASK_CHARACTER="•",TYPES={numeric:{inputmode:"numeric",pattern:"[0-9]*",filter:/[^0-9]/g},alphanumeric:{inputmode:"text",pattern:"[A-Za-z0-9]*",filter:/[^A-Za-z0-9]/g},alpha:{inputmode:"text",pattern:"[A-Za-z]*",filter:/[^A-Za-z]/g}},Default$8={groups:null,length:null,mask:!1,separator:"·",type:"numeric"},DefaultType$8={groups:"(array|null)",length:"(number|null)",mask:"boolean",separator:"string",type:"string"};class OtpInput extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne("input",this._element),this._input&&(this._type=TYPES[this._config.type]||TYPES.numeric,this._length=this._resolveLength(),this._slots=[],this._setupInput(),this._renderSlots(),this._addEventListeners(),this._render())}static get Default(){return Default$8}static get DefaultType(){return DefaultType$8}static get NAME(){return NAME$9}getValue(){return this._input.value}setValue(e){this._input.value=this._sanitize(String(e)),this._render(),this._checkComplete()}clear(){this._input.value="",this._render(),this._input.focus()}focus(){this._input.focus();const e=this._input.value.length;this._input.setSelectionRange(e,e),this._render()}dispose(){EventHandler.off(this._input,"input",this._onInput),EventHandler.off(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.off(this._input,e,this._onSync);this._slotsContainer?.remove(),this._element.classList.remove("otp-rendered"),super.dispose()}_resolveLength(){if(this._config.length)return this._config.length;const e=Number.parseInt(this._input.getAttribute("maxlength"),10);return Number.isNaN(e)||e<1?6:e}_setupInput(){const e=this._input;"number"!==e.type&&"password"!==e.type||(e.type="text"),e.classList.add("otp-input"),e.setAttribute("maxlength",String(this._length)),e.setAttribute("inputmode",this._type.inputmode),e.setAttribute("pattern",this._type.pattern),e.getAttribute("autocomplete")||e.setAttribute("autocomplete","one-time-code"),e.value&&(e.value=this._sanitize(e.value))}_renderSlots(){const e=document.createElement("div");e.className="otp-slots",e.setAttribute("aria-hidden","true");const{groups:t}=this._config;let n=0,s=0;for(let i=0;i0&&(s++,s===t[n]&&ithis._handleInput(),this._onFocus=()=>this.focus(),this._onSync=()=>this._render(),EventHandler.on(this._input,"input",this._onInput),EventHandler.on(this._input,"focus",this._onFocus);for(const e of SYNC_EVENTS)EventHandler.on(this._input,e,this._onSync)}_handleInput(){const e=this._sanitize(this._input.value);e!==this._input.value&&(this._input.value=e),this._render(),EventHandler.trigger(this._element,EVENT_INPUT$1,{value:this._input.value}),this._checkComplete()}_sanitize(e){return e.replace(this._type.filter,"").slice(0,this._length)}_render(){const{value:e}=this._input,t=document.activeElement===this._input,n=Math.min(this._input.selectionStart??e.length,this._length-1);for(const[s,i]of this._slots.entries()){const o=e[s]??"";i.textContent=o&&this._config.mask?"•":o,i.classList.toggle("otp-slot-filled",Boolean(o)),i.classList.toggle("otp-slot-active",t&&s===n)}}_checkComplete(){const{value:e}=this._input;e.length===this._length&&EventHandler.trigger(this._element,EVENT_COMPLETE,{value:e})}}EventHandler.on(document,EVENT_DOMCONTENT_LOADED,()=>{for(const e of SelectorEngine.find("[data-bs-otp]"))OtpInput.getOrCreateInstance(e)});const NAME$8="chips",DATA_KEY$5="bs.chips",EVENT_KEY$5=".bs.chips",DATA_API_KEY$2=".data-api",EVENT_ADD="add.bs.chips",EVENT_REMOVE="remove.bs.chips",EVENT_CHANGE$1="change.bs.chips",EVENT_SELECT="select.bs.chips",SELECTOR_DATA_CHIPS="[data-bs-chips]",SELECTOR_GHOST_INPUT=".form-ghost",SELECTOR_CHIP=".chip",SELECTOR_CHIP_DISMISS=".chip-dismiss",CLASS_NAME_CHIP="chip",CLASS_NAME_CHIP_DISMISS="chip-dismiss",CLASS_NAME_ACTIVE$2="active",DEFAULT_DISMISS_ICON='',Default$7={separator:",",allowDuplicates:!1,maxChips:null,placeholder:"",dismissible:!0,dismissIcon:DEFAULT_DISMISS_ICON,createOnBlur:!0},DefaultType$7={separator:"(string|null)",allowDuplicates:"boolean",maxChips:"(number|null)",placeholder:"string",dismissible:"boolean",dismissIcon:"string",createOnBlur:"boolean"};class Chips extends BaseComponent{constructor(e,t){super(e,t),this._input=SelectorEngine.findOne(".form-ghost",this._element),this._chips=[],this._selectedChips=new Set,this._anchorChip=null,this._input||this._createInput(),this._initializeExistingChips(),this._addEventListeners()}static get Default(){return Default$7}static get DefaultType(){return DefaultType$7}static get NAME(){return NAME$8}add(e){const t=String(e).trim();if(!t)return null;if(!this._config.allowDuplicates&&this._chips.includes(t))return null;if(null!==this._config.maxChips&&this._chips.length>=this._config.maxChips)return null;if(EventHandler.trigger(this._element,EVENT_ADD,{value:t,relatedTarget:this._input}).defaultPrevented)return null;const n=this._createChip(t);return this._element.insertBefore(n,this._input),this._chips.push(t),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),n}remove(e){let t,n;return"string"==typeof e?(n=e,t=this._findChipByValue(n)):(t=e,n=this._getChipValue(t)),!(!t||!n)&&(!EventHandler.trigger(this._element,EVENT_REMOVE,{value:n,chip:t,relatedTarget:this._input}).defaultPrevented&&(this._selectedChips.delete(t),this._anchorChip===t&&(this._anchorChip=null),t.remove(),this._chips=this._chips.filter(e=>e!==n),EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:this.getValues()}),!0))}removeSelected(){const e=[...this._selectedChips];for(const t of e)this.remove(t);this._input?.focus()}getValues(){return[...this._chips]}getSelectedValues(){return[...this._selectedChips].map(e=>this._getChipValue(e))}clear(){const e=SelectorEngine.find(".chip",this._element);for(const t of e)t.remove();this._chips=[],this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_CHANGE$1,{values:[]})}clearSelection(){for(const e of this._selectedChips)e.classList.remove("active");this._selectedChips.clear(),this._anchorChip=null,EventHandler.trigger(this._element,EVENT_SELECT,{selected:[]})}selectChip(e,t={}){const{addToSelection:n=!1,rangeSelect:s=!1}=t,i=this._getChipElements();if(i.includes(e)){if(s&&this._anchorChip){const t=i.indexOf(this._anchorChip),s=i.indexOf(e),o=Math.min(t,s),r=Math.max(t,s);n||this.clearSelection();for(let e=o;e<=r;e++)this._selectedChips.add(i[e]),i[e].classList.add("active")}else n?this._selectedChips.has(e)?(this._selectedChips.delete(e),e.classList.remove("active")):(this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e):(this.clearSelection(),this._selectedChips.add(e),e.classList.add("active"),this._anchorChip=e);EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}focus(){this._input?.focus()}_getChipElements(){return SelectorEngine.find(".chip",this._element)}_createInput(){const e=document.createElement("input");e.type="text",e.className="form-ghost",this._config.placeholder&&(e.placeholder=this._config.placeholder),this._element.append(e),this._input=e}_initializeExistingChips(){const e=SelectorEngine.find(".chip",this._element);for(const t of e){const e=this._getChipValue(t);e&&(this._chips.push(e),this._setupChip(t))}}_setupChip(e){e.setAttribute("tabindex","0"),this._config.dismissible&&!SelectorEngine.findOne(".chip-dismiss",e)&&e.append(this._createDismissButton())}_createChip(e){const t=document.createElement("span");return t.className="chip",t.dataset.bsChipValue=e,t.append(document.createTextNode(e)),this._setupChip(t),t}_createDismissButton(){const e=document.createElement("button");return e.type="button",e.className="chip-dismiss",e.setAttribute("aria-label","Remove"),e.setAttribute("tabindex","-1"),e.innerHTML=this._config.dismissIcon,e}_findChipByValue(e){return this._getChipElements().find(t=>this._getChipValue(t)===e)}_getChipValue(e){if(e.dataset.bsChipValue)return e.dataset.bsChipValue;const t=e.cloneNode(!0),n=SelectorEngine.findOne(".chip-dismiss",t);return n&&n.remove(),t.textContent?.trim()||""}_addEventListeners(){EventHandler.on(this._input,"keydown",e=>this._handleInputKeydown(e)),EventHandler.on(this._input,"input",e=>this._handleInput(e)),EventHandler.on(this._input,"paste",e=>this._handlePaste(e)),EventHandler.on(this._input,"focus",()=>this.clearSelection()),this._config.createOnBlur&&EventHandler.on(this._input,"blur",e=>{e.relatedTarget?.closest(".chip")||this._createChipFromInput()}),EventHandler.on(this._element,"click",".chip",e=>{if(e.target.closest(".chip-dismiss"))return;const t=e.target.closest(".chip");t&&(e.preventDefault(),this.selectChip(t,{addToSelection:e.metaKey||e.ctrlKey,rangeSelect:e.shiftKey}),t.focus())}),EventHandler.on(this._element,"click",".chip-dismiss",e=>{e.stopPropagation();const t=e.target.closest(".chip");t&&(this.remove(t),this._input?.focus())}),EventHandler.on(this._element,"keydown",".chip",e=>{this._handleChipKeydown(e)}),EventHandler.on(this._element,"click",e=>{e.target===this._element&&(this.clearSelection(),this._input?.focus())})}_handleInputKeydown(e){const{key:t}=e;switch(t){case"Enter":e.preventDefault(),this._createChipFromInput();break;case"Backspace":case"Delete":if(""===this._input.value){e.preventDefault();const t=this._getChipElements();if(t.length>0){const e=t.at(-1);this.selectChip(e),e.focus()}}break;case"ArrowLeft":if(0===this._input.selectionStart&&0===this._input.selectionEnd){e.preventDefault();const t=this._getChipElements();if(t.length>0){const n=t.at(-1);e.shiftKey?this.selectChip(n,{addToSelection:!0}):this.selectChip(n),n.focus()}}break;case"Escape":this._input.value="",this.clearSelection(),this._input.blur()}}_handleChipKeydown(e){const{key:t}=e,n=e.target.closest(".chip");if(!n)return;const s=this._getChipElements(),i=s.indexOf(n);switch(t){case"Backspace":case"Delete":e.preventDefault(),this._handleChipDelete(i,s);break;case"ArrowLeft":e.preventDefault(),this._navigateChip(s,i,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._navigateChip(s,i,1,e.shiftKey);break;case"Home":e.preventDefault(),this._navigateToEdge(s,0,e.shiftKey);break;case"End":case"Escape":e.preventDefault(),this.clearSelection(),this._input?.focus();break;case"a":this._handleSelectAll(e,s)}}_handleChipDelete(e,t){if(0===this._selectedChips.size)return;const n=Math.min(e,t.length-this._selectedChips.size-1);this.removeSelected();const s=this._getChipElements();if(s.length>0){const e=Math.max(0,Math.min(n,s.length-1));s[e].focus(),this.selectChip(s[e])}else this._input?.focus()}_navigateChip(e,t,n,s){const i=t+n;if(n<0&&i>=0){const t=e[i];this.selectChip(t,s?{addToSelection:!0,rangeSelect:!0}:{}),t.focus()}else if(n>0&&i0&&(this.clearSelection(),this._input?.focus())}_navigateToEdge(e,t,n){if(0===e.length)return;const s=e[t];this.selectChip(s,n?{rangeSelect:!0}:{}),s.focus()}_handleSelectAll(e,t){if(e.metaKey||e.ctrlKey){e.preventDefault();for(const e of t)this._selectedChips.add(e),e.classList.add("active");EventHandler.trigger(this._element,EVENT_SELECT,{selected:this.getSelectedValues()})}}_handleInput(e){const{value:t}=e.target,{separator:n}=this._config;if(n&&t.includes(n)){const e=t.split(n);for(const t of e.slice(0,-1))this.add(t.trim());this._input.value=e.at(-1)}}_handlePaste(e){const{separator:t}=this._config;if(!t)return;const n=(e.clipboardData||window.clipboardData).getData("text");if(n.includes(t)){e.preventDefault();const s=n.split(t);for(const e of s)this.add(e.trim())}}_createChipFromInput(){const e=this._input.value.trim();e&&(this.add(e),this._input.value="")}}EventHandler.on(document,"DOMContentLoaded.bs.chips.data-api",()=>{for(const e of SelectorEngine.find("[data-bs-chips]"))Chips.getOrCreateInstance(e)});const ARIA_ATTRIBUTE_PATTERN=/^aria-[\w-]*$/i,DefaultAllowlist={"*":["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:[]},uriAttributes=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),SAFE_URL_PATTERN=/^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,DATA_URL_PATTERN=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i,allowedAttribute=(e,t)=>{const n=e.nodeName.toLowerCase();return t.includes(n)?!uriAttributes.has(n)||Boolean(SAFE_URL_PATTERN.test(e.nodeValue)||DATA_URL_PATTERN.test(e.nodeValue)):t.filter(e=>e instanceof RegExp).some(e=>e.test(n))};function sanitizeHtml(e,t,n){if(!e.length)return e;if(n&&"function"==typeof n)return n(e);const s=(new window.DOMParser).parseFromString(e,"text/html"),i=[...s.body.querySelectorAll("*")];for(const e of i){const n=e.nodeName.toLowerCase();if(!Object.keys(t).includes(n)){e.remove();continue}const s=[...e.attributes],i=[...t["*"]||[],...t[n]||[]];for(const t of s)allowedAttribute(t,i)||e.removeAttribute(t.nodeName)}return s.body.innerHTML}const NAME$7="TemplateFactory",Default$6={allowList:DefaultAllowlist,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:""},DefaultType$6={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},DefaultContentType={entry:"(string|element|function|null)",selector:"(string|element)"};class TemplateFactory extends Config{constructor(e){super(),this._config=this._getConfig(e)}static get Default(){return Default$6}static get DefaultType(){return DefaultType$6}static get NAME(){return NAME$7}getContent(){return Object.values(this._config.content).map(e=>this._resolvePossibleFunction(e)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(e){return this._checkContent(e),this._config.content={...this._config.content,...e},this}toHtml(){const e=document.createElement("div");e.innerHTML=this._maybeSanitize(this._config.template);for(const[t,n]of Object.entries(this._config.content))this._setContent(e,n,t);const t=e.children[0],n=this._resolvePossibleFunction(this._config.extraClass);return n&&t.classList.add(...n.split(" ")),t}_typeCheckConfig(e){super._typeCheckConfig(e),this._checkContent(e.content)}_checkContent(e){for(const[t,n]of Object.entries(e))super._typeCheckConfig({selector:t,entry:n},DefaultContentType)}_setContent(e,t,n){const s=SelectorEngine.findOne(n,e);s&&((t=this._resolvePossibleFunction(t))?isElement(t)?this._putElementInTemplate(getElement(t),s):this._config.html?s.innerHTML=this._maybeSanitize(t):s.textContent=t:s.remove())}_maybeSanitize(e){return this._config.sanitize?sanitizeHtml(e,this._config.allowList,this._config.sanitizeFn):e}_resolvePossibleFunction(e){return execute(e,[void 0,this])}_putElementInTemplate(e,t){if(this._config.html)return t.innerHTML="",void t.append(e);t.textContent=e.textContent}}const NAME$6="tooltip",DISALLOWED_ATTRIBUTES=new Set(["sanitize","allowList","sanitizeFn"]),ESCAPE_KEY="Escape",CLASS_NAME_FADE$2="fade",CLASS_NAME_MODAL="modal",CLASS_NAME_SHOW$2="show",SELECTOR_TOOLTIP_INNER=".tooltip-inner",SELECTOR_MODAL=".modal",SELECTOR_DATA_TOGGLE$3='[data-bs-toggle="tooltip"]',EVENT_MODAL_HIDE="hide.bs.modal",TRIGGER_HOVER="hover",TRIGGER_FOCUS="focus",TRIGGER_CLICK="click",TRIGGER_MANUAL="manual",EVENT_HIDE$2="hide",EVENT_HIDDEN$2="hidden",EVENT_SHOW$2="show",EVENT_SHOWN$2="shown",EVENT_INSERTED="inserted",EVENT_CLICK$3="click",EVENT_FOCUSIN$2="focusin",EVENT_FOCUSOUT$1="focusout",EVENT_MOUSEENTER$1="mouseenter",EVENT_MOUSELEAVE="mouseleave",EVENT_KEYDOWN$1="keydown",AttachmentMap={AUTO:"auto",TOP:"top",RIGHT:isRTL()?"left":"right",BOTTOM:"bottom",LEFT:isRTL()?"right":"left"},Default$5={allowList:DefaultAllowlist,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",floatingConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},DefaultType$5={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",floatingConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Tooltip extends BaseComponent{constructor(e,t){if(void 0===computePosition)throw new TypeError("Bootstrap's tooltips require Floating UI (https://floating-ui.com)");super(e,t),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._floatingCleanup=null,this._keydownHandler=null,this._templateFactory=null,this._newContent=null,this._mediaQueryListeners=[],this._responsivePlacements=null,this.tip=null,this._parseResponsivePlacements(),this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Default$5}static get DefaultType(){return DefaultType$5}static get NAME(){return NAME$6}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),this._removeEscapeListener(),EventHandler.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposeFloating(),this._disposeMediaQueryListeners(),super.dispose()}async show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const e=EventHandler.trigger(this._element,this.constructor.eventName("show")),t=(findShadowRoot(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(e.defaultPrevented||!t)return void(this._isHovered=!1);this._disposeFloating();const n=this._getTipElement();this._element.setAttribute("aria-describedby",n.getAttribute("id"));let{container:s}=this._config;const i=this._element.closest("dialog[open]");if(i&&s===document.body&&(s=i),this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(n),EventHandler.trigger(this._element,this.constructor.eventName("inserted"))),await this._createFloating(n),n.classList.add("show"),this._setEscapeListener(),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.on(e,"mouseover",noop);this._queueCallback(()=>{EventHandler.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!EventHandler.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._removeEscapeListener(),this._getTipElement().classList.remove("show"),"ontouchstart"in document.documentElement)for(const e of document.body.children)EventHandler.off(e,"mouseover",noop);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposeFloating(),this._element.removeAttribute("aria-describedby"),EventHandler.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._floatingCleanup&&this.tip&&this._updateFloatingPosition()}_isWithContent(){return Boolean(this._getTitle())||this._hasNewContent()}_hasNewContent(){return Boolean(this._newContent)&&Object.values(this._newContent).some(Boolean)}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(e){const t=this._getTemplateFactory(e).toHtml();t.classList.remove("fade","show"),t.classList.add(`bs-${this.constructor.NAME}-auto`);const n=getUID(this.constructor.NAME).toString();return t.setAttribute("id",n),this._isAnimated()&&t.classList.add("fade"),t}setContent(e){this._newContent=e,this._isShown()&&(this._disposeFloating(),this.show())}_getTemplateFactory(e){return this._templateFactory?this._templateFactory.changeContent(e):this._templateFactory=new TemplateFactory({...this._config,content:e,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[SELECTOR_TOOLTIP_INNER]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(e){return this.constructor.getOrCreateInstance(e.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains("fade")}_isShown(){return this.tip&&this.tip.classList.contains("show")}_getPlacement(e){if(this._responsivePlacements){const e=getResponsivePlacement(this._responsivePlacements,"top");return AttachmentMap[e.toUpperCase()]||e}const t=execute(this._config.placement,[this,e,this._element]);return AttachmentMap[t.toUpperCase()]||t}_parseResponsivePlacements(){"string"==typeof this._config.placement?(this._responsivePlacements=parseResponsivePlacement(this._config.placement,"top"),this._responsivePlacements&&this._setupMediaQueryListeners()):this._responsivePlacements=null}_setupMediaQueryListeners(){this._disposeMediaQueryListeners(),this._mediaQueryListeners=createBreakpointListeners(()=>{this._isShown()&&this._updateFloatingPosition()})}_disposeMediaQueryListeners(){disposeBreakpointListeners(this._mediaQueryListeners),this._mediaQueryListeners=[]}async _createFloating(e){const t=this._getPlacement(e),n=e.querySelector(`.${this.constructor.NAME}-arrow`);await this._updateFloatingPosition(e,t,n),this._floatingCleanup=autoUpdate(this._element,e,()=>this._updateFloatingPosition(e,null,n))}async _updateFloatingPosition(e=this.tip,t=null,n=null){if(!e)return;t||(t=this._getPlacement(e)),n||(n=e.querySelector(`.${this.constructor.NAME}-arrow`));const s=this._getFloatingMiddleware(n),i=this._getFloatingConfig(t,s),{x:o,y:r,placement:l,middlewareData:a}=await computePosition(this._element,e,i);if(Object.assign(e.style,{position:"absolute",left:`${o}px`,top:`${r}px`}),n&&(n.style.position="absolute"),Manipulator.setDataAttribute(e,"placement",l),n&&a.arrow){const{x:e,y:t}=a.arrow,s=l.startsWith("top")||l.startsWith("bottom");Object.assign(n.style,{left:s&&null!==e?`${e}px`:"",top:s||null===t?"":`${t}px`,right:"",bottom:""})}}_getOffset(){const{offset:e}=this._config;return"string"==typeof e?e.split(",").map(e=>Number.parseInt(e,10)):"function"==typeof e?({placement:t,rects:n})=>e({placement:t,reference:n.reference,floating:n.floating},this._element):e}_resolvePossibleFunction(e){return execute(e,[this._element,this._element])}_getFloatingMiddleware(e){const t=this._getOffset(),n=[offset("function"==typeof t?t:{mainAxis:t[1]||0,crossAxis:t[0]||0}),flip({fallbackPlacements:this._config.fallbackPlacements}),shift({boundary:"clippingParents"===this._config.boundary?"clippingAncestors":this._config.boundary})];return e&&n.push(arrow({element:e})),n}_getFloatingConfig(e,t){const n={placement:e,middleware:t};return{...n,...execute(this._config.floatingConfig,[void 0,n])}}_setListeners(){const e=this._config.trigger.split(" ");for(const t of e)if("click"===t)EventHandler.on(this._element,this.constructor.eventName("click"),this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger.click=!(t._isShown()&&t._activeTrigger.click),t.toggle()});else if("manual"!==t){const e="hover"===t?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),n="hover"===t?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");EventHandler.on(this._element,e,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusin"===e.type?"focus":"hover"]=!0,t._enter()}),EventHandler.on(this._element,n,this._config.selector,e=>{const t=this._initializeOnDelegatedTarget(e);t._activeTrigger["focusout"===e.type?"focus":"hover"]=t._element.contains(e.relatedTarget),t._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},EventHandler.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler)}_setEscapeListener(){this._keydownHandler||(this._keydownHandler=e=>{"Escape"===e.key&&this._isShown()&&this.tip.isConnected&&(e.preventDefault(),e.stopPropagation(),this.hide())},this._element.ownerDocument.addEventListener("keydown",this._keydownHandler,!0))}_removeEscapeListener(){this._keydownHandler&&(this._element.ownerDocument.removeEventListener("keydown",this._keydownHandler,!0),this._keydownHandler=null)}_fixTitle(){const e=this._element.getAttribute("title");e&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",e),this._element.setAttribute("data-bs-original-title",e),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(e,t){clearTimeout(this._timeout),this._timeout=setTimeout(e,t)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(e){const t=Manipulator.getDataAttributes(this._element);for(const e of Object.keys(t))DISALLOWED_ATTRIBUTES.has(e)&&delete t[e];return e={...t,..."object"==typeof e&&e?e:{}},e=this._mergeConfigObj(e),e=this._configAfterMerge(e),this._typeCheckConfig(e),e}_configAfterMerge(e){return e.container=!1===e.container?document.body:getElement(e.container),"number"==typeof e.delay&&(e.delay={show:e.delay,hide:e.delay}),"number"!=typeof e.title&&"boolean"!=typeof e.title||(e.title=e.title.toString()),"number"!=typeof e.content&&"boolean"!=typeof e.content||(e.content=e.content.toString()),e}_getDelegateConfig(){const e={};for(const[t,n]of Object.entries(this._config))this.constructor.Default[t]!==n&&(e[t]=n);return e.selector=!1,e.trigger="manual",e}_disposeFloating(){this._floatingCleanup&&(this._floatingCleanup(),this._floatingCleanup=null),this.tip&&(this.tip.remove(),this.tip=null)}}const initTooltip=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$3);t&&Tooltip.getOrCreateInstance(t)};EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$3,initTooltip),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$3,initTooltip);const NAME$5="popover",SELECTOR_TITLE=".popover-header",SELECTOR_CONTENT=".popover-body",SELECTOR_DATA_TOGGLE$2='[data-bs-toggle="popover"]',EVENT_CLICK$2="click",EVENT_FOCUSIN$1="focusin",EVENT_MOUSEENTER="mouseenter",Default$4={...Tooltip.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},DefaultType$4={...Tooltip.DefaultType,content:"(null|string|element|function)"};class Popover extends Tooltip{static get Default(){return Default$4}static get DefaultType(){return DefaultType$4}static get NAME(){return NAME$5}_isWithContent(){return Boolean(this._getTitle()||this._getContent())||this._hasNewContent()}_getContentForTemplate(){return{[SELECTOR_TITLE]:this._getTitle(),[SELECTOR_CONTENT]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}}const initPopover=e=>{const t=e.target.closest(SELECTOR_DATA_TOGGLE$2);t&&("click"===e.type&&e.preventDefault(),Popover.getOrCreateInstance(t))};EventHandler.on(document,"click",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"focusin",SELECTOR_DATA_TOGGLE$2,initPopover),EventHandler.on(document,"mouseenter",SELECTOR_DATA_TOGGLE$2,initPopover);const NAME$4="range",DATA_KEY$4="bs.range",EVENT_KEY$4=".bs.range",DATA_API_KEY$1=".data-api",EVENT_CHANGED="changed.bs.range",EVENT_DOM_CONTENT_LOADED="DOMContentLoaded.bs.range.data-api",EVENT_INPUT="input",EVENT_CHANGE="change",SELECTOR_RANGE=".form-range",SELECTOR_INPUT=".form-range-input",CLASS_NAME_BUBBLE="form-range-bubble",CLASS_NAME_TICKS="form-range-ticks",CLASS_NAME_TICK="form-range-tick",CLASS_NAME_TICK_LABEL="form-range-tick-label",PROPERTY_FILL="--bs-range-fill",Default$3={bubble:!1,formatter:null},DefaultType$3={bubble:"(boolean|null)",formatter:"(function|null)"};class Range extends BaseComponent{constructor(e,t){super(e,t),this._element&&(this._input=SelectorEngine.findOne(SELECTOR_INPUT,this._element),this._input&&(this._bubble=null,this._bubbleText=null,this._ticks=null,this._updateHandler=()=>this._update(),this._config.bubble&&this._createBubble(),this._createTicks(),this._addEventListeners(),this._update()))}static get Default(){return Default$3}static get DefaultType(){return DefaultType$3}static get NAME(){return NAME$4}update(){this._update()}dispose(){EventHandler.off(this._input,"input",this._updateHandler),EventHandler.off(this._input,"change",this._updateHandler),this._bubble?.remove(),this._ticks?.remove(),super.dispose()}_configAfterMerge(e){return null===e.bubble&&(e.bubble=!0),e}_addEventListeners(){EventHandler.on(this._input,"input",this._updateHandler),EventHandler.on(this._input,"change",this._updateHandler)}_min(){return""===this._input.min?0:Number.parseFloat(this._input.min)}_max(){return""===this._input.max?100:Number.parseFloat(this._input.max)}_value(){return Number.parseFloat(this._input.value)}_ratio(){const e=this._max()-this._min();return e>0?(this._value()-this._min())/e:0}_update(){this._element.style.setProperty(PROPERTY_FILL,`${this._ratio()}`),this._bubbleText&&(this._bubbleText.textContent=this._format(this._value())),EventHandler.trigger(this._input,EVENT_CHANGED,{value:this._value()})}_format(e){return"function"==typeof this._config.formatter?this._config.formatter(e):String(e)}_createBubble(){this._bubble=document.createElement("output"),this._bubble.className=`${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`,this._bubble.setAttribute("aria-hidden","true");const e=document.createElement("div");e.className="tooltip-arrow",this._bubbleText=document.createElement("div"),this._bubbleText.className="tooltip-inner",this._bubble.append(e,this._bubbleText),this._input.insertAdjacentElement("afterend",this._bubble)}_createTicks(){const e=this._input.getAttribute("list"),t=e?document.getElementById(e):null;if(!t)return;const n=this._min(),s=this._max()-n||1,i=[];for(const e of SelectorEngine.find("option",t)){const t=Number.parseFloat(e.value);if(!Number.isNaN(t)){const o=Math.min(Math.max((t-n)/s,0),1);i.push({ratio:o,label:e.label})}}if(0===i.length)return;i.sort((e,t)=>e.ratio-t.ratio),this._ticks=document.createElement("div"),this._ticks.className=CLASS_NAME_TICKS,this._ticks.setAttribute("aria-hidden","true");const o=[0,...i.map(e=>e.ratio),1];this._ticks.style.gridTemplateColumns=o.slice(1).map((e,t)=>e-o[t]+"fr").join(" ");for(const[e,t]of i.entries()){const n=document.createElement("span");if(n.className=CLASS_NAME_TICK,n.style.gridColumnStart=`${e+2}`,t.label){const e=document.createElement("span");e.className=CLASS_NAME_TICK_LABEL,e.textContent=t.label,n.append(e)}this._ticks.append(n)}this._element.append(this._ticks)}}EventHandler.on(document,EVENT_DOM_CONTENT_LOADED,()=>{for(const e of SelectorEngine.find(".form-range"))Range.getOrCreateInstance(e)});const NAME$3="scrollspy",DATA_KEY$3="bs.scrollspy",EVENT_KEY$3=`.${DATA_KEY$3}`,DATA_API_KEY=".data-api",EVENT_ACTIVATE=`activate${EVENT_KEY$3}`,EVENT_CLICK$1=`click${EVENT_KEY$3}`,EVENT_SCROLL=`scroll${EVENT_KEY$3}`,EVENT_SCROLLEND=`scrollend${EVENT_KEY$3}`,EVENT_RESIZE=`resize${EVENT_KEY$3}`,EVENT_LOAD_DATA_API$1=`load${EVENT_KEY$3}.data-api`,CLASS_NAME_MENU_ITEM="menu-item",CLASS_NAME_ACTIVE$1="active",SELECTOR_DATA_SPY='[data-bs-spy="scroll"]',SELECTOR_TARGET_LINKS="[href]",SELECTOR_NAV_LIST_GROUP=".nav, .list-group",SELECTOR_NAV_LINKS=".nav-link",SELECTOR_NAV_ITEMS=".nav-item",SELECTOR_LIST_ITEMS=".list-group-item",SELECTOR_LINK_ITEMS=".nav-link, .nav-item > .nav-link, .list-group-item",SELECTOR_MENU_TOGGLE$1='[data-bs-toggle="menu"]',SCROLL_IDLE_TIMEOUT=100,RESIZE_DEBOUNCE=100,Default$2={rootMargin:null,smoothScroll:!1,target:null,threshold:[0],topMargin:"12%"},DefaultType$2={rootMargin:"(string|null)",smoothScroll:"boolean",target:"element",threshold:"array",topMargin:"string"};class ScrollSpy extends BaseComponent{constructor(e,t){super(e,t),this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map,this._intersecting=new Set,this._activeTarget=null,this._lastActive=null,this._atBottom=!1,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._observer=null,this._sentinel=null,this._sentinelObserver=null,this._pendingNavigation=null,this._settleTimeout=null,this._settleHandler=null,this._scrollIdleHandler=null,this._resizeHandler=null,this._resizeTimeout=null,this.refresh()}static get Default(){return Default$2}static get DefaultType(){return DefaultType$2}static get NAME(){return NAME$3}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e);this._setUpSentinel(),this._maybeAddResizeListener()}dispose(){this._observer?.disconnect(),this._teardownSentinel(),this._disarmSettle(),this._removeResizeListener(),EventHandler.off(this._config.target,EVENT_CLICK$1),super.dispose()}_configAfterMerge(e){return e.target=getElement(e.target)||document.body,"string"==typeof e.threshold&&(e.threshold=e.threshold.split(",").map(e=>Number.parseFloat(e))),e}_getNewObserver(){const e={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin??this._getDerivedRootMargin()};return new IntersectionObserver(e=>this._onIntersect(e),e)}_onIntersect(e){for(const t of e)t.isIntersecting?this._intersecting.add(t.target):this._intersecting.delete(t.target);this._computeActive()}_computeActive(){if(!this._element?.isConnected||0===this._sections.length)return;let e=null;if(this._atBottom)e=this._sections.at(-1);else{for(const t of this._sections)this._intersecting.has(t)&&(e=t);e||=this._lastActive??this._sections.at(0)}if(!e)return;this._lastActive=e;const t=this._linkBySection.get(e);t&&this._process(t)}_parseTopMargin(){const e=String(this._config.topMargin);return{value:Number.parseFloat(e)||0,unit:e.endsWith("%")?"%":"px"}}_getDerivedRootMargin(){const{value:e,unit:t}=this._parseTopMargin();let n=e;if("px"===t){const t=this._rootElement?this._rootElement.clientHeight:document.documentElement.clientHeight||window.innerHeight;n=t?e/t*100:12}return`0px 0px -${Math.min(Math.max(100-n,0),100)}% 0px`}_usesPixelMargin(){return!this._config.rootMargin&&"px"===this._parseTopMargin().unit}_setUpSentinel(){if(this._teardownSentinel(),0===this._sections.length)return;const e=document.createElement("div");e.setAttribute("aria-hidden","true"),e.style.cssText="position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;",this._element.append(e),this._sentinel=e,this._sentinelObserver=new IntersectionObserver(e=>this._onSentinel(e),{root:this._rootElement,threshold:[0]}),this._sentinelObserver.observe(e)}_onSentinel(e){const t=e.at(-1);this._atBottom=Boolean(t?.isIntersecting)&&this._isOverflowing(),this._computeActive()}_isOverflowing(){const e=this._rootElement||document.scrollingElement||document.documentElement;return e.scrollHeight>e.clientHeight}_teardownSentinel(){this._sentinelObserver?.disconnect(),this._sentinelObserver=null,this._sentinel?.remove(),this._sentinel=null,this._atBottom=!1}_maybeAddResizeListener(){this._removeResizeListener(),this._usesPixelMargin()&&(this._resizeHandler=()=>{clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(()=>this._rebuildObserver(),100)},EventHandler.on(window,EVENT_RESIZE,this._resizeHandler))}_removeResizeListener(){clearTimeout(this._resizeTimeout),this._resizeTimeout=null,this._resizeHandler&&(EventHandler.off(window,EVENT_RESIZE,this._resizeHandler),this._resizeHandler=null)}_rebuildObserver(){if(this._observer){this._observer.disconnect(),this._intersecting.clear(),this._observer=this._getNewObserver();for(const e of this._sections)this._observer.observe(e)}}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(EventHandler.off(this._config.target,EVENT_CLICK$1),EventHandler.on(this._config.target,EVENT_CLICK$1,"[href]",e=>{const t=e.target.closest("[href]"),n=t&&this._sectionByLink.get(t);if(!n||!this._element)return;e.preventDefault();const s=this._rootElement||window,i=n.offsetTop-this._element.offsetTop,o=this._rootElement?this._rootElement.scrollTop:window.scrollY??window.pageYOffset;if(matchMedia("(prefers-reduced-motion: reduce)").matches||Math.abs(o-i)<=2)return s.scrollTo?s.scrollTo({top:i,behavior:"auto"}):s.scrollTop=i,void this._settleNavigation(t.hash,n);this._pendingNavigation={hash:t.hash,section:n},this._armSettle(),s.scrollTo?s.scrollTo({top:i,behavior:"smooth"}):s.scrollTop=i}))}_armSettle(){this._disarmSettle();const e=this._getSettleTarget();this._settleHandler=()=>this._onSettle(),this._scrollIdleHandler=()=>{clearTimeout(this._settleTimeout),this._settleTimeout=setTimeout(()=>this._onSettle(),100)},EventHandler.on(e,EVENT_SCROLLEND,this._settleHandler),EventHandler.on(e,EVENT_SCROLL,this._scrollIdleHandler)}_disarmSettle(){clearTimeout(this._settleTimeout),this._settleTimeout=null;const e=this._getSettleTarget();this._settleHandler&&(EventHandler.off(e,EVENT_SCROLLEND,this._settleHandler),this._settleHandler=null),this._scrollIdleHandler&&(EventHandler.off(e,EVENT_SCROLL,this._scrollIdleHandler),this._scrollIdleHandler=null)}_getSettleTarget(){return this._rootElement||document}_onSettle(){if(this._disarmSettle(),!this._pendingNavigation)return;const{hash:e,section:t}=this._pendingNavigation;this._settleNavigation(e,t)}_settleNavigation(e,t){this._pendingNavigation=null,window.history?.replaceState&&window.history.replaceState(null,"",e),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.focus({preventScroll:!0})}_initializeTargetsAndObservables(){this._sections=[],this._linkBySection=new Map,this._sectionByLink=new Map;const e=SelectorEngine.find("[href]",this._config.target),t=new Set;for(const n of e){if(!n.hash||isDisabled(n))continue;const e=decodeFragment(n.hash.slice(1));if(!e)continue;const s=document.getElementById(e);s&&this._element.contains(s)&&isVisible(s)&&(this._sectionByLink.set(n,s),this._linkBySection.set(s,n),t.has(s)||(t.add(s),this._sections.push(s)))}this._sections.sort((e,t)=>e.getBoundingClientRect().top-t.getBoundingClientRect().top)}_process(e){this._activeTarget!==e&&(this._clearActiveClass(this._config.target),this._activeTarget=e,e.classList.add("active"),this._activateParents(e),EventHandler.trigger(this._element,EVENT_ACTIVATE,{relatedTarget:e}))}_activateParents(e){if(e.classList.contains("menu-item")){const t=e.closest(".menu")?.previousElementSibling;return void(t?.matches(SELECTOR_MENU_TOGGLE$1)&&t.classList.add("active"))}for(const t of SelectorEngine.parents(e,".nav, .list-group"))for(const e of SelectorEngine.prev(t,SELECTOR_LINK_ITEMS))e.classList.add("active")}_clearActiveClass(e){e.classList.remove("active");const t=SelectorEngine.find("[href].active",e);for(const e of t)e.classList.remove("active")}}function decodeFragment(e){try{return decodeURIComponent(e)}catch{return e}}EventHandler.on(window,EVENT_LOAD_DATA_API$1,()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_SPY))ScrollSpy.getOrCreateInstance(e)});const NAME$2="tab",DATA_KEY$2="bs.tab",EVENT_KEY$2=".bs.tab",EVENT_HIDE$1="hide.bs.tab",EVENT_HIDDEN$1="hidden.bs.tab",EVENT_SHOW$1="show.bs.tab",EVENT_SHOWN$1="shown.bs.tab",EVENT_CLICK_DATA_API="click.bs.tab",EVENT_KEYDOWN="keydown.bs.tab",EVENT_LOAD_DATA_API="load.bs.tab",ARROW_LEFT_KEY="ArrowLeft",ARROW_RIGHT_KEY="ArrowRight",ARROW_UP_KEY="ArrowUp",ARROW_DOWN_KEY="ArrowDown",HOME_KEY="Home",END_KEY="End",CLASS_NAME_ACTIVE="active",CLASS_NAME_FADE$1="fade",CLASS_NAME_SHOW$1="show",SELECTOR_MENU_TOGGLE='[data-bs-toggle="menu"]',SELECTOR_MENU=".menu",NOT_SELECTOR_MENU_TOGGLE=`:not(${SELECTOR_MENU_TOGGLE})`,SELECTOR_TAB_PANEL='.list-group, .nav, [role="tablist"]',SELECTOR_OUTER=".nav-item, .list-group-item",SELECTOR_INNER=`.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`,SELECTOR_DATA_TOGGLE$1='[data-bs-toggle="tab"]',SELECTOR_INNER_ELEM=`${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE$1}`,SELECTOR_DATA_TOGGLE_ACTIVE='.active[data-bs-toggle="tab"]';class Tab extends BaseComponent{constructor(e){super(e),this._parent=this._element.closest(SELECTOR_TAB_PANEL),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),EventHandler.on(this._element,EVENT_KEYDOWN,e=>this._keydown(e)))}static get NAME(){return"tab"}show(){const e=this._element;if(this._elemIsActive(e))return;const t=this._getActiveElem(),n=t?EventHandler.trigger(t,EVENT_HIDE$1,{relatedTarget:e}):null;EventHandler.trigger(e,EVENT_SHOW$1,{relatedTarget:t}).defaultPrevented||n&&n.defaultPrevented||(this._deactivate(t,e),this._activate(e,t))}_activate(e,t){e&&(e.classList.add("active"),this._activate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.removeAttribute("tabindex"),e.setAttribute("aria-selected",!0),this._toggleMenu(e,!0),EventHandler.trigger(e,EVENT_SHOWN$1,{relatedTarget:t})):e.classList.add("show")},e,e.classList.contains("fade")))}_deactivate(e,t){e&&(e.classList.remove("active"),e.blur(),this._deactivate(SelectorEngine.getElementFromSelector(e)),this._queueCallback(()=>{"tab"===e.getAttribute("role")?(e.setAttribute("aria-selected",!1),e.setAttribute("tabindex","-1"),this._toggleMenu(e,!1),EventHandler.trigger(e,EVENT_HIDDEN$1,{relatedTarget:t})):e.classList.remove("show")},e,e.classList.contains("fade")))}_keydown(e){if(![ARROW_LEFT_KEY,ARROW_RIGHT_KEY,ARROW_UP_KEY,ARROW_DOWN_KEY,HOME_KEY,END_KEY].includes(e.key))return;if(e.altKey||e.ctrlKey||e.metaKey)return;e.stopPropagation(),e.preventDefault();const t=this._getChildren().filter(e=>!isDisabled(e));let n;if([HOME_KEY,END_KEY].includes(e.key))n=e.key===HOME_KEY?t[0]:t.at(-1);else{const s=[ARROW_RIGHT_KEY,ARROW_DOWN_KEY].includes(e.key);n=getNextActiveElement(t,e.target,s,!0)}n&&(n.focus({preventScroll:!0}),Tab.getOrCreateInstance(n).show())}_getChildren(){return SelectorEngine.find(SELECTOR_INNER_ELEM,this._parent)}_getActiveElem(){return this._getChildren().find(e=>this._elemIsActive(e))||null}_setInitialAttributes(e,t){this._setAttributeIfNotExists(e,"role","tablist");for(const e of t)this._setInitialAttributesOnChild(e)}_setInitialAttributesOnChild(e){e=this._getInnerElement(e);const t=this._elemIsActive(e),n=this._getOuterElement(e);e.setAttribute("aria-selected",t),n!==e&&this._setAttributeIfNotExists(n,"role","presentation"),t||e.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(e,"role","tab"),this._setInitialAttributesOnTargetPanel(e)}_setInitialAttributesOnTargetPanel(e){const t=SelectorEngine.getElementFromSelector(e);t&&(this._setAttributeIfNotExists(t,"role","tabpanel"),e.id&&this._setAttributeIfNotExists(t,"aria-labelledby",`${e.id}`))}_toggleMenu(e,t){const n=this._getOuterElement(e),s=SelectorEngine.findOne(SELECTOR_MENU_TOGGLE,n);if(!s)return;const i=SelectorEngine.findOne(".menu",n);s.classList.toggle("active",t),i&&i.classList.toggle("show",t),s.setAttribute("aria-expanded",t)}_setAttributeIfNotExists(e,t,n){e.hasAttribute(t)||e.setAttribute(t,n)}_elemIsActive(e){return e.classList.contains("active")}_getInnerElement(e){return e.matches(SELECTOR_INNER_ELEM)?e:SelectorEngine.findOne(SELECTOR_INNER_ELEM,e)}_getOuterElement(e){return e.closest(SELECTOR_OUTER)||e}}EventHandler.on(document,"click.bs.tab",SELECTOR_DATA_TOGGLE$1,function(e){["A","AREA"].includes(this.tagName)&&e.preventDefault(),isDisabled(this)||Tab.getOrCreateInstance(this).show()}),EventHandler.on(window,"load.bs.tab",()=>{for(const e of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE))Tab.getOrCreateInstance(e)});const NAME$1="toast",DATA_KEY$1="bs.toast",EVENT_KEY$1=".bs.toast",EVENT_MOUSEOVER="mouseover.bs.toast",EVENT_MOUSEOUT="mouseout.bs.toast",EVENT_FOCUSIN="focusin.bs.toast",EVENT_FOCUSOUT="focusout.bs.toast",EVENT_HIDE="hide.bs.toast",EVENT_HIDDEN="hidden.bs.toast",EVENT_SHOW="show.bs.toast",EVENT_SHOWN="shown.bs.toast",CLASS_NAME_FADE="fade",CLASS_NAME_HIDE="hide",CLASS_NAME_SHOW="show",CLASS_NAME_SHOWING="showing",DefaultType$1={animation:"boolean",autohide:"boolean",delay:"number"},Default$1={animation:!0,autohide:!0,delay:5e3};class Toast extends BaseComponent{constructor(e,t){super(e,t),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return Default$1}static get DefaultType(){return DefaultType$1}static get NAME(){return NAME$1}show(){EventHandler.trigger(this._element,EVENT_SHOW).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),reflow(this._element),this._element.classList.add("show","showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),EventHandler.trigger(this._element,EVENT_SHOWN),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(EventHandler.trigger(this._element,EVENT_HIDE).defaultPrevented||(this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.add("hide"),this._element.classList.remove("showing","show"),EventHandler.trigger(this._element,EVENT_HIDDEN)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove("show"),super.dispose()}isShown(){return this._element.classList.contains("show")}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(e,t){switch(e.type){case"mouseover":case"mouseout":this._hasMouseInteraction=t;break;case"focusin":case"focusout":this._hasKeyboardInteraction=t}if(t)return void this._clearTimeout();const n=e.relatedTarget;this._element===n||this._element.contains(n)||this._maybeScheduleHide()}_setListeners(){EventHandler.on(this._element,EVENT_MOUSEOVER,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_MOUSEOUT,e=>this._onInteraction(e,!1)),EventHandler.on(this._element,EVENT_FOCUSIN,e=>this._onInteraction(e,!0)),EventHandler.on(this._element,EVENT_FOCUSOUT,e=>this._onInteraction(e,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}}enableDismissTrigger(Toast);const NAME="toggler",DATA_KEY="bs.toggler",EVENT_KEY=`.${DATA_KEY}`,EVENT_TOGGLE=`toggle${EVENT_KEY}`,EVENT_TOGGLED=`toggled${EVENT_KEY}`,EVENT_CLICK="click",SELECTOR_DATA_TOGGLE='[data-bs-toggle="toggler"]',DefaultType={attribute:"string",value:"(string|number|boolean)"},Default={attribute:"class",value:null};class Toggler extends BaseComponent{static get Default(){return Default}static get DefaultType(){return DefaultType}static get NAME(){return NAME}toggle(){EventHandler.trigger(this._element,EVENT_TOGGLE).defaultPrevented||(this._execute(),EventHandler.trigger(this._element,EVENT_TOGGLED))}_execute(){const{attribute:e,value:t}=this._config;"id"!==e&&("class"!==e?this._element.getAttribute(e)!==String(t)?this._element.setAttribute(e,t):this._element.removeAttribute(e):this._element.classList.toggle(t))}}eventActionOnPlugin(Toggler,"click",SELECTOR_DATA_TOGGLE,"toggle");export{Alert,Button,Carousel,Chips,Collapse,Combobox,Datepicker,Dialog,Drawer,Menu,NavOverflow,OtpInput,Popover,Range,ScrollSpy,Strength,Tab,Toast,Toggler,Tooltip}; diff --git a/assets/javascripts/bootstrap/alert.js b/assets/javascripts/bootstrap/alert.js index 4e3d7db8..7a31f2d6 100644 --- a/assets/javascripts/bootstrap/alert.js +++ b/assets/javascripts/bootstrap/alert.js @@ -1,89 +1,65 @@ /*! - * Bootstrap alert.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap alert.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Alert = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index)); -})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { enableDismissTrigger } from './util/component-functions.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'alert'; - const DATA_KEY = 'bs.alert'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_CLOSE = `close${EVENT_KEY}`; - const EVENT_CLOSED = `closed${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; +const NAME = 'alert'; +const DATA_KEY = 'bs.alert'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_CLOSE = `close${EVENT_KEY}`; +const EVENT_CLOSED = `closed${EVENT_KEY}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_SHOW = 'show'; - /** - * Class definition - */ +/** + * Class definition + */ - class Alert extends BaseComponent { - // Getters - static get NAME() { - return NAME; - } - - // Public - close() { - const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); - if (closeEvent.defaultPrevented) { - return; - } - this._element.classList.remove(CLASS_NAME_SHOW); - const isAnimated = this._element.classList.contains(CLASS_NAME_FADE); - this._queueCallback(() => this._destroyElement(), this._element, isAnimated); - } - - // Private - _destroyElement() { - this._element.remove(); - EventHandler.trigger(this._element, EVENT_CLOSED); - this.dispose(); - } +class Alert extends BaseComponent { + // Getters + static get NAME() { + return NAME; + } - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Alert.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); + // Public + close() { + const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE); + if (closeEvent.defaultPrevented) { + return; } + this._element.classList.remove(CLASS_NAME_SHOW); + const isAnimated = this._element.classList.contains(CLASS_NAME_FADE); + this._queueCallback(() => this._destroyElement(), this._element, isAnimated); } - /** - * Data API implementation - */ - - componentFunctions_js.enableDismissTrigger(Alert, 'close'); - - /** - * jQuery - */ + // Private + _destroyElement() { + this._element.remove(); + EventHandler.trigger(this._element, EVENT_CLOSED); + this.dispose(); + } +} - index_js.defineJQueryPlugin(Alert); +/** + * Data API implementation + */ - return Alert; +enableDismissTrigger(Alert, 'close'); -})); +export { Alert as default }; diff --git a/assets/javascripts/bootstrap/base-component.js b/assets/javascripts/bootstrap/base-component.js index 991b190d..98a1c98b 100644 --- a/assets/javascripts/bootstrap/base-component.js +++ b/assets/javascripts/bootstrap/base-component.js @@ -1,85 +1,95 @@ /*! - * Bootstrap base-component.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap base-component.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./dom/data.js'), require('./dom/event-handler.js'), require('./util/config.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./dom/data', './dom/event-handler', './util/config', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BaseComponent = factory(global.Data, global.EventHandler, global.Config, global.Index)); -})(this, (function (Data, EventHandler, Config, index_js) { 'use strict'; +import Data from './dom/data.js'; +import EventHandler from './dom/event-handler.js'; +import Config from './util/config.js'; +import { getElement, executeAfterTransition } from './util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap base-component.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap base-component.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const VERSION = '5.3.8'; +const VERSION = '6.0.0-alpha1'; - /** - * Class definition - */ +/** + * Class definition + */ - class BaseComponent extends Config { - constructor(element, config) { - super(); - element = index_js.getElement(element); - if (!element) { - return; - } - this._element = element; - this._config = this._getConfig(config); - Data.set(this._element, this.constructor.DATA_KEY, this); +class BaseComponent extends Config { + constructor(element, config) { + super(); + element = getElement(element); + if (!element) { + return; } + this._element = element; + this._config = this._getConfig(config); - // Public - dispose() { - Data.remove(this._element, this.constructor.DATA_KEY); - EventHandler.off(this._element, this.constructor.EVENT_KEY); - for (const propertyName of Object.getOwnPropertyNames(this)) { - this[propertyName] = null; - } + // Dispose any existing instance bound to this element before registering the new one, + // so its event listeners and timers are cleaned up instead of leaking + const existingInstance = Data.get(this._element, this.constructor.DATA_KEY); + if (existingInstance) { + existingInstance.dispose(); } + Data.set(this._element, this.constructor.DATA_KEY, this); + } - // Private - _queueCallback(callback, element, isAnimated = true) { - index_js.executeAfterTransition(callback, element, isAnimated); - } - _getConfig(config) { - config = this._mergeConfigObj(config, this._element); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; + // Public + dispose() { + Data.remove(this._element, this.constructor.DATA_KEY); + EventHandler.off(this._element, this.constructor.EVENT_KEY); + for (const propertyName of Object.getOwnPropertyNames(this)) { + this[propertyName] = null; } + } - // Static - static getInstance(element) { - return Data.get(index_js.getElement(element), this.DATA_KEY); - } - static getOrCreateInstance(element, config = {}) { - return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); - } - static get VERSION() { - return VERSION; - } - static get DATA_KEY() { - return `bs.${this.NAME}`; - } - static get EVENT_KEY() { - return `.${this.DATA_KEY}`; - } - static eventName(name) { - return `${name}${this.EVENT_KEY}`; - } + // Private + _queueCallback(callback, element, isAnimated = true) { + executeAfterTransition(() => { + // Don't run the completion callback if the instance was disposed mid-transition + if (!this._element) { + return; + } + callback(); + }, element, isAnimated); + } + _getConfig(config) { + config = this._mergeConfigObj(config, this._element); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; } - return BaseComponent; + // Static + static getInstance(element) { + return Data.get(getElement(element), this.DATA_KEY); + } + static getOrCreateInstance(element, config = {}) { + return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null); + } + static get VERSION() { + return VERSION; + } + static get DATA_KEY() { + return `bs.${this.NAME}`; + } + static get EVENT_KEY() { + return `.${this.DATA_KEY}`; + } + static eventName(name) { + return `${name}${this.EVENT_KEY}`; + } +} -})); +export { BaseComponent as default }; diff --git a/assets/javascripts/bootstrap/button.js b/assets/javascripts/bootstrap/button.js index 8514e9b0..69288ab1 100644 --- a/assets/javascripts/bootstrap/button.js +++ b/assets/javascripts/bootstrap/button.js @@ -1,78 +1,57 @@ /*! - * Bootstrap button.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap button.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Button = factory(global.BaseComponent, global.EventHandler, global.Index)); -})(this, (function (BaseComponent, EventHandler, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap button.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'button'; - const DATA_KEY = 'bs.button'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const CLASS_NAME_ACTIVE = 'active'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]'; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - - /** - * Class definition - */ - - class Button extends BaseComponent { - // Getters - static get NAME() { - return NAME; - } - - // Public - toggle() { - // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method - this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE)); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Button.getOrCreateInstance(this); - if (config === 'toggle') { - data[config](); - } - }); - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap button.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'button'; +const DATA_KEY = 'bs.button'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const CLASS_NAME_ACTIVE = 'active'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="button"]'; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; + +/** + * Class definition + */ + +class Button extends BaseComponent { + // Getters + static get NAME() { + return NAME; } - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { - event.preventDefault(); - const button = event.target.closest(SELECTOR_DATA_TOGGLE); - const data = Button.getOrCreateInstance(button); - data.toggle(); - }); - - /** - * jQuery - */ + // Public + toggle() { + // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method + this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE)); + } +} - index_js.defineJQueryPlugin(Button); +/** + * Data API implementation + */ - return Button; +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => { + event.preventDefault(); + const button = event.target.closest(SELECTOR_DATA_TOGGLE); + const data = Button.getOrCreateInstance(button); + data.toggle(); +}); -})); +export { Button as default }; diff --git a/assets/javascripts/bootstrap/carousel.js b/assets/javascripts/bootstrap/carousel.js index 9263da81..61196040 100644 --- a/assets/javascripts/bootstrap/carousel.js +++ b/assets/javascripts/bootstrap/carousel.js @@ -1,387 +1,804 @@ /*! - * Bootstrap carousel.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap carousel.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js'), require('./util/swipe.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index', './util/swipe'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Carousel = factory(global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index, global.Swipe)); -})(this, (function (BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js, Swipe) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap carousel.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'carousel'; - const DATA_KEY = 'bs.carousel'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch - - const ORDER_NEXT = 'next'; - const ORDER_PREV = 'prev'; - const DIRECTION_LEFT = 'left'; - const DIRECTION_RIGHT = 'right'; - const EVENT_SLIDE = `slide${EVENT_KEY}`; - const EVENT_SLID = `slid${EVENT_KEY}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; - const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`; - const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`; - const EVENT_DRAG_START = `dragstart${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_CAROUSEL = 'carousel'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_SLIDE = 'slide'; - const CLASS_NAME_END = 'carousel-item-end'; - const CLASS_NAME_START = 'carousel-item-start'; - const CLASS_NAME_NEXT = 'carousel-item-next'; - const CLASS_NAME_PREV = 'carousel-item-prev'; - const SELECTOR_ACTIVE = '.active'; - const SELECTOR_ITEM = '.carousel-item'; - const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; - const SELECTOR_ITEM_IMG = '.carousel-item img'; - const SELECTOR_INDICATORS = '.carousel-indicators'; - const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; - const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'; - const KEY_TO_DIRECTION = { - [ARROW_LEFT_KEY]: DIRECTION_RIGHT, - [ARROW_RIGHT_KEY]: DIRECTION_LEFT - }; - const Default = { - interval: 5000, - keyboard: true, - pause: 'hover', - ride: false, - touch: true, - wrap: true - }; - const DefaultType = { - interval: '(number|boolean)', - // TODO:v6 remove boolean support - keyboard: 'boolean', - pause: '(string|boolean)', - ride: '(boolean|string)', - touch: 'boolean', - wrap: 'boolean' - }; - - /** - * Class definition - */ - - class Carousel extends BaseComponent { - constructor(element, config) { - super(element, config); - this._interval = null; - this._activeElement = null; - this._isSliding = false; - this.touchTimeout = null; - this._swipeHelper = null; - this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); - this._addEventListeners(); - if (this._config.ride === CLASS_NAME_CAROUSEL) { - this.cycle(); +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isVisible, isRTL } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'carousel'; +const DATA_KEY = 'bs.carousel'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const DIRECTION_LEFT = 'left'; +const DIRECTION_RIGHT = 'right'; +const EVENT_SLIDE = `slide${EVENT_KEY}`; +const EVENT_SLID = `slid${EVENT_KEY}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; +const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`; +const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_CAROUSEL = 'carousel'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE = 'carousel-fade'; +const CLASS_NAME_CENTER = 'carousel-center'; +const CLASS_NAME_AUTO = 'carousel-auto'; +const CLASS_NAME_CLONE = 'carousel-item-clone'; +const CLASS_NAME_PAUSED = 'paused'; +// Added to the root while the autoplay timer is running, so CSS can fill the +// active indicator like a progress bar over the current slide's interval. +const CLASS_NAME_PLAYING = 'carousel-playing'; + +// Shipped (`--bs-`-prefixed) custom property the indicator fill animation reads +// for its duration. The build prefixes every custom property, so the bare +// `--carousel-interval` used in the SCSS source becomes this at runtime. +const PROPERTY_INTERVAL = '--bs-carousel-interval'; + +// Duration (ms) of the JS-driven slide animation used for programmatic +// navigation (prev/next, indicators, wrap, and loop). We step `scrollLeft` +// ourselves over this window instead of calling `scrollBy({behavior:'smooth'})`, +// because Safari mis-scales programmatic smooth scrolls under page zoom — a +// one-slide jump sails well past the target (by the zoom factor) and the +// restored snap then visibly yanks the slide back. Animating by hand is immune +// to that and gives every jump a consistent duration. +const SCROLL_DURATION = 300; + +// How far below the most-visible slide a slide's IntersectionRatio can be while +// still counting as the active (left-most) slide. After a programmatic scroll +// the viewport rests a sub-pixel past the snap offset, leaving the intended +// slide a hair less visible than its fully-in neighbors; the tolerance prevents +// that rounding from skipping the active index forward. +const ACTIVE_RATIO_TOLERANCE = 0.05; +const SELECTOR_ACTIVE = '.active'; +// Exclude transient loop clones so index math, indicators, and active-slide +// detection only ever see the real slides. +const SELECTOR_ITEM = `.carousel-item:not(.${CLASS_NAME_CLONE})`; +const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM; +const SELECTOR_INNER = '.carousel-inner'; +const SELECTOR_INDICATORS = '.carousel-indicators'; +const SELECTOR_PLAY_PAUSE = '.carousel-control-play-pause'; +const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'; +const SELECTOR_DATA_SLIDE_PREV = '[data-bs-slide="prev"]'; +const SELECTOR_DATA_SLIDE_NEXT = '[data-bs-slide="next"]'; +const SELECTOR_DATA_AUTOPLAY = '[data-bs-autoplay="true"]'; +const KEY_TO_DIRECTION = { + [ARROW_LEFT_KEY]: DIRECTION_RIGHT, + [ARROW_RIGHT_KEY]: DIRECTION_LEFT +}; +const ENDS_STOP = 'stop'; +const ENDS_WRAP = 'wrap'; +const ENDS_LOOP = 'loop'; +const Default = { + autoplay: false, + ends: ENDS_LOOP, + interval: 5000, + keyboard: true, + pause: 'hover' +}; +const DefaultType = { + autoplay: 'boolean', + ends: 'string', + interval: 'number', + keyboard: 'boolean', + pause: '(string|boolean)' +}; + +// Standard ease-in-out cubic, so the JS-driven scroll accelerates and +// decelerates like a native smooth scroll rather than moving linearly. +const easeInOutCubic = progress => progress < 0.5 ? 4 * progress * progress * progress : 1 - (-2 * progress + 2) ** 3 / 2; + +/** + * Class definition + */ + +class Carousel extends BaseComponent { + constructor(element, config) { + super(element, config); + + // The scroll viewport. The browser owns sliding, dragging, momentum, and + // keyboard scrolling; this controller only layers on autoplay, the + // prev/next/indicator controls, and active-slide syncing. + this._viewport = SelectorEngine.findOne(SELECTOR_INNER, this._element) || this._element; + this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element); + this._playPauseElement = SelectorEngine.findOne(SELECTOR_PLAY_PAUSE, this._element); + // Prev/next controls scoped to the carousel root (covers inline and stacked + // layouts). External controls placed outside `.carousel` aren't managed. + this._prevControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_PREV, this._element); + this._nextControls = SelectorEngine.find(SELECTOR_DATA_SLIDE_NEXT, this._element); + this._interval = null; + this._observer = null; + // rAF handle for the in-flight JS-driven scroll animation (see `_animateScroll`). + this._scrollFrame = null; + // True while a seamless loop transition is animating, so the + // IntersectionObserver and re-entrant navigation don't interfere. + this._looping = false; + this._visibility = new Map(); + // Runtime autoplay intent. Starts from the `autoplay` option, but is turned + // off once the user takes control (clicks a control, uses the keyboard, + // swipes/drags, or presses pause) so we don't move content out from under + // them (WCAG 2.2.2 Pause, Stop, Hide). + this._playing = this._config.autoplay; + this._activeIndex = this._initialActiveIndex(); + this._addEventListeners(); + this._observeItems(); + this._refreshActiveState(); + if (this._playing) { + this.cycle(); + } + this._updatePlayPauseControl(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + next() { + this.to(this._navIndex() + 1); + } + nextWhenVisible() { + // Don't advance when the page or the carousel isn't visible + if (document.visibilityState === 'visible' && isVisible(this._element)) { + this.next(); + } + } + prev() { + this.to(this._navIndex() - 1); + } + pause() { + this._clearInterval(); + // Freeze the indicator progress fill; it resets to empty until cycling + // resumes and `_scheduleAutoplay` restarts it from scratch. + this._element.classList.remove(CLASS_NAME_PLAYING); + } + cycle() { + this._clearInterval(); + this._scheduleAutoplay(); + this._element.classList.add(CLASS_NAME_PLAYING); + } + to(index) { + // Ignore navigation while a seamless loop transition is animating + if (this._looping) { + return; + } + const items = this._getItems(); + const rawIndex = Number.parseInt(index, 10); + + // Seamless loop: continue forward/backward into a transient clone instead of + // the visible `wrap` jump. Only the simple single-slide scroll layout + // qualifies, and reduced motion falls back to the plain wrap below. + if (this._config.ends === ENDS_LOOP && !this._prefersReducedMotion() && this._canLoop()) { + if (rawIndex > items.length - 1) { + this._loopTransition(true); + return; + } + if (rawIndex < 0) { + this._loopTransition(false); + return; } } + const targetIndex = this._normalizeIndex(rawIndex, items.length); + // Measure "current" from the live scroll position: `_activeIndex` updates + // asynchronously, so an indicator/control used mid-scroll must compare + // against where the viewport actually rests (`_navIndex` returns the tracked + // active index for fade/non-scrollable layouts). + const currentIndex = this._navIndex(); + if (targetIndex === null || targetIndex === currentIndex) { + return; + } + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[targetIndex], + direction: this._direction(currentIndex, targetIndex), + from: currentIndex, + to: targetIndex + }); + if (slideEvent.defaultPrevented) { + return; + } + if (this._isFade()) { + this._fadeTo(targetIndex); + return; + } - // Getters - static get Default() { - return Default; + // Scroll mode: the IntersectionObserver fires `slid` and syncs state once + // the new slide settles into view. + this._scrollToIndex(targetIndex); + } + dispose() { + // Stop autoplay first: otherwise a pending timer would fire after the + // instance is torn down and throw on the now-null `_element`. + this._clearInterval(); + if (this._observer) { + this._observer.disconnect(); } - static get DefaultType() { - return DefaultType; + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); } - static get NAME() { - return NAME; + + // Tidy up any in-flight loop transition: drop a stray clone and restore + // native snapping, so the viewport isn't left mid-animation. + for (const clone of SelectorEngine.find(`.${CLASS_NAME_CLONE}`, this._viewport)) { + clone.remove(); } + this._viewport.style.scrollSnapType = ''; - // Public - next() { - this._slide(ORDER_NEXT); + // The pointerdown listener lives on the viewport (`.carousel-inner`), which + // `super.dispose()` doesn't clean up—it only drops listeners on `_element`. + EventHandler.off(this._viewport, EVENT_KEY); + super.dispose(); + } + + // Private + // Normalize an unknown `ends` value so navigation and end-control logic can't + // disagree about whether the carousel wraps. + _configAfterMerge(config) { + if (![ENDS_STOP, ENDS_WRAP, ENDS_LOOP].includes(config.ends)) { + config.ends = Default.ends; + } + return config; + } + _initialActiveIndex() { + const active = SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const index = active ? this._getItems().indexOf(active) : 0; + return Math.max(index, 0); + } + _addEventListeners() { + if (this._config.keyboard) { + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } + if (this._config.pause === 'hover') { + EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause()); + EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle()); } - nextWhenVisible() { - // FIXME TODO use `document.visibilityState` - // Don't call next when the page isn't visible - // or the carousel or its parent isn't visible - if (!document.hidden && index_js.isVisible(this._element)) { + + // Dragging, swiping, or tapping the track is an explicit interaction + EventHandler.on(this._viewport, EVENT_POINTERDOWN, () => this._pauseFromInteraction()); + } + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return; + } + const direction = KEY_TO_DIRECTION[event.key]; + if (direction) { + event.preventDefault(); + this._pauseFromInteraction(); + if (direction === DIRECTION_RIGHT) { + this.prev(); + } else { this.next(); } } - prev() { - this._slide(ORDER_PREV); + } + _observeItems() { + // Fade mode stacks slides instead of scrolling, so there's nothing to observe + if (this._isFade() || typeof IntersectionObserver === 'undefined') { + return; } - pause() { - if (this._isSliding) { - index_js.triggerTransitionEnd(this._element); - } - this._clearInterval(); + this._observer = new IntersectionObserver(entries => this._handleIntersection(entries), { + root: this._viewport, + threshold: [0, 0.25, 0.5, 0.75, 1] + }); + for (const item of this._getItems()) { + this._observer.observe(item); } - cycle() { - this._clearInterval(); - this._updateInterval(); - this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval); + } + _handleIntersection(entries) { + // A loop transition deliberately scrolls onto a transient clone; ignore the + // visibility churn so it doesn't move the active index mid-animation. + if (this._looping) { + return; } - _maybeEnableCycle() { - if (!this._config.ride) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.cycle()); - return; - } - this.cycle(); + for (const entry of entries) { + this._visibility.set(entry.target, entry.isIntersecting ? entry.intersectionRatio : 0); } - to(index) { - const items = this._getItems(); - if (index > items.length - 1 || index < 0) { - return; - } - if (this._isSliding) { - EventHandler.one(this._element, EVENT_SLID, () => this.to(index)); - return; - } - const activeIndex = this._getItemIndex(this._getActive()); - if (activeIndex === index) { - return; + const items = this._getItems(); + const ratios = items.map(item => this._visibility.get(item) ?? 0); + const maxRatio = Math.max(...ratios); + + // Pick the left-most slide that's *near* fully visible rather than the strict + // global maximum. After a programmatic scroll the viewport rests ~1px past + // the target snap offset, so the intended left-most slide reports a ratio a + // hair below the deeper, fully-visible ones (e.g. 0.997 vs 1.0). A strict max + // would skip past it and inflate the active index by one, which breaks + // multi-item next/prev. The tolerance keeps the intended slide active while + // peeking slivers (well below the max) are still ignored. + let bestIndex = this._activeIndex; + if (maxRatio > 0) { + bestIndex = ratios.findIndex(ratio => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE); + } + this._setActive(bestIndex); + // Keep the end controls in sync with the scroll position even when the + // active index doesn't change (e.g. the final stretch of a multi-item + // scroll, where the left-most slide is already the last reachable one). + this._updateEndControls(); + } + + // The index a `next()`/`prev()` step is measured from. Scroll layouts read it + // from the live scroll position instead of `this._activeIndex`, because the + // IntersectionObserver updates that asynchronously: after one step the index + // can still be stale, so the next step would compute the same target and + // silently no-op (the "the button does nothing / can't reach the end slide" + // symptom). Fade and non-scrollable layouts have no scroll position to read, + // so they keep using the tracked active index (also what the unit tests rely + // on when there's no real layout). + _navIndex() { + if (this._isFade() || this._viewport.scrollWidth - this._viewport.clientWidth <= 0) { + return this._activeIndex; + } + let index = this._activeIndex; + let smallestDelta = Number.POSITIVE_INFINITY; + for (const [itemIndex, item] of this._getItems().entries()) { + // The slide currently resting at the active position has ~zero delta. + const delta = Math.abs(this._scrollDelta(item)); + if (delta < smallestDelta) { + smallestDelta = delta; + index = itemIndex; } - const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV; - this._slide(order, items[index]); } - dispose() { - if (this._swipeHelper) { - this._swipeHelper.dispose(); + return index; + } + _scrollToIndex(index) { + const item = this._getItems()[index]; + if (!item) { + return; + } + const left = this._scrollDelta(item); + if (Math.abs(left) < 1) { + return; + } + + // `scroll-snap-stop: always` would clamp a programmatic scroll to a single + // snap point, breaking multi-slide jumps (an indicator click, `to()`, or + // wrapping from the last slide back to the first). Suspend snapping while we + // animate, then restore it once we arrive so the slide rests precisely on the + // snap point (honouring peek/gap). + const targetLeft = this._viewport.scrollLeft + left; + this._viewport.style.scrollSnapType = 'none'; + this._animateScroll(targetLeft, () => { + this._viewport.style.scrollSnapType = ''; + // Without IntersectionObserver nothing else fires `slid`/updates the active + // slide after a programmatic scroll, so do it here. With the observer + // present this is a no-op (it already moved the active index to `index`). + if (!this._observer) { + this._setActive(index); } - super.dispose(); + + // The IntersectionObserver doesn't fire once the viewport has stopped, so + // refresh the end controls here to catch the final settle landing exactly + // on the scroll extent (e.g. disabling `next` at the last view). + this._updateEndControls(); + }); + } + + // Animate `this._viewport.scrollLeft` to `targetLeft` over `SCROLL_DURATION`, + // stepping the position ourselves each frame (the caller suspends snapping + // first and restores it in `onComplete`). This replaces + // `scrollBy({behavior:'smooth'})`, whose Safari page-zoom bug made programmatic + // jumps overshoot the target and snap back. Because we set every frame's + // absolute position with an instant scroll, the animation can't overshoot and + // every jump takes the same time, in every browser. + _animateScroll(targetLeft, onComplete) { + if (this._scrollFrame !== null) { + cancelAnimationFrame(this._scrollFrame); + this._scrollFrame = null; } + const startLeft = this._viewport.scrollLeft; + const distance = targetLeft - startLeft; - // Private - _configAfterMerge(config) { - config.defaultInterval = config.interval; - return config; + // Reduced motion (or no rAF, e.g. unit tests): jump straight to the target. + if (this._prefersReducedMotion() || typeof requestAnimationFrame === 'undefined') { + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + onComplete(); + return; } - _addEventListeners() { - if (this._config.keyboard) { - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + let startTime = null; + const step = now => { + if (startTime === null) { + startTime = now; } - if (this._config.pause === 'hover') { - EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause()); - EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle()); - } - if (this._config.touch && Swipe.isSupported()) { - this._addTouchEventListeners(); + const progress = Math.min((now - startTime) / SCROLL_DURATION, 1); + // `'instant'` (not the default) because the viewport sets + // `scroll-behavior: smooth` in CSS; without it each step would itself + // animate and fight this loop. + this._viewport.scrollTo({ + left: startLeft + distance * easeInOutCubic(progress), + behavior: 'instant' + }); + if (progress < 1) { + this._scrollFrame = requestAnimationFrame(step); + return; } + + // Land exactly on target, guarding against floating-point drift. + this._viewport.scrollTo({ + left: targetLeft, + behavior: 'instant' + }); + this._scrollFrame = null; + onComplete(); + }; + this._scrollFrame = requestAnimationFrame(step); + } + + // Horizontal distance to scroll the viewport so `element` rests where the + // active slide should sit. Scroll the viewport itself rather than calling + // `element.scrollIntoView()`: the latter scrolls *every* scrollable ancestor + // (including the page), so an autoplaying carousel below the fold would yank + // the whole page to itself on each tick. Using bounding rects keeps it + // direction-agnostic (works in RTL). + _scrollDelta(element) { + const viewportRect = this._viewport.getBoundingClientRect(); + const rect = element.getBoundingClientRect(); + if (this._element.classList.contains(CLASS_NAME_CENTER)) { + return rect.left + rect.width / 2 - (viewportRect.left + viewportRect.width / 2); } - _addTouchEventListeners() { - for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { - EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()); - } - const endCallBack = () => { - if (this._config.pause !== 'hover') { - return; - } - - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - this.pause(); - if (this.touchTimeout) { - clearTimeout(this.touchTimeout); - } - this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval); - }; - const swipeConfig = { - leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)), - rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)), - endCallback: endCallBack - }; - this._swipeHelper = new Swipe(this._element, swipeConfig); - } - _keydown(event) { - if (/input|textarea/i.test(event.target.tagName)) { - return; - } - const direction = KEY_TO_DIRECTION[event.key]; - if (direction) { - event.preventDefault(); - this._slide(this._directionToOrder(direction)); - } + // Start alignment: rest the slide at the scroll-padding (peek) offset, which + // is exactly where scroll-snap will settle. Aligning flush to the edge + // instead would make the browser re-snap by `peek` once snapping is restored, + // producing a visible secondary nudge after the programmatic scroll. + const padStart = Number.parseFloat(getComputedStyle(this._viewport).scrollPaddingInlineStart) || 0; + return isRTL() ? rect.right - (viewportRect.right - padStart) : rect.left - (viewportRect.left + padStart); + } + + // Seamless loop: continue past an end into a one-off clone of the destination + // slide, then teleport to the real slide so there's no visible backward jump. + _loopTransition(isNext) { + const items = this._getItems(); + const last = items.length - 1; + const fromIndex = this._activeIndex; + const toIndex = isNext ? 0 : last; + const direction = this._loopDirection(isNext); + const slideEvent = EventHandler.trigger(this._element, EVENT_SLIDE, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + if (slideEvent.defaultPrevented) { + return; } - _getItemIndex(element) { - return this._getItems().indexOf(element); + this._looping = true; + const clone = (isNext ? items[0] : items[last]).cloneNode(true); + clone.classList.add(CLASS_NAME_CLONE); + clone.classList.remove(CLASS_NAME_ACTIVE); + clone.removeAttribute('id'); + // Also strip ids from the cloned subtree to avoid duplicate ids while the + // clone is on screen. + for (const node of SelectorEngine.find('[id]', clone)) { + node.removeAttribute('id'); } - _setActiveIndicatorElement(index) { - if (!this._indicatorsElement) { - return; - } - const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); - activeIndicator.classList.remove(CLASS_NAME_ACTIVE); - activeIndicator.removeAttribute('aria-current'); - const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); - if (newActiveIndicator) { - newActiveIndicator.classList.add(CLASS_NAME_ACTIVE); - newActiveIndicator.setAttribute('aria-current', 'true'); - } + clone.setAttribute('aria-hidden', 'true'); + clone.inert = true; + this._viewport.style.scrollSnapType = 'none'; + if (isNext) { + this._viewport.append(clone); + } else { + this._viewport.prepend(clone); + // Prepending shifts the real slides to the right; instantly re-align the + // current slide so the insertion doesn't flash before we animate. + this._jumpScroll(this._scrollDelta(items[fromIndex])); } - _updateInterval() { - const element = this._activeElement || this._getActive(); - if (!element) { - return; - } - const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10); - this._config.interval = elementInterval || this._config.defaultInterval; + this._animateScroll(this._viewport.scrollLeft + this._scrollDelta(clone), () => { + // Teleport to the real destination without animation. JS runs to + // completion before the browser paints, so removing the clone and the + // compensating scroll land in a single frame (no visible flash). + clone.remove(); + this._jumpScroll(this._scrollDelta(items[toIndex])); + this._activeIndex = toIndex; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[toIndex], + direction, + from: fromIndex, + to: toIndex + }); + this._viewport.style.scrollSnapType = ''; + this._looping = false; + }); + } + _loopDirection(isNext) { + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; } - _slide(order, element = null) { - if (this._isSliding) { - return; - } - const activeElement = this._getActive(); - const isNext = order === ORDER_NEXT; - const nextElement = element || index_js.getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap); - if (nextElement === activeElement) { - return; - } - const nextElementIndex = this._getItemIndex(nextElement); - const triggerEvent = eventName => { - return EventHandler.trigger(this._element, eventName, { - relatedTarget: nextElement, - direction: this._orderToDirection(order), - from: this._getItemIndex(activeElement), - to: nextElementIndex + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + + // Instant (non-animated) scroll with snapping suspended, used to teleport the + // viewport during a loop transition. `behavior: 'instant'` is required because + // the viewport sets `scroll-behavior: smooth` in CSS, and `'auto'` would defer + // to it and animate the teleport (a visible backward slide). + _jumpScroll(delta) { + this._viewport.style.scrollSnapType = 'none'; + this._viewport.scrollBy({ + left: delta, + top: 0, + behavior: 'instant' + }); + } + + // Fade mode just swaps the active class; the CSS opacity transition on + // `.carousel-item` performs the crossfade over `--carousel-fade-duration` (and + // collapses to an instant swap under reduced motion, via the `transition` + // mixin). It deliberately avoids the View Transition API: a view transition + // crossfades a page snapshot over its own (shorter) duration while this CSS + // fade also runs underneath, so the two animations overlap and visibly stutter. + _fadeTo(index) { + this._setActive(index); + } + _setActive(index) { + const items = this._getItems(); + if (index === this._activeIndex || !items[index]) { + return; + } + const from = this._activeIndex; + this._activeIndex = index; + this._refreshActiveState(); + EventHandler.trigger(this._element, EVENT_SLID, { + relatedTarget: items[index], + direction: this._direction(from, index), + from, + to: index + }); + } + _refreshActiveState() { + const items = this._getItems(); + for (const [index, item] of items.entries()) { + item.classList.toggle(CLASS_NAME_ACTIVE, index === this._activeIndex); + } + this._setActiveIndicatorElement(this._activeIndex); + this._updateEndControls(); + } + _updateEndControls() { + // Only `ends: 'stop'` has real ends; under `wrap`/`loop` you can always + // advance, so disabling end controls would be meaningless. When stopping, + // disable the prev control at the start of the scroll range and the next + // control at the end so there are no dead end-buttons. + if (this._config.ends !== ENDS_STOP) { + return; + } + const viewport = this._viewport; + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + let atStart; + let atEnd; + if (maxScroll > 0) { + // Scrollable: measure the real scroll extent so this works for multi-item, + // peek, and variable-width layouts where the last slide can never become + // the left-most (active) one. `Math.abs` keeps it correct in RTL, where + // `scrollLeft` runs from 0 down to negative. + const progress = Math.abs(viewport.scrollLeft); + atStart = progress <= 1; + atEnd = progress >= maxScroll - 1; + } else { + // Not scrollable (or no layout yet, e.g. in unit tests): fall back to the + // active index for the single-slide case. + const last = this._getItems().length - 1; + atStart = this._activeIndex <= 0; + atEnd = this._activeIndex >= last; + } + this._setControlsDisabled(this._prevControls, atStart); + this._setControlsDisabled(this._nextControls, atEnd); + } + _setControlsDisabled(controls, disabled) { + for (const control of controls) { + // a11y: if we're about to disable the focused control, move focus to the + // opposite (still-enabled) control so focus isn't lost. + if (disabled && control === document.activeElement) { + const opposite = controls === this._prevControls ? this._nextControls : this._prevControls; + const fallback = opposite[0] ?? this._viewport; + // `preventScroll` so moving focus doesn't yank the page/viewport to the + // newly-focused control mid-navigation. + fallback.focus({ + preventScroll: true }); - }; - const slideEvent = triggerEvent(EVENT_SLIDE); - if (slideEvent.defaultPrevented) { - return; - } - if (!activeElement || !nextElement) { - // Some weirdness is happening, so we bail - // TODO: change tests that use empty divs to avoid this check - return; - } - const isCycling = Boolean(this._interval); - this.pause(); - this._isSliding = true; - this._setActiveIndicatorElement(nextElementIndex); - this._activeElement = nextElement; - const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END; - const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV; - nextElement.classList.add(orderClassName); - index_js.reflow(nextElement); - activeElement.classList.add(directionalClassName); - nextElement.classList.add(directionalClassName); - const completeCallBack = () => { - nextElement.classList.remove(directionalClassName, orderClassName); - nextElement.classList.add(CLASS_NAME_ACTIVE); - activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName); - this._isSliding = false; - triggerEvent(EVENT_SLID); - }; - this._queueCallback(completeCallBack, activeElement, this._isAnimated()); - if (isCycling) { - this.cycle(); } + control.disabled = disabled; } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_SLIDE); + } + _setActiveIndicatorElement(index) { + if (!this._indicatorsElement) { + return; } - _getActive() { - return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element); + const active = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement); + if (active) { + active.classList.remove(CLASS_NAME_ACTIVE); + active.removeAttribute('aria-current'); } - _getItems() { - return SelectorEngine.find(SELECTOR_ITEM, this._element); + const newActive = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement); + if (newActive) { + newActive.classList.add(CLASS_NAME_ACTIVE); + newActive.setAttribute('aria-current', 'true'); } - _clearInterval() { - if (this._interval) { - clearInterval(this._interval); - this._interval = null; - } + } + _normalizeIndex(index, length) { + if (Number.isNaN(index) || length === 0) { + return null; } - _directionToOrder(direction) { - if (index_js.isRTL()) { - return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT; - } - return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV; + if (index < 0) { + return this._wrapsAround() ? length - 1 : null; } - _orderToDirection(order) { - if (index_js.isRTL()) { - return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT; - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Carousel.getOrCreateInstance(this, config); - if (typeof config === 'number') { - data.to(config); - return; - } - if (typeof config === 'string') { - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); + if (index > length - 1) { + return this._wrapsAround() ? 0 : null; } + return index; } - /** - * Data API implementation - */ + // Whether navigating past an end wraps to the other end. `loop` continues + // seamlessly where it can (see `_canLoop`) and otherwise behaves like `wrap`. + _wrapsAround() { + return this._config.ends === ENDS_WRAP || this._config.ends === ENDS_LOOP; + } + + // Seamless looping is only supported for the simple single-slide scroll + // layout. Multi-item, peek, center, and variable-width layouts fall back to + // the plain `wrap` jump. + _canLoop() { + if (this._isFade() || this._getItems().length < 2) { + return false; + } + const styles = getComputedStyle(this._element); + const num = name => Number.parseFloat(styles.getPropertyValue(name)) || 0; + + // These are the shipped, `--bs-`-prefixed custom properties (the build + // prefixes every custom property), not the bare names used in the SCSS source. + return (num('--bs-carousel-items') || 1) === 1 && num('--bs-carousel-items-peek') === 0 && !this._element.classList.contains(CLASS_NAME_CENTER) && !this._element.classList.contains(CLASS_NAME_AUTO); + } + _direction(from, to) { + const isNext = to > from; + if (isRTL()) { + return isNext ? DIRECTION_RIGHT : DIRECTION_LEFT; + } + return isNext ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + _scheduleAutoplay(index = this._activeIndex) { + const interval = this._itemInterval(index); + // Expose the wait so the active indicator's CSS fill matches it. + this._element.style.setProperty(PROPERTY_INTERVAL, `${interval}ms`); + this._interval = setTimeout(() => { + // Capture the slide the advance lands on *before* navigating: the active + // index only updates once the scroll settles (asynchronously), so reading + // it after `nextWhenVisible()` would schedule the next wait from the slide + // we're leaving — making per-item `data-bs-interval`s lag by one slide. + const upcoming = this._upcomingIndex(); + this.nextWhenVisible(); + + // Nothing comes after the last slide when `ends: 'stop'`; stop cycling + // instead of re-arming a timer that can never advance. + if (upcoming === null) { + this.pause(); + return; + } + this._scheduleAutoplay(upcoming); + }, interval); + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + // The slide the next autoplay tick will rest on, derived from the live scroll + // position (which still reflects the current slide when the timer fires). + // Returns `null` when there's nowhere left to advance (`ends: stop` at the end). + _upcomingIndex() { + return this._normalizeIndex(this._navIndex() + 1, this._getItems().length); + } + _itemInterval(index = this._activeIndex) { + const item = this._getItems()[index]; + const interval = item ? Number.parseInt(item.getAttribute('data-bs-interval'), 10) : Number.NaN; + return Number.isNaN(interval) ? this._config.interval : interval; + } + _maybeEnableCycle() { + if (!this._playing) { return; } - event.preventDefault(); - const carousel = Carousel.getOrCreateInstance(target); - const slideIndex = this.getAttribute('data-bs-slide-to'); - if (slideIndex) { - carousel.to(slideIndex); - carousel._maybeEnableCycle(); + this.cycle(); + } + + // Turn autoplay off for good once the user interacts with the carousel + _pauseFromInteraction() { + this._playing = false; + this.pause(); + this._updatePlayPauseControl(); + } + _togglePlayPause() { + if (this._playing) { + this._pauseFromInteraction(); return; } - if (Manipulator.getDataAttribute(this, 'slide') === 'next') { - carousel.next(); - carousel._maybeEnableCycle(); + this._playing = true; + this.cycle(); + this._updatePlayPauseControl(); + } + _updatePlayPauseControl() { + if (!this._playPauseElement) { return; } - carousel.prev(); - carousel._maybeEnableCycle(); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE); - for (const carousel of carousels) { - Carousel.getOrCreateInstance(carousel); + this._playPauseElement.classList.toggle(CLASS_NAME_PAUSED, !this._playing); + const label = this._playPauseElement.getAttribute(this._playing ? 'data-bs-pause-label' : 'data-bs-play-label'); + if (label) { + this._playPauseElement.setAttribute('aria-label', label); + } + } + _isFade() { + return this._element.classList.contains(CLASS_NAME_FADE); + } + _prefersReducedMotion() { + return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + } + _getItems() { + return SelectorEngine.find(SELECTOR_ITEM, this._element); + } + _clearInterval() { + if (this._interval) { + clearTimeout(this._interval); + this._interval = null; } - }); + } +} - /** - * jQuery - */ +/** + * Data API implementation + */ - index_js.defineJQueryPlugin(Carousel); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + const carousel = Carousel.getOrCreateInstance(target); - return Carousel; + // Manually cycling the carousel is an explicit interaction, so stop autoplay + carousel._pauseFromInteraction(); + const slideIndex = this.getAttribute('data-bs-slide-to'); + if (slideIndex) { + carousel.to(slideIndex); + return; + } + if (Manipulator.getDataAttribute(this, 'slide') === 'next') { + carousel.next(); + return; + } + carousel.prev(); +}); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_PLAY_PAUSE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) { + return; + } + event.preventDefault(); + Carousel.getOrCreateInstance(target)._togglePlayPause(); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + const carousels = SelectorEngine.find(SELECTOR_DATA_AUTOPLAY); + for (const carousel of carousels) { + Carousel.getOrCreateInstance(carousel); + } +}); -})); +export { Carousel as default }; diff --git a/assets/javascripts/bootstrap/chips.js b/assets/javascripts/bootstrap/chips.js new file mode 100644 index 00000000..26727f5a --- /dev/null +++ b/assets/javascripts/bootstrap/chips.js @@ -0,0 +1,584 @@ +/*! + * Bootstrap chips.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap chips.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'chips'; +const DATA_KEY = 'bs.chips'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ADD = `add${EVENT_KEY}`; +const EVENT_REMOVE = `remove${EVENT_KEY}`; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SELECT = `select${EVENT_KEY}`; +const SELECTOR_DATA_CHIPS = '[data-bs-chips]'; +const SELECTOR_GHOST_INPUT = '.form-ghost'; +const SELECTOR_CHIP = '.chip'; +const SELECTOR_CHIP_DISMISS = '.chip-dismiss'; +const CLASS_NAME_CHIP = 'chip'; +const CLASS_NAME_CHIP_DISMISS = 'chip-dismiss'; +const CLASS_NAME_ACTIVE = 'active'; +const DEFAULT_DISMISS_ICON = ''; +const Default = { + separator: ',', + allowDuplicates: false, + maxChips: null, + placeholder: '', + dismissible: true, + dismissIcon: DEFAULT_DISMISS_ICON, + createOnBlur: true +}; +const DefaultType = { + separator: '(string|null)', + allowDuplicates: 'boolean', + maxChips: '(number|null)', + placeholder: 'string', + dismissible: 'boolean', + dismissIcon: 'string', + createOnBlur: 'boolean' +}; + +/** + * Class definition + */ + +class Chips extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_GHOST_INPUT, this._element); + this._chips = []; + this._selectedChips = new Set(); + this._anchorChip = null; // For shift+click range selection + + if (!this._input) { + this._createInput(); + } + this._initializeExistingChips(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + add(value) { + const trimmedValue = String(value).trim(); + if (!trimmedValue) { + return null; + } + + // Check for duplicates + if (!this._config.allowDuplicates && this._chips.includes(trimmedValue)) { + return null; + } + + // Check max chips limit + if (this._config.maxChips !== null && this._chips.length >= this._config.maxChips) { + return null; + } + const addEvent = EventHandler.trigger(this._element, EVENT_ADD, { + value: trimmedValue, + relatedTarget: this._input + }); + if (addEvent.defaultPrevented) { + return null; + } + const chip = this._createChip(trimmedValue); + this._element.insertBefore(chip, this._input); + this._chips.push(trimmedValue); + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: this.getValues() + }); + return chip; + } + remove(chipOrValue) { + let chip; + let value; + if (typeof chipOrValue === 'string') { + value = chipOrValue; + chip = this._findChipByValue(value); + } else { + chip = chipOrValue; + value = this._getChipValue(chip); + } + if (!chip || !value) { + return false; + } + const removeEvent = EventHandler.trigger(this._element, EVENT_REMOVE, { + value, + chip, + relatedTarget: this._input + }); + if (removeEvent.defaultPrevented) { + return false; + } + + // Remove from selection + this._selectedChips.delete(chip); + if (this._anchorChip === chip) { + this._anchorChip = null; + } + + // Remove from DOM and array + chip.remove(); + this._chips = this._chips.filter(v => v !== value); + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: this.getValues() + }); + return true; + } + removeSelected() { + const chipsToRemove = [...this._selectedChips]; + for (const chip of chipsToRemove) { + this.remove(chip); + } + this._input?.focus(); + } + getValues() { + return [...this._chips]; + } + getSelectedValues() { + return [...this._selectedChips].map(chip => this._getChipValue(chip)); + } + clear() { + const chips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of chips) { + chip.remove(); + } + this._chips = []; + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_CHANGE, { + values: [] + }); + } + clearSelection() { + for (const chip of this._selectedChips) { + chip.classList.remove(CLASS_NAME_ACTIVE); + } + this._selectedChips.clear(); + this._anchorChip = null; + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: [] + }); + } + selectChip(chip, options = {}) { + const { + addToSelection = false, + rangeSelect = false + } = options; + const chipElements = this._getChipElements(); + if (!chipElements.includes(chip)) { + return; + } + if (rangeSelect && this._anchorChip) { + // Range selection from anchor to chip + const anchorIndex = chipElements.indexOf(this._anchorChip); + const chipIndex = chipElements.indexOf(chip); + const start = Math.min(anchorIndex, chipIndex); + const end = Math.max(anchorIndex, chipIndex); + if (!addToSelection) { + this.clearSelection(); + } + for (let i = start; i <= end; i++) { + this._selectedChips.add(chipElements[i]); + chipElements[i].classList.add(CLASS_NAME_ACTIVE); + } + } else if (addToSelection) { + // Toggle selection + if (this._selectedChips.has(chip)) { + this._selectedChips.delete(chip); + chip.classList.remove(CLASS_NAME_ACTIVE); + } else { + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE); + this._anchorChip = chip; + } + } else { + // Single selection + this.clearSelection(); + this._selectedChips.add(chip); + chip.classList.add(CLASS_NAME_ACTIVE); + this._anchorChip = chip; + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + focus() { + this._input?.focus(); + } + + // Private + _getChipElements() { + return SelectorEngine.find(SELECTOR_CHIP, this._element); + } + _createInput() { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-ghost'; + if (this._config.placeholder) { + input.placeholder = this._config.placeholder; + } + this._element.append(input); + this._input = input; + } + _initializeExistingChips() { + const existingChips = SelectorEngine.find(SELECTOR_CHIP, this._element); + for (const chip of existingChips) { + const value = this._getChipValue(chip); + if (value) { + this._chips.push(value); + this._setupChip(chip); + } + } + } + _setupChip(chip) { + // Make chip focusable + chip.setAttribute('tabindex', '0'); + + // Add dismiss button if needed + if (this._config.dismissible && !SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, chip)) { + chip.append(this._createDismissButton()); + } + } + _createChip(value) { + const chip = document.createElement('span'); + chip.className = CLASS_NAME_CHIP; + chip.dataset.bsChipValue = value; + + // Add text node + chip.append(document.createTextNode(value)); + + // Setup chip (tabindex, dismiss button) + this._setupChip(chip); + return chip; + } + _createDismissButton() { + const button = document.createElement('button'); + button.type = 'button'; + button.className = CLASS_NAME_CHIP_DISMISS; + button.setAttribute('aria-label', 'Remove'); + button.setAttribute('tabindex', '-1'); // Not in tab order, chips handle keyboard + button.innerHTML = this._config.dismissIcon; + return button; + } + _findChipByValue(value) { + const chips = this._getChipElements(); + return chips.find(chip => this._getChipValue(chip) === value); + } + _getChipValue(chip) { + if (chip.dataset.bsChipValue) { + return chip.dataset.bsChipValue; + } + const clone = chip.cloneNode(true); + const dismiss = SelectorEngine.findOne(SELECTOR_CHIP_DISMISS, clone); + if (dismiss) { + dismiss.remove(); + } + return clone.textContent?.trim() || ''; + } + _addEventListeners() { + // Input events + EventHandler.on(this._input, 'keydown', event => this._handleInputKeydown(event)); + EventHandler.on(this._input, 'input', event => this._handleInput(event)); + EventHandler.on(this._input, 'paste', event => this._handlePaste(event)); + EventHandler.on(this._input, 'focus', () => this.clearSelection()); + if (this._config.createOnBlur) { + EventHandler.on(this._input, 'blur', event => { + // Don't create chip if clicking on a chip + if (!event.relatedTarget?.closest(SELECTOR_CHIP)) { + this._createChipFromInput(); + } + }); + } + + // Chip click events (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP, event => { + // Ignore clicks on dismiss button + if (event.target.closest(SELECTOR_CHIP_DISMISS)) { + return; + } + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + event.preventDefault(); + this.selectChip(chip, { + addToSelection: event.metaKey || event.ctrlKey, + rangeSelect: event.shiftKey + }); + chip.focus(); + } + }); + + // Dismiss button clicks (delegated) + EventHandler.on(this._element, 'click', SELECTOR_CHIP_DISMISS, event => { + event.stopPropagation(); + const chip = event.target.closest(SELECTOR_CHIP); + if (chip) { + this.remove(chip); + this._input?.focus(); + } + }); + + // Chip keyboard events (delegated) + EventHandler.on(this._element, 'keydown', SELECTOR_CHIP, event => { + this._handleChipKeydown(event); + }); + + // Focus input when clicking container background + EventHandler.on(this._element, 'click', event => { + if (event.target === this._element) { + this.clearSelection(); + this._input?.focus(); + } + }); + } + _handleInputKeydown(event) { + const { + key + } = event; + switch (key) { + case 'Enter': + { + event.preventDefault(); + this._createChipFromInput(); + break; + } + case 'Backspace': + case 'Delete': + { + if (this._input.value === '') { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + // Select last chip and focus it + const lastChip = chips.at(-1); + this.selectChip(lastChip); + lastChip.focus(); + } + } + break; + } + case 'ArrowLeft': + { + if (this._input.selectionStart === 0 && this._input.selectionEnd === 0) { + event.preventDefault(); + const chips = this._getChipElements(); + if (chips.length > 0) { + const lastChip = chips.at(-1); + if (event.shiftKey) { + this.selectChip(lastChip, { + addToSelection: true + }); + } else { + this.selectChip(lastChip); + } + lastChip.focus(); + } + } + break; + } + case 'Escape': + { + this._input.value = ''; + this.clearSelection(); + this._input.blur(); + break; + } + + // No default + } + } + _handleChipKeydown(event) { + const { + key + } = event; + const chip = event.target.closest(SELECTOR_CHIP); + if (!chip) { + return; + } + const chips = this._getChipElements(); + const currentIndex = chips.indexOf(chip); + switch (key) { + case 'Backspace': + case 'Delete': + { + event.preventDefault(); + this._handleChipDelete(currentIndex, chips); + break; + } + case 'ArrowLeft': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, -1, event.shiftKey); + break; + } + case 'ArrowRight': + { + event.preventDefault(); + this._navigateChip(chips, currentIndex, 1, event.shiftKey); + break; + } + case 'Home': + { + event.preventDefault(); + this._navigateToEdge(chips, 0, event.shiftKey); + break; + } + case 'End': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + case 'a': + { + this._handleSelectAll(event, chips); + break; + } + case 'Escape': + { + event.preventDefault(); + this.clearSelection(); + this._input?.focus(); + break; + } + + // No default + } + } + _handleChipDelete(currentIndex, chips) { + if (this._selectedChips.size === 0) { + return; + } + const nextIndex = Math.min(currentIndex, chips.length - this._selectedChips.size - 1); + this.removeSelected(); + const remainingChips = this._getChipElements(); + if (remainingChips.length > 0) { + const focusIndex = Math.max(0, Math.min(nextIndex, remainingChips.length - 1)); + remainingChips[focusIndex].focus(); + this.selectChip(remainingChips[focusIndex]); + } else { + this._input?.focus(); + } + } + _navigateChip(chips, currentIndex, direction, shiftKey) { + const targetIndex = currentIndex + direction; + if (direction < 0 && targetIndex >= 0) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0 && targetIndex < chips.length) { + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + addToSelection: true, + rangeSelect: true + } : {}); + targetChip.focus(); + } else if (direction > 0) { + this.clearSelection(); + this._input?.focus(); + } + } + _navigateToEdge(chips, targetIndex, shiftKey) { + if (chips.length === 0) { + return; + } + const targetChip = chips[targetIndex]; + this.selectChip(targetChip, shiftKey ? { + rangeSelect: true + } : {}); + targetChip.focus(); + } + _handleSelectAll(event, chips) { + if (!(event.metaKey || event.ctrlKey)) { + return; + } + event.preventDefault(); + for (const c of chips) { + this._selectedChips.add(c); + c.classList.add(CLASS_NAME_ACTIVE); + } + EventHandler.trigger(this._element, EVENT_SELECT, { + selected: this.getSelectedValues() + }); + } + _handleInput(event) { + const { + value + } = event.target; + const { + separator + } = this._config; + if (separator && value.includes(separator)) { + const parts = value.split(separator); + for (const part of parts.slice(0, -1)) { + this.add(part.trim()); + } + this._input.value = parts.at(-1); + } + } + _handlePaste(event) { + const { + separator + } = this._config; + if (!separator) { + return; + } + const pastedData = (event.clipboardData || window.clipboardData).getData('text'); + if (pastedData.includes(separator)) { + event.preventDefault(); + const parts = pastedData.split(separator); + for (const part of parts) { + this.add(part.trim()); + } + } + } + _createChipFromInput() { + const value = this._input.value.trim(); + if (value) { + this.add(value); + this._input.value = ''; + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_CHIPS)) { + Chips.getOrCreateInstance(element); + } +}); + +export { Chips as default }; diff --git a/assets/javascripts/bootstrap/collapse.js b/assets/javascripts/bootstrap/collapse.js index 7465cf02..933c36c3 100644 --- a/assets/javascripts/bootstrap/collapse.js +++ b/assets/javascripts/bootstrap/collapse.js @@ -1,248 +1,222 @@ /*! - * Bootstrap collapse.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap collapse.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Collapse = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap collapse.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'collapse'; - const DATA_KEY = 'bs.collapse'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_COLLAPSE = 'collapse'; - const CLASS_NAME_COLLAPSING = 'collapsing'; - const CLASS_NAME_COLLAPSED = 'collapsed'; - const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; - const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; - const WIDTH = 'width'; - const HEIGHT = 'height'; - const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'; - const Default = { - parent: null, - toggle: true - }; - const DefaultType = { - parent: '(null|element)', - toggle: 'boolean' - }; - - /** - * Class definition - */ - - class Collapse extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isTransitioning = false; - this._triggerArray = []; - const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE); - for (const elem of toggleList) { - const selector = SelectorEngine.getSelectorFromElement(elem); - const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); - if (selector !== null && filterElement.length) { - this._triggerArray.push(elem); - } - } - this._initializeChildren(); - if (!this._config.parent) { - this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); - } - if (this._config.toggle) { - this.toggle(); - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { reflow, getElement } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap collapse.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'collapse'; +const DATA_KEY = 'bs.collapse'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_COLLAPSE = 'collapse'; +const CLASS_NAME_COLLAPSING = 'collapsing'; +const CLASS_NAME_COLLAPSED = 'collapsed'; +const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`; +const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'; +const WIDTH = 'width'; +const HEIGHT = 'height'; +const SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="collapse"]'; +const Default = { + parent: null, + toggle: true +}; +const DefaultType = { + parent: '(null|element)', + toggle: 'boolean' +}; + +/** + * Class definition + */ + +class Collapse extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._triggerArray = []; + const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE); + for (const elem of toggleList) { + const selector = SelectorEngine.getSelectorFromElement(elem); + const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element); + if (selector !== null && filterElement.length) { + this._triggerArray.push(elem); + } + } + this._initializeChildren(); + if (!this._config.parent) { + this._addAriaAndCollapsedClass(this._triggerArray, this._isShown()); + } + if (this._config.toggle) { + this.toggle(); } + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Public - toggle() { - if (this._isShown()) { - this.hide(); - } else { - this.show(); - } + // Public + toggle() { + if (this._isShown()) { + this.hide(); + } else { + this.show(); } - show() { - if (this._isTransitioning || this._isShown()) { - return; - } - let activeChildren = []; - - // find active children - if (this._config.parent) { - activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { - toggle: false - })); - } - if (activeChildren.length && activeChildren[0]._isTransitioning) { - return; - } - const startEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (startEvent.defaultPrevented) { - return; - } - for (const activeInstance of activeChildren) { - activeInstance.hide(); - } - const dimension = this._getDimension(); - this._element.classList.remove(CLASS_NAME_COLLAPSE); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.style[dimension] = 0; - this._addAriaAndCollapsedClass(this._triggerArray, true); - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); - this._element.style[dimension] = ''; - EventHandler.trigger(this._element, EVENT_SHOWN); - }; - const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); - const scrollSize = `scroll${capitalizedDimension}`; - this._queueCallback(complete, this._element, true); - this._element.style[dimension] = `${this._element[scrollSize]}px`; - } - hide() { - if (this._isTransitioning || !this._isShown()) { - return; - } - const startEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (startEvent.defaultPrevented) { - return; - } - const dimension = this._getDimension(); - this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_COLLAPSING); - this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); - for (const trigger of this._triggerArray) { - const element = SelectorEngine.getElementFromSelector(trigger); - if (element && !this._isShown(element)) { - this._addAriaAndCollapsedClass([trigger], false); - } - } - this._isTransitioning = true; - const complete = () => { - this._isTransitioning = false; - this._element.classList.remove(CLASS_NAME_COLLAPSING); - this._element.classList.add(CLASS_NAME_COLLAPSE); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.style[dimension] = ''; - this._queueCallback(complete, this._element, true); + } + show() { + if (this._isTransitioning || this._isShown()) { + return; } + let activeChildren = []; - // Private - _isShown(element = this._element) { - return element.classList.contains(CLASS_NAME_SHOW); + // find active children + if (this._config.parent) { + activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, { + toggle: false + })); } - _configAfterMerge(config) { - config.toggle = Boolean(config.toggle); // Coerce string values - config.parent = index_js.getElement(config.parent); - return config; + if (activeChildren.length && activeChildren[0]._isTransitioning) { + return; } - _getDimension() { - return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + const startEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (startEvent.defaultPrevented) { + return; } - _initializeChildren() { - if (!this._config.parent) { - return; - } - const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE); - for (const element of children) { - const selected = SelectorEngine.getElementFromSelector(element); - if (selected) { - this._addAriaAndCollapsedClass([element], this._isShown(selected)); - } - } - } - _getFirstLevelChildren(selector) { - const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); - // remove children if greater depth - return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); - } - _addAriaAndCollapsedClass(triggerArray, isOpen) { - if (!triggerArray.length) { - return; - } - for (const element of triggerArray) { - element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); - element.setAttribute('aria-expanded', isOpen); - } + for (const activeInstance of activeChildren) { + activeInstance.hide(); } + const dimension = this._getDimension(); + this._element.classList.remove(CLASS_NAME_COLLAPSE); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.style[dimension] = 0; + this._addAriaAndCollapsedClass(this._triggerArray, true); + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); + this._element.style[dimension] = ''; + EventHandler.trigger(this._element, EVENT_SHOWN); + }; + const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1); + const scrollSize = `scroll${capitalizedDimension}`; + this._queueCallback(complete, this._element, true); + this._element.style[dimension] = `${this._element[scrollSize]}px`; + } + hide() { + if (this._isTransitioning || !this._isShown()) { + return; + } + const startEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (startEvent.defaultPrevented) { + return; + } + const dimension = this._getDimension(); + this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`; + reflow(this._element); + this._element.classList.add(CLASS_NAME_COLLAPSING); + this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW); + for (const trigger of this._triggerArray) { + const element = SelectorEngine.getElementFromSelector(trigger); + if (element && !this._isShown(element)) { + this._addAriaAndCollapsedClass([trigger], false); + } + } + this._isTransitioning = true; + const complete = () => { + this._isTransitioning = false; + this._element.classList.remove(CLASS_NAME_COLLAPSING); + this._element.classList.add(CLASS_NAME_COLLAPSE); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.style[dimension] = ''; + this._queueCallback(complete, this._element, true); + } - // Static - static jQueryInterface(config) { - const _config = {}; - if (typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false; + // Private + _isShown(element = this._element) { + return element.classList.contains(CLASS_NAME_SHOW); + } + _configAfterMerge(config) { + config.toggle = Boolean(config.toggle); // Coerce string values + config.parent = getElement(config.parent); + return config; + } + _getDimension() { + return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT; + } + _initializeChildren() { + if (!this._config.parent) { + return; + } + const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE); + for (const element of children) { + const selected = SelectorEngine.getElementFromSelector(element); + if (selected) { + this._addAriaAndCollapsedClass([element], this._isShown(selected)); } - return this.each(function () { - const data = Collapse.getOrCreateInstance(this, _config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - } - }); } } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - // preventDefault only for elements (which change the URL) not inside the collapsible element - if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { - event.preventDefault(); + _getFirstLevelChildren(selector) { + const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent); + // remove children if greater depth + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)); + } + _addAriaAndCollapsedClass(triggerArray, isOpen) { + if (!triggerArray.length) { + return; } - for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { - Collapse.getOrCreateInstance(element, { - toggle: false - }).toggle(); + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen); + element.setAttribute('aria-expanded', isOpen); } - }); - - /** - * jQuery - */ + } +} - index_js.defineJQueryPlugin(Collapse); +/** + * Data API implementation + */ - return Collapse; +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + // preventDefault only for elements (which change the URL) not inside the collapsible element + if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') { + event.preventDefault(); + } + for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) { + Collapse.getOrCreateInstance(element, { + toggle: false + }).toggle(); + } +}); -})); +export { Collapse as default }; diff --git a/assets/javascripts/bootstrap/combobox.js b/assets/javascripts/bootstrap/combobox.js new file mode 100644 index 00000000..542e30ab --- /dev/null +++ b/assets/javascripts/bootstrap/combobox.js @@ -0,0 +1,397 @@ +/*! + * Bootstrap combobox.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import Menu from './menu.js'; +import { isDisabled, isVisible, getNextActiveElement } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap combobox.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'combobox'; +const DATA_KEY = 'bs.combobox'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ESCAPE_KEY = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SELECTED = 'selected'; +const CLASS_NAME_PLACEHOLDER = 'combobox-placeholder'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="combobox"]'; +const SELECTOR_MENU = '.menu'; +const SELECTOR_MENU_ITEM = '.menu-item[data-bs-value]'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item[data-bs-value]:not(.disabled):not(:disabled)'; +const SELECTOR_VALUE = '.combobox-value'; +const SELECTOR_SEARCH_INPUT = '.combobox-search-input'; +const SELECTOR_NO_RESULTS = '.combobox-no-results'; +const Default = { + boundary: 'clippingParents', + multiple: false, + name: null, + offset: [0, 2], + placeholder: '', + placement: 'bottom-start', + search: false, + searchNormalize: false +}; +const DefaultType = { + boundary: '(string|element)', + multiple: 'boolean', + name: '(string|null)', + offset: '(array|string|function)', + placeholder: 'string', + placement: 'string', + search: 'boolean', + searchNormalize: 'boolean' +}; + +/** + * Class definition + */ + +class Combobox extends BaseComponent { + constructor(element, config) { + super(element, config); + this._toggle = this._element; + this._menu = SelectorEngine.next(this._toggle, SELECTOR_MENU)[0]; + this._valueDisplay = SelectorEngine.findOne(SELECTOR_VALUE, this._toggle); + this._searchInput = SelectorEngine.findOne(SELECTOR_SEARCH_INPUT, this._menu); + this._noResults = SelectorEngine.findOne(SELECTOR_NO_RESULTS, this._menu); + this._hiddenInput = null; + this._menuInstance = null; + this._createHiddenInput(); + this._createMenuInstance(); + this._syncInitialSelection(); + this._addEventListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._toggle) || this._isShown()) { + return; + } + const showEvent = EventHandler.trigger(this._toggle, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._menuInstance.show(); + if (this._searchInput) { + this._searchInput.value = ''; + this._filterItems(''); + requestAnimationFrame(() => this._searchInput.focus()); + } + EventHandler.trigger(this._toggle, EVENT_SHOWN); + } + hide() { + if (!this._isShown()) { + return; + } + const hideEvent = EventHandler.trigger(this._toggle, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + this._menuInstance.hide(); + EventHandler.trigger(this._toggle, EVENT_HIDDEN); + } + dispose() { + if (this._menuInstance) { + this._menuInstance.dispose(); + this._menuInstance = null; + } + if (this._hiddenInput) { + this._hiddenInput.remove(); + this._hiddenInput = null; + } + EventHandler.off(this._menu, EVENT_KEY); + EventHandler.off(this._toggle, EVENT_KEY); + super.dispose(); + } + + // Private + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW); + } + _createHiddenInput() { + const { + name + } = this._config; + if (!name) { + return; + } + this._hiddenInput = document.createElement('input'); + this._hiddenInput.type = 'hidden'; + this._hiddenInput.name = name; + this._hiddenInput.value = ''; + this._toggle.parentNode.insertBefore(this._hiddenInput, this._toggle); + } + _createMenuInstance() { + this._menuInstance = new Menu(this._toggle, { + menu: this._menu, + autoClose: this._config.multiple ? 'outside' : true, + boundary: this._config.boundary, + offset: this._config.offset, + placement: this._config.placement + }); + } + _syncInitialSelection() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length > 0) { + this._updateToggleText(); + this._updateHiddenInput(); + } else { + this._showPlaceholder(); + } + } + _addEventListeners() { + EventHandler.on(this._menu, 'click', SELECTOR_MENU_ITEM, event => { + const item = event.target.closest(SELECTOR_MENU_ITEM); + if (!item || isDisabled(item)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + this._selectItem(item); + }); + EventHandler.on(this._toggle, 'keydown', event => { + this._handleToggleKeydown(event); + }); + EventHandler.on(this._menu, 'keydown', event => { + this._handleMenuKeydown(event); + }); + if (this._searchInput) { + EventHandler.on(this._searchInput, 'input', () => { + this._filterItems(this._searchInput.value); + }); + EventHandler.on(this._searchInput, 'keydown', event => { + if (event.key === ARROW_DOWN_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + items[0].focus(); + } + } + if (event.key === ESCAPE_KEY) { + this.hide(); + this._toggle.focus(); + } + }); + } + } + _selectItem(item) { + if (this._config.multiple) { + item.classList.toggle(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', item.classList.contains(CLASS_NAME_SELECTED)); + } else { + const previouslySelected = SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + for (const prev of previouslySelected) { + prev.classList.remove(CLASS_NAME_SELECTED); + prev.setAttribute('aria-selected', 'false'); + } + item.classList.add(CLASS_NAME_SELECTED); + item.setAttribute('aria-selected', 'true'); + } + this._updateToggleText(); + this._updateHiddenInput(); + const value = this._config.multiple ? this._getSelectedItems().map(el => el.dataset.bsValue) : item.dataset.bsValue; + EventHandler.trigger(this._toggle, EVENT_CHANGE, { + value, + item + }); + if (!this._config.multiple) { + this.hide(); + this._toggle.focus(); + } + } + _updateToggleText() { + const selectedItems = this._getSelectedItems(); + if (selectedItems.length === 0) { + this._showPlaceholder(); + return; + } + this._valueDisplay.classList.remove(CLASS_NAME_PLACEHOLDER); + if (this._config.multiple && selectedItems.length > 1) { + this._valueDisplay.textContent = `${selectedItems.length} selected`; + } else { + const item = selectedItems[0]; + const label = SelectorEngine.findOne('.menu-item-content > span:first-child', item); + this._valueDisplay.textContent = label ? label.textContent : item.textContent.trim(); + } + } + _showPlaceholder() { + const { + placeholder + } = this._config; + if (placeholder) { + this._valueDisplay.textContent = placeholder; + this._valueDisplay.classList.add(CLASS_NAME_PLACEHOLDER); + } + } + _updateHiddenInput() { + if (!this._hiddenInput) { + return; + } + const selectedItems = this._getSelectedItems(); + const values = selectedItems.map(el => el.dataset.bsValue); + this._hiddenInput.value = this._config.multiple ? values.join(',') : values[0] || ''; + } + _getSelectedItems() { + return SelectorEngine.find(`.${CLASS_NAME_SELECTED}`, this._menu); + } + _getVisibleItems() { + return SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(item => isVisible(item)); + } + _filterItems(query) { + const normalizedQuery = this._normalizeText(query.toLowerCase().trim()); + const items = SelectorEngine.find(SELECTOR_MENU_ITEM, this._menu); + let visibleCount = 0; + for (const item of items) { + const text = this._normalizeText(item.textContent.toLowerCase().trim()); + const matches = !normalizedQuery || text.includes(normalizedQuery); + item.style.display = matches ? '' : 'none'; + if (matches) { + visibleCount++; + } + } + if (this._noResults) { + this._noResults.classList.toggle('d-none', visibleCount > 0); + } + } + _normalizeText(text) { + if (this._config.searchNormalize) { + return text.normalize('NFD').replace(/[\u0300-\u036F]/g, ''); + } + return text; + } + _handleToggleKeydown(event) { + const { + key + } = event; + if (key === ARROW_DOWN_KEY || key === ARROW_UP_KEY) { + event.preventDefault(); + if (!this._isShown()) { + this.show(); + } + const items = this._getVisibleItems(); + if (items.length > 0) { + const target = key === ARROW_DOWN_KEY ? items[0] : items.at(-1); + target.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !this._isShown()) { + event.preventDefault(); + this.show(); + } + } + _handleMenuKeydown(event) { + const { + key, + target + } = event; + if (key === ESCAPE_KEY) { + event.preventDefault(); + event.stopPropagation(); + this.hide(); + this._toggle.focus(); + return; + } + if (key === TAB_KEY) { + this.hide(); + return; + } + const isInput = target.matches('input'); + if (key === ARROW_DOWN_KEY || key === ARROW_UP_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); + } + return; + } + if (key === HOME_KEY || key === END_KEY) { + event.preventDefault(); + const items = this._getVisibleItems(); + if (items.length > 0) { + const targetItem = key === HOME_KEY ? items[0] : items.at(-1); + targetItem.focus(); + } + return; + } + if ((key === ENTER_KEY || key === SPACE_KEY) && !isInput) { + event.preventDefault(); + const item = target.closest(SELECTOR_MENU_ITEM); + if (item && !isDisabled(item)) { + this._selectItem(item); + } + } + } + + // Static + static jQueryInterface(config) { + return this.each(function () { + const data = Combobox.getOrCreateInstance(this, config); + if (typeof config !== 'string') { + return; + } + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`); + } + data[config](); + }); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + event.preventDefault(); + Combobox.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const toggle of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) { + Combobox.getOrCreateInstance(toggle); + } +}); + +export { Combobox as default }; diff --git a/assets/javascripts/bootstrap/datepicker.js b/assets/javascripts/bootstrap/datepicker.js new file mode 100644 index 00000000..3f93c4da --- /dev/null +++ b/assets/javascripts/bootstrap/datepicker.js @@ -0,0 +1,439 @@ +/*! + * Bootstrap datepicker.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { Calendar } from 'vanilla-calendar-pro'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { isDisabled } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap datepicker.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'datepicker'; +const DATA_KEY = 'bs.datepicker'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_CHANGE = `change${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_FOCUSIN_DATA_API = `focusin${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="datepicker"]'; +const HIDE_DELAY = 100; // ms delay before hiding after selection + +const Default = { + datepickerTheme: null, + // 'light', 'dark', 'auto' - explicit theme for datepicker popover only + dateMin: null, + dateMax: null, + dateFormat: null, + // Intl.DateTimeFormat options, or function(date, locale) => string + displayElement: null, + // Element to show formatted date (defaults to element for buttons) + displayMonthsCount: 1, + // Number of months to display side-by-side + firstWeekday: 1, + // Monday + inline: false, + // Render calendar inline (no popup) + locale: 'default', + positionElement: null, + // Element to position calendar relative to (defaults to input) + selectedDates: [], + selectionMode: 'single', + // 'single', 'multiple', 'multiple-ranged' + placement: 'left', + // 'left', 'center', 'right', 'auto' + vcpOptions: {} // Pass-through for any VCP option +}; +const DefaultType = { + datepickerTheme: '(null|string)', + dateMin: '(null|string|number|object)', + dateMax: '(null|string|number|object)', + dateFormat: '(null|object|function)', + displayElement: '(null|string|element|boolean)', + displayMonthsCount: 'number', + firstWeekday: 'number', + inline: 'boolean', + locale: 'string', + positionElement: '(null|string|element)', + selectedDates: 'array', + selectionMode: 'string', + placement: 'string', + vcpOptions: 'object' +}; + +/** + * Class definition + */ + +class Datepicker extends BaseComponent { + constructor(element, config) { + super(element, config); + this._calendar = null; + this._isShown = false; + this._initCalendar(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + return this._isShown ? this.hide() : this.show(); + } + show() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || isDisabled(this._element) || this._isShown) { + return; + } + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; + } + this._calendar.show(); + this._isShown = true; + EventHandler.trigger(this._element, EVENT_SHOWN); + } + hide() { + if (this._config.inline) { + return; // Inline calendars are always visible + } + if (!this._calendar || !this._isShown) { + return; + } + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; + } + this._calendar.hide(); + this._isShown = false; + EventHandler.trigger(this._element, EVENT_HIDDEN); + } + dispose() { + if (this._themeObserver) { + this._themeObserver.disconnect(); + this._themeObserver = null; + } + if (this._calendar) { + this._calendar.destroy(); + } + this._calendar = null; + super.dispose(); + } + getSelectedDates() { + const dates = this._calendar?.context?.selectedDates; + return dates ? [...dates] : []; + } + setSelectedDates(dates) { + if (this._calendar) { + this._calendar.set({ + selectedDates: dates + }); + } + } + + // Private + _initCalendar() { + this._isInput = this._element.tagName === 'INPUT'; + this._isInline = this._config.inline; + + // For inline mode, look for a hidden input child to bind to + if (this._isInline && !this._isInput) { + this._boundInput = this._element.querySelector('input[type="hidden"], input[name]'); + } + this._positionElement = this._resolvePositionElement(); + this._displayElement = this._resolveDisplayElement(); + const calendarOptions = this._buildCalendarOptions(); + + // Create calendar on the position element (for correct popup positioning) + // but value updates still go to this._element (the input) + this._calendar = new Calendar(this._positionElement, calendarOptions); + this._calendar.init(); + + // Watch for theme changes on ancestor elements (for live theme switching) + this._setupThemeObserver(); + + // Set initial value if input has a value + if (this._isInput && this._element.value) { + this._parseInputValue(); + } + + // Populate input/display with preselected dates + this._updateDisplayWithSelectedDates(); + } + _updateDisplayWithSelectedDates() { + const { + selectedDates + } = this._config; + if (!selectedDates || selectedDates.length === 0) { + return; + } + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + _resolvePositionElement() { + let { + positionElement + } = this._config; + if (typeof positionElement === 'string') { + positionElement = document.querySelector(positionElement); + } + + // Use input's parent if in form-adorn + if (!positionElement && this._isInput && !this._isInline) { + const parent = this._element.closest('.form-adorn'); + if (parent) { + positionElement = parent; + } + } + return positionElement || this._element; + } + _resolveDisplayElement() { + const { + displayElement + } = this._config; + if (typeof displayElement === 'string') { + return document.querySelector(displayElement); + } + + // For buttons/non-inputs (not inline), look for a [data-bs-datepicker-display] child + if (displayElement === true || displayElement === null && !this._isInput && !this._isInline) { + const displayChild = this._element.querySelector('[data-bs-datepicker-display]'); + return displayChild || this._element; + } + return displayElement; + } + _getThemeAncestor() { + return this._element.closest('[data-bs-theme]'); + } + _getEffectiveTheme() { + // Priority: explicit datepickerTheme config > inherited from ancestor > none + const { + datepickerTheme + } = this._config; + if (datepickerTheme) { + return datepickerTheme; + } + const ancestor = this._getThemeAncestor(); + return ancestor?.getAttribute('data-bs-theme') || null; + } + _syncThemeAttribute(element) { + if (!element) { + return; + } + const theme = this._getEffectiveTheme(); + if (theme) { + // Copy theme to popover (needed because VCP appends to body, breaking CSS inheritance) + element.setAttribute('data-bs-theme', theme); + } else { + // No theme - remove attribute to allow natural inheritance + element.removeAttribute('data-bs-theme'); + } + } + _setupThemeObserver() { + // Watch for theme changes on ancestor elements + const ancestor = this._getThemeAncestor(); + if (!ancestor || this._config.datepickerTheme) { + // No ancestor to watch, or explicit datepickerTheme overrides + return; + } + this._themeObserver = new MutationObserver(() => { + this._syncThemeAttribute(this._calendar?.context?.mainElement); + }); + this._themeObserver.observe(ancestor, { + attributes: true, + attributeFilter: ['data-bs-theme'] + }); + } + _buildCalendarOptions() { + // Get theme for VCP - use 'system' for auto-detection if no explicit theme + const theme = this._getEffectiveTheme(); + // VCP uses 'system' for auto, Bootstrap uses 'auto' + const vcpTheme = !theme || theme === 'auto' ? 'system' : theme; + const calendarOptions = { + ...this._config.vcpOptions, + inputMode: !this._isInline, + positionToInput: this._config.placement, + firstWeekday: this._config.firstWeekday, + locale: this._config.locale, + selectionDatesMode: this._config.selectionMode, + selectedDates: this._config.selectedDates, + displayMonthsCount: this._config.displayMonthsCount, + type: this._config.displayMonthsCount > 1 ? 'multiple' : 'default', + selectedTheme: vcpTheme, + themeAttrDetect: '[data-bs-theme]', + onClickDate: (self, event) => this._handleDateClick(self, event), + onInit: self => { + this._syncThemeAttribute(self.context.mainElement); + }, + onShow: () => { + this._isShown = true; + this._syncThemeAttribute(this._calendar.context.mainElement); + }, + onHide: () => { + this._isShown = false; + } + }; + + // Navigate to the month of the first selected date + if (this._config.selectedDates.length > 0) { + const firstDate = this._parseDate(this._config.selectedDates[0]); + calendarOptions.selectedMonth = firstDate.getMonth(); + calendarOptions.selectedYear = firstDate.getFullYear(); + } + if (this._config.dateMin) { + calendarOptions.dateMin = this._config.dateMin; + } + if (this._config.dateMax) { + calendarOptions.dateMax = this._config.dateMax; + } + return calendarOptions; + } + _handleDateClick(self, event) { + const selectedDates = [...self.context.selectedDates]; + if (selectedDates.length > 0) { + const formattedDate = this._formatDateForInput(selectedDates); + if (this._isInput) { + this._element.value = formattedDate; + } + if (this._boundInput) { + this._boundInput.value = selectedDates.join(','); + } + if (this._displayElement) { + this._displayElement.textContent = formattedDate; + } + } + EventHandler.trigger(this._element, EVENT_CHANGE, { + dates: selectedDates, + event + }); + this._maybeHideAfterSelection(selectedDates); + } + _maybeHideAfterSelection(selectedDates) { + if (this._isInline) { + return; + } + const shouldHide = this._config.selectionMode === 'single' && selectedDates.length > 0 || this._config.selectionMode === 'multiple-ranged' && selectedDates.length >= 2; + if (shouldHide) { + setTimeout(() => this.hide(), HIDE_DELAY); + } + } + _parseDate(dateStr) { + const [year, month, day] = dateStr.split('-'); + return new Date(year, month - 1, day); + } + _formatDate(dateStr) { + const date = this._parseDate(dateStr); + const locale = this._config.locale === 'default' ? undefined : this._config.locale; + const { + dateFormat + } = this._config; + + // Custom function formatter + if (typeof dateFormat === 'function') { + return dateFormat(date, locale); + } + + // Intl.DateTimeFormat options object + if (dateFormat && typeof dateFormat === 'object') { + return new Intl.DateTimeFormat(locale, dateFormat).format(date); + } + + // Default: locale-aware formatting + return date.toLocaleDateString(locale); + } + _formatDateForInput(dates) { + if (dates.length === 0) { + return ''; + } + if (dates.length === 1) { + return this._formatDate(dates[0]); + } + + // For date ranges, use en-dash; for multiple dates, use comma + const separator = this._config.selectionMode === 'multiple-ranged' ? ' – ' : ', '; + return dates.map(d => this._formatDate(d)).join(separator); + } + _parseInputValue() { + // Try to parse the input value as a date + const value = this._element.value.trim(); + if (!value) { + return; + } + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const formatted = `${year}-${month}-${day}`; + this._calendar.set({ + selectedDates: [formatted] + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + // Only handle if not an input (inputs use focus) + // Skip inline datepickers (they're always visible) + if (this.tagName === 'INPUT' || this.dataset.bsInline === 'true') { + return; + } + event.preventDefault(); + Datepicker.getOrCreateInstance(this).toggle(); +}); +EventHandler.on(document, EVENT_FOCUSIN_DATA_API, SELECTOR_DATA_TOGGLE, function () { + // Handle focus for input elements + if (this.tagName !== 'INPUT') { + return; + } + Datepicker.getOrCreateInstance(this).show(); +}); + +// Auto-initialize inline datepickers on DOMContentLoaded +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of document.querySelectorAll(`${SELECTOR_DATA_TOGGLE}[data-bs-inline="true"]`)) { + Datepicker.getOrCreateInstance(element); + } +}); + +export { Datepicker as default }; diff --git a/assets/javascripts/bootstrap/dialog-base.js b/assets/javascripts/bootstrap/dialog-base.js new file mode 100644 index 00000000..9381df05 --- /dev/null +++ b/assets/javascripts/bootstrap/dialog-base.js @@ -0,0 +1,277 @@ +/*! + * Bootstrap dialog-base.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import Data from './dom/data.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog-base.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const CLASS_NAME_OPEN = 'dialog-open'; + +/** + * Class definition + * + * Shared base class for Dialog and Drawer components that use + * the native element. Provides common behavior for: + * - Show/hide/toggle lifecycle with events + * - Opening/closing via showModal()/show()/close() + * - Escape key handling (modal and non-modal) + * - Backdrop click handling + * - Static backdrop transition ("bounce") + * - Body scroll prevention + * - Transition coordination + * - Child component cleanup (tooltips, popovers, toasts) + */ + +class DialogBase extends BaseComponent { + constructor(element, config) { + super(element, config); + this._isTransitioning = false; + this._openedAsModal = false; + this._addDialogListeners(); + } + + // Getters — subclasses override NAME with their own component name. + static get NAME() { + return 'dialogbase'; + } + + // Public — shared lifecycle methods + + toggle(relatedTarget) { + return this._element.open ? this.hide() : this.show(relatedTarget); + } + show(relatedTarget) { + if (this._element.open || this._isTransitioning) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName('show'), { + relatedTarget + }); + if (showEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._onBeforeShow(); + const { + modal, + preventBodyScroll + } = this._getShowOptions(); + this._showElement({ + modal, + preventBodyScroll + }); + this._queueCallback(() => { + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('shown'), { + relatedTarget + }); + }, this._element, this._isAnimated()); + } + hide() { + if (!this._element.open || this._isTransitioning) { + return; + } + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName('hide')); + if (hideEvent.defaultPrevented) { + return; + } + this._isTransitioning = true; + this._hideElement(); + this._queueCallback(() => { + // For subclasses that defer close() until the exit transition ends + // (so the dialog stays in the top layer with its ::backdrop), close() + // happens here instead of in _hideElement(). + if (this._element.open) { + this._closeAndCleanup(); + } + this._element.classList.remove('hiding'); + this._onAfterHide(); + this._isTransitioning = false; + EventHandler.trigger(this._element, this.constructor.eventName('hidden')); + }, this._element, this._isAnimated()); + } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup(); + } + super.dispose(); + } + + // Protected — hooks for subclasses to override + + _getShowOptions() { + return { + modal: true, + preventBodyScroll: true + }; + } + _onBeforeShow() { + // No-op by default — Dialog overrides to add nonmodal class + } + _onAfterHide() { + // No-op by default — Dialog overrides to remove nonmodal class + } + _isAnimated() { + return !this._element.classList.contains(this._getInstantClassName()); + } + _getInstantClassName() { + return 'dialog-instant'; + } + _getStaticClassName() { + return 'dialog-static'; + } + _onCancel() { + // No-op by default — Dialog overrides to fire cancel event + } + + // Protected — shared mechanics + + _showElement({ + modal = true, + preventBodyScroll = true + } = {}) { + this._openedAsModal = modal; + if (modal) { + this._element.showModal(); + } else { + this._element.show(); + } + if (preventBodyScroll) { + // Lock scroll on the root element (not ) so it lands on the same + // element that carries `scrollbar-gutter: stable`. Co-locating them keeps + // the gutter reserved while the scrollbar is hidden, so the page doesn't + // shift (and the ::backdrop covers the gutter instead of leaving a strip). + document.documentElement.classList.add(CLASS_NAME_OPEN); + } + } + _hideElement() { + this._hideChildComponents(); + + // Add .hiding before close() so CSS exit transitions can play. + // Without this, the navbar's `:not([open])` transition-kill rule + // would prevent the slide-out animation. + this._element.classList.add('hiding'); + + // Subclasses can defer close() until after the exit transition by + // returning true from _shouldDeferClose(). This is needed for the + // native modal centered case: close() removes the dialog + // from the top layer immediately, which strips its auto-centering + // and the ::backdrop, breaking the exit animation. + if (!this._shouldDeferClose()) { + this._closeAndCleanup(); + } + } + + // Closes the native and tears down scroll prevention. + // Safe to call multiple times — close() is a no-op on a closed dialog. + _closeAndCleanup() { + this._element.close(); + this._openedAsModal = false; + + // Only restore scroll if no other modal dialogs are open + if (!document.querySelector('dialog[open]:modal')) { + document.documentElement.classList.remove(CLASS_NAME_OPEN); + } + } + + // Hook: return true to keep the dialog in the top layer (i.e., delay + // calling close()) until the exit transition completes. The base class + // closes synchronously; Dialog overrides this for animated modal cases. + _shouldDeferClose() { + return false; + } + _triggerBackdropTransition() { + const hidePreventedEvent = EventHandler.trigger(this._element, this.constructor.eventName('hidePrevented')); + if (hidePreventedEvent.defaultPrevented) { + return; + } + const staticClass = this._getStaticClassName(); + this._element.classList.add(staticClass); + this._queueCallback(() => { + this._element.classList.remove(staticClass); + }, this._element); + } + + // Hide any tooltips, popovers, or toasts inside the dialog before closing. + // These components append to the dialog (for top-layer rendering) and would + // otherwise persist visibly after close(). + _hideChildComponents() { + const selector = '[data-bs-toggle="tooltip"], [data-bs-toggle="popover"]'; + for (const el of SelectorEngine.find(selector, this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + + // Hide any visible toasts + for (const el of SelectorEngine.find('.toast.show', this._element)) { + const instance = Data.getAny(el); + if (instance && typeof instance.hide === 'function') { + instance.hide(); + } + } + } + + // Private + + _addDialogListeners() { + const eventKey = this.constructor.EVENT_KEY; + + // Handle native cancel event (Escape key) — only fires for modal dialogs + EventHandler.on(this._element, 'cancel', event => { + event.preventDefault(); + if (!this._config.keyboard) { + this._triggerBackdropTransition(); + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show()) + EventHandler.on(this._element, `keydown${eventKey}`, event => { + if (event.key !== 'Escape' || this._openedAsModal) { + return; + } + event.preventDefault(); + if (!this._config.keyboard) { + return; + } + this._onCancel(); + this.hide(); + }); + + // Handle backdrop clicks — only applies to modal dialogs + EventHandler.on(this._element, `click${eventKey}`, event => { + if (event.target !== this._element || !this._openedAsModal) { + return; + } + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition(); + return; + } + this.hide(); + }); + } +} + +export { DialogBase as default }; diff --git a/assets/javascripts/bootstrap/dialog.js b/assets/javascripts/bootstrap/dialog.js new file mode 100644 index 00000000..1c5cccd1 --- /dev/null +++ b/assets/javascripts/bootstrap/dialog.js @@ -0,0 +1,168 @@ +/*! + * Bootstrap dialog.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import DialogBase from './dialog-base.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { isVisible } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap dialog.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'dialog'; +const DATA_KEY = 'bs.dialog'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_CANCEL = `cancel${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_NONMODAL = 'dialog-nonmodal'; +const CLASS_NAME_INSTANT = 'dialog-instant'; +const CLASS_NAME_SWAP_IN = 'dialog-swap-in'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dialog"]'; +const Default = { + backdrop: true, + keyboard: true, + modal: true +}; +const DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + modal: 'boolean' +}; + +/** + * Class definition + */ + +class Dialog extends DialogBase { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + handleUpdate() { + // Provided for API consistency with Modal. + } + + // Protected — hook overrides + + _getShowOptions() { + return { + modal: this._config.modal, + preventBodyScroll: this._config.modal + }; + } + _onBeforeShow() { + if (!this._config.modal) { + this._element.classList.add(CLASS_NAME_NONMODAL); + } + } + _onAfterHide() { + this._element.classList.remove(CLASS_NAME_NONMODAL); + } + + // Keep the dialog in the top layer until the exit transition ends. This + // preserves the browser's modal centering and the native ::backdrop, both + // of which disappear synchronously the moment close() is called. Without + // this, the dialog would jump to the top of the page and the backdrop + // blur would vanish instantly while the dialog faded — making the exit + // animation appear to skip entirely. + _shouldDeferClose() { + return this._isAnimated(); + } + _onCancel() { + EventHandler.trigger(this._element, EVENT_CANCEL); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + EventHandler.one(target, EVENT_SHOW, showEvent => { + if (showEvent.defaultPrevented) { + return; + } + EventHandler.one(target, EVENT_HIDDEN, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + }); + + // Get config from trigger's data attributes + const config = Manipulator.getDataAttributes(this); + + // Check if trigger is inside an open dialog (dialog swapping) + const currentDialog = this.closest('dialog[open]'); + const shouldSwap = currentDialog && currentDialog !== target; + if (shouldSwap) { + // Swap strategy (seamless backdrop, no flash): + // 1. Mark the incoming dialog with .dialog-swap-in so its ::backdrop + // skips the @starting-style fade-in and appears fully opaque on + // its very first frame in the top layer. + // 2. Open the incoming dialog (showModal). + // 3. Close the outgoing dialog synchronously — no exit transition, no + // .hiding — so its ::backdrop is removed in the same frame the + // incoming dialog's backdrop appears. Since both backdrops render + // the same color, the user sees one continuous backdrop. Two + // simultaneously-visible backdrops would composite to ~75% darker, + // and a fading-out + fading-in pair would dip to ~75% opacity — + // either would look like a flash. + // 4. Clean up the .dialog-swap-in flag once the incoming dialog + // finishes its entry transition. + const newDialog = Dialog.getOrCreateInstance(target, config); + target.classList.add(CLASS_NAME_SWAP_IN); + newDialog.show(this); + EventHandler.one(target, `shown${EVENT_KEY}`, () => { + target.classList.remove(CLASS_NAME_SWAP_IN); + }); + const currentInstance = Dialog.getInstance(currentDialog); + if (currentInstance) { + // Force synchronous close: .dialog-instant makes _isAnimated() false, + // which makes _shouldDeferClose() false, so hide() calls close() + // immediately (no deferred .hiding path). The class is removed after + // the (now-synchronous) hidden event fires. + currentDialog.classList.add(CLASS_NAME_INSTANT); + EventHandler.one(currentDialog, EVENT_HIDDEN, () => { + currentDialog.classList.remove(CLASS_NAME_INSTANT); + }); + currentInstance.hide(); + } + return; + } + const data = Dialog.getOrCreateInstance(target, config); + data.toggle(this); +}); +enableDismissTrigger(Dialog); + +export { Dialog as default }; diff --git a/assets/javascripts/bootstrap/dom/data.js b/assets/javascripts/bootstrap/dom/data.js index 3ab4bdbd..9955e319 100644 --- a/assets/javascripts/bootstrap/dom/data.js +++ b/assets/javascripts/bootstrap/dom/data.js @@ -1,62 +1,60 @@ /*! - * Bootstrap data.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap data.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Data = factory()); -})(this, (function () { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/data.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/data.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Constants + */ - /** - * Constants - */ - - const elementMap = new Map(); - const data = { - set(element, key, instance) { - if (!elementMap.has(element)) { - elementMap.set(element, new Map()); - } - const instanceMap = elementMap.get(element); - - // make it clear we only want one instance per element - // can be removed later when multiple key/instances are fine to be used - if (!instanceMap.has(key) && instanceMap.size !== 0) { - // eslint-disable-next-line no-console - console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`); - return; - } - instanceMap.set(key, instance); - }, - get(element, key) { - if (elementMap.has(element)) { - return elementMap.get(element).get(key) || null; - } - return null; - }, - remove(element, key) { - if (!elementMap.has(element)) { - return; - } - const instanceMap = elementMap.get(element); - instanceMap.delete(key); +const elementMap = new Map(); +const data = { + set(element, key, instance) { + if (!elementMap.has(element)) { + elementMap.set(element, new Map()); + } + const instanceMap = elementMap.get(element); - // free up element references if there are no instances left for an element - if (instanceMap.size === 0) { - elementMap.delete(element); - } + // make it clear we only want one instance per element + // can be removed later when multiple key/instances are fine to be used + if (!instanceMap.has(key) && instanceMap.size !== 0) { + // eslint-disable-next-line no-console + console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${[...instanceMap.keys()][0]}.`); + return; + } + instanceMap.set(key, instance); + }, + get(element, key) { + if (elementMap.has(element)) { + return elementMap.get(element).get(key) || null; } - }; + return null; + }, + getAny(element) { + if (elementMap.has(element)) { + return elementMap.get(element).values().next().value || null; + } + return null; + }, + remove(element, key) { + if (!elementMap.has(element)) { + return; + } + const instanceMap = elementMap.get(element); + instanceMap.delete(key); - return data; + // free up element references if there are no instances left for an element + if (instanceMap.size === 0) { + elementMap.delete(element); + } + } +}; -})); +export { data as default }; diff --git a/assets/javascripts/bootstrap/dom/event-handler.js b/assets/javascripts/bootstrap/dom/event-handler.js index 8d84431b..131ce8e0 100644 --- a/assets/javascripts/bootstrap/dom/event-handler.js +++ b/assets/javascripts/bootstrap/dom/event-handler.js @@ -1,236 +1,204 @@ /*! - * Bootstrap event-handler.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap event-handler.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) : - typeof define === 'function' && define.amd ? define(['../util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.EventHandler = factory(global.Index)); -})(this, (function (index_js) { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/event-handler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/event-handler.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * Constants + */ +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; +const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll', 'scrollend']); - /** - * Constants - */ +/** + * Private methods + */ - const namespaceRegex = /[^.]*(?=\..*)\.|.*/; - const stripNameRegex = /\..*/; - const stripUidRegex = /::\d+$/; - const eventRegistry = {}; // Events storage - let uidEvent = 1; - const customEvents = { - mouseenter: 'mouseover', - mouseleave: 'mouseout' +function makeEventUid(element, uid) { + return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; +} +function getElementEvents(element) { + const uid = makeEventUid(element); + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + return eventRegistry[uid]; +} +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { + delegateTarget: element + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + return fn.apply(element, [event]); }; - const nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']); - - /** - * Private methods - */ - - function makeEventUid(element, uid) { - return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++; - } - function getElementEvents(element) { - const uid = makeEventUid(element); - element.uidEvent = uid; - eventRegistry[uid] = eventRegistry[uid] || {}; - return eventRegistry[uid]; - } - function bootstrapHandler(element, fn) { - return function handler(event) { - hydrateObj(event, { - delegateTarget: element - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, fn); +} +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + for (let { + target + } = event; target && target !== this; target = target.parentNode) { + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + hydrateObj(event, { + delegateTarget: target + }); + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + return fn.apply(target, [event]); } - return fn.apply(element, [event]); - }; + } + }; +} +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); +} +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + const callable = isDelegated ? delegationFunction : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + return [isDelegated, callable, typeEvent]; +} +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; } - function bootstrapDelegationHandler(element, selector, fn) { - return function handler(event) { - const domElements = element.querySelectorAll(selector); - for (let { - target - } = event; target && target !== this; target = target.parentNode) { - for (const domElement of domElements) { - if (domElement !== target) { - continue; - } - hydrateObj(event, { - delegateTarget: target - }); - if (handler.oneOff) { - EventHandler.off(element, event.type, selector, fn); - } - return fn.apply(target, [event]); + let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = fn => { + return function (event) { + if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { + return fn.call(this, event); } - } + }; }; + callable = wrapFunction(callable); } - function findHandler(events, callable, delegationSelector = null) { - return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector); + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + return; } - function normalizeParameters(originalTypeEvent, handler, delegationFunction) { - const isDelegated = typeof handler === 'string'; - // TODO: tooltip passes `false` instead of selector, so we need to check - const callable = isDelegated ? delegationFunction : handler || delegationFunction; - let typeEvent = getTypeEvent(originalTypeEvent); - if (!nativeEvents.has(typeEvent)) { - typeEvent = originalTypeEvent; + const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); + const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + element.addEventListener(typeEvent, fn, isDelegated); +} +function removeHandler(element, events, typeEvent, handler, delegationSelector) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + if (!fn) { + return; + } + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } - return [isDelegated, callable, typeEvent]; } - function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { +} +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.bs.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} +const EventHandler = { + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false); + }, + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true); + }, + off(element, originalTypeEvent, handler, delegationFunction) { if (typeof originalTypeEvent !== 'string' || !element) { return; } - let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - - // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position - // this prevents the handler from being dispatched the same way as mouseover or mouseout does - if (originalTypeEvent in customEvents) { - const wrapFunction = fn => { - return function (event) { - if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) { - return fn.call(this, event); - } - }; - }; - callable = wrapFunction(callable); - } + const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); + const inNamespace = typeEvent !== originalTypeEvent; const events = getElementEvents(element); - const handlers = events[typeEvent] || (events[typeEvent] = {}); - const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); - if (previousFunction) { - previousFunction.oneOff = previousFunction.oneOff && oneOff; + const storeElementEvent = events[typeEvent] || {}; + const isNamespace = originalTypeEvent.startsWith('.'); + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); return; } - const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, '')); - const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable); - fn.delegationSelector = isDelegated ? handler : null; - fn.callable = callable; - fn.oneOff = oneOff; - fn.uidEvent = uid; - handlers[uid] = fn; - element.addEventListener(typeEvent, fn, isDelegated); - } - function removeHandler(element, events, typeEvent, handler, delegationSelector) { - const fn = findHandler(events[typeEvent], handler, delegationSelector); - if (!fn) { - return; + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); + } } - element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); - delete events[typeEvent][fn.uidEvent]; - } - function removeNamespacedHandlers(element, events, typeEvent, namespace) { - const storeElementEvent = events[typeEvent] || {}; - for (const [handlerKey, event] of Object.entries(storeElementEvent)) { - if (handlerKey.includes(namespace)) { + for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); } } + }, + trigger(element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + const evt = hydrateObj(new Event(event, { + bubbles: true, + cancelable: true + }), args); + element.dispatchEvent(evt); + return evt; } - function getTypeEvent(event) { - // allow to get the native events from namespaced events ('click.bs.button' --> 'click') - event = event.replace(stripNameRegex, ''); - return customEvents[event] || event; - } - const EventHandler = { - on(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, false); - }, - one(element, event, handler, delegationFunction) { - addHandler(element, event, handler, delegationFunction, true); - }, - off(element, originalTypeEvent, handler, delegationFunction) { - if (typeof originalTypeEvent !== 'string' || !element) { - return; - } - const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); - const inNamespace = typeEvent !== originalTypeEvent; - const events = getElementEvents(element); - const storeElementEvent = events[typeEvent] || {}; - const isNamespace = originalTypeEvent.startsWith('.'); - if (typeof callable !== 'undefined') { - // Simplest case: handler is passed, remove that listener ONLY. - if (!Object.keys(storeElementEvent).length) { - return; - } - removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); - return; - } - if (isNamespace) { - for (const elementEvent of Object.keys(events)) { - removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); - } - } - for (const [keyHandlers, event] of Object.entries(storeElementEvent)) { - const handlerKey = keyHandlers.replace(stripUidRegex, ''); - if (!inNamespace || originalTypeEvent.includes(handlerKey)) { - removeHandler(element, events, typeEvent, event.callable, event.delegationSelector); +}; +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; } - } - }, - trigger(element, event, args) { - if (typeof event !== 'string' || !element) { - return null; - } - const $ = index_js.getjQuery(); - const typeEvent = getTypeEvent(event); - const inNamespace = event !== typeEvent; - let jQueryEvent = null; - let bubbles = true; - let nativeDispatch = true; - let defaultPrevented = false; - if (inNamespace && $) { - jQueryEvent = $.Event(event, args); - $(element).trigger(jQueryEvent); - bubbles = !jQueryEvent.isPropagationStopped(); - nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); - defaultPrevented = jQueryEvent.isDefaultPrevented(); - } - const evt = hydrateObj(new Event(event, { - bubbles, - cancelable: true - }), args); - if (defaultPrevented) { - evt.preventDefault(); - } - if (nativeDispatch) { - element.dispatchEvent(evt); - } - if (evt.defaultPrevented && jQueryEvent) { - jQueryEvent.preventDefault(); - } - return evt; - } - }; - function hydrateObj(obj, meta = {}) { - for (const [key, value] of Object.entries(meta)) { - try { - obj[key] = value; - } catch (_unused) { - Object.defineProperty(obj, key, { - configurable: true, - get() { - return value; - } - }); - } + }); } - return obj; } + return obj; +} - return EventHandler; - -})); +export { EventHandler as default }; diff --git a/assets/javascripts/bootstrap/dom/manipulator.js b/assets/javascripts/bootstrap/dom/manipulator.js index 4ccad124..fcc13ea9 100644 --- a/assets/javascripts/bootstrap/dom/manipulator.js +++ b/assets/javascripts/bootstrap/dom/manipulator.js @@ -1,71 +1,63 @@ /*! - * Bootstrap manipulator.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap manipulator.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Manipulator = factory()); -})(this, (function () { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/manipulator.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/manipulator.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - function normalizeData(value) { - if (value === 'true') { - return true; - } - if (value === 'false') { - return false; - } - if (value === Number(value).toString()) { - return Number(value); - } - if (value === '' || value === 'null') { - return null; - } - if (typeof value !== 'string') { - return value; - } - try { - return JSON.parse(decodeURIComponent(value)); - } catch (_unused) { - return value; - } +function normalizeData(value) { + if (value === 'true') { + return true; } - function normalizeDataKey(key) { - return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); + if (value === 'false') { + return false; } - const Manipulator = { - setDataAttribute(element, key, value) { - element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); - }, - removeDataAttribute(element, key) { - element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); - }, - getDataAttributes(element) { - if (!element) { - return {}; - } - const attributes = {}; - const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); - for (const key of bsKeys) { - let pureKey = key.replace(/^bs/, ''); - pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); - attributes[pureKey] = normalizeData(element.dataset[key]); - } - return attributes; - }, - getDataAttribute(element, key) { - return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + if (value === Number(value).toString()) { + return Number(value); + } + if (value === '' || value === 'null') { + return null; + } + if (typeof value !== 'string') { + return value; + } + try { + return JSON.parse(decodeURIComponent(value)); + } catch { + return value; + } +} +function normalizeDataKey(key) { + return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`); +} +const Manipulator = { + setDataAttribute(element, key, value) { + element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value); + }, + removeDataAttribute(element, key) { + element.removeAttribute(`data-bs-${normalizeDataKey(key)}`); + }, + getDataAttributes(element) { + if (!element) { + return {}; } - }; - - return Manipulator; + const attributes = {}; + const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig')); + for (const key of bsKeys) { + let pureKey = key.replace(/^bs/, ''); + pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1); + attributes[pureKey] = normalizeData(element.dataset[key]); + } + return attributes; + }, + getDataAttribute(element, key) { + return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`)); + } +}; -})); +export { Manipulator as default }; diff --git a/assets/javascripts/bootstrap/dom/selector-engine.js b/assets/javascripts/bootstrap/dom/selector-engine.js index fedaed54..57b229d0 100644 --- a/assets/javascripts/bootstrap/dom/selector-engine.js +++ b/assets/javascripts/bootstrap/dom/selector-engine.js @@ -1,103 +1,100 @@ /*! - * Bootstrap selector-engine.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap selector-engine.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../util/index.js')) : - typeof define === 'function' && define.amd ? define(['../util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SelectorEngine = factory(global.Index)); -})(this, (function (index_js) { 'use strict'; +import { isDisabled, isVisible, parseSelector } from '../util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap dom/selector-engine.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap dom/selector-engine.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - const getSelector = element => { - let selector = element.getAttribute('data-bs-target'); - if (!selector || selector === '#') { - let hrefAttribute = element.getAttribute('href'); +const getSelector = element => { + let selector = element.getAttribute('data-bs-target'); + if (!selector || selector === '#') { + let hrefAttribute = element.getAttribute('href'); - // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { - return null; - } + // The only valid content that could double as a selector are IDs or classes, + // so everything starting with `#` or `.`. If a "real" URL is used as the selector, + // `document.querySelector` will rightfully complain it is invalid. + // See https://github.com/twbs/bootstrap/issues/32273 + if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) { + return null; + } - // Just in case some CMS puts out a full URL with the anchor appended - if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { - hrefAttribute = `#${hrefAttribute.split('#')[1]}`; - } - selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + // Just in case some CMS puts out a full URL with the anchor appended + if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) { + hrefAttribute = `#${hrefAttribute.split('#')[1]}`; } - return selector ? selector.split(',').map(sel => index_js.parseSelector(sel)).join(',') : null; - }; - const SelectorEngine = { - find(selector, element = document.documentElement) { - return [].concat(...Element.prototype.querySelectorAll.call(element, selector)); - }, - findOne(selector, element = document.documentElement) { - return Element.prototype.querySelector.call(element, selector); - }, - children(element, selector) { - return [].concat(...element.children).filter(child => child.matches(selector)); - }, - parents(element, selector) { - const parents = []; - let ancestor = element.parentNode.closest(selector); - while (ancestor) { - parents.push(ancestor); - ancestor = ancestor.parentNode.closest(selector); - } - return parents; - }, - prev(element, selector) { - let previous = element.previousElementSibling; - while (previous) { - if (previous.matches(selector)) { - return [previous]; - } - previous = previous.previousElementSibling; - } - return []; - }, - // TODO: this is now unused; remove later along with prev() - next(element, selector) { - let next = element.nextElementSibling; - while (next) { - if (next.matches(selector)) { - return [next]; - } - next = next.nextElementSibling; + selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; + } + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; +}; +const SelectorEngine = { + find(selector, element = document.documentElement) { + return [...Element.prototype.querySelectorAll.call(element, selector)]; + }, + findOne(selector, element = document.documentElement) { + return Element.prototype.querySelector.call(element, selector); + }, + children(element, selector) { + return [...element.children].filter(child => child.matches(selector)); + }, + parents(element, selector) { + const parents = []; + let ancestor = element.parentNode.closest(selector); + while (ancestor) { + parents.push(ancestor); + ancestor = ancestor.parentNode.closest(selector); + } + return parents; + }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector); + }, + prev(element, selector) { + let previous = element.previousElementSibling; + while (previous) { + if (previous.matches(selector)) { + return [previous]; } - return []; - }, - focusableChildren(element) { - const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable="true"]'].map(selector => `${selector}:not([tabindex^="-"])`).join(','); - return this.find(focusables, element).filter(el => !index_js.isDisabled(el) && index_js.isVisible(el)); - }, - getSelectorFromElement(element) { - const selector = getSelector(element); - if (selector) { - return SelectorEngine.findOne(selector) ? selector : null; + previous = previous.previousElementSibling; + } + return []; + }, + // TODO: this is now unused; remove later along with prev() + next(element, selector) { + let next = element.nextElementSibling; + while (next) { + if (next.matches(selector)) { + return [next]; } - return null; - }, - getElementFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.findOne(selector) : null; - }, - getMultipleElementsFromSelector(element) { - const selector = getSelector(element); - return selector ? SelectorEngine.find(selector) : []; + next = next.nextElementSibling; } - }; - - return SelectorEngine; + return []; + }, + focusableChildren(element) { + 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)); + }, + getSelectorFromElement(element) { + const selector = getSelector(element); + if (selector) { + return SelectorEngine.findOne(selector) ? selector : null; + } + return null; + }, + getElementFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.findOne(selector) : null; + }, + getMultipleElementsFromSelector(element) { + const selector = getSelector(element); + return selector ? SelectorEngine.find(selector) : []; + } +}; -})); +export { SelectorEngine as default }; diff --git a/assets/javascripts/bootstrap/drawer.js b/assets/javascripts/bootstrap/drawer.js new file mode 100644 index 00000000..fb1f701a --- /dev/null +++ b/assets/javascripts/bootstrap/drawer.js @@ -0,0 +1,167 @@ +/*! + * Bootstrap drawer.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import DialogBase from './dialog-base.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import Swipe from './util/swipe.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { isDisabled, isVisible, isRTL } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap drawer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'drawer'; +const DATA_KEY = 'bs.drawer'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_RESIZE = `resize${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="drawer"]'; +const Default = { + backdrop: true, + keyboard: true, + scroll: false +}; +const DefaultType = { + backdrop: '(boolean|string)', + keyboard: 'boolean', + scroll: 'boolean' +}; + +/** + * Class definition + */ + +class Drawer extends DialogBase { + constructor(element, config) { + super(element, config); + this._swipeHelper = null; + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + dispose() { + if (this._swipeHelper) { + this._swipeHelper.dispose(); + } + super.dispose(); + } + + // Protected — hook overrides + + _getShowOptions() { + const useModal = Boolean(this._config.backdrop) || !this._config.scroll; + return { + modal: useModal, + preventBodyScroll: !this._config.scroll + }; + } + _onBeforeShow() { + this._initSwipe(); + } + _getInstantClassName() { + return 'drawer-instant'; + } + _getStaticClassName() { + return 'drawer-static'; + } + + // Private + + _initSwipe() { + if (this._swipeHelper || !Swipe.isSupported()) { + return; + } + + // Determine which swipe direction dismisses based on placement + const swipeConfig = {}; + const element = this._element; + if (element.classList.contains('drawer-bottom')) { + swipeConfig.downCallback = () => this.hide(); + } else if (element.classList.contains('drawer-top')) { + swipeConfig.upCallback = () => this.hide(); + } else if (element.classList.contains('drawer-end')) { + // RTL: swipe left to dismiss end drawer + if (isRTL()) { + swipeConfig.leftCallback = () => this.hide(); + } else { + swipeConfig.rightCallback = () => this.hide(); + } + } else if (isRTL()) { + // drawer-start (default): swipe right to dismiss in RTL + swipeConfig.rightCallback = () => this.hide(); + } else { + // drawer-start (default): swipe left to dismiss in LTR + swipeConfig.leftCallback = () => this.hide(); + } + this._swipeHelper = new Swipe(element, swipeConfig); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = SelectorEngine.getElementFromSelector(this); + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + EventHandler.one(target, EVENT_HIDDEN, () => { + if (isVisible(this)) { + this.focus({ + preventScroll: true + }); + } + }); + + // Avoid conflict when clicking a toggler of a drawer, while another is open + const alreadyOpen = SelectorEngine.findOne('dialog.drawer[open]'); + if (alreadyOpen && alreadyOpen !== target) { + Drawer.getInstance(alreadyOpen).hide(); + } + const data = Drawer.getOrCreateInstance(target); + data.toggle(this); +}); +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const selector of SelectorEngine.find('dialog.drawer[open]')) { + Drawer.getOrCreateInstance(selector).show(); + } +}); +EventHandler.on(window, EVENT_RESIZE, () => { + for (const element of SelectorEngine.find('dialog[open][class*="\\:drawer"]')) { + if (getComputedStyle(element).position !== 'fixed') { + Drawer.getOrCreateInstance(element).hide(); + } + } +}); +enableDismissTrigger(Drawer); + +export { Drawer as default }; diff --git a/assets/javascripts/bootstrap/dropdown.js b/assets/javascripts/bootstrap/dropdown.js deleted file mode 100644 index 7bd5ae0c..00000000 --- a/assets/javascripts/bootstrap/dropdown.js +++ /dev/null @@ -1,401 +0,0 @@ -/*! - * Bootstrap dropdown.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dropdown = factory(global["@popperjs/core"], global.BaseComponent, global.EventHandler, global.Manipulator, global.SelectorEngine, global.Index)); -})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, SelectorEngine, index_js) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap dropdown.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'dropdown'; - const DATA_KEY = 'bs.dropdown'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ESCAPE_KEY = 'Escape'; - const TAB_KEY = 'Tab'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button - - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_DROPUP = 'dropup'; - const CLASS_NAME_DROPEND = 'dropend'; - const CLASS_NAME_DROPSTART = 'dropstart'; - const CLASS_NAME_DROPUP_CENTER = 'dropup-center'; - const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'; - const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`; - const SELECTOR_MENU = '.dropdown-menu'; - const SELECTOR_NAVBAR = '.navbar'; - const SELECTOR_NAVBAR_NAV = '.navbar-nav'; - const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'; - const PLACEMENT_TOP = index_js.isRTL() ? 'top-end' : 'top-start'; - const PLACEMENT_TOPEND = index_js.isRTL() ? 'top-start' : 'top-end'; - const PLACEMENT_BOTTOM = index_js.isRTL() ? 'bottom-end' : 'bottom-start'; - const PLACEMENT_BOTTOMEND = index_js.isRTL() ? 'bottom-start' : 'bottom-end'; - const PLACEMENT_RIGHT = index_js.isRTL() ? 'left-start' : 'right-start'; - const PLACEMENT_LEFT = index_js.isRTL() ? 'right-start' : 'left-start'; - const PLACEMENT_TOPCENTER = 'top'; - const PLACEMENT_BOTTOMCENTER = 'bottom'; - const Default = { - autoClose: true, - boundary: 'clippingParents', - display: 'dynamic', - offset: [0, 2], - popperConfig: null, - reference: 'toggle' - }; - const DefaultType = { - autoClose: '(boolean|string)', - boundary: '(string|element)', - display: 'string', - offset: '(array|string|function)', - popperConfig: '(null|object|function)', - reference: '(string|element|object)' - }; - - /** - * Class definition - */ - - class Dropdown extends BaseComponent { - constructor(element, config) { - super(element, config); - this._popper = null; - this._parent = this._element.parentNode; // dropdown wrapper - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle() { - return this._isShown() ? this.hide() : this.show(); - } - show() { - if (index_js.isDisabled(this._element) || this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget); - if (showEvent.defaultPrevented) { - return; - } - this._createPopper(); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', index_js.noop); - } - } - this._element.focus(); - this._element.setAttribute('aria-expanded', true); - this._menu.classList.add(CLASS_NAME_SHOW); - this._element.classList.add(CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget); - } - hide() { - if (index_js.isDisabled(this._element) || !this._isShown()) { - return; - } - const relatedTarget = { - relatedTarget: this._element - }; - this._completeHide(relatedTarget); - } - dispose() { - if (this._popper) { - this._popper.destroy(); - } - super.dispose(); - } - update() { - this._inNavbar = this._detectNavbar(); - if (this._popper) { - this._popper.update(); - } - } - - // Private - _completeHide(relatedTarget) { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget); - if (hideEvent.defaultPrevented) { - return; - } - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', index_js.noop); - } - } - if (this._popper) { - this._popper.destroy(); - } - this._menu.classList.remove(CLASS_NAME_SHOW); - this._element.classList.remove(CLASS_NAME_SHOW); - this._element.setAttribute('aria-expanded', 'false'); - Manipulator.removeDataAttribute(this._menu, 'popper'); - EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget); - } - _getConfig(config) { - config = super._getConfig(config); - if (typeof config.reference === 'object' && !index_js.isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { - // Popper virtual elements require a getBoundingClientRect method - throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); - } - return config; - } - _createPopper() { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)'); - } - let referenceElement = this._element; - if (this._config.reference === 'parent') { - referenceElement = this._parent; - } else if (index_js.isElement(this._config.reference)) { - referenceElement = index_js.getElement(this._config.reference); - } else if (typeof this._config.reference === 'object') { - referenceElement = this._config.reference; - } - const popperConfig = this._getPopperConfig(); - this._popper = Popper__namespace.createPopper(referenceElement, this._menu, popperConfig); - } - _isShown() { - return this._menu.classList.contains(CLASS_NAME_SHOW); - } - _getPlacement() { - const parentDropdown = this._parent; - if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { - return PLACEMENT_RIGHT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) { - return PLACEMENT_LEFT; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) { - return PLACEMENT_TOPCENTER; - } - if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) { - return PLACEMENT_BOTTOMCENTER; - } - - // We need to trim the value because custom properties can also include spaces - const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'; - if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) { - return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP; - } - return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM; - } - _detectNavbar() { - return this._element.closest(SELECTOR_NAVBAR) !== null; - } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); - } - return offset; - } - _getPopperConfig() { - const defaultBsPopperConfig = { - placement: this._getPlacement(), - modifiers: [{ - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }] - }; - - // Disable Popper if we have a static display or Dropdown is in Navbar - if (this._inNavbar || this._config.display === 'static') { - Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove - defaultBsPopperConfig.modifiers = [{ - name: 'applyStyles', - enabled: false - }]; - } - return { - ...defaultBsPopperConfig, - ...index_js.execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; - } - _selectMenuItem({ - key, - target - }) { - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => index_js.isVisible(element)); - if (!items.length) { - return; - } - - // if target isn't included in items (e.g. when expanding the dropdown) - // allow cycling to get the last item in case key equals ARROW_UP_KEY - index_js.getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Dropdown.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } - static clearMenus(event) { - if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) { - return; - } - const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN); - for (const toggle of openToggles) { - const context = Dropdown.getInstance(toggle); - if (!context || context._config.autoClose === false) { - continue; - } - 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) { - continue; - } - - // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu - if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName))) { - continue; - } - const relatedTarget = { - relatedTarget: context._element - }; - if (event.type === 'click') { - relatedTarget.clickEvent = event; - } - context._completeHide(relatedTarget); - } - } - static dataApiKeydownHandler(event) { - // If not an UP | DOWN | ESCAPE key => not a dropdown command - // If input/textarea && if key is other than ESCAPE => not a dropdown command - - const isInput = /input|textarea/i.test(event.target.tagName); - const isEscapeEvent = event.key === ESCAPE_KEY; - const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key); - if (!isUpOrDownEvent && !isEscapeEvent) { - return; - } - if (isInput && !isEscapeEvent) { - return; - } - event.preventDefault(); - - // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ - 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.delegateTarget.parentNode); - const instance = Dropdown.getOrCreateInstance(getToggleButton); - if (isUpOrDownEvent) { - event.stopPropagation(); - instance.show(); - instance._selectMenuItem(event); - return; - } - if (instance._isShown()) { - // else is escape and we check if it is shown - event.stopPropagation(); - instance.hide(); - getToggleButton.focus(); - } - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler); - EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus); - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - event.preventDefault(); - Dropdown.getOrCreateInstance(this).toggle(); - }); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Dropdown); - - return Dropdown; - -})); diff --git a/assets/javascripts/bootstrap/menu.js b/assets/javascripts/bootstrap/menu.js new file mode 100644 index 00000000..e0746aad --- /dev/null +++ b/assets/javascripts/bootstrap/menu.js @@ -0,0 +1,818 @@ +/*! + * Bootstrap menu.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isDisabled, noop, isElement, getElement, execute, isRTL, isVisible, getNextActiveElement } from './util/index.js'; +import { getResponsivePlacement, parseResponsivePlacement, createBreakpointListeners, disposeBreakpointListeners } from './util/floating-ui.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap menu.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'menu'; +const DATA_KEY = 'bs.menu'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const ESCAPE_KEY = 'Escape'; +const TAB_KEY = 'Tab'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const ENTER_KEY = 'Enter'; +const SPACE_KEY = ' '; +const RIGHT_MOUSE_BUTTON = 2; +const SUBMENU_CLOSE_DELAY = 100; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`; +const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="menu"]:not(.disabled):not(:disabled)'; +const SELECTOR_MENU = '.menu'; +const SELECTOR_SUBMENU = '.submenu'; +const SELECTOR_SUBMENU_TOGGLE = '.submenu > .menu-item'; +const SELECTOR_NAVBAR_NAV = '.navbar-nav'; +const SELECTOR_VISIBLE_ITEMS = '.menu-item:not(.disabled):not(:disabled)'; +const DEFAULT_PLACEMENT = 'bottom-start'; +const SUBMENU_PLACEMENT = 'end-start'; +const resolveLogicalPlacement = placement => { + if (isRTL()) { + return placement.replace(/^start(?=-|$)/, 'right').replace(/^end(?=-|$)/, 'left'); + } + return placement.replace(/^start(?=-|$)/, 'left').replace(/^end(?=-|$)/, 'right'); +}; +const triangleSign = (p1, p2, p3) => (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y); +const Default = { + autoClose: true, + boundary: 'clippingParents', + container: false, + display: 'dynamic', + offset: [0, 2], + floatingConfig: null, + menu: null, + placement: DEFAULT_PLACEMENT, + reference: 'toggle', + strategy: 'absolute', + submenuTrigger: 'both', + submenuDelay: SUBMENU_CLOSE_DELAY +}; +const DefaultType = { + autoClose: '(boolean|string)', + boundary: '(string|element)', + container: '(string|element|boolean)', + display: 'string', + offset: '(array|string|function)', + floatingConfig: '(null|object|function)', + menu: '(null|element)', + placement: 'string', + reference: '(string|element|object)', + strategy: 'string', + submenuTrigger: 'string', + submenuDelay: 'number' +}; + +/** + * Class definition + */ + +class Menu extends BaseComponent { + static _openInstances = new Set(); + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s menus require Floating UI (https://floating-ui.com)'); + } + super(element, config); + this._floatingCleanup = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; + this._parent = this._element.parentNode; // menu wrapper + this._openSubmenus = new Map(); + this._submenuCloseTimeouts = new Map(); + this._hoverIntentData = null; + this._menu = this._config.menu || this._findMenu(); + + // When the menu was discovered from the DOM, refine the wrapper to the closest + // ancestor that actually contains it, so the toggle doesn't have to be a direct + // sibling of `.menu` (e.g. when wrapped by web components). The wrapper still + // receives `.show` and acts as the `reference: 'parent'` positioning anchor. + if (!this._config.menu && this._menu) { + this._parent = this._findWrapper(this._menu); + } + this._isSubmenu = this._parent.classList?.contains('submenu'); + this._menuOriginalParent = this._menu?.parentNode; + this._parseResponsivePlacements(); + this._setupSubmenuListeners(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + return this._isShown() ? this.hide() : this.show(); + } + show() { + if (isDisabled(this._element) || this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget); + if (showEvent.defaultPrevented) { + return; + } + this._moveMenuToContainer(); + this._createFloating(); + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + this._element.focus({ + focusVisible: false + }); + this._element.setAttribute('aria-expanded', 'true'); + this._menu.classList.add(CLASS_NAME_SHOW); + this._element.classList.add(CLASS_NAME_SHOW); + if (this._parent) { + this._parent.classList.add(CLASS_NAME_SHOW); + } + Menu._openInstances.add(this); + EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget); + } + hide() { + if (isDisabled(this._element) || !this._isShown()) { + return; + } + const relatedTarget = { + relatedTarget: this._element + }; + this._completeHide(relatedTarget); + } + dispose() { + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._disposeMediaQueryListeners(); + this._closeAllSubmenus(); + this._clearAllSubmenuTimeouts(); + Menu._openInstances.delete(this); + super.dispose(); + } + update() { + if (this._floatingCleanup) { + this._updateFloatingPosition(); + } + } + + // Private + _findMenu() { + // Fall back to the closest ancestor that contains a menu so the toggle can be + // nested deeper than a direct sibling of `.menu`. + const wrapper = SelectorEngine.closest(this._element, `:has(${SELECTOR_MENU})`); + return SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, wrapper || this._parent); + } + _findWrapper(menu) { + let wrapper = this._element.parentNode; + while (wrapper instanceof Element && !wrapper.contains(menu)) { + wrapper = wrapper.parentNode; + } + return wrapper instanceof Element ? wrapper : this._element.parentNode; + } + _completeHide(relatedTarget) { + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget); + if (hideEvent.defaultPrevented) { + return; + } + this._closeAllSubmenus(); + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); + } + } + this._disposeFloating(); + this._restoreMenuToOriginalParent(); + this._menu.classList.remove(CLASS_NAME_SHOW); + this._element.classList.remove(CLASS_NAME_SHOW); + if (this._parent) { + this._parent.classList.remove(CLASS_NAME_SHOW); + } + this._element.setAttribute('aria-expanded', 'false'); + Manipulator.removeDataAttribute(this._menu, 'placement'); + Manipulator.removeDataAttribute(this._menu, 'display'); + Menu._openInstances.delete(this); + EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget); + } + _getConfig(config) { + config = super._getConfig(config); + if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') { + throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`); + } + return config; + } + _createFloating() { + if (this._config.display === 'static') { + Manipulator.setDataAttribute(this._menu, 'display', 'static'); + return; + } + let referenceElement = this._element; + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } + this._updateFloatingPosition(referenceElement); + this._floatingCleanup = autoUpdate(referenceElement, this._menu, () => this._updateFloatingPosition(referenceElement)); + } + async _updateFloatingPosition(referenceElement = null) { + if (!this._menu) { + return; + } + if (!referenceElement) { + if (this._config.reference === 'parent') { + referenceElement = this._parent; + } else if (isElement(this._config.reference)) { + referenceElement = getElement(this._config.reference); + } else if (typeof this._config.reference === 'object') { + referenceElement = this._config.reference; + } else { + referenceElement = this._element; + } + } + const placement = this._getPlacement(); + const middleware = this._getFloatingMiddleware(); + const floatingConfig = this._getFloatingConfig(placement, middleware); + await this._applyFloatingPosition(referenceElement, this._menu, floatingConfig.placement, floatingConfig.middleware, floatingConfig.strategy); + } + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW); + } + _getPlacement() { + const placement = this._responsivePlacements ? getResponsivePlacement(this._responsivePlacements, DEFAULT_PLACEMENT) : this._config.placement; + return resolveLogicalPlacement(placement); + } + _parseResponsivePlacements() { + this._responsivePlacements = parseResponsivePlacement(this._config.placement, DEFAULT_PLACEMENT); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { + if (this._isShown()) { + this._updateFloatingPosition(); + } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + _getOffset() { + const { + offset: offsetConfig + } = this._config; + if (typeof offsetConfig === 'string') { + return offsetConfig.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offsetConfig === 'function') { + return ({ + placement, + rects + }) => { + const result = offsetConfig({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; + }; + } + return offsetConfig; + } + _getFloatingMiddleware() { + const offsetValue = this._getOffset(); + const middleware = [offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), flip({ + fallbackPlacements: this._getFallbackPlacements() + }), shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; + return middleware; + } + _getFallbackPlacements() { + const placement = this._getPlacement(); + const fallbackMap = { + bottom: ['top', 'bottom-start', 'bottom-end', 'top-start', 'top-end'], + 'bottom-start': ['top-start', 'bottom-end', 'top-end'], + 'bottom-end': ['top-end', 'bottom-start', 'top-start'], + top: ['bottom', 'top-start', 'top-end', 'bottom-start', 'bottom-end'], + 'top-start': ['bottom-start', 'top-end', 'bottom-end'], + 'top-end': ['bottom-end', 'top-start', 'bottom-start'], + right: ['left', 'right-start', 'right-end', 'left-start', 'left-end'], + 'right-start': ['left-start', 'right-end', 'left-end', 'top-start', 'bottom-start'], + 'right-end': ['left-end', 'right-start', 'left-start', 'top-end', 'bottom-end'], + left: ['right', 'left-start', 'left-end', 'right-start', 'right-end'], + 'left-start': ['right-start', 'left-end', 'right-end', 'top-start', 'bottom-start'], + 'left-end': ['right-end', 'left-start', 'right-start', 'top-end', 'bottom-end'] + }; + return fallbackMap[placement] || ['top', 'bottom', 'right', 'left']; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware, + strategy: this._config.strategy + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + } + _getContainer() { + const { + container + } = this._config; + if (container === false) { + return null; + } + return container === true ? document.body : getElement(container); + } + _moveMenuToContainer() { + const container = this._getContainer(); + if (!container || !this._menu) { + return; + } + if (this._menu.parentNode !== container) { + container.append(this._menu); + } + } + _restoreMenuToOriginalParent() { + if (!this._menuOriginalParent || !this._menu) { + return; + } + if (this._menu.parentNode !== this._menuOriginalParent) { + this._menuOriginalParent.append(this._menu); + } + } + async _applyFloatingPosition(reference, floating, placement, middleware, strategy = 'absolute') { + if (!floating.isConnected) { + return null; + } + const { + x, + y, + placement: finalPlacement + } = await computePosition(reference, floating, { + placement, + middleware, + strategy + }); + if (!floating.isConnected) { + return null; + } + Object.assign(floating.style, { + position: strategy, + left: `${x}px`, + top: `${y}px`, + margin: '0' + }); + Manipulator.setDataAttribute(floating, 'placement', finalPlacement); + return finalPlacement; + } + + // ------------------------------------------------------------------------- + // Submenu handling + // ------------------------------------------------------------------------- + + _setupSubmenuListeners() { + if (this._config.submenuTrigger === 'hover' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'mouseenter', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerEnter(event); + }); + EventHandler.on(this._menu, 'mouseleave', SELECTOR_SUBMENU, event => { + this._onSubmenuLeave(event); + }); + EventHandler.on(this._menu, 'mousemove', event => { + this._trackMousePosition(event); + }); + } + if (this._config.submenuTrigger === 'click' || this._config.submenuTrigger === 'both') { + EventHandler.on(this._menu, 'click', SELECTOR_SUBMENU_TOGGLE, event => { + this._onSubmenuTriggerClick(event); + }); + } + } + _onSubmenuTriggerEnter(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu) { + return; + } + this._cancelSubmenuCloseTimeout(submenu); + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + _onSubmenuLeave(event) { + const submenuWrapper = event.target.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu || !this._openSubmenus.has(submenu)) { + return; + } + if (this._isMovingTowardSubmenu(event, submenu)) { + return; + } + this._scheduleSubmenuClose(submenu, submenuWrapper); + } + _onSubmenuTriggerClick(event) { + const trigger = event.target.closest(SELECTOR_SUBMENU_TOGGLE); + if (!trigger) { + return; + } + event.preventDefault(); + event.stopPropagation(); + const submenuWrapper = trigger.closest(SELECTOR_SUBMENU); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (!submenu) { + return; + } + if (this._openSubmenus.has(submenu)) { + this._closeSubmenu(submenu, submenuWrapper); + } else { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(trigger, submenu, submenuWrapper); + } + } + _openSubmenu(trigger, submenu, submenuWrapper) { + if (this._openSubmenus.has(submenu)) { + return; + } + trigger.setAttribute('aria-expanded', 'true'); + trigger.setAttribute('aria-haspopup', 'true'); + + // Keep the submenu transparent until Floating UI applies the first position, so + // it doesn't flash at its CSS fallback position (top: 0, over the parent menu) + // before being moved into place. `opacity` (unlike `visibility`/`display`) keeps + // the submenu measurable for flip/shift and focusable for keyboard navigation. + submenu.style.opacity = '0'; + submenu.classList.add(CLASS_NAME_SHOW); + submenuWrapper.classList.add(CLASS_NAME_SHOW); + const cleanup = this._createSubmenuFloating(trigger, submenu, submenuWrapper); + this._openSubmenus.set(submenu, cleanup); + EventHandler.on(submenu, 'mouseenter', () => { + this._cancelSubmenuCloseTimeout(submenu); + }); + } + _closeSubmenu(submenu, submenuWrapper) { + if (!this._openSubmenus.has(submenu)) { + return; + } + const nestedSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, submenu); + for (const nested of nestedSubmenus) { + const nestedWrapper = nested.closest(SELECTOR_SUBMENU); + this._closeSubmenu(nested, nestedWrapper); + } + const trigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, submenuWrapper); + const cleanup = this._openSubmenus.get(submenu); + if (cleanup) { + cleanup(); + } + this._openSubmenus.delete(submenu); + EventHandler.off(submenu, 'mouseenter'); + if (trigger) { + trigger.setAttribute('aria-expanded', 'false'); + } + submenu.classList.remove(CLASS_NAME_SHOW); + submenuWrapper.classList.remove(CLASS_NAME_SHOW); + + // Keep the Floating UI position styles in place while the submenu fades out. + // Clearing them here would let the submenu snap back to its CSS fallback + // (`top: 0`, over the parent menu) for the duration of the close transition, + // causing it to flash over the parent. They get recomputed on the next open + // (and the opacity gate in `_openSubmenu` hides any stale position until then). + submenu.style.opacity = ''; + } + _closeAllSubmenus() { + for (const [submenu] of this._openSubmenus) { + const submenuWrapper = submenu.closest(SELECTOR_SUBMENU); + this._closeSubmenu(submenu, submenuWrapper); + } + } + _closeSiblingSubmenus(currentSubmenuWrapper) { + const parent = currentSubmenuWrapper.parentNode; + const siblingSubmenus = SelectorEngine.find(`${SELECTOR_SUBMENU} > ${SELECTOR_MENU}.${CLASS_NAME_SHOW}`, parent); + for (const siblingMenu of siblingSubmenus) { + const siblingWrapper = siblingMenu.closest(SELECTOR_SUBMENU); + if (siblingWrapper !== currentSubmenuWrapper) { + this._closeSubmenu(siblingMenu, siblingWrapper); + } + } + } + _createSubmenuFloating(trigger, submenu, submenuWrapper) { + const referenceElement = submenuWrapper; + const placement = resolveLogicalPlacement(SUBMENU_PLACEMENT); + const middleware = [offset({ + mainAxis: 0, + crossAxis: -4 + }), flip({ + fallbackPlacements: [resolveLogicalPlacement('start-start'), resolveLogicalPlacement('end-end'), resolveLogicalPlacement('start-end')] + }), shift({ + padding: 8 + })]; + const updatePosition = () => this._applyFloatingPosition(referenceElement, submenu, placement, middleware).then(finalPlacement => { + // Reveal the submenu now that it has been positioned (see `_openSubmenu`); + // clearing the inline opacity lets the CSS fade-in transition take over. + submenu.style.opacity = ''; + return finalPlacement; + }); + updatePosition(); + return autoUpdate(referenceElement, submenu, updatePosition); + } + _scheduleSubmenuClose(submenu, submenuWrapper) { + this._cancelSubmenuCloseTimeout(submenu); + const timeoutId = setTimeout(() => { + this._closeSubmenu(submenu, submenuWrapper); + this._submenuCloseTimeouts.delete(submenu); + }, this._config.submenuDelay); + this._submenuCloseTimeouts.set(submenu, timeoutId); + } + _cancelSubmenuCloseTimeout(submenu) { + const timeoutId = this._submenuCloseTimeouts.get(submenu); + if (timeoutId) { + clearTimeout(timeoutId); + this._submenuCloseTimeouts.delete(submenu); + } + } + _clearAllSubmenuTimeouts() { + for (const timeoutId of this._submenuCloseTimeouts.values()) { + clearTimeout(timeoutId); + } + this._submenuCloseTimeouts.clear(); + } + + // ------------------------------------------------------------------------- + // Hover intent / Safe triangle + // ------------------------------------------------------------------------- + + _trackMousePosition(event) { + this._hoverIntentData = { + x: event.clientX, + y: event.clientY, + timestamp: Date.now() + }; + } + _isMovingTowardSubmenu(event, submenu) { + if (!this._hoverIntentData) { + return false; + } + const submenuRect = submenu.getBoundingClientRect(); + const currentPos = { + x: event.clientX, + y: event.clientY + }; + const lastPos = { + x: this._hoverIntentData.x, + y: this._hoverIntentData.y + }; + const isRtl = isRTL(); + const targetX = isRtl ? submenuRect.right : submenuRect.left; + const topCorner = { + x: targetX, + y: submenuRect.top + }; + const bottomCorner = { + x: targetX, + y: submenuRect.bottom + }; + return this._pointInTriangle(currentPos, lastPos, topCorner, bottomCorner); + } + _pointInTriangle(point, v1, v2, v3) { + const d1 = triangleSign(point, v1, v2); + const d2 = triangleSign(point, v2, v3); + const d3 = triangleSign(point, v3, v1); + const hasNeg = d1 < 0 || d2 < 0 || d3 < 0; + const hasPos = d1 > 0 || d2 > 0 || d3 > 0; + return !(hasNeg && hasPos); + } + + // ------------------------------------------------------------------------- + // Keyboard navigation + // ------------------------------------------------------------------------- + + _selectMenuItem({ + key, + target + }) { + const currentMenu = target.closest(SELECTOR_MENU) || this._menu; + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu).filter(element => isVisible(element)); + if (!items.length) { + return; + } + getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus(); + } + _handleSubmenuKeydown(event) { + const { + key, + target + } = event; + const isRtl = isRTL(); + const enterKey = isRtl ? ARROW_LEFT_KEY : ARROW_RIGHT_KEY; + const exitKey = isRtl ? ARROW_RIGHT_KEY : ARROW_LEFT_KEY; + const submenuWrapper = target.closest(SELECTOR_SUBMENU); + const isSubmenuTrigger = submenuWrapper && target.matches(SELECTOR_SUBMENU_TOGGLE); + if ((key === ENTER_KEY || key === SPACE_KEY) && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === enterKey && isSubmenuTrigger) { + event.preventDefault(); + event.stopPropagation(); + const submenu = SelectorEngine.findOne(SELECTOR_MENU, submenuWrapper); + if (submenu) { + this._closeSiblingSubmenus(submenuWrapper); + this._openSubmenu(target, submenu, submenuWrapper); + requestAnimationFrame(() => { + const firstItem = SelectorEngine.findOne(SELECTOR_VISIBLE_ITEMS, submenu); + if (firstItem) { + firstItem.focus(); + } + }); + } + return true; + } + if (key === exitKey) { + const currentMenu = target.closest(SELECTOR_MENU); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper) { + event.preventDefault(); + event.stopPropagation(); + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + this._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return true; + } + } + if (key === HOME_KEY || key === END_KEY) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = target.closest(SELECTOR_MENU); + const items = SelectorEngine.find(`:scope > ${SELECTOR_VISIBLE_ITEMS}`, currentMenu).filter(element => isVisible(element)); + if (items.length) { + const targetItem = key === HOME_KEY ? items[0] : items.at(-1); + targetItem.focus(); + } + return true; + } + return false; + } + static clearMenus(event) { + if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY) { + return; + } + for (const instance of Menu._openInstances) { + if (instance._config.autoClose === false) { + continue; + } + const composedPath = event.composedPath(); + const isMenuTarget = composedPath.includes(instance._menu); + if (composedPath.includes(instance._element) || instance._config.autoClose === 'inside' && !isMenuTarget || instance._config.autoClose === 'outside' && isMenuTarget) { + continue; + } + + // Don't auto-close when interacting with a form inside the menu — clicks + // on a form's labels, buttons, etc. (not just inputs) should keep it open. + const formAncestor = event.target.closest?.('form'); + const isInsideMenuForm = Boolean(formAncestor) && instance._menu.contains(formAncestor); + if (instance._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY || /input|select|option|textarea|form/i.test(event.target.tagName) || isInsideMenuForm)) { + continue; + } + const relatedTarget = { + relatedTarget: instance._element + }; + if (event.type === 'click') { + relatedTarget.clickEvent = event; + } + instance._completeHide(relatedTarget); + } + } + static dataApiKeydownHandler(event) { + // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the + // menu doesn't hijack their arrow keys. + const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable; + const isEscapeEvent = event.key === ESCAPE_KEY; + const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key); + const isLeftOrRightEvent = [ARROW_LEFT_KEY, ARROW_RIGHT_KEY].includes(event.key); + const isHomeOrEndEvent = [HOME_KEY, END_KEY].includes(event.key); + const isEnterOrSpaceEvent = [ENTER_KEY, SPACE_KEY].includes(event.key); + const isSubmenuTrigger = event.target.matches(SELECTOR_SUBMENU_TOGGLE); + if (!isUpOrDownEvent && !isEscapeEvent && !isLeftOrRightEvent && !isHomeOrEndEvent && !(isEnterOrSpaceEvent && isSubmenuTrigger)) { + return; + } + if (isInput && !isEscapeEvent) { + return; + } + 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.delegateTarget.parentNode); + if (!getToggleButton) { + return; + } + const instance = Menu.getOrCreateInstance(getToggleButton); + if ((isLeftOrRightEvent || isHomeOrEndEvent || isEnterOrSpaceEvent && isSubmenuTrigger) && instance._handleSubmenuKeydown(event)) { + return; + } + if (isUpOrDownEvent) { + event.preventDefault(); + event.stopPropagation(); + instance.show(); + instance._selectMenuItem(event); + return; + } + if (isEscapeEvent && instance._isShown()) { + event.preventDefault(); + event.stopPropagation(); + const currentMenu = event.target.closest(SELECTOR_MENU); + const parentSubmenuWrapper = currentMenu?.closest(SELECTOR_SUBMENU); + if (parentSubmenuWrapper && instance._openSubmenus.size > 0) { + const parentTrigger = SelectorEngine.findOne(SELECTOR_SUBMENU_TOGGLE, parentSubmenuWrapper); + instance._closeSubmenu(currentMenu, parentSubmenuWrapper); + if (parentTrigger) { + parentTrigger.focus(); + } + return; + } + instance.hide(); + getToggleButton.focus(); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Menu.dataApiKeydownHandler); +EventHandler.on(document, EVENT_CLICK_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_KEYUP_DATA_API, Menu.clearMenus); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + event.preventDefault(); + Menu.getOrCreateInstance(this).toggle(); +}); + +export { Menu as default }; diff --git a/assets/javascripts/bootstrap/modal.js b/assets/javascripts/bootstrap/modal.js deleted file mode 100644 index 8427cc75..00000000 --- a/assets/javascripts/bootstrap/modal.js +++ /dev/null @@ -1,319 +0,0 @@ -/*! - * Bootstrap modal.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap modal.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'modal'; - const DATA_KEY = 'bs.modal'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const ESCAPE_KEY = 'Escape'; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_RESIZE = `resize${EVENT_KEY}`; - const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`; - const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_OPEN = 'modal-open'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_STATIC = 'modal-static'; - const OPEN_SELECTOR = '.modal.show'; - const SELECTOR_DIALOG = '.modal-dialog'; - const SELECTOR_MODAL_BODY = '.modal-body'; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'; - const Default = { - backdrop: true, - focus: true, - keyboard: true - }; - const DefaultType = { - backdrop: '(boolean|string)', - focus: 'boolean', - keyboard: 'boolean' - }; - - /** - * Class definition - */ - - class Modal extends BaseComponent { - constructor(element, config) { - super(element, config); - this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element); - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._isShown = false; - this._isTransitioning = false; - this._scrollBar = new ScrollBarHelper(); - this._addEventListeners(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown || this._isTransitioning) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._isTransitioning = true; - this._scrollBar.hide(); - document.body.classList.add(CLASS_NAME_OPEN); - this._adjustDialog(); - this._backdrop.show(() => this._showElement(relatedTarget)); - } - hide() { - if (!this._isShown || this._isTransitioning) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - this._isShown = false; - this._isTransitioning = true; - this._focustrap.deactivate(); - this._element.classList.remove(CLASS_NAME_SHOW); - this._queueCallback(() => this._hideModal(), this._element, this._isAnimated()); - } - dispose() { - EventHandler.off(window, EVENT_KEY); - EventHandler.off(this._dialog, EVENT_KEY); - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } - handleUpdate() { - this._adjustDialog(); - } - - // Private - _initializeBackDrop() { - return new Backdrop({ - isVisible: Boolean(this._config.backdrop), - // 'static' option will be translated to true, and booleans will keep their value, - isAnimated: this._isAnimated() - }); - } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); - } - _showElement(relatedTarget) { - // try to append dynamic modal - if (!document.body.contains(this._element)) { - document.body.append(this._element); - } - this._element.style.display = 'block'; - this._element.removeAttribute('aria-hidden'); - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.scrollTop = 0; - const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog); - if (modalBody) { - modalBody.scrollTop = 0; - } - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW); - const transitionComplete = () => { - if (this._config.focus) { - this._focustrap.activate(); - } - this._isTransitioning = false; - EventHandler.trigger(this._element, EVENT_SHOWN, { - relatedTarget - }); - }; - this._queueCallback(transitionComplete, this._dialog, this._isAnimated()); - } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - this._triggerBackdropTransition(); - }); - EventHandler.on(window, EVENT_RESIZE, () => { - if (this._isShown && !this._isTransitioning) { - this._adjustDialog(); - } - }); - EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => { - // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks - EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => { - if (this._element !== event.target || this._element !== event2.target) { - return; - } - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition(); - return; - } - if (this._config.backdrop) { - this.hide(); - } - }); - }); - } - _hideModal() { - this._element.style.display = 'none'; - this._element.setAttribute('aria-hidden', true); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - this._isTransitioning = false; - this._backdrop.hide(() => { - document.body.classList.remove(CLASS_NAME_OPEN); - this._resetAdjustments(); - this._scrollBar.reset(); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }); - } - _isAnimated() { - return this._element.classList.contains(CLASS_NAME_FADE); - } - _triggerBackdropTransition() { - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - if (hideEvent.defaultPrevented) { - return; - } - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const initialOverflowY = this._element.style.overflowY; - // return if the following background transition hasn't yet completed - if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { - return; - } - if (!isModalOverflowing) { - this._element.style.overflowY = 'hidden'; - } - this._element.classList.add(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.classList.remove(CLASS_NAME_STATIC); - this._queueCallback(() => { - this._element.style.overflowY = initialOverflowY; - }, this._dialog); - }, this._dialog); - this._element.focus(); - } - - /** - * The following methods are used to handle overflowing modals - */ - - _adjustDialog() { - const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight; - const scrollbarWidth = this._scrollBar.getWidth(); - const isBodyOverflowing = scrollbarWidth > 0; - if (isBodyOverflowing && !isModalOverflowing) { - const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight'; - this._element.style[property] = `${scrollbarWidth}px`; - } - if (!isBodyOverflowing && isModalOverflowing) { - const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft'; - this._element.style[property] = `${scrollbarWidth}px`; - } - } - _resetAdjustments() { - this._element.style.paddingLeft = ''; - this._element.style.paddingRight = ''; - } - - // Static - static jQueryInterface(config, relatedTarget) { - return this.each(function () { - const data = Modal.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](relatedTarget); - }); - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - EventHandler.one(target, EVENT_SHOW, showEvent => { - if (showEvent.defaultPrevented) { - // only register focus restorer if modal will actually get shown - return; - } - EventHandler.one(target, EVENT_HIDDEN, () => { - if (index_js.isVisible(this)) { - this.focus(); - } - }); - }); - - // avoid conflict when clicking modal toggler while another one is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen) { - Modal.getInstance(alreadyOpen).hide(); - } - const data = Modal.getOrCreateInstance(target); - data.toggle(this); - }); - componentFunctions_js.enableDismissTrigger(Modal); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Modal); - - return Modal; - -})); diff --git a/assets/javascripts/bootstrap/nav-overflow.js b/assets/javascripts/bootstrap/nav-overflow.js new file mode 100644 index 00000000..0954cbc8 --- /dev/null +++ b/assets/javascripts/bootstrap/nav-overflow.js @@ -0,0 +1,309 @@ +/*! + * Bootstrap nav-overflow.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap nav-overflow.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'navoverflow'; +const DATA_KEY = 'bs.navoverflow'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_UPDATE = `update${EVENT_KEY}`; +const EVENT_OVERFLOW = `overflow${EVENT_KEY}`; +const CLASS_NAME_OVERFLOW = 'nav-overflow'; +const CLASS_NAME_OVERFLOW_MENU = 'nav-overflow-menu'; +const CLASS_NAME_HIDDEN = 'd-none'; +const SELECTOR_NAV_ITEM = '.nav-item'; +const SELECTOR_NAV_LINK = '.nav-link'; +const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'; +const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'; +const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'; +const CLASS_NAME_KEEP = 'nav-overflow-keep'; +const Default = { + collapseBelow: 0, + iconPlacement: 'start', + menuPlacement: 'bottom-end', + moreText: 'More', + moreIcon: '', + threshold: 0 // Minimum items to keep visible before showing overflow +}; +const DefaultType = { + collapseBelow: '(number|string)', + iconPlacement: 'string', + menuPlacement: 'string', + moreText: 'string', + moreIcon: 'string', + threshold: 'number' +}; + +/** + * Class definition + */ + +class NavOverflow extends BaseComponent { + constructor(element, config) { + super(element, config); + this._items = []; + this._overflowItems = []; + this._overflowMenu = null; + this._overflowToggle = null; + this._resizeObserver = null; + this._collapseBelow = 0; + this._isInitialized = false; + this._init(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + update() { + this._calculateOverflow(); + EventHandler.trigger(this._element, EVENT_UPDATE); + } + dispose() { + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + + // Move items back to original positions + this._restoreItems(); + + // Remove overflow menu + if (this._overflowToggle && this._overflowToggle.parentElement) { + this._overflowToggle.parentElement.remove(); + } + super.dispose(); + } + + // Private + _init() { + // Add overflow class to nav + this._element.classList.add(CLASS_NAME_OVERFLOW); + + // Get all nav items + this._items = [...SelectorEngine.find(SELECTOR_NAV_ITEM, this._element)]; + + // Store original order data + for (const [index, item] of this._items.entries()) { + item.dataset.bsNavOrder = index; + } + + // Resolve collapseBelow threshold once + this._collapseBelow = this._resolveCollapseBelow(); + + // Create overflow menu if it doesn't exist + this._createOverflowMenu(); + + // Setup resize observer + this._setupResizeObserver(); + + // Initial calculation + this._calculateOverflow(); + this._isInitialized = true; + } + _createOverflowMenu() { + // Check if overflow menu already exists + this._overflowToggle = SelectorEngine.findOne(SELECTOR_OVERFLOW_TOGGLE, this._element); + if (this._overflowToggle) { + this._overflowMenu = SelectorEngine.findOne(SELECTOR_OVERFLOW_MENU, this._element); + return; + } + const iconHtml = this._resolveIcon(); + const iconSpan = `${iconHtml}`; + const textSpan = `${this._config.moreText}`; + const toggleContent = this._config.iconPlacement === 'end' ? `${textSpan}${iconSpan}` : `${iconSpan}${textSpan}`; + const overflowItem = document.createElement('li'); + overflowItem.className = 'nav-item nav-overflow-item'; + overflowItem.innerHTML = ` + + ${toggleContent} + + + `; + this._element.append(overflowItem); + this._overflowToggle = overflowItem.querySelector(SELECTOR_OVERFLOW_TOGGLE); + this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU); + } + _resolveIcon() { + const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element); + if (!customIconElement) { + return this._config.moreIcon; + } + const iconClone = customIconElement.cloneNode(true); + iconClone.removeAttribute('data-bs-overflow-icon'); + const iconHtml = iconClone.outerHTML; + customIconElement.remove(); + return iconHtml; + } + _resolveCollapseBelow() { + const value = this._config.collapseBelow; + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value !== '') { + const cssValue = getComputedStyle(document.documentElement).getPropertyValue(`--bs-breakpoint-${value}`); + return Number.parseFloat(cssValue) || 0; + } + return 0; + } + _setupResizeObserver() { + if (typeof ResizeObserver === 'undefined') { + // Fallback for older browsers + EventHandler.on(window, 'resize', () => this._calculateOverflow()); + return; + } + this._resizeObserver = new ResizeObserver(() => { + this._calculateOverflow(); + }); + this._resizeObserver.observe(this._element); + } + _calculateOverflow() { + // First, restore all items to measure properly + this._restoreItems(); + const navWidth = this._element.offsetWidth; + const overflowItem = this._overflowToggle?.closest('.nav-item'); + + // When below the collapseBelow threshold, force all items into overflow + if (this._collapseBelow > 0 && navWidth < this._collapseBelow) { + const itemsToOverflow = this._items.filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + this._moveToOverflow(itemsToOverflow); + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + return; + } + const overflowWidth = overflowItem?.offsetWidth || 0; + + // Keep items are always visible; subtract their widths so the threshold + // reflects actual available space for non-keep items. + const keepWidth = this._items.filter(item => item.classList.contains(CLASS_NAME_KEEP)).reduce((sum, item) => sum + item.offsetWidth, 0); + let usedWidth = 0; + const itemsToOverflow = []; + const overflowThreshold = navWidth - overflowWidth - keepWidth - 10; // 10px buffer + + // Calculate which items need to overflow (skip items with keep class) + for (const item of this._items) { + // Never overflow items with the keep class + if (item.classList.contains(CLASS_NAME_KEEP)) { + continue; + } + usedWidth += item.offsetWidth; + if (usedWidth > overflowThreshold) { + itemsToOverflow.push(item); + } + } + + // Check if we need threshold minimum visible + const visibleCount = this._items.length - itemsToOverflow.length; + if (visibleCount < this._config.threshold && this._items.length > this._config.threshold) { + // Add more items to overflow until we reach threshold (but not keep items) + const toMove = this._items.slice(this._config.threshold).filter(item => !item.classList.contains(CLASS_NAME_KEEP)); + itemsToOverflow.length = 0; + itemsToOverflow.push(...toMove); + } + + // Move items to overflow menu + this._moveToOverflow(itemsToOverflow); + + // Show/hide overflow toggle + if (overflowItem) { + if (itemsToOverflow.length > 0) { + overflowItem.classList.remove(CLASS_NAME_HIDDEN); + } else { + overflowItem.classList.add(CLASS_NAME_HIDDEN); + } + } + + // Trigger overflow event if items changed + if (itemsToOverflow.length > 0) { + EventHandler.trigger(this._element, EVENT_OVERFLOW, { + overflowCount: itemsToOverflow.length, + visibleCount: this._items.length - itemsToOverflow.length + }); + } + } + _moveToOverflow(items) { + if (!this._overflowMenu) { + return; + } + + // Clear existing overflow items + this._overflowMenu.innerHTML = ''; + this._overflowItems = []; + for (const item of items) { + const link = SelectorEngine.findOne(SELECTOR_NAV_LINK, item); + if (!link) { + continue; + } + const clonedLink = link.cloneNode(true); + clonedLink.className = 'menu-item'; + if (link.classList.contains('active')) { + clonedLink.classList.add('active'); + } + if (link.classList.contains('disabled') || link.hasAttribute('disabled')) { + clonedLink.classList.add('disabled'); + } + this._overflowMenu.append(clonedLink); + + // Hide original item + item.classList.add(CLASS_NAME_HIDDEN); + item.dataset.bsNavOverflow = 'true'; + this._overflowItems.push(item); + } + } + _restoreItems() { + for (const item of this._items) { + item.classList.remove(CLASS_NAME_HIDDEN); + delete item.dataset.bsNavOverflow; + } + if (this._overflowMenu) { + this._overflowMenu.innerHTML = ''; + } + this._overflowItems = []; + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, 'DOMContentLoaded', () => { + for (const element of SelectorEngine.find('[data-bs-toggle="nav-overflow"]')) { + NavOverflow.getOrCreateInstance(element); + } +}); + +export { NavOverflow as default }; diff --git a/assets/javascripts/bootstrap/offcanvas.js b/assets/javascripts/bootstrap/offcanvas.js deleted file mode 100644 index a6aa6c74..00000000 --- a/assets/javascripts/bootstrap/offcanvas.js +++ /dev/null @@ -1,245 +0,0 @@ -/*! - * Bootstrap offcanvas.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Offcanvas = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap offcanvas.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'offcanvas'; - const DATA_KEY = 'bs.offcanvas'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const ESCAPE_KEY = 'Escape'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const CLASS_NAME_HIDING = 'hiding'; - const CLASS_NAME_BACKDROP = 'offcanvas-backdrop'; - const OPEN_SELECTOR = '.offcanvas.show'; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_RESIZE = `resize${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; - const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'; - const Default = { - backdrop: true, - keyboard: true, - scroll: false - }; - const DefaultType = { - backdrop: '(boolean|string)', - keyboard: 'boolean', - scroll: 'boolean' - }; - - /** - * Class definition - */ - - class Offcanvas extends BaseComponent { - constructor(element, config) { - super(element, config); - this._isShown = false; - this._backdrop = this._initializeBackDrop(); - this._focustrap = this._initializeFocusTrap(); - this._addEventListeners(); - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - toggle(relatedTarget) { - return this._isShown ? this.hide() : this.show(relatedTarget); - } - show(relatedTarget) { - if (this._isShown) { - return; - } - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { - relatedTarget - }); - if (showEvent.defaultPrevented) { - return; - } - this._isShown = true; - this._backdrop.show(); - if (!this._config.scroll) { - new ScrollBarHelper().hide(); - } - this._element.setAttribute('aria-modal', true); - this._element.setAttribute('role', 'dialog'); - this._element.classList.add(CLASS_NAME_SHOWING); - const completeCallBack = () => { - if (!this._config.scroll || this._config.backdrop) { - this._focustrap.activate(); - } - this._element.classList.add(CLASS_NAME_SHOW); - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN, { - relatedTarget - }); - }; - this._queueCallback(completeCallBack, this._element, true); - } - hide() { - if (!this._isShown) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - this._focustrap.deactivate(); - this._element.blur(); - this._isShown = false; - this._element.classList.add(CLASS_NAME_HIDING); - this._backdrop.hide(); - const completeCallback = () => { - this._element.classList.remove(CLASS_NAME_SHOW, CLASS_NAME_HIDING); - this._element.removeAttribute('aria-modal'); - this._element.removeAttribute('role'); - if (!this._config.scroll) { - new ScrollBarHelper().reset(); - } - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._queueCallback(completeCallback, this._element, true); - } - dispose() { - this._backdrop.dispose(); - this._focustrap.deactivate(); - super.dispose(); - } - - // Private - _initializeBackDrop() { - const clickCallback = () => { - if (this._config.backdrop === 'static') { - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - return; - } - this.hide(); - }; - - // 'static' option will be translated to true, and booleans will keep their value - const isVisible = Boolean(this._config.backdrop); - return new Backdrop({ - className: CLASS_NAME_BACKDROP, - isVisible, - isAnimated: true, - rootElement: this._element.parentNode, - clickCallback: isVisible ? clickCallback : null - }); - } - _initializeFocusTrap() { - return new FocusTrap({ - trapElement: this._element - }); - } - _addEventListeners() { - EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (event.key !== ESCAPE_KEY) { - return; - } - if (this._config.keyboard) { - this.hide(); - return; - } - EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED); - }); - } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Offcanvas.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - }); - } - } - - /** - * Data API implementation - */ - - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - const target = SelectorEngine.getElementFromSelector(this); - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - EventHandler.one(target, EVENT_HIDDEN, () => { - // focus on trigger when it is closed - if (index_js.isVisible(this)) { - this.focus(); - } - }); - - // avoid conflict when clicking a toggler of an offcanvas, while another is open - const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR); - if (alreadyOpen && alreadyOpen !== target) { - Offcanvas.getInstance(alreadyOpen).hide(); - } - const data = Offcanvas.getOrCreateInstance(target); - data.toggle(this); - }); - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { - Offcanvas.getOrCreateInstance(selector).show(); - } - }); - EventHandler.on(window, EVENT_RESIZE, () => { - for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) { - if (getComputedStyle(element).position !== 'fixed') { - Offcanvas.getOrCreateInstance(element).hide(); - } - } - }); - componentFunctions_js.enableDismissTrigger(Offcanvas); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Offcanvas); - - return Offcanvas; - -})); diff --git a/assets/javascripts/bootstrap/otp-input.js b/assets/javascripts/bootstrap/otp-input.js new file mode 100644 index 00000000..8b39f168 --- /dev/null +++ b/assets/javascripts/bootstrap/otp-input.js @@ -0,0 +1,265 @@ +/*! + * Bootstrap otp-input.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap otp-input.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'otpInput'; +const DATA_KEY = 'bs.otpInput'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_COMPLETE = `complete${EVENT_KEY}`; +const EVENT_INPUT = `input${EVENT_KEY}`; +const EVENT_DOMCONTENT_LOADED = `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`; +const SELECTOR_DATA_OTP = '[data-bs-otp]'; +const SELECTOR_INPUT = 'input'; + +// Events that should refresh the active-slot highlight as the caret moves +const SYNC_EVENTS = ['blur', 'keyup', 'click', 'select']; +const CLASS_NAME_INPUT = 'otp-input'; +const CLASS_NAME_RENDERED = 'otp-rendered'; +const CLASS_NAME_SLOTS = 'otp-slots'; +const CLASS_NAME_SLOT = 'otp-slot'; +const CLASS_NAME_SLOT_FILLED = 'otp-slot-filled'; +const CLASS_NAME_SLOT_ACTIVE = 'otp-slot-active'; +const CLASS_NAME_SEPARATOR = 'otp-separator'; +const MASK_CHARACTER = '•'; + +// Per-type input mode, validation pattern, and a filter that strips disallowed characters +const TYPES = { + numeric: { + inputmode: 'numeric', + pattern: '[0-9]*', + filter: /[^0-9]/g + }, + alphanumeric: { + inputmode: 'text', + pattern: '[A-Za-z0-9]*', + filter: /[^A-Za-z0-9]/g + }, + alpha: { + inputmode: 'text', + pattern: '[A-Za-z]*', + filter: /[^A-Za-z]/g + } +}; +const Default = { + groups: null, + length: null, + mask: false, + separator: '·', + type: 'numeric' +}; +const DefaultType = { + groups: '(array|null)', + length: '(number|null)', + mask: 'boolean', + separator: 'string', + type: 'string' +}; + +/** + * Class definition + */ + +class OtpInput extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._type = TYPES[this._config.type] || TYPES.numeric; + this._length = this._resolveLength(); + this._slots = []; + this._setupInput(); + this._renderSlots(); + this._addEventListeners(); + this._render(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + getValue() { + return this._input.value; + } + setValue(value) { + this._input.value = this._sanitize(String(value)); + this._render(); + this._checkComplete(); + } + clear() { + this._input.value = ''; + this._render(); + this._input.focus(); + } + focus() { + this._input.focus(); + // Place the caret after the last entered character + const end = this._input.value.length; + this._input.setSelectionRange(end, end); + this._render(); + } + dispose() { + EventHandler.off(this._input, 'input', this._onInput); + EventHandler.off(this._input, 'focus', this._onFocus); + for (const type of SYNC_EVENTS) { + EventHandler.off(this._input, type, this._onSync); + } + this._slotsContainer?.remove(); + this._element.classList.remove(CLASS_NAME_RENDERED); + super.dispose(); + } + + // Private + _resolveLength() { + if (this._config.length) { + return this._config.length; + } + const maxLength = Number.parseInt(this._input.getAttribute('maxlength'), 10); + return Number.isNaN(maxLength) || maxLength < 1 ? 6 : maxLength; + } + _setupInput() { + const input = this._input; + + // A single text field backs the whole control so screen readers, password + // managers, and SMS autofill treat it like any other input. + if (input.type === 'number' || input.type === 'password') { + input.type = 'text'; + } + input.classList.add(CLASS_NAME_INPUT); + input.setAttribute('maxlength', String(this._length)); + input.setAttribute('inputmode', this._type.inputmode); + input.setAttribute('pattern', this._type.pattern); + if (!input.getAttribute('autocomplete')) { + input.setAttribute('autocomplete', 'one-time-code'); + } + + // Filter any pre-filled value through the configured type + if (input.value) { + input.value = this._sanitize(input.value); + } + } + _renderSlots() { + const container = document.createElement('div'); + container.className = CLASS_NAME_SLOTS; + container.setAttribute('aria-hidden', 'true'); + const { + groups + } = this._config; + let groupIndex = 0; + let inGroup = 0; + for (let i = 0; i < this._length; i++) { + const slot = document.createElement('div'); + slot.className = CLASS_NAME_SLOT; + container.append(slot); + this._slots.push(slot); + + // Insert a visual separator between configured groups + if (Array.isArray(groups) && groups.length > 0) { + inGroup++; + if (inGroup === groups[groupIndex] && i < this._length - 1) { + const separator = document.createElement('div'); + separator.className = CLASS_NAME_SEPARATOR; + separator.textContent = this._config.separator; + container.append(separator); + groupIndex = Math.min(groupIndex + 1, groups.length - 1); + inGroup = 0; + } + } + } + this._slotsContainer = container; + this._element.append(container); + this._element.classList.add(CLASS_NAME_RENDERED); + } + _addEventListeners() { + // Listeners are attached with bare event names (not namespaced) because + // `input` is not in EventHandler's native-events list; we keep references + // so they can be removed on dispose. + this._onInput = () => this._handleInput(); + this._onFocus = () => this.focus(); + this._onSync = () => this._render(); + EventHandler.on(this._input, 'input', this._onInput); + EventHandler.on(this._input, 'focus', this._onFocus); + + // Keep the active-slot highlight in sync with the caret + for (const type of SYNC_EVENTS) { + EventHandler.on(this._input, type, this._onSync); + } + } + _handleInput() { + const sanitized = this._sanitize(this._input.value); + if (sanitized !== this._input.value) { + this._input.value = sanitized; + } + this._render(); + EventHandler.trigger(this._element, EVENT_INPUT, { + value: this._input.value + }); + this._checkComplete(); + } + _sanitize(value) { + return value.replace(this._type.filter, '').slice(0, this._length); + } + _render() { + const { + value + } = this._input; + const isFocused = document.activeElement === this._input; + // The active slot follows the caret, clamped to the last slot when the value is full + const caret = Math.min(this._input.selectionStart ?? value.length, this._length - 1); + for (const [index, slot] of this._slots.entries()) { + const char = value[index] ?? ''; + slot.textContent = char && this._config.mask ? MASK_CHARACTER : char; + slot.classList.toggle(CLASS_NAME_SLOT_FILLED, Boolean(char)); + slot.classList.toggle(CLASS_NAME_SLOT_ACTIVE, isFocused && index === caret); + } + } + _checkComplete() { + const { + value + } = this._input; + if (value.length === this._length) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { + value + }); + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOMCONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_OTP)) { + OtpInput.getOrCreateInstance(element); + } +}); + +export { OtpInput as default }; diff --git a/assets/javascripts/bootstrap/popover.js b/assets/javascripts/bootstrap/popover.js index b81306fa..2c599706 100644 --- a/assets/javascripts/bootstrap/popover.js +++ b/assets/javascripts/bootstrap/popover.js @@ -1,95 +1,101 @@ /*! - * Bootstrap popover.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap popover.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./tooltip.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./tooltip', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Popover = factory(global.Tooltip, global.Index)); -})(this, (function (Tooltip, index_js) { 'use strict'; +import Tooltip from './tooltip.js'; +import EventHandler from './dom/event-handler.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap popover.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap popover.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'popover'; - const SELECTOR_TITLE = '.popover-header'; - const SELECTOR_CONTENT = '.popover-body'; - const Default = { - ...Tooltip.Default, - content: '', - offset: [0, 8], - placement: 'right', - template: '' + '' + '' + '' + '', - trigger: 'click' - }; - const DefaultType = { - ...Tooltip.DefaultType, - content: '(null|string|element|function)' - }; +const NAME = 'popover'; +const SELECTOR_TITLE = '.popover-header'; +const SELECTOR_CONTENT = '.popover-body'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="popover"]'; +const EVENT_CLICK = 'click'; +const EVENT_FOCUSIN = 'focusin'; +const EVENT_MOUSEENTER = 'mouseenter'; +const Default = { + ...Tooltip.Default, + content: '', + offset: [0, 8], + placement: 'right', + template: '' + '' + '' + '' + '', + trigger: 'click' +}; +const DefaultType = { + ...Tooltip.DefaultType, + content: '(null|string|element|function)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class Popover extends Tooltip { - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } +class Popover extends Tooltip { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Overrides + _isWithContent() { + return Boolean(this._getTitle() || this._getContent()) || this._hasNewContent(); + } - // Overrides - _isWithContent() { - return this._getTitle() || this._getContent(); - } + // Private + _getContentForTemplate() { + return { + [SELECTOR_TITLE]: this._getTitle(), + [SELECTOR_CONTENT]: this._getContent() + }; + } + _getContent() { + return this._resolvePossibleFunction(this._config.content); + } +} - // Private - _getContentForTemplate() { - return { - [SELECTOR_TITLE]: this._getTitle(), - [SELECTOR_CONTENT]: this._getContent() - }; - } - _getContent() { - return this._resolvePossibleFunction(this._config.content); - } +/** + * Data API implementation - auto-initialize popovers + */ - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Popover.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); - } +const initPopover = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE); + if (!target) { + return; } - /** - * jQuery - */ + // Prevent default for click events to avoid navigation (e.g. ) + if (event.type === 'click') { + event.preventDefault(); + } - index_js.defineJQueryPlugin(Popover); + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (click/focus/hover), so we don't toggle or call `_enter` here — doing so + // would duplicate handlers and leave stale state on `_activeTrigger`. + Popover.getOrCreateInstance(target); +}; - return Popover; +// Auto-initialize popovers on first interaction for click, hover, and focus triggers +EventHandler.on(document, EVENT_CLICK, SELECTOR_DATA_TOGGLE, initPopover); +EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initPopover); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initPopover); -})); +export { Popover as default }; diff --git a/assets/javascripts/bootstrap/range.js b/assets/javascripts/bootstrap/range.js new file mode 100644 index 00000000..4985a2c8 --- /dev/null +++ b/assets/javascripts/bootstrap/range.js @@ -0,0 +1,213 @@ +/*! + * Bootstrap range.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap range.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'range'; +const DATA_KEY = 'bs.range'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_CHANGED = `changed${EVENT_KEY}`; +const EVENT_DOM_CONTENT_LOADED = `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`; + +// `input` is not in EventHandler's native-event list, so it can't be namespaced; bind it raw +const EVENT_INPUT = 'input'; +const EVENT_CHANGE = 'change'; +const SELECTOR_RANGE = '.form-range'; +const SELECTOR_INPUT = '.form-range-input'; +const CLASS_NAME_BUBBLE = 'form-range-bubble'; +const CLASS_NAME_TICKS = 'form-range-ticks'; +const CLASS_NAME_TICK = 'form-range-tick'; +const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'; + +// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the +// plugin must write the prefixed names to interoperate with the rendered CSS. +const PROPERTY_FILL = '--bs-range-fill'; +const Default = { + bubble: false, + // Show a value bubble above the thumb + formatter: null // (value) => string, for the bubble and tick labels +}; +const DefaultType = { + bubble: '(boolean|null)', + formatter: '(function|null)' +}; + +/** + * Class definition + */ + +class Range extends BaseComponent { + constructor(element, config) { + super(element, config); + + // BaseComponent bails (no `_element`) when the element can't be resolved + if (!this._element) { + return; + } + this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element); + if (!this._input) { + return; + } + this._bubble = null; + this._bubbleText = null; + this._ticks = null; + this._updateHandler = () => this._update(); + if (this._config.bubble) { + this._createBubble(); + } + this._createTicks(); + this._addEventListeners(); + this._update(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + update() { + this._update(); + } + dispose() { + EventHandler.off(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.off(this._input, EVENT_CHANGE, this._updateHandler); + this._bubble?.remove(); + this._ticks?.remove(); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + // A bare `data-bs-bubble` attribute normalizes to `null`; treat it as enabled + if (config.bubble === null) { + config.bubble = true; + } + return config; + } + _addEventListeners() { + EventHandler.on(this._input, EVENT_INPUT, this._updateHandler); + EventHandler.on(this._input, EVENT_CHANGE, this._updateHandler); + } + _min() { + return this._input.min === '' ? 0 : Number.parseFloat(this._input.min); + } + _max() { + return this._input.max === '' ? 100 : Number.parseFloat(this._input.max); + } + _value() { + return Number.parseFloat(this._input.value); + } + _ratio() { + const span = this._max() - this._min(); + return span > 0 ? (this._value() - this._min()) / span : 0; + } + _update() { + // The fill ratio drives the track gradient and the bubble/tick positions, all in CSS + this._element.style.setProperty(PROPERTY_FILL, `${this._ratio()}`); + if (this._bubbleText) { + this._bubbleText.textContent = this._format(this._value()); + } + EventHandler.trigger(this._input, EVENT_CHANGED, { + value: this._value() + }); + } + _format(value) { + return typeof this._config.formatter === 'function' ? this._config.formatter(value) : String(value); + } + _createBubble() { + // Reuse the tooltip markup so we don't duplicate the pill and arrow styles + this._bubble = document.createElement('output'); + this._bubble.className = `${CLASS_NAME_BUBBLE} tooltip bs-tooltip-top show`; + this._bubble.setAttribute('aria-hidden', 'true'); + + // Match the Tooltip template's block-level markup: `.tooltip-inner` has no `display` rule, + // so an inline `` would let its padding bleed outside the bubble and clip the arrow. + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + this._bubbleText = document.createElement('div'); + this._bubbleText.className = 'tooltip-inner'; + this._bubble.append(arrow, this._bubbleText); + this._input.insertAdjacentElement('afterend', this._bubble); + } + _createTicks() { + const listId = this._input.getAttribute('list'); + const datalist = listId ? document.getElementById(listId) : null; + if (!datalist) { + return; + } + const min = this._min(); + const span = this._max() - min || 1; + const points = []; + for (const option of SelectorEngine.find('option', datalist)) { + const value = Number.parseFloat(option.value); + if (!Number.isNaN(value)) { + // Clamp to [0, 1] so out-of-range options can't produce negative `fr` tracks + const ratio = Math.min(Math.max((value - min) / span, 0), 1); + points.push({ + ratio, + label: option.label + }); + } + } + if (points.length === 0) { + return; + } + points.sort((a, b) => a.ratio - b.ratio); + this._ticks = document.createElement('div'); + this._ticks.className = CLASS_NAME_TICKS; + this._ticks.setAttribute('aria-hidden', 'true'); + + // Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line + const stops = [0, ...points.map(point => point.ratio), 1]; + this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' '); + for (const [index, point] of points.entries()) { + const tick = document.createElement('span'); + tick.className = CLASS_NAME_TICK; + tick.style.gridColumnStart = `${index + 2}`; + if (point.label) { + const label = document.createElement('span'); + label.className = CLASS_NAME_TICK_LABEL; + label.textContent = point.label; + tick.append(label); + } + this._ticks.append(tick); + } + this._element.append(this._ticks); + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, EVENT_DOM_CONTENT_LOADED, () => { + for (const element of SelectorEngine.find(SELECTOR_RANGE)) { + Range.getOrCreateInstance(element); + } +}); + +export { Range as default }; diff --git a/assets/javascripts/bootstrap/scrollspy.js b/assets/javascripts/bootstrap/scrollspy.js index af342ccc..144f992d 100644 --- a/assets/javascripts/bootstrap/scrollspy.js +++ b/assets/javascripts/bootstrap/scrollspy.js @@ -1,274 +1,520 @@ /*! - * Bootstrap scrollspy.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap scrollspy.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollspy = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'scrollspy'; - const DATA_KEY = 'bs.scrollspy'; - const EVENT_KEY = `.${DATA_KEY}`; - const DATA_API_KEY = '.data-api'; - const EVENT_ACTIVATE = `activate${EVENT_KEY}`; - const EVENT_CLICK = `click${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; - const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; - const CLASS_NAME_ACTIVE = 'active'; - const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; - const SELECTOR_TARGET_LINKS = '[href]'; - const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; - const SELECTOR_NAV_LINKS = '.nav-link'; - const SELECTOR_NAV_ITEMS = '.nav-item'; - const SELECTOR_LIST_ITEMS = '.list-group-item'; - const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; - const SELECTOR_DROPDOWN = '.dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const Default = { - offset: null, - // TODO: v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: '0px 0px -25%', - smoothScroll: false, - target: null, - threshold: [0.1, 0.5, 1] - }; - const DefaultType = { - offset: '(number|null)', - // TODO v6 @deprecated, keep it for backwards compatibility reasons - rootMargin: 'string', - smoothScroll: 'boolean', - target: 'element', - threshold: 'array' - }; - - /** - * Class definition - */ - - class ScrollSpy extends BaseComponent { - constructor(element, config) { - super(element, config); - - // this._element is the observablesContainer and config.target the menu links wrapper - this._targetLinks = new Map(); - this._observableSections = new Map(); - this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; - this._activeTarget = null; - this._observer = null; - this._previousScrollData = { - visibleEntryTop: 0, - parentScrollTop: 0 - }; - this.refresh(); // initialize - } +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { getElement, isDisabled, isVisible } from './util/index.js'; - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; +/** + * -------------------------------------------------------------------------- + * Bootstrap scrollspy.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'scrollspy'; +const DATA_KEY = 'bs.scrollspy'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_ACTIVATE = `activate${EVENT_KEY}`; +const EVENT_CLICK = `click${EVENT_KEY}`; +const EVENT_SCROLL = `scroll${EVENT_KEY}`; +const EVENT_SCROLLEND = `scrollend${EVENT_KEY}`; +const EVENT_RESIZE = `resize${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; +const CLASS_NAME_MENU_ITEM = 'menu-item'; +const CLASS_NAME_ACTIVE = 'active'; +const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; +const SELECTOR_TARGET_LINKS = '[href]'; +const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; +const SELECTOR_NAV_LINKS = '.nav-link'; +const SELECTOR_NAV_ITEMS = '.nav-item'; +const SELECTOR_LIST_ITEMS = '.list-group-item'; +const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; + +// How long (ms) to wait after the last scroll event before settling a pending +// smooth-scroll navigation, when the native `scrollend` event is unavailable. +const SCROLL_IDLE_TIMEOUT = 100; +// Debounce (ms) for rebuilding the observer on resize (px activation lines only). +const RESIZE_DEBOUNCE = 100; +const Default = { + // `rootMargin` is the raw IntersectionObserver root-box override. When set it + // takes precedence over `topMargin` and is passed straight to the observer. + // Leave it null and use `topMargin` for everyday use. + rootMargin: null, + smoothScroll: false, + target: null, + threshold: [0], + // Position of the activation line, measured from the top of the scroll root. + // The active section is the deepest one whose top has scrolled to/above it. + // Accepts a percentage (`12%`) or pixels (`96px`, e.g. below a sticky navbar). + topMargin: '12%' +}; +const DefaultType = { + rootMargin: '(string|null)', + smoothScroll: 'boolean', + target: 'element', + threshold: 'array', + topMargin: 'string' +}; + +/** + * Class definition + */ + +class ScrollSpy extends BaseComponent { + constructor(element, config) { + super(element, config); + + // this._element is the observablesContainer and config.target the menu links wrapper + this._sections = []; // observable section elements, in DOM order + this._linkBySection = new Map(); // section element -> nav link + this._sectionByLink = new Map(); // nav link -> section element (for smooth scroll) + this._intersecting = new Set(); // sections currently crossing the activation line + this._activeTarget = null; + this._lastActive = null; // last activated section (keep-last across gaps) + this._atBottom = false; + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._observer = null; + this._sentinel = null; + this._sentinelObserver = null; + this._pendingNavigation = null; + this._settleTimeout = null; + this._settleHandler = null; + this._scrollIdleHandler = null; + this._resizeHandler = null; + this._resizeTimeout = null; + this.refresh(); // initialize + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + refresh() { + this._initializeTargetsAndObservables(); + this._maybeEnableSmoothScroll(); + + // (Re)build the activation observer. + this._observer?.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); } - static get NAME() { - return NAME; + + // Detect the bottom-of-page case (a short last section whose top never + // reaches the activation line) natively, via a dedicated sentinel observer. + this._setUpSentinel(); + + // A px activation line doesn't track viewport height the way `%` does, so + // rebuild the observer (debounced) on resize when px units are in play. + this._maybeAddResizeListener(); + } + dispose() { + this._observer?.disconnect(); + this._teardownSentinel(); + this._disarmSettle(); + this._removeResizeListener(); + EventHandler.off(this._config.target, EVENT_CLICK); + super.dispose(); + } + + // Private + _configAfterMerge(config) { + config.target = getElement(config.target) || document.body; + if (typeof config.threshold === 'string') { + config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); } + return config; + } + + // --- Detection (IntersectionObserver-driven) ----------------------------- - // Public - refresh() { - this._initializeTargetsAndObservables(); - this._maybeEnableSmoothScroll(); - if (this._observer) { - this._observer.disconnect(); + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: this._config.threshold, + rootMargin: this._config.rootMargin ?? this._getDerivedRootMargin() + }; + return new IntersectionObserver(entries => this._onIntersect(entries), options); + } + _onIntersect(entries) { + for (const entry of entries) { + if (entry.isIntersecting) { + this._intersecting.add(entry.target); } else { - this._observer = this._getNewObserver(); + this._intersecting.delete(entry.target); } - for (const section of this._observableSections.values()) { - this._observer.observe(section); + } + this._computeActive(); + } + + // Single source of truth for active selection, derived only from IO state — + // no per-frame layout reads. The active section is the deepest (DOM-order) + // one currently crossing the activation line; in a gap we keep the last one; + // above the first section the first stays active; at the very bottom the last + // section wins. + _computeActive() { + // Guard against observer callbacks that outlive a disposed/detached instance. + if (!this._element?.isConnected || this._sections.length === 0) { + return; + } + let active = null; + if (this._atBottom) { + active = this._sections.at(-1); + } else { + for (const section of this._sections) { + if (this._intersecting.has(section)) { + active = section; + } } + + // No section crosses the line: keep the last active (content gap), or fall + // back to the first section at the top of the page. + active ||= this._lastActive ?? this._sections.at(0); } - dispose() { - this._observer.disconnect(); - super.dispose(); + if (!active) { + return; } + this._lastActive = active; + const link = this._linkBySection.get(active); + if (link) { + this._process(link); + } + } - // Private - _configAfterMerge(config) { - // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case - config.target = index_js.getElement(config.target) || document.body; + // Single source of truth for the `topMargin` option: its numeric value and + // whether it's expressed as a percentage of the root height or in pixels. + _parseTopMargin() { + const value = String(this._config.topMargin); + return { + value: Number.parseFloat(value) || 0, + unit: value.endsWith('%') ? '%' : 'px' + }; + } - // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only - config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin; - if (typeof config.threshold === 'string') { - config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value)); - } - return config; - } - _maybeEnableSmoothScroll() { - if (!this._config.smoothScroll) { - return; - } + // Collapse the observer root to a strip from the top down to the activation + // line, so a section is "intersecting" exactly while it crosses that line. + _getDerivedRootMargin() { + const { + value, + unit + } = this._parseTopMargin(); + let percent = value; - // unregister any previous listeners - EventHandler.off(this._config.target, EVENT_CLICK); - EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { - const observableSection = this._observableSections.get(event.target.hash); - if (observableSection) { - event.preventDefault(); - const root = this._rootElement || window; - const height = observableSection.offsetTop - this._element.offsetTop; - if (root.scrollTo) { - root.scrollTo({ - top: height, - behavior: 'smooth' - }); - return; - } - - // Chrome 60 doesn't support `scrollTo` - root.scrollTop = height; - } - }); + // Express a pixel activation line as a percentage of the root height. + if (unit === 'px') { + const rootHeight = this._rootElement ? this._rootElement.clientHeight : document.documentElement.clientHeight || window.innerHeight; + percent = rootHeight ? value / rootHeight * 100 : 12; } - _getNewObserver() { - const options = { - root: this._rootElement, - threshold: this._config.threshold, - rootMargin: this._config.rootMargin - }; - return new IntersectionObserver(entries => this._observerCallback(entries), options); + + // Clamp so the bottom inset stays a valid (non-negative) rootMargin even if + // the line sits outside the root box. + const bottom = Math.min(Math.max(100 - percent, 0), 100); + return `0px 0px -${bottom}% 0px`; + } + + // Whether the activation line is derived from a pixel `topMargin` (in which + // case it must be recomputed on resize). An explicit `rootMargin` is owned by + // the caller, and a `%` topMargin is recomputed by the browser automatically. + _usesPixelMargin() { + return !this._config.rootMargin && this._parseTopMargin().unit === 'px'; + } + + // --- Bottom sentinel ----------------------------------------------------- + + _setUpSentinel() { + this._teardownSentinel(); + if (this._sections.length === 0) { + return; } + const sentinel = document.createElement('div'); + sentinel.setAttribute('aria-hidden', 'true'); + sentinel.style.cssText = 'position:relative;width:0;height:0;margin:0;padding:0;border:0;visibility:hidden;'; + this._element.append(sentinel); + this._sentinel = sentinel; + this._sentinelObserver = new IntersectionObserver(entries => this._onSentinel(entries), { + root: this._rootElement, + threshold: [0] + }); + this._sentinelObserver.observe(sentinel); + } + _onSentinel(entries) { + const entry = entries.at(-1); + // Only treat the sentinel as "bottom reached" when content actually + // overflows; otherwise everything is visible and there's nothing to spy. + this._atBottom = Boolean(entry?.isIntersecting) && this._isOverflowing(); + this._computeActive(); + } + _isOverflowing() { + const scroller = this._rootElement || document.scrollingElement || document.documentElement; + return scroller.scrollHeight > scroller.clientHeight; + } + _teardownSentinel() { + this._sentinelObserver?.disconnect(); + this._sentinelObserver = null; + this._sentinel?.remove(); + this._sentinel = null; + this._atBottom = false; + } - // The logic of selection - _observerCallback(entries) { - const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); - const activate = entry => { - this._previousScrollData.visibleEntryTop = entry.target.offsetTop; - this._process(targetElement(entry)); - }; - const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; - const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; - this._previousScrollData.parentScrollTop = parentScrollTop; - for (const entry of entries) { - if (!entry.isIntersecting) { - this._activeTarget = null; - this._clearActiveClass(targetElement(entry)); - continue; - } - const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; - // if we are scrolling down, pick the bigger offsetTop - if (userScrollsDown && entryIsLowerThanPrevious) { - activate(entry); - // if parent isn't scrolled, let's keep the first visible item, breaking the iteration - if (!parentScrollTop) { - return; - } - continue; - } + // --- Resize (px activation lines only) ----------------------------------- - // if we are scrolling up, pick the smallest offsetTop - if (!userScrollsDown && !entryIsLowerThanPrevious) { - activate(entry); - } - } + _maybeAddResizeListener() { + this._removeResizeListener(); + if (!this._usesPixelMargin()) { + return; } - _initializeTargetsAndObservables() { - this._targetLinks = new Map(); - this._observableSections = new Map(); - const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); - for (const anchor of targetLinks) { - // ensure that the anchor has an id and is not disabled - if (!anchor.hash || index_js.isDisabled(anchor)) { - continue; - } - const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element); + this._resizeHandler = () => { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(() => this._rebuildObserver(), RESIZE_DEBOUNCE); + }; + EventHandler.on(window, EVENT_RESIZE, this._resizeHandler); + } + _removeResizeListener() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + if (this._resizeHandler) { + EventHandler.off(window, EVENT_RESIZE, this._resizeHandler); + this._resizeHandler = null; + } + } + _rebuildObserver() { + if (!this._observer) { + return; + } + this._observer.disconnect(); + this._intersecting.clear(); + this._observer = this._getNewObserver(); + for (const section of this._sections) { + this._observer.observe(section); + } + } - // ensure that the observableSection exists & is visible - if (index_js.isVisible(observableSection)) { - this._targetLinks.set(decodeURI(anchor.hash), anchor); - this._observableSections.set(anchor.hash, observableSection); - } - } + // --- Smooth-scroll settle (hash + focus) --------------------------------- + + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; } - _process(target) { - if (this._activeTarget === target) { + + // Unregister any previous listener so refresh() doesn't stack them. + EventHandler.off(this._config.target, EVENT_CLICK); + EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { + const link = event.target.closest(SELECTOR_TARGET_LINKS); + const section = link && this._sectionByLink.get(link); + if (!section || !this._element) { return; } - this._clearActiveClass(this._config.target); - this._activeTarget = target; - target.classList.add(CLASS_NAME_ACTIVE); - this._activateParents(target); - EventHandler.trigger(this._element, EVENT_ACTIVATE, { - relatedTarget: target - }); - } - _activateParents(target) { - // Activate dropdown parents - if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { - SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE); + event.preventDefault(); + const root = this._rootElement || window; + const height = section.offsetTop - this._element.offsetTop; + const currentTop = this._rootElement ? this._rootElement.scrollTop : window.scrollY ?? window.pageYOffset; + const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + + // If we're already there (or motion is reduced), there will be no scroll + // — and thus no `scrollend` — to wait for, so settle immediately. This + // avoids a stuck pending navigation that never restores hash/focus. + if (reduceMotion || Math.abs(currentTop - height) <= 2) { + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'auto' + }); + } else { + root.scrollTop = height; + } + this._settleNavigation(link.hash, section); return; } - for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { - // Set triggered links parents as active - // With both and markup a parent is the previous sibling of any nav ancestor - for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { - item.classList.add(CLASS_NAME_ACTIVE); - } + + // Defer the URL-hash and focus updates until the scroll settles, so we + // don't thrash the address bar mid-animation (and so the native hash + // navigation we just prevented is restored once we arrive). + this._pendingNavigation = { + hash: link.hash, + section + }; + this._armSettle(); + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + } else { + root.scrollTop = height; } + }); + } + + // Arm a one-shot settle for the in-flight smooth scroll. `scrollend` is the + // primary signal; a transient scroll-idle timer covers engines without it. + // Both are removed on settle, so a later unrelated scroll can't replay it. + _armSettle() { + this._disarmSettle(); + const target = this._getSettleTarget(); + this._settleHandler = () => this._onSettle(); + this._scrollIdleHandler = () => { + clearTimeout(this._settleTimeout); + this._settleTimeout = setTimeout(() => this._onSettle(), SCROLL_IDLE_TIMEOUT); + }; + EventHandler.on(target, EVENT_SCROLLEND, this._settleHandler); + EventHandler.on(target, EVENT_SCROLL, this._scrollIdleHandler); + } + _disarmSettle() { + clearTimeout(this._settleTimeout); + this._settleTimeout = null; + const target = this._getSettleTarget(); + if (this._settleHandler) { + EventHandler.off(target, EVENT_SCROLLEND, this._settleHandler); + this._settleHandler = null; } - _clearActiveClass(parent) { - parent.classList.remove(CLASS_NAME_ACTIVE); - const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent); - for (const node of activeNodes) { - node.classList.remove(CLASS_NAME_ACTIVE); - } + if (this._scrollIdleHandler) { + EventHandler.off(target, EVENT_SCROLL, this._scrollIdleHandler); + this._scrollIdleHandler = null; } + } + _getSettleTarget() { + return this._rootElement || document; + } + _onSettle() { + this._disarmSettle(); + if (!this._pendingNavigation) { + return; + } + const { + hash, + section + } = this._pendingNavigation; + this._settleNavigation(hash, section); + } + _settleNavigation(hash, section) { + this._pendingNavigation = null; - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = ScrollSpy.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // Restore the URL hash (without adding a history entry) now that we've + // arrived, and move focus to the section for keyboard/AT users. + if (window.history?.replaceState) { + window.history.replaceState(null, '', hash); + } + if (!section.hasAttribute('tabindex')) { + section.setAttribute('tabindex', '-1'); } + section.focus({ + preventScroll: true + }); } - /** - * Data API implementation - */ + // --- Targets / observables ---------------------------------------------- + + _initializeTargetsAndObservables() { + this._sections = []; + this._linkBySection = new Map(); + this._sectionByLink = new Map(); + const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target); + const seen = new Set(); + for (const anchor of targetLinks) { + if (!anchor.hash || isDisabled(anchor)) { + continue; + } + + // Resolve by id (decoded) rather than building a CSS selector, so any + // literal id works — dots, slashes, colons, and percent-encoded chars — + // without escaping. + const id = decodeFragment(anchor.hash.slice(1)); + if (!id) { + continue; + } + const section = document.getElementById(id); + // ensure the section exists, is scoped to this element, and is visible + if (!section || !this._element.contains(section) || !isVisible(section)) { + continue; + } + this._sectionByLink.set(anchor, section); + this._linkBySection.set(section, anchor); // last link wins for a section + + if (!seen.has(section)) { + seen.add(section); + this._sections.push(section); + } + } - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { - ScrollSpy.getOrCreateInstance(spy); + // Keep sections in top-to-bottom order so "deepest" selection is + // well-defined. Read once here (refresh/resize), never on the hot path. + this._sections.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); + } + _process(target) { + if (this._activeTarget === target) { + return; } - }); + this._clearActiveClass(this._config.target); + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE); + this._activateParents(target); + EventHandler.trigger(this._element, EVENT_ACTIVATE, { + relatedTarget: target + }); + } + _activateParents(target) { + // Activate menu parents + if (target.classList.contains(CLASS_NAME_MENU_ITEM)) { + const menuToggle = target.closest('.menu')?.previousElementSibling; + if (menuToggle?.matches(SELECTOR_MENU_TOGGLE)) { + menuToggle.classList.add(CLASS_NAME_ACTIVE); + } + return; + } + for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both and markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE); + } + } + } + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE); + const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent); + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE); + } + } +} - /** - * jQuery - */ +// Decode a URL fragment id, tolerating malformed escapes (returns it as-is). +function decodeFragment(hash) { + try { + return decodeURIComponent(hash); + } catch { + return hash; + } +} - index_js.defineJQueryPlugin(ScrollSpy); +/** + * Data API implementation + */ - return ScrollSpy; +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } +}); -})); +export { ScrollSpy as default }; diff --git a/assets/javascripts/bootstrap/strength.js b/assets/javascripts/bootstrap/strength.js new file mode 100644 index 00000000..033f8d6f --- /dev/null +++ b/assets/javascripts/bootstrap/strength.js @@ -0,0 +1,240 @@ +/*! + * Bootstrap strength.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap strength.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'strength'; +const DATA_KEY = 'bs.strength'; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = '.data-api'; +const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY}`; +const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'; +const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']; +const Default = { + input: null, + // Selector or element for password input + minLength: 8, + messages: { + weak: 'Weak', + fair: 'Fair', + good: 'Good', + strong: 'Strong' + }, + weights: { + minLength: 1, + extraLength: 1, + lowercase: 1, + uppercase: 1, + numbers: 1, + special: 1, + multipleSpecial: 1, + longPassword: 1 + }, + thresholds: [2, 4, 6], + // weak ≤2, fair ≤4, good ≤6, strong >6 + scorer: null // Custom scoring function (password) => number +}; +const DefaultType = { + input: '(string|element|null)', + minLength: 'number', + messages: 'object', + weights: 'object', + thresholds: 'array', + scorer: '(function|null)' +}; + +/** + * Class definition + */ + +class Strength extends BaseComponent { + constructor(element, config) { + super(element, config); + this._input = this._getInput(); + this._segments = SelectorEngine.find('.strength-segment', this._element); + this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement); + this._currentStrength = null; + if (this._input) { + this._addEventListeners(); + // Check initial value + this._evaluate(); + } + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + getStrength() { + return this._currentStrength; + } + evaluate() { + this._evaluate(); + } + + // Private + _getInput() { + if (this._config.input) { + return typeof this._config.input === 'string' ? SelectorEngine.findOne(this._config.input) : this._config.input; + } + + // Look for preceding password input + const parent = this._element.parentElement; + return SelectorEngine.findOne('input[type="password"]', parent); + } + _addEventListeners() { + EventHandler.on(this._input, 'input', () => this._evaluate()); + EventHandler.on(this._input, 'change', () => this._evaluate()); + } + _evaluate() { + const password = this._input.value; + const score = this._calculateScore(password); + const strength = this._scoreToStrength(score); + if (strength !== this._currentStrength) { + this._currentStrength = strength; + this._updateUI(strength, score); + EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, { + strength, + score, + password: password.length > 0 ? '***' : '' // Don't expose actual password + }); + } + } + _calculateScore(password) { + if (!password) { + return 0; + } + + // Use custom scorer if provided + if (typeof this._config.scorer === 'function') { + return this._config.scorer(password); + } + const { + weights + } = this._config; + let score = 0; + + // Length scoring + if (password.length >= this._config.minLength) { + score += weights.minLength; + } + if (password.length >= this._config.minLength + 4) { + score += weights.extraLength; + } + + // Character variety + if (/[a-z]/.test(password)) { + score += weights.lowercase; + } + if (/[A-Z]/.test(password)) { + score += weights.uppercase; + } + if (/\d/.test(password)) { + score += weights.numbers; + } + + // Special characters + if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.special; + } + + // Extra points for more special chars or length + if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) { + score += weights.multipleSpecial; + } + if (password.length >= 16) { + score += weights.longPassword; + } + return score; + } + _scoreToStrength(score) { + if (score === 0) { + return null; + } + const [weak, fair, good] = this._config.thresholds; + if (score <= weak) { + return 'weak'; + } + if (score <= fair) { + return 'fair'; + } + if (score <= good) { + return 'good'; + } + return 'strong'; + } + _updateUI(strength) { + // Update data attribute on element + if (strength) { + this._element.dataset.bsStrength = strength; + } else { + delete this._element.dataset.bsStrength; + } + + // Update segmented meter + const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1; + for (const [index, segment] of this._segments.entries()) { + if (index <= strengthIndex) { + segment.classList.add('active'); + } else { + segment.classList.remove('active'); + } + } + + // Update text feedback + if (this._textElement) { + if (strength && this._config.messages[strength]) { + this._textElement.textContent = this._config.messages[strength]; + this._textElement.dataset.bsStrength = strength; + + // Also set the color via inheriting from parent or using CSS variable + const colorMap = { + weak: 'danger', + fair: 'warning', + good: 'info', + strong: 'success' + }; + this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`); + } else { + this._textElement.textContent = ''; + delete this._textElement.dataset.bsStrength; + } + } + } +} + +/** + * Data API implementation + */ + +EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) { + Strength.getOrCreateInstance(element); + } +}); + +export { Strength as default }; diff --git a/assets/javascripts/bootstrap/tab.js b/assets/javascripts/bootstrap/tab.js index 94088201..7a77c0e9 100644 --- a/assets/javascripts/bootstrap/tab.js +++ b/assets/javascripts/bootstrap/tab.js @@ -1,284 +1,265 @@ /*! - * Bootstrap tab.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap tab.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tab = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import SelectorEngine from './dom/selector-engine.js'; +import { isDisabled, getNextActiveElement } from './util/index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap tab.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'tab'; - const DATA_KEY = 'bs.tab'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`; - const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; - const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`; - const ARROW_LEFT_KEY = 'ArrowLeft'; - const ARROW_RIGHT_KEY = 'ArrowRight'; - const ARROW_UP_KEY = 'ArrowUp'; - const ARROW_DOWN_KEY = 'ArrowDown'; - const HOME_KEY = 'Home'; - const END_KEY = 'End'; - const CLASS_NAME_ACTIVE = 'active'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const CLASS_DROPDOWN = 'dropdown'; - const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; - const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; - const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; - const SELECTOR_OUTER = '.nav-item, .list-group-item'; - const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; - const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'; // TODO: could only be `tab` in v6 - const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; - const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`; +const NAME = 'tab'; +const DATA_KEY = 'bs.tab'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`; +const EVENT_KEYDOWN = `keydown${EVENT_KEY}`; +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`; +const ARROW_LEFT_KEY = 'ArrowLeft'; +const ARROW_RIGHT_KEY = 'ArrowRight'; +const ARROW_UP_KEY = 'ArrowUp'; +const ARROW_DOWN_KEY = 'ArrowDown'; +const HOME_KEY = 'Home'; +const END_KEY = 'End'; +const CLASS_NAME_ACTIVE = 'active'; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_MENU_TOGGLE = '[data-bs-toggle="menu"]'; +const SELECTOR_MENU = '.menu'; +const NOT_SELECTOR_MENU_TOGGLE = `:not(${SELECTOR_MENU_TOGGLE})`; +const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; +const SELECTOR_OUTER = '.nav-item, .list-group-item'; +const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_MENU_TOGGLE}, .list-group-item${NOT_SELECTOR_MENU_TOGGLE}, [role="tab"]${NOT_SELECTOR_MENU_TOGGLE}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"]'; +const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`; +const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"]`; - /** - * Class definition - */ +/** + * Class definition + */ - class Tab extends BaseComponent { - constructor(element) { - super(element); - this._parent = this._element.closest(SELECTOR_TAB_PANEL); - if (!this._parent) { - return; - // TODO: should throw exception in v6 - // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`) - } - - // Set up initial aria attributes - this._setInitialAttributes(this._parent, this._getChildren()); - EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); +class Tab extends BaseComponent { + constructor(element) { + super(element); + this._parent = this._element.closest(SELECTOR_TAB_PANEL); + if (!this._parent) { + return; + // TODO: should throw exception in v6 + // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_TAB_PANEL}`) } - // Getters - static get NAME() { - return NAME; - } + // Set up initial aria attributes + this._setInitialAttributes(this._parent, this._getChildren()); + EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event)); + } - // Public - show() { - // Shows this elem and deactivate the active sibling if exists - const innerElem = this._element; - if (this._elemIsActive(innerElem)) { - return; - } + // Getters + static get NAME() { + return NAME; + } - // Search for active tab on same parent to deactivate it - const active = this._getActiveElem(); - const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, { - relatedTarget: innerElem - }) : null; - const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { - relatedTarget: active - }); - if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { - return; - } - this._deactivate(active, innerElem); - this._activate(innerElem, active); + // Public + show() { + // Shows this elem and deactivate the active sibling if exists + const innerElem = this._element; + if (this._elemIsActive(innerElem)) { + return; } - // Private - _activate(element, relatedElem) { - if (!element) { - return; - } - element.classList.add(CLASS_NAME_ACTIVE); - this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + // Search for active tab on same parent to deactivate it + const active = this._getActiveElem(); + const hideEvent = active ? EventHandler.trigger(active, EVENT_HIDE, { + relatedTarget: innerElem + }) : null; + const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { + relatedTarget: active + }); + if (showEvent.defaultPrevented || hideEvent && hideEvent.defaultPrevented) { + return; + } + this._deactivate(active, innerElem); + this._activate(innerElem, active); + } - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.add(CLASS_NAME_SHOW); - return; - } - element.removeAttribute('tabindex'); - element.setAttribute('aria-selected', true); - this._toggleDropDown(element, true); - EventHandler.trigger(element, EVENT_SHOWN, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + // Private + _activate(element, relatedElem) { + if (!element) { + return; } - _deactivate(element, relatedElem) { - if (!element) { + element.classList.add(CLASS_NAME_ACTIVE); + this._activate(SelectorEngine.getElementFromSelector(element)); // Search and activate/show the proper section + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.add(CLASS_NAME_SHOW); return; } - element.classList.remove(CLASS_NAME_ACTIVE); - element.blur(); - this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too - - const complete = () => { - if (element.getAttribute('role') !== 'tab') { - element.classList.remove(CLASS_NAME_SHOW); - return; - } - element.setAttribute('aria-selected', false); - element.setAttribute('tabindex', '-1'); - this._toggleDropDown(element, false); - EventHandler.trigger(element, EVENT_HIDDEN, { - relatedTarget: relatedElem - }); - }; - this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + element.removeAttribute('tabindex'); + element.setAttribute('aria-selected', true); + this._toggleMenu(element, true); + EventHandler.trigger(element, EVENT_SHOWN, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + } + _deactivate(element, relatedElem) { + if (!element) { + return; } - _keydown(event) { - if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + element.classList.remove(CLASS_NAME_ACTIVE); + element.blur(); + this._deactivate(SelectorEngine.getElementFromSelector(element)); // Search and deactivate the shown section too + + const complete = () => { + if (element.getAttribute('role') !== 'tab') { + element.classList.remove(CLASS_NAME_SHOW); return; } - event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page - event.preventDefault(); - const children = this._getChildren().filter(element => !index_js.isDisabled(element)); - let nextActiveElement; - if ([HOME_KEY, END_KEY].includes(event.key)) { - nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]; - } else { - const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); - nextActiveElement = index_js.getNextActiveElement(children, event.target, isNext, true); - } - if (nextActiveElement) { - nextActiveElement.focus({ - preventScroll: true - }); - Tab.getOrCreateInstance(nextActiveElement).show(); - } + element.setAttribute('aria-selected', false); + element.setAttribute('tabindex', '-1'); + this._toggleMenu(element, false); + EventHandler.trigger(element, EVENT_HIDDEN, { + relatedTarget: relatedElem + }); + }; + this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE)); + } + _keydown(event) { + if (![ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key)) { + return; } - _getChildren() { - // collection of inner elements - return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + + // Don't hijack modifier+arrow shortcuts (e.g. Alt+Left/Right for browser + // history navigation); only the bare keys drive tablist navigation. + if (event.altKey || event.ctrlKey || event.metaKey) { + return; } - _getActiveElem() { - return this._getChildren().find(child => this._elemIsActive(child)) || null; + event.stopPropagation(); // stopPropagation/preventDefault both added to support up/down keys without scrolling the page + event.preventDefault(); + const children = this._getChildren().filter(element => !isDisabled(element)); + let nextActiveElement; + if ([HOME_KEY, END_KEY].includes(event.key)) { + nextActiveElement = event.key === HOME_KEY ? children[0] : children.at(-1); + } else { + const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key); + nextActiveElement = getNextActiveElement(children, event.target, isNext, true); } - _setInitialAttributes(parent, children) { - this._setAttributeIfNotExists(parent, 'role', 'tablist'); - for (const child of children) { - this._setInitialAttributesOnChild(child); - } + if (nextActiveElement) { + nextActiveElement.focus({ + preventScroll: true + }); + Tab.getOrCreateInstance(nextActiveElement).show(); } - _setInitialAttributesOnChild(child) { - child = this._getInnerElement(child); - const isActive = this._elemIsActive(child); - const outerElem = this._getOuterElement(child); - child.setAttribute('aria-selected', isActive); - if (outerElem !== child) { - this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); - } - if (!isActive) { - child.setAttribute('tabindex', '-1'); - } - this._setAttributeIfNotExists(child, 'role', 'tab'); - - // set attributes to the related panel too - this._setInitialAttributesOnTargetPanel(child); + } + _getChildren() { + // collection of inner elements + return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent); + } + _getActiveElem() { + return this._getChildren().find(child => this._elemIsActive(child)) || null; + } + _setInitialAttributes(parent, children) { + this._setAttributeIfNotExists(parent, 'role', 'tablist'); + for (const child of children) { + this._setInitialAttributesOnChild(child); } - _setInitialAttributesOnTargetPanel(child) { - const target = SelectorEngine.getElementFromSelector(child); - if (!target) { - return; - } - this._setAttributeIfNotExists(target, 'role', 'tabpanel'); - if (child.id) { - this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); - } + } + _setInitialAttributesOnChild(child) { + child = this._getInnerElement(child); + const isActive = this._elemIsActive(child); + const outerElem = this._getOuterElement(child); + child.setAttribute('aria-selected', isActive); + if (outerElem !== child) { + this._setAttributeIfNotExists(outerElem, 'role', 'presentation'); } - _toggleDropDown(element, open) { - const outerElem = this._getOuterElement(element); - if (!outerElem.classList.contains(CLASS_DROPDOWN)) { - return; - } - const toggle = (selector, className) => { - const element = SelectorEngine.findOne(selector, outerElem); - if (element) { - element.classList.toggle(className, open); - } - }; - toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE); - toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW); - outerElem.setAttribute('aria-expanded', open); + if (!isActive) { + child.setAttribute('tabindex', '-1'); } - _setAttributeIfNotExists(element, attribute, value) { - if (!element.hasAttribute(attribute)) { - element.setAttribute(attribute, value); - } + this._setAttributeIfNotExists(child, 'role', 'tab'); + + // set attributes to the related panel too + this._setInitialAttributesOnTargetPanel(child); + } + _setInitialAttributesOnTargetPanel(child) { + const target = SelectorEngine.getElementFromSelector(child); + if (!target) { + return; } - _elemIsActive(elem) { - return elem.classList.contains(CLASS_NAME_ACTIVE); + this._setAttributeIfNotExists(target, 'role', 'tabpanel'); + if (child.id) { + this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`); } - - // Try to get the inner element (usually the .nav-link) - _getInnerElement(elem) { - return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } + _toggleMenu(element, open) { + const outerElem = this._getOuterElement(element); + const menuToggle = SelectorEngine.findOne(SELECTOR_MENU_TOGGLE, outerElem); + if (!menuToggle) { + return; } - - // Try to get the outer element (usually the .nav-item) - _getOuterElement(elem) { - return elem.closest(SELECTOR_OUTER) || elem; + const menu = SelectorEngine.findOne(SELECTOR_MENU, outerElem); + menuToggle.classList.toggle(CLASS_NAME_ACTIVE, open); + if (menu) { + menu.classList.toggle(CLASS_NAME_SHOW, open); } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tab.getOrCreateInstance(this); - if (typeof config !== 'string') { - return; - } - if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + menuToggle.setAttribute('aria-expanded', open); + } + _setAttributeIfNotExists(element, attribute, value) { + if (!element.hasAttribute(attribute)) { + element.setAttribute(attribute, value); } } + _elemIsActive(elem) { + return elem.classList.contains(CLASS_NAME_ACTIVE); + } - /** - * Data API implementation - */ + // Try to get the inner element (usually the .nav-link) + _getInnerElement(elem) { + return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem); + } - EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - Tab.getOrCreateInstance(this).show(); - }); + // Try to get the outer element (usually the .nav-item) + _getOuterElement(elem) { + return elem.closest(SELECTOR_OUTER) || elem; + } +} - /** - * Initialize on focus - */ - EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { - Tab.getOrCreateInstance(element); - } - }); - /** - * jQuery - */ +/** + * Data API implementation + */ - index_js.defineJQueryPlugin(Tab); +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + Tab.getOrCreateInstance(this).show(); +}); - return Tab; +/** + * Initialize on focus + */ +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) { + Tab.getOrCreateInstance(element); + } +}); -})); +export { Tab as default }; diff --git a/assets/javascripts/bootstrap/toast.js b/assets/javascripts/bootstrap/toast.js index 3e8903fc..5df6e268 100644 --- a/assets/javascripts/bootstrap/toast.js +++ b/assets/javascripts/bootstrap/toast.js @@ -1,197 +1,175 @@ /*! - * Bootstrap toast.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap toast.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./util/component-functions.js'), require('./util/index.js')) : - typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './util/component-functions', './util/index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Toast = factory(global.BaseComponent, global.EventHandler, global.ComponentFunctions, global.Index)); -})(this, (function (BaseComponent, EventHandler, componentFunctions_js, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap toast.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'toast'; - const DATA_KEY = 'bs.toast'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; - const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; - const EVENT_HIDE = `hide${EVENT_KEY}`; - const EVENT_HIDDEN = `hidden${EVENT_KEY}`; - const EVENT_SHOW = `show${EVENT_KEY}`; - const EVENT_SHOWN = `shown${EVENT_KEY}`; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility - const CLASS_NAME_SHOW = 'show'; - const CLASS_NAME_SHOWING = 'showing'; - const DefaultType = { - animation: 'boolean', - autohide: 'boolean', - delay: 'number' - }; - const Default = { - animation: true, - autohide: true, - delay: 5000 - }; - - /** - * Class definition - */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { enableDismissTrigger } from './util/component-functions.js'; +import { reflow } from './util/index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap toast.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toast'; +const DATA_KEY = 'bs.toast'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`; +const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`; +const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; +const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`; +const EVENT_HIDE = `hide${EVENT_KEY}`; +const EVENT_HIDDEN = `hidden${EVENT_KEY}`; +const EVENT_SHOW = `show${EVENT_KEY}`; +const EVENT_SHOWN = `shown${EVENT_KEY}`; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_HIDE = 'hide'; // @deprecated - kept here only for backwards compatibility +const CLASS_NAME_SHOW = 'show'; +const CLASS_NAME_SHOWING = 'showing'; +const DefaultType = { + animation: 'boolean', + autohide: 'boolean', + delay: 'number' +}; +const Default = { + animation: true, + autohide: true, + delay: 5000 +}; + +/** + * Class definition + */ + +class Toast extends BaseComponent { + constructor(element, config) { + super(element, config); + this._timeout = null; + this._hasMouseInteraction = false; + this._hasKeyboardInteraction = false; + this._setListeners(); + } - class Toast extends BaseComponent { - constructor(element, config) { - super(element, config); - this._timeout = null; - this._hasMouseInteraction = false; - this._hasKeyboardInteraction = false; - this._setListeners(); - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; + // Public + show() { + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); + if (showEvent.defaultPrevented) { + return; } - - // Public - show() { - const showEvent = EventHandler.trigger(this._element, EVENT_SHOW); - if (showEvent.defaultPrevented) { - return; - } - this._clearTimeout(); - if (this._config.animation) { - this._element.classList.add(CLASS_NAME_FADE); - } - const complete = () => { - this._element.classList.remove(CLASS_NAME_SHOWING); - EventHandler.trigger(this._element, EVENT_SHOWN); - this._maybeScheduleHide(); - }; - this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated - index_js.reflow(this._element); - this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + this._clearTimeout(); + if (this._config.animation) { + this._element.classList.add(CLASS_NAME_FADE); } - hide() { - if (!this.isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); - if (hideEvent.defaultPrevented) { - return; - } - const complete = () => { - this._element.classList.add(CLASS_NAME_HIDE); // @deprecated - this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); - EventHandler.trigger(this._element, EVENT_HIDDEN); - }; - this._element.classList.add(CLASS_NAME_SHOWING); - this._queueCallback(complete, this._element, this._config.animation); + const complete = () => { + this._element.classList.remove(CLASS_NAME_SHOWING); + EventHandler.trigger(this._element, EVENT_SHOWN); + this._maybeScheduleHide(); + }; + this._element.classList.remove(CLASS_NAME_HIDE); // @deprecated + reflow(this._element); + this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + hide() { + if (!this.isShown()) { + return; } - dispose() { - this._clearTimeout(); - if (this.isShown()) { - this._element.classList.remove(CLASS_NAME_SHOW); - } - super.dispose(); + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE); + if (hideEvent.defaultPrevented) { + return; } - isShown() { - return this._element.classList.contains(CLASS_NAME_SHOW); + const complete = () => { + this._element.classList.add(CLASS_NAME_HIDE); // @deprecated + this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW); + EventHandler.trigger(this._element, EVENT_HIDDEN); + }; + this._element.classList.add(CLASS_NAME_SHOWING); + this._queueCallback(complete, this._element, this._config.animation); + } + dispose() { + this._clearTimeout(); + if (this.isShown()) { + this._element.classList.remove(CLASS_NAME_SHOW); } + super.dispose(); + } + isShown() { + return this._element.classList.contains(CLASS_NAME_SHOW); + } - // Private - _maybeScheduleHide() { - if (!this._config.autohide) { - return; - } - if (this._hasMouseInteraction || this._hasKeyboardInteraction) { - return; - } - this._timeout = setTimeout(() => { - this.hide(); - }, this._config.delay); + // Private + _maybeScheduleHide() { + if (!this._config.autohide) { + return; } - _onInteraction(event, isInteracting) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - { - this._hasMouseInteraction = isInteracting; - break; - } - case 'focusin': - case 'focusout': - { - this._hasKeyboardInteraction = isInteracting; - break; - } - } - if (isInteracting) { - this._clearTimeout(); - return; - } - const nextElement = event.relatedTarget; - if (this._element === nextElement || this._element.contains(nextElement)) { - return; - } - this._maybeScheduleHide(); + if (this._hasMouseInteraction || this._hasKeyboardInteraction) { + return; } - _setListeners() { - EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); - EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); - EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + this._timeout = setTimeout(() => { + this.hide(); + }, this._config.delay); + } + _onInteraction(event, isInteracting) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + { + this._hasMouseInteraction = isInteracting; + break; + } + case 'focusin': + case 'focusout': + { + this._hasKeyboardInteraction = isInteracting; + break; + } } - _clearTimeout() { - clearTimeout(this._timeout); - this._timeout = null; + if (isInteracting) { + this._clearTimeout(); + return; } - - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Toast.getOrCreateInstance(this, config); - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](this); - } - }); + const nextElement = event.relatedTarget; + if (this._element === nextElement || this._element.contains(nextElement)) { + return; } + this._maybeScheduleHide(); } + _setListeners() { + EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)); + EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)); + EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)); + } + _clearTimeout() { + clearTimeout(this._timeout); + this._timeout = null; + } +} - /** - * Data API implementation - */ - - componentFunctions_js.enableDismissTrigger(Toast); - - /** - * jQuery - */ - - index_js.defineJQueryPlugin(Toast); +/** + * Data API implementation + */ - return Toast; +enableDismissTrigger(Toast); -})); +export { Toast as default }; diff --git a/assets/javascripts/bootstrap/toggler.js b/assets/javascripts/bootstrap/toggler.js new file mode 100644 index 00000000..589f72e1 --- /dev/null +++ b/assets/javascripts/bootstrap/toggler.js @@ -0,0 +1,93 @@ +/*! + * Bootstrap toggler.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import { eventActionOnPlugin } from './util/component-functions.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap toggler.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'toggler'; +const DATA_KEY = 'bs.toggler'; +const EVENT_KEY = `.${DATA_KEY}`; +const EVENT_TOGGLE = `toggle${EVENT_KEY}`; +const EVENT_TOGGLED = `toggled${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="toggler"]'; +const DefaultType = { + attribute: 'string', + value: '(string|number|boolean)' +}; +const Default = { + attribute: 'class', + value: null +}; + +/** + * Class definition + */ + +class Toggler extends BaseComponent { + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + toggle() { + const toggleEvent = EventHandler.trigger(this._element, EVENT_TOGGLE); + if (toggleEvent.defaultPrevented) { + return; + } + this._execute(); + EventHandler.trigger(this._element, EVENT_TOGGLED); + } + + // Private + _execute() { + const { + attribute, + value + } = this._config; + if (attribute === 'id') { + return; // You have to be kidding + } + if (attribute === 'class') { + this._element.classList.toggle(value); + return; + } + + // Compare as strings since getAttribute() always returns a string + if (this._element.getAttribute(attribute) === String(value)) { + this._element.removeAttribute(attribute); + return; + } + this._element.setAttribute(attribute, value); + } +} + +/** + * Data API implementation + */ + +eventActionOnPlugin(Toggler, EVENT_CLICK, SELECTOR_DATA_TOGGLE, 'toggle'); + +export { Toggler as default }; diff --git a/assets/javascripts/bootstrap/tooltip.js b/assets/javascripts/bootstrap/tooltip.js index d0f823d8..1f5bfcca 100644 --- a/assets/javascripts/bootstrap/tooltip.js +++ b/assets/javascripts/bootstrap/tooltip.js @@ -1,545 +1,688 @@ /*! - * Bootstrap tooltip.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap tooltip.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@popperjs/core'), require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./util/index.js'), require('./util/sanitizer.js'), require('./util/template-factory.js')) : - typeof define === 'function' && define.amd ? define(['@popperjs/core', './base-component', './dom/event-handler', './dom/manipulator', './util/index', './util/sanitizer', './util/template-factory'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Tooltip = factory(global["@popperjs/core"], global.BaseComponent, global.EventHandler, global.Manipulator, global.Index, global.Sanitizer, global.TemplateFactory)); -})(this, (function (Popper, BaseComponent, EventHandler, Manipulator, index_js, sanitizer_js, TemplateFactory) { 'use strict'; - - function _interopNamespaceDefault(e) { - const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }); - if (e) { - for (const k in e) { - if (k !== 'default') { - const d = Object.getOwnPropertyDescriptor(e, k); - Object.defineProperty(n, k, d.get ? d : { - enumerable: true, - get: () => e[k] - }); - } - } - } - n.default = e; - return Object.freeze(n); - } - - const Popper__namespace = /*#__PURE__*/_interopNamespaceDefault(Popper); - - /** - * -------------------------------------------------------------------------- - * Bootstrap tooltip.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'tooltip'; - const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_MODAL = 'modal'; - const CLASS_NAME_SHOW = 'show'; - const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; - const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; - const EVENT_MODAL_HIDE = 'hide.bs.modal'; - const TRIGGER_HOVER = 'hover'; - const TRIGGER_FOCUS = 'focus'; - const TRIGGER_CLICK = 'click'; - const TRIGGER_MANUAL = 'manual'; - const EVENT_HIDE = 'hide'; - const EVENT_HIDDEN = 'hidden'; - const EVENT_SHOW = 'show'; - const EVENT_SHOWN = 'shown'; - const EVENT_INSERTED = 'inserted'; - const EVENT_CLICK = 'click'; - const EVENT_FOCUSIN = 'focusin'; - const EVENT_FOCUSOUT = 'focusout'; - const EVENT_MOUSEENTER = 'mouseenter'; - const EVENT_MOUSELEAVE = 'mouseleave'; - const AttachmentMap = { - AUTO: 'auto', - TOP: 'top', - RIGHT: index_js.isRTL() ? 'left' : 'right', - BOTTOM: 'bottom', - LEFT: index_js.isRTL() ? 'right' : 'left' - }; - const Default = { - allowList: sanitizer_js.DefaultAllowlist, - animation: true, - boundary: 'clippingParents', - container: false, - customClass: '', - delay: 0, - fallbackPlacements: ['top', 'right', 'bottom', 'left'], - html: false, - offset: [0, 6], - placement: 'top', - popperConfig: null, - sanitize: true, - sanitizeFn: null, - selector: false, - template: '' + '' + '' + '', - title: '', - trigger: 'hover focus' - }; - const DefaultType = { - allowList: 'object', - animation: 'boolean', - boundary: '(string|element)', - container: '(string|element|boolean)', - customClass: '(string|function)', - delay: '(number|object)', - fallbackPlacements: 'array', - html: 'boolean', - offset: '(array|string|function)', - placement: '(string|function)', - popperConfig: '(null|object|function)', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - selector: '(string|boolean)', - template: 'string', - title: '(string|element|function)', - trigger: 'string' - }; - - /** - * Class definition - */ - - class Tooltip extends BaseComponent { - constructor(element, config) { - if (typeof Popper__namespace === 'undefined') { - throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)'); - } - super(element, config); - - // Private - this._isEnabled = true; - this._timeout = 0; - this._isHovered = null; - this._activeTrigger = {}; - this._popper = null; - this._templateFactory = null; - this._newContent = null; - - // Protected - this.tip = null; - this._setListeners(); - if (!this._config.selector) { - this._fixTitle(); - } - } +import { computePosition, autoUpdate, offset, flip, shift, arrow } from '@floating-ui/dom'; +import BaseComponent from './base-component.js'; +import EventHandler from './dom/event-handler.js'; +import Manipulator from './dom/manipulator.js'; +import { isRTL, findShadowRoot, noop, getUID, execute, getElement } from './util/index.js'; +import { DefaultAllowlist } from './util/sanitizer.js'; +import TemplateFactory from './util/template-factory.js'; +import { getResponsivePlacement, parseResponsivePlacement, createBreakpointListeners, disposeBreakpointListeners } from './util/floating-ui.js'; - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; +/** + * -------------------------------------------------------------------------- + * Bootstrap tooltip.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Constants + */ + +const NAME = 'tooltip'; +const DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']); +const ESCAPE_KEY = 'Escape'; +const CLASS_NAME_FADE = 'fade'; +const CLASS_NAME_MODAL = 'modal'; +const CLASS_NAME_SHOW = 'show'; +const SELECTOR_TOOLTIP_INNER = '.tooltip-inner'; +const SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`; +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tooltip"]'; +const EVENT_MODAL_HIDE = 'hide.bs.modal'; +const TRIGGER_HOVER = 'hover'; +const TRIGGER_FOCUS = 'focus'; +const TRIGGER_CLICK = 'click'; +const TRIGGER_MANUAL = 'manual'; +const EVENT_HIDE = 'hide'; +const EVENT_HIDDEN = 'hidden'; +const EVENT_SHOW = 'show'; +const EVENT_SHOWN = 'shown'; +const EVENT_INSERTED = 'inserted'; +const EVENT_CLICK = 'click'; +const EVENT_FOCUSIN = 'focusin'; +const EVENT_FOCUSOUT = 'focusout'; +const EVENT_MOUSEENTER = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; +const EVENT_KEYDOWN = 'keydown'; +const AttachmentMap = { + AUTO: 'auto', + TOP: 'top', + RIGHT: isRTL() ? 'left' : 'right', + BOTTOM: 'bottom', + LEFT: isRTL() ? 'right' : 'left' +}; +const Default = { + allowList: DefaultAllowlist, + animation: true, + boundary: 'clippingParents', + container: false, + customClass: '', + delay: 0, + fallbackPlacements: ['top', 'right', 'bottom', 'left'], + html: false, + offset: [0, 6], + placement: 'top', + floatingConfig: null, + sanitize: true, + sanitizeFn: null, + selector: false, + template: '' + '' + '' + '', + title: '', + trigger: 'hover focus' +}; +const DefaultType = { + allowList: 'object', + animation: 'boolean', + boundary: '(string|element)', + container: '(string|element|boolean)', + customClass: '(string|function)', + delay: '(number|object)', + fallbackPlacements: 'array', + html: 'boolean', + offset: '(array|string|function)', + placement: '(string|function)', + floatingConfig: '(null|object|function)', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + selector: '(string|boolean)', + template: 'string', + title: '(string|element|function)', + trigger: 'string' +}; + +/** + * Class definition + */ + +class Tooltip extends BaseComponent { + constructor(element, config) { + if (typeof computePosition === 'undefined') { + throw new TypeError('Bootstrap\'s tooltips require Floating UI (https://floating-ui.com)'); } + super(element, config); + + // Private + this._isEnabled = true; + this._timeout = 0; + this._isHovered = null; + this._activeTrigger = {}; + this._floatingCleanup = null; + this._keydownHandler = null; + this._templateFactory = null; + this._newContent = null; + this._mediaQueryListeners = []; + this._responsivePlacements = null; - // Public - enable() { - this._isEnabled = true; + // Protected + this.tip = null; + this._parseResponsivePlacements(); + this._setListeners(); + if (!this._config.selector) { + this._fixTitle(); } - disable() { - this._isEnabled = false; + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } + + // Public + enable() { + this._isEnabled = true; + } + disable() { + this._isEnabled = false; + } + toggleEnabled() { + this._isEnabled = !this._isEnabled; + } + toggle() { + if (!this._isEnabled) { + return; } - toggleEnabled() { - this._isEnabled = !this._isEnabled; + if (this._isShown()) { + this._leave(); + return; } - toggle() { - if (!this._isEnabled) { - return; - } - if (this._isShown()) { + this._enter(); + } + dispose() { + clearTimeout(this._timeout); + this._removeEscapeListener(); + EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + if (this._element.getAttribute('data-bs-original-title')) { + this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); + } + this._disposeFloating(); + this._disposeMediaQueryListeners(); + super.dispose(); + } + async show() { + if (this._element.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (!(this._isWithContent() && this._isEnabled)) { + return; + } + const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW)); + const shadowRoot = findShadowRoot(this._element); + const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); + if (showEvent.defaultPrevented || !isInTheDom) { + // Reset the transient hover/active state so a prevented (or not-in-DOM) + // show doesn't leave `_isHovered` stuck true — otherwise a click-triggered + // tip would hit the `_enter()` early-return on every later click and never + // reopen. + this._isHovered = false; + return; + } + this._disposeFloating(); + const tip = this._getTipElement(); + this._element.setAttribute('aria-describedby', tip.getAttribute('id')); + let { + container + } = this._config; + const closestDialog = this._element.closest('dialog[open]'); + if (closestDialog && container === document.body) { + container = closestDialog; + } + if (!this._element.ownerDocument.documentElement.contains(this.tip)) { + container.append(tip); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); + } + await this._createFloating(tip); + tip.classList.add(CLASS_NAME_SHOW); + + // Allow dismissing the tooltip with the Escape key (WCAG 1.4.13) + this._setEscapeListener(); + + // If this is a touch-enabled device we add extra + // empty mouseover listeners to the body's immediate children; + // only needed because of broken event delegation on iOS + // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.on(element, 'mouseover', noop); + } + } + const complete = () => { + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN)); + if (this._isHovered === false) { this._leave(); - return; } - this._enter(); + this._isHovered = false; + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + hide() { + if (!this._isShown()) { + return; } - dispose() { - clearTimeout(this._timeout); - EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); - if (this._element.getAttribute('data-bs-original-title')) { - this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title')); - } - this._disposePopper(); - super.dispose(); + const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)); + if (hideEvent.defaultPrevented) { + return; } - show() { - if (this._element.style.display === 'none') { - throw new Error('Please use show on visible elements'); - } - if (!(this._isWithContent() && this._isEnabled)) { - return; - } - const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW)); - const shadowRoot = index_js.findShadowRoot(this._element); - const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element); - if (showEvent.defaultPrevented || !isInTheDom) { - return; - } + this._removeEscapeListener(); + const tip = this._getTipElement(); + tip.classList.remove(CLASS_NAME_SHOW); - // TODO: v6 remove this or make it optional - this._disposePopper(); - const tip = this._getTipElement(); - this._element.setAttribute('aria-describedby', tip.getAttribute('id')); - const { - container - } = this._config; - if (!this._element.ownerDocument.documentElement.contains(this.tip)) { - container.append(tip); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED)); - } - this._popper = this._createPopper(tip); - tip.classList.add(CLASS_NAME_SHOW); - - // If this is a touch-enabled device we add extra - // empty mouseover listeners to the body's immediate children; - // only needed because of broken event delegation on iOS - // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.on(element, 'mouseover', index_js.noop); - } + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + for (const element of document.body.children) { + EventHandler.off(element, 'mouseover', noop); } - const complete = () => { - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN)); - if (this._isHovered === false) { - this._leave(); - } - this._isHovered = false; - }; - this._queueCallback(complete, this.tip, this._isAnimated()); } - hide() { - if (!this._isShown()) { - return; - } - const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE)); - if (hideEvent.defaultPrevented) { + this._activeTrigger[TRIGGER_CLICK] = false; + this._activeTrigger[TRIGGER_FOCUS] = false; + this._activeTrigger[TRIGGER_HOVER] = false; + this._isHovered = null; // it is a trick to support manual triggering + + const complete = () => { + if (this._isWithActiveTrigger()) { return; } - const tip = this._getTipElement(); - tip.classList.remove(CLASS_NAME_SHOW); - - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - if ('ontouchstart' in document.documentElement) { - for (const element of [].concat(...document.body.children)) { - EventHandler.off(element, 'mouseover', index_js.noop); - } - } - this._activeTrigger[TRIGGER_CLICK] = false; - this._activeTrigger[TRIGGER_FOCUS] = false; - this._activeTrigger[TRIGGER_HOVER] = false; - this._isHovered = null; // it is a trick to support manual triggering - - const complete = () => { - if (this._isWithActiveTrigger()) { - return; - } - if (!this._isHovered) { - this._disposePopper(); - } - this._element.removeAttribute('aria-describedby'); - EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN)); - }; - this._queueCallback(complete, this.tip, this._isAnimated()); - } - update() { - if (this._popper) { - this._popper.update(); + if (!this._isHovered) { + this._disposeFloating(); } + this._element.removeAttribute('aria-describedby'); + EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN)); + }; + this._queueCallback(complete, this.tip, this._isAnimated()); + } + update() { + if (this._floatingCleanup && this.tip) { + this._updateFloatingPosition(); } + } - // Protected - _isWithContent() { - return Boolean(this._getTitle()); + // Protected + _isWithContent() { + return Boolean(this._getTitle()) || this._hasNewContent(); + } + + // Content supplied via setContent() (a `{ selector: content }` map) overrides + // the configured title/content when rendering, so it should also satisfy the + // show() gate — otherwise a tip whose content is only set via setContent() + // can never be shown. + _hasNewContent() { + return Boolean(this._newContent) && Object.values(this._newContent).some(Boolean); + } + _getTipElement() { + if (!this.tip) { + this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); } - _getTipElement() { - if (!this.tip) { - this.tip = this._createTipElement(this._newContent || this._getContentForTemplate()); - } - return this.tip; + return this.tip; + } + _createTipElement(content) { + const tip = this._getTemplateFactory(content).toHtml(); + tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW); + tip.classList.add(`bs-${this.constructor.NAME}-auto`); + const tipId = getUID(this.constructor.NAME).toString(); + tip.setAttribute('id', tipId); + if (this._isAnimated()) { + tip.classList.add(CLASS_NAME_FADE); + } + return tip; + } + setContent(content) { + this._newContent = content; + if (this._isShown()) { + this._disposeFloating(); + this.show(); } - _createTipElement(content) { - const tip = this._getTemplateFactory(content).toHtml(); + } + _getTemplateFactory(content) { + if (this._templateFactory) { + this._templateFactory.changeContent(content); + } else { + this._templateFactory = new TemplateFactory({ + ...this._config, + // the `content` var has to be after `this._config` + // to override config.content in case of popover + content, + extraClass: this._resolvePossibleFunction(this._config.customClass) + }); + } + return this._templateFactory; + } + _getContentForTemplate() { + return { + [SELECTOR_TOOLTIP_INNER]: this._getTitle() + }; + } + _getTitle() { + return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); + } - // TODO: remove this check in v6 - if (!tip) { - return null; - } - tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW); - // TODO: v6 the following can be achieved with CSS only - tip.classList.add(`bs-${this.constructor.NAME}-auto`); - const tipId = index_js.getUID(this.constructor.NAME).toString(); - tip.setAttribute('id', tipId); - if (this._isAnimated()) { - tip.classList.add(CLASS_NAME_FADE); - } - return tip; + // Private + _initializeOnDelegatedTarget(event) { + return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); + } + _isAnimated() { + return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE); + } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW); + } + _getPlacement(tip) { + // If we have responsive placements, get the one for current viewport + if (this._responsivePlacements) { + const placement = getResponsivePlacement(this._responsivePlacements, 'top'); + return AttachmentMap[placement.toUpperCase()] || placement; } - setContent(content) { - this._newContent = content; + + // Execute placement (can be a function) + const placement = execute(this._config.placement, [this, tip, this._element]); + return AttachmentMap[placement.toUpperCase()] || placement; + } + _parseResponsivePlacements() { + // Only parse if placement is a string (not a function) + if (typeof this._config.placement !== 'string') { + this._responsivePlacements = null; + return; + } + this._responsivePlacements = parseResponsivePlacement(this._config.placement, 'top'); + if (this._responsivePlacements) { + this._setupMediaQueryListeners(); + } + } + _setupMediaQueryListeners() { + this._disposeMediaQueryListeners(); + this._mediaQueryListeners = createBreakpointListeners(() => { if (this._isShown()) { - this._disposePopper(); - this.show(); + this._updateFloatingPosition(); } + }); + } + _disposeMediaQueryListeners() { + disposeBreakpointListeners(this._mediaQueryListeners); + this._mediaQueryListeners = []; + } + async _createFloating(tip) { + const placement = this._getPlacement(tip); + const arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + + // Initial position update + await this._updateFloatingPosition(tip, placement, arrowElement); + + // Set up auto-update for scroll/resize + this._floatingCleanup = autoUpdate(this._element, tip, () => this._updateFloatingPosition(tip, null, arrowElement)); + } + async _updateFloatingPosition(tip = this.tip, placement = null, arrowElement = null) { + if (!tip) { + return; + } + if (!placement) { + placement = this._getPlacement(tip); + } + if (!arrowElement) { + arrowElement = tip.querySelector(`.${this.constructor.NAME}-arrow`); + } + const middleware = this._getFloatingMiddleware(arrowElement); + const floatingConfig = this._getFloatingConfig(placement, middleware); + const { + x, + y, + placement: finalPlacement, + middlewareData + } = await computePosition(this._element, tip, floatingConfig); + + // Apply position to tooltip + Object.assign(tip.style, { + position: 'absolute', + left: `${x}px`, + top: `${y}px` + }); + + // Ensure arrow is absolutely positioned within tooltip + if (arrowElement) { + arrowElement.style.position = 'absolute'; } - _getTemplateFactory(content) { - if (this._templateFactory) { - this._templateFactory.changeContent(content); - } else { - this._templateFactory = new TemplateFactory({ - ...this._config, - // the `content` var has to be after `this._config` - // to override config.content in case of popover - content, - extraClass: this._resolvePossibleFunction(this._config.customClass) - }); - } - return this._templateFactory; + + // Set placement attribute for CSS arrow styling + Manipulator.setDataAttribute(tip, 'placement', finalPlacement); + + // Position arrow along the edge (center it) if present + // The CSS handles which edge to place it on via data-bs-placement + if (arrowElement && middlewareData.arrow) { + const { + x: arrowX, + y: arrowY + } = middlewareData.arrow; + const isVertical = finalPlacement.startsWith('top') || finalPlacement.startsWith('bottom'); + + // Only set the cross-axis position (centering along the edge) + // The main-axis position (which edge) is handled by CSS + Object.assign(arrowElement.style, { + left: isVertical && arrowX !== null ? `${arrowX}px` : '', + top: !isVertical && arrowY !== null ? `${arrowY}px` : '', + // Reset the other axis to let CSS handle it + right: '', + bottom: '' + }); } - _getContentForTemplate() { - return { - [SELECTOR_TOOLTIP_INNER]: this._getTitle() + } + _getOffset() { + const { + offset + } = this._config; + if (typeof offset === 'string') { + return offset.split(',').map(value => Number.parseInt(value, 10)); + } + if (typeof offset === 'function') { + // Floating UI passes different args, adapt the interface for offset function callbacks + return ({ + placement, + rects + }) => { + const result = offset({ + placement, + reference: rects.reference, + floating: rects.floating + }, this._element); + return result; }; } - _getTitle() { - return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title'); - } + return offset; + } + _resolvePossibleFunction(arg) { + return execute(arg, [this._element, this._element]); + } + _getFloatingMiddleware(arrowElement) { + const offsetValue = this._getOffset(); + const middleware = [ + // Offset middleware - handles distance from reference + offset(typeof offsetValue === 'function' ? offsetValue : { + mainAxis: offsetValue[1] || 0, + crossAxis: offsetValue[0] || 0 + }), + // Flip middleware - handles fallback placements + flip({ + fallbackPlacements: this._config.fallbackPlacements + }), + // Shift middleware - prevents overflow + shift({ + boundary: this._config.boundary === 'clippingParents' ? 'clippingAncestors' : this._config.boundary + })]; - // Private - _initializeOnDelegatedTarget(event) { - return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig()); - } - _isAnimated() { - return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE); + // Arrow middleware - positions the arrow element + if (arrowElement) { + middleware.push(arrow({ + element: arrowElement + })); } - _isShown() { - return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW); - } - _createPopper(tip) { - const placement = index_js.execute(this._config.placement, [this, tip, this._element]); - const attachment = AttachmentMap[placement.toUpperCase()]; - return Popper__namespace.createPopper(this._element, tip, this._getPopperConfig(attachment)); - } - _getOffset() { - const { - offset - } = this._config; - if (typeof offset === 'string') { - return offset.split(',').map(value => Number.parseInt(value, 10)); - } - if (typeof offset === 'function') { - return popperData => offset(popperData, this._element); + return middleware; + } + _getFloatingConfig(placement, middleware) { + const defaultConfig = { + placement, + middleware + }; + return { + ...defaultConfig, + ...execute(this._config.floatingConfig, [undefined, defaultConfig]) + }; + } + _setListeners() { + const triggers = this._config.trigger.split(' '); + for (const trigger of triggers) { + if (trigger === 'click') { + EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); + context.toggle(); + }); + } else if (trigger !== TRIGGER_MANUAL) { + const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN); + const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT); + EventHandler.on(this._element, eventIn, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; + context._enter(); + }); + EventHandler.on(this._element, eventOut, this._config.selector, event => { + const context = this._initializeOnDelegatedTarget(event); + context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); + context._leave(); + }); } - return offset; - } - _resolvePossibleFunction(arg) { - return index_js.execute(arg, [this._element, this._element]); - } - _getPopperConfig(attachment) { - const defaultBsPopperConfig = { - placement: attachment, - modifiers: [{ - name: 'flip', - options: { - fallbackPlacements: this._config.fallbackPlacements - } - }, { - name: 'offset', - options: { - offset: this._getOffset() - } - }, { - name: 'preventOverflow', - options: { - boundary: this._config.boundary - } - }, { - name: 'arrow', - options: { - element: `.${this.constructor.NAME}-arrow` - } - }, { - name: 'preSetPlacement', - enabled: true, - phase: 'beforeMain', - fn: data => { - // Pre-set Popper's placement attribute in order to read the arrow sizes properly. - // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement - this._getTipElement().setAttribute('data-popper-placement', data.state.placement); - } - }] - }; - return { - ...defaultBsPopperConfig, - ...index_js.execute(this._config.popperConfig, [undefined, defaultBsPopperConfig]) - }; } - _setListeners() { - const triggers = this._config.trigger.split(' '); - for (const trigger of triggers) { - if (trigger === 'click') { - EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK]); - context.toggle(); - }); - } else if (trigger !== TRIGGER_MANUAL) { - const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN); - const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT); - EventHandler.on(this._element, eventIn, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true; - context._enter(); - }); - EventHandler.on(this._element, eventOut, this._config.selector, event => { - const context = this._initializeOnDelegatedTarget(event); - context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget); - context._leave(); - }); - } + this._hideModalHandler = () => { + if (this._element) { + this.hide(); } - this._hideModalHandler = () => { - if (this._element) { - this.hide(); - } - }; - EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + }; + EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler); + } + _setEscapeListener() { + if (this._keydownHandler) { + return; } - _fixTitle() { - const title = this._element.getAttribute('title'); - if (!title) { + this._keydownHandler = event => { + if (event.key !== ESCAPE_KEY || !this._isShown() || !this.tip.isConnected) { return; } - if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { - this._element.setAttribute('aria-label', title); - } - this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility - this._element.removeAttribute('title'); + + // Dismiss the tooltip and consume the keystroke so it doesn't reach + // ancestor components (e.g. a parent dialog). This way the first Escape + // only closes the tooltip, and a subsequent one can close the dialog — + // matching the behavior of the dropdown menu. + event.preventDefault(); + event.stopPropagation(); + this.hide(); + }; + + // Listen in the capture phase so this runs before the dialog's own keydown + // handler, and on the document so it works regardless of where focus is + // (e.g. for hover-triggered tooltips). EventHandler only uses the capture + // phase for delegated listeners, so attach natively here. + this._element.ownerDocument.addEventListener(EVENT_KEYDOWN, this._keydownHandler, true); + } + _removeEscapeListener() { + if (!this._keydownHandler) { + return; } - _enter() { - if (this._isShown() || this._isHovered) { - this._isHovered = true; - return; - } + this._element.ownerDocument.removeEventListener(EVENT_KEYDOWN, this._keydownHandler, true); + this._keydownHandler = null; + } + _fixTitle() { + const title = this._element.getAttribute('title'); + if (!title) { + return; + } + if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) { + this._element.setAttribute('aria-label', title); + } + this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility + this._element.removeAttribute('title'); + } + _enter() { + if (this._isShown() || this._isHovered) { this._isHovered = true; - this._setTimeout(() => { - if (this._isHovered) { - this.show(); - } - }, this._config.delay.show); + return; } - _leave() { - if (this._isWithActiveTrigger()) { - return; + this._isHovered = true; + this._setTimeout(() => { + if (this._isHovered) { + this.show(); } - this._isHovered = false; - this._setTimeout(() => { - if (!this._isHovered) { - this.hide(); - } - }, this._config.delay.hide); - } - _setTimeout(handler, timeout) { - clearTimeout(this._timeout); - this._timeout = setTimeout(handler, timeout); - } - _isWithActiveTrigger() { - return Object.values(this._activeTrigger).includes(true); - } - _getConfig(config) { - const dataAttributes = Manipulator.getDataAttributes(this._element); - for (const dataAttribute of Object.keys(dataAttributes)) { - if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { - delete dataAttributes[dataAttribute]; - } + }, this._config.delay.show); + } + _leave() { + if (this._isWithActiveTrigger()) { + return; + } + this._isHovered = false; + this._setTimeout(() => { + if (!this._isHovered) { + this.hide(); } - config = { - ...dataAttributes, - ...(typeof config === 'object' && config ? config : {}) + }, this._config.delay.hide); + } + _setTimeout(handler, timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(handler, timeout); + } + _isWithActiveTrigger() { + return Object.values(this._activeTrigger).includes(true); + } + _getConfig(config) { + const dataAttributes = Manipulator.getDataAttributes(this._element); + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute]; + } + } + config = { + ...dataAttributes, + ...(typeof config === 'object' && config ? config : {}) + }; + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + config.container = config.container === false ? document.body : getElement(config.container); + if (typeof config.delay === 'number') { + config.delay = { + show: config.delay, + hide: config.delay }; - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - config.container = config.container === false ? document.body : index_js.getElement(config.container); - if (typeof config.delay === 'number') { - config.delay = { - show: config.delay, - hide: config.delay - }; - } - if (typeof config.title === 'number') { - config.title = config.title.toString(); - } - if (typeof config.content === 'number') { - config.content = config.content.toString(); - } - return config; - } - _getDelegateConfig() { - const config = {}; - for (const [key, value] of Object.entries(this._config)) { - if (this.constructor.Default[key] !== value) { - config[key] = value; - } - } - config.selector = false; - config.trigger = 'manual'; - - // In the future can be replaced with: - // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) - // `Object.fromEntries(keysWithDifferentValues)` - return config; - } - _disposePopper() { - if (this._popper) { - this._popper.destroy(); - this._popper = null; - } - if (this.tip) { - this.tip.remove(); - this.tip = null; + } + + // Coerce number/boolean title and content to strings. `data-bs-title="true"` + // / `data-bs-content="false"` are auto-converted to booleans by the data-API, + // which would otherwise fail the (null|string|element|function) type check. + if (typeof config.title === 'number' || typeof config.title === 'boolean') { + config.title = config.title.toString(); + } + if (typeof config.content === 'number' || typeof config.content === 'boolean') { + config.content = config.content.toString(); + } + return config; + } + _getDelegateConfig() { + const config = {}; + for (const [key, value] of Object.entries(this._config)) { + if (this.constructor.Default[key] !== value) { + config[key] = value; } } + config.selector = false; + config.trigger = 'manual'; - // Static - static jQueryInterface(config) { - return this.each(function () { - const data = Tooltip.getOrCreateInstance(this, config); - if (typeof config !== 'string') { - return; - } - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`); - } - data[config](); - }); + // In the future can be replaced with: + // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]]) + // `Object.fromEntries(keysWithDifferentValues)` + return config; + } + _disposeFloating() { + if (this._floatingCleanup) { + this._floatingCleanup(); + this._floatingCleanup = null; + } + if (this.tip) { + this.tip.remove(); + this.tip = null; } } +} - /** - * jQuery - */ +/** + * Data API implementation - auto-initialize tooltips + */ + +const initTooltip = event => { + const target = event.target.closest(SELECTOR_DATA_TOGGLE); + if (!target) { + return; + } - index_js.defineJQueryPlugin(Tooltip); + // Lazily create the instance. The instance's own `_setListeners()` registers + // the appropriate listeners on the element for the configured triggers + // (hover/focus by default), so we don't mutate `_activeTrigger` or call + // `_enter` here — doing so would show tooltips for triggers the user didn't + // opt into (e.g. `focusin` firing for click-focused buttons in Chromium, + // even when `trigger="hover"` or `trigger="manual"`) and leave stale state + // on `_activeTrigger`. + Tooltip.getOrCreateInstance(target); +}; - return Tooltip; +// Auto-initialize tooltips on first interaction for hover and focus triggers +EventHandler.on(document, EVENT_FOCUSIN, SELECTOR_DATA_TOGGLE, initTooltip); +EventHandler.on(document, EVENT_MOUSEENTER, SELECTOR_DATA_TOGGLE, initTooltip); -})); +export { Tooltip as default }; diff --git a/assets/javascripts/bootstrap/util/backdrop.js b/assets/javascripts/bootstrap/util/backdrop.js deleted file mode 100644 index dad1188a..00000000 --- a/assets/javascripts/bootstrap/util/backdrop.js +++ /dev/null @@ -1,138 +0,0 @@ -/*! - * Bootstrap backdrop.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Backdrop = factory(global.EventHandler, global.Config, global.Index)); -})(this, (function (EventHandler, Config, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/backdrop.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'backdrop'; - const CLASS_NAME_FADE = 'fade'; - const CLASS_NAME_SHOW = 'show'; - const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`; - const Default = { - className: 'modal-backdrop', - clickCallback: null, - isAnimated: false, - isVisible: true, - // if false, we use the backdrop helper without adding any element to the dom - rootElement: 'body' // give the choice to place backdrop under different elements - }; - const DefaultType = { - className: 'string', - clickCallback: '(function|null)', - isAnimated: 'boolean', - isVisible: 'boolean', - rootElement: '(element|string)' - }; - - /** - * Class definition - */ - - class Backdrop extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isAppended = false; - this._element = null; - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - show(callback) { - if (!this._config.isVisible) { - index_js.execute(callback); - return; - } - this._append(); - const element = this._getElement(); - if (this._config.isAnimated) { - index_js.reflow(element); - } - element.classList.add(CLASS_NAME_SHOW); - this._emulateAnimation(() => { - index_js.execute(callback); - }); - } - hide(callback) { - if (!this._config.isVisible) { - index_js.execute(callback); - return; - } - this._getElement().classList.remove(CLASS_NAME_SHOW); - this._emulateAnimation(() => { - this.dispose(); - index_js.execute(callback); - }); - } - dispose() { - if (!this._isAppended) { - return; - } - EventHandler.off(this._element, EVENT_MOUSEDOWN); - this._element.remove(); - this._isAppended = false; - } - - // Private - _getElement() { - if (!this._element) { - const backdrop = document.createElement('div'); - backdrop.className = this._config.className; - if (this._config.isAnimated) { - backdrop.classList.add(CLASS_NAME_FADE); - } - this._element = backdrop; - } - return this._element; - } - _configAfterMerge(config) { - // use getElement() with the default "body" to get a fresh Element on each instantiation - config.rootElement = index_js.getElement(config.rootElement); - return config; - } - _append() { - if (this._isAppended) { - return; - } - const element = this._getElement(); - this._config.rootElement.append(element); - EventHandler.on(element, EVENT_MOUSEDOWN, () => { - index_js.execute(this._config.clickCallback); - }); - this._isAppended = true; - } - _emulateAnimation(callback) { - index_js.executeAfterTransition(callback, this._getElement(), this._config.isAnimated); - } - } - - return Backdrop; - -})); diff --git a/assets/javascripts/bootstrap/util/component-functions.js b/assets/javascripts/bootstrap/util/component-functions.js index d2cd7744..11ccac7b 100644 --- a/assets/javascripts/bootstrap/util/component-functions.js +++ b/assets/javascripts/bootstrap/util/component-functions.js @@ -1,41 +1,63 @@ /*! - * Bootstrap component-functions.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap component-functions.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['exports', '../dom/event-handler', '../dom/selector-engine', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ComponentFunctions = {}, global.EventHandler, global.SelectorEngine, global.Index)); -})(this, (function (exports, EventHandler, SelectorEngine, index_js) { 'use strict'; +import EventHandler from '../dom/event-handler.js'; +import SelectorEngine from '../dom/selector-engine.js'; +import { isDisabled } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/component-functions.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/component-functions.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - const enableDismissTrigger = (component, method = 'hide') => { - const clickEvent = `click.dismiss${component.EVENT_KEY}`; - const name = component.NAME; - EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { - if (['A', 'AREA'].includes(this.tagName)) { - event.preventDefault(); - } - if (index_js.isDisabled(this)) { - return; - } - const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); - const instance = component.getOrCreateInstance(target); +const enableDismissTrigger = (component, method = 'hide') => { + const clickEvent = `click.dismiss${component.EVENT_KEY}`; + const name = component.NAME; + EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + if (isDisabled(this)) { + return; + } + const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`); + const instance = component.getOrCreateInstance(target); - // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method + instance[method](); + }); +}; +const eventActionOnPlugin = (Plugin, onEvent, stringSelector, method, callback = null) => { + eventAction(`${onEvent}.${Plugin.NAME}`, stringSelector, data => { + const instances = data.targets.filter(Boolean).map(element => Plugin.getOrCreateInstance(element)); + if (typeof callback === 'function') { + callback({ + ...data, + instances + }); + } + for (const instance of instances) { instance[method](); + } + }); +}; +const eventAction = (onEvent, stringSelector, callback) => { + const selector = `${stringSelector}:not(.disabled):not(:disabled)`; + EventHandler.on(document, onEvent, selector, function (event) { + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault(); + } + const selector = SelectorEngine.getSelectorFromElement(this); + const targets = selector ? SelectorEngine.find(selector) : [this]; + callback({ + targets, + event }); - }; + }); +}; - exports.enableDismissTrigger = enableDismissTrigger; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); +export { enableDismissTrigger, eventActionOnPlugin }; diff --git a/assets/javascripts/bootstrap/util/config.js b/assets/javascripts/bootstrap/util/config.js index b35fc078..a9c141c1 100644 --- a/assets/javascripts/bootstrap/util/config.js +++ b/assets/javascripts/bootstrap/util/config.js @@ -1,67 +1,62 @@ /*! - * Bootstrap config.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap config.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/manipulator', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Config = factory(global.Manipulator, global.Index)); -})(this, (function (Manipulator, index_js) { 'use strict'; +import Manipulator from '../dom/manipulator.js'; +import { isElement, toType } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/config.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/config.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Class definition - */ +/** + * Class definition + */ - class Config { - // Getters - static get Default() { - return {}; - } - static get DefaultType() { - return {}; - } - static get NAME() { - throw new Error('You have to implement the static method "NAME", for each component!'); - } - _getConfig(config) { - config = this._mergeConfigObj(config); - config = this._configAfterMerge(config); - this._typeCheckConfig(config); - return config; - } - _configAfterMerge(config) { - return config; - } - _mergeConfigObj(config, element) { - const jsonConfig = index_js.isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse +class Config { + // Getters + static get Default() { + return {}; + } + static get DefaultType() { + return {}; + } + static get NAME() { + throw new Error('You have to implement the static method "NAME", for each component!'); + } + _getConfig(config) { + config = this._mergeConfigObj(config); + config = this._configAfterMerge(config); + this._typeCheckConfig(config); + return config; + } + _configAfterMerge(config) { + return config; + } + _mergeConfigObj(config, element) { + const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse - return { - ...this.constructor.Default, - ...(typeof jsonConfig === 'object' ? jsonConfig : {}), - ...(index_js.isElement(element) ? Manipulator.getDataAttributes(element) : {}), - ...(typeof config === 'object' ? config : {}) - }; - } - _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { - for (const [property, expectedTypes] of Object.entries(configTypes)) { - const value = config[property]; - const valueType = index_js.isElement(value) ? 'element' : index_js.toType(value); - if (!new RegExp(expectedTypes).test(valueType)) { - throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); - } + return { + ...this.constructor.Default, + ...(typeof jsonConfig === 'object' ? jsonConfig : {}), + ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}), + ...(typeof config === 'object' ? config : {}) + }; + } + _typeCheckConfig(config, configTypes = this.constructor.DefaultType) { + for (const [property, expectedTypes] of Object.entries(configTypes)) { + const value = config[property]; + const valueType = isElement(value) ? 'element' : toType(value); + if (!new RegExp(expectedTypes).test(valueType)) { + throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); } } } +} - return Config; - -})); +export { Config as default }; diff --git a/assets/javascripts/bootstrap/util/floating-ui.js b/assets/javascripts/bootstrap/util/floating-ui.js new file mode 100644 index 00000000..62e06bd2 --- /dev/null +++ b/assets/javascripts/bootstrap/util/floating-ui.js @@ -0,0 +1,137 @@ +/*! + * Bootstrap floating-ui.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +import { isRTL } from './index.js'; + +/** + * -------------------------------------------------------------------------- + * Bootstrap util/floating-ui.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + + +/** + * Breakpoints for responsive placement (matches SCSS $breakpoints) + */ +const BREAKPOINTS = { + sm: 576, + md: 768, + lg: 1024, + xl: 1280, + '2xl': 1536 +}; + +/** + * Default placement with RTL support + */ +const getDefaultPlacement = (fallback = 'bottom') => { + if (fallback.includes('-start') || fallback.includes('-end')) { + const [side, alignment] = fallback.split('-'); + const flippedAlignment = alignment === 'start' ? 'end' : 'start'; + return isRTL() ? `${side}-${flippedAlignment}` : fallback; + } + return fallback; +}; + +/** + * Parse a placement string that may contain responsive prefixes + * Example: "bottom-start md:top-end lg:right" returns { xs: 'bottom-start', md: 'top-end', lg: 'right' } + * + * @param {string} placementString - The placement string to parse + * @param {string} defaultPlacement - The default placement to use for xs/base + * @returns {object|null} - Object with breakpoint keys and placement values, or null if not responsive + */ +const parseResponsivePlacement = (placementString, defaultPlacement = 'bottom') => { + // Check if placement contains responsive prefixes (e.g., "bottom-start md:top-end") + if (!placementString || !placementString.includes(':')) { + return null; + } + + // Parse the placement string into breakpoint-keyed object + const parts = placementString.split(/\s+/); + const placements = { + xs: defaultPlacement + }; // Default fallback + + for (const part of parts) { + if (part.includes(':')) { + // Responsive placement like "md:top-end" + const [breakpoint, placement] = part.split(':'); + if (BREAKPOINTS[breakpoint] !== undefined) { + placements[breakpoint] = placement; + } + } else { + // Base placement (no prefix = xs/default) + placements.xs = part; + } + } + return placements; +}; + +/** + * Get the active placement for the current viewport width + * + * @param {object} responsivePlacements - Object with breakpoint keys and placement values + * @param {string} defaultPlacement - Fallback placement + * @returns {string} - The active placement for current viewport + */ +const getResponsivePlacement = (responsivePlacements, defaultPlacement = 'bottom') => { + if (!responsivePlacements) { + return defaultPlacement; + } + + // Get current viewport width + const viewportWidth = window.innerWidth; + + // Find the largest breakpoint that matches + let activePlacement = responsivePlacements.xs || defaultPlacement; + + // Check breakpoints in order (sm, md, lg, xl, 2xl) + const breakpointOrder = ['sm', 'md', 'lg', 'xl', '2xl']; + for (const breakpoint of breakpointOrder) { + const minWidth = BREAKPOINTS[breakpoint]; + if (viewportWidth >= minWidth && responsivePlacements[breakpoint]) { + activePlacement = responsivePlacements[breakpoint]; + } + } + return activePlacement; +}; + +/** + * Create media query listeners for responsive placement changes + * + * @param {Function} callback - Callback to run when breakpoint changes + * @returns {Array} - Array of { mql, handler } objects for cleanup + */ +const createBreakpointListeners = callback => { + const listeners = []; + for (const breakpoint of Object.keys(BREAKPOINTS)) { + const minWidth = BREAKPOINTS[breakpoint]; + const mql = window.matchMedia(`(min-width: ${minWidth}px)`); + mql.addEventListener('change', callback); + listeners.push({ + mql, + handler: callback + }); + } + return listeners; +}; + +/** + * Clean up media query listeners + * + * @param {Array} listeners - Array of { mql, handler } objects + */ +const disposeBreakpointListeners = listeners => { + for (const { + mql, + handler + } of listeners) { + mql.removeEventListener('change', handler); + } +}; + +export { BREAKPOINTS, createBreakpointListeners, disposeBreakpointListeners, getDefaultPlacement, getResponsivePlacement, parseResponsivePlacement }; diff --git a/assets/javascripts/bootstrap/util/focustrap.js b/assets/javascripts/bootstrap/util/focustrap.js deleted file mode 100644 index efad33db..00000000 --- a/assets/javascripts/bootstrap/util/focustrap.js +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * Bootstrap focustrap.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('../dom/selector-engine.js'), require('./config.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', '../dom/selector-engine', './config'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Focustrap = factory(global.EventHandler, global.SelectorEngine, global.Config)); -})(this, (function (EventHandler, SelectorEngine, Config) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/focustrap.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const NAME = 'focustrap'; - const DATA_KEY = 'bs.focustrap'; - const EVENT_KEY = `.${DATA_KEY}`; - const EVENT_FOCUSIN = `focusin${EVENT_KEY}`; - const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`; - const TAB_KEY = 'Tab'; - const TAB_NAV_FORWARD = 'forward'; - const TAB_NAV_BACKWARD = 'backward'; - const Default = { - autofocus: true, - trapElement: null // The element to trap focus inside of - }; - const DefaultType = { - autofocus: 'boolean', - trapElement: 'element' - }; - - /** - * Class definition - */ - - class FocusTrap extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - this._isActive = false; - this._lastTabNavDirection = null; - } - - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } - - // Public - activate() { - if (this._isActive) { - return; - } - if (this._config.autofocus) { - this._config.trapElement.focus(); - } - EventHandler.off(document, EVENT_KEY); // guard against infinite focus loop - EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event)); - EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event)); - this._isActive = true; - } - deactivate() { - if (!this._isActive) { - return; - } - this._isActive = false; - EventHandler.off(document, EVENT_KEY); - } - - // Private - _handleFocusin(event) { - const { - trapElement - } = this._config; - if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { - return; - } - const elements = SelectorEngine.focusableChildren(trapElement); - if (elements.length === 0) { - trapElement.focus(); - } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) { - elements[elements.length - 1].focus(); - } else { - elements[0].focus(); - } - } - _handleKeydown(event) { - if (event.key !== TAB_KEY) { - return; - } - this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD; - } - } - - return FocusTrap; - -})); diff --git a/assets/javascripts/bootstrap/util/index.js b/assets/javascripts/bootstrap/util/index.js index 0d31eed0..57bee61d 100644 --- a/assets/javascripts/bootstrap/util/index.js +++ b/assets/javascripts/bootstrap/util/index.js @@ -1,280 +1,226 @@ /*! - * Bootstrap index.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap index.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Index = {})); -})(this, (function (exports) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/index.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - const MAX_UID = 1000000; - const MILLISECONDS_MULTIPLIER = 1000; - const TRANSITION_END = 'transitionend'; - - /** - * Properly escape IDs selectors to handle weird IDs - * @param {string} selector - * @returns {string} - */ - const parseSelector = selector => { - if (selector && window.CSS && window.CSS.escape) { - // document.querySelector needs escaping to handle IDs (html5+) containing for instance / - selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); - } - return selector; - }; - - // Shout-out Angus Croll (https://goo.gl/pxwQGp) - const toType = object => { - if (object === null || object === undefined) { - return `${object}`; - } - return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); - }; - - /** - * Public Util API - */ - - const getUID = prefix => { - do { - prefix += Math.floor(Math.random() * MAX_UID); - } while (document.getElementById(prefix)); - return prefix; - }; - const getTransitionDurationFromElement = element => { - if (!element) { - return 0; - } - - // Get transition-duration of the element - let { - transitionDuration, - transitionDelay - } = window.getComputedStyle(element); - const floatTransitionDuration = Number.parseFloat(transitionDuration); - const floatTransitionDelay = Number.parseFloat(transitionDelay); - - // Return 0 if element or transition duration is not found - if (!floatTransitionDuration && !floatTransitionDelay) { - return 0; - } - - // If multiple durations are defined, take the first - transitionDuration = transitionDuration.split(',')[0]; - transitionDelay = transitionDelay.split(',')[0]; - return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; - }; - const triggerTransitionEnd = element => { - element.dispatchEvent(new Event(TRANSITION_END)); - }; - const isElement = object => { - if (!object || typeof object !== 'object') { +/** + * -------------------------------------------------------------------------- + * Bootstrap util/index.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ + +const MAX_UID = 1_000_000; +const MILLISECONDS_MULTIPLIER = 1000; +const TRANSITION_END = 'transitionend'; + +/** + * Properly escape IDs selectors to handle weird IDs + * @param {string} selector + * @returns {string} + */ +const parseSelector = selector => { + if (selector && window.CSS && window.CSS.escape) { + // document.querySelector needs escaping to handle IDs (html5+) containing for instance / + selector = selector.replace(/#([^\s"#']+)/g, (match, id) => `#${CSS.escape(id)}`); + } + return selector; +}; + +// Shout-out Angus Croll (https://goo.gl/pxwQGp) +const toType = object => { + if (object === null || object === undefined) { + return `${object}`; + } + return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase(); +}; + +/** + * Public Util API + */ + +const getUID = prefix => { + do { + prefix += Math.floor(Math.random() * MAX_UID); + } while (document.getElementById(prefix)); + return prefix; +}; +const getTransitionDurationFromElement = element => { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { + transitionDuration, + transitionDelay + } = window.getComputedStyle(element); + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + transitionDuration = transitionDuration.split(',')[0]; + transitionDelay = transitionDelay.split(',')[0]; + return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER; +}; +const triggerTransitionEnd = element => { + element.dispatchEvent(new Event(TRANSITION_END)); +}; +const isElement = object => { + if (!object || typeof object !== 'object') { + return false; + } + return typeof object.nodeType !== 'undefined'; +}; +const getElement = object => { + if (isElement(object)) { + return object; + } + if (typeof object === 'string' && object.length > 0) { + return document.querySelector(parseSelector(object)); + } + return null; +}; +const isVisible = element => { + if (!isElement(element) || element.getClientRects().length === 0) { + return false; + } + const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; + // Handle `details` element as its content may falsely appear visible when it is closed + const closedDetails = element.closest('details:not([open])'); + if (!closedDetails) { + return elementIsVisible; + } + if (closedDetails !== element) { + const summary = element.closest('summary'); + if (summary && summary.parentNode !== closedDetails) { return false; } - if (typeof object.jquery !== 'undefined') { - object = object[0]; - } - return typeof object.nodeType !== 'undefined'; - }; - const getElement = object => { - // it's a jQuery object or a node element - if (isElement(object)) { - return object.jquery ? object[0] : object; - } - if (typeof object === 'string' && object.length > 0) { - return document.querySelector(parseSelector(object)); - } - return null; - }; - const isVisible = element => { - if (!isElement(element) || element.getClientRects().length === 0) { + if (summary === null) { return false; } - const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'; - // Handle `details` element as its content may falsie appear visible when it is closed - const closedDetails = element.closest('details:not([open])'); - if (!closedDetails) { - return elementIsVisible; - } - if (closedDetails !== element) { - const summary = element.closest('summary'); - if (summary && summary.parentNode !== closedDetails) { - return false; - } - if (summary === null) { - return false; - } - } - return elementIsVisible; - }; - const isDisabled = element => { - if (!element || element.nodeType !== Node.ELEMENT_NODE) { - return true; - } - if (element.classList.contains('disabled')) { - return true; - } - if (typeof element.disabled !== 'undefined') { - return element.disabled; - } - return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; - }; - const findShadowRoot = element => { - if (!document.documentElement.attachShadow) { - return null; - } - - // Can find the shadow root otherwise it'll return the document - if (typeof element.getRootNode === 'function') { - const root = element.getRootNode(); - return root instanceof ShadowRoot ? root : null; - } - if (element instanceof ShadowRoot) { - return element; - } - - // when we don't find a shadow root - if (!element.parentNode) { - return null; - } - return findShadowRoot(element.parentNode); - }; - const noop = () => {}; - - /** - * Trick to restart an element's animation - * - * @param {HTMLElement} element - * @return void - * - * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation - */ - const reflow = element => { - element.offsetHeight; // eslint-disable-line no-unused-expressions - }; - const getjQuery = () => { - if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { - return window.jQuery; - } + } + return elementIsVisible; +}; +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true; + } + if (element.classList.contains('disabled')) { + return true; + } + if (typeof element.disabled !== 'undefined') { + return element.disabled; + } + return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'; +}; +const findShadowRoot = element => { + if (!document.documentElement.attachShadow) { return null; - }; - const DOMContentLoadedCallbacks = []; - const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { - // add listener on the first call when the document is in loading state - if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { - for (const callback of DOMContentLoadedCallbacks) { - callback(); - } - }); - } - DOMContentLoadedCallbacks.push(callback); - } else { - callback(); - } - }; - const isRTL = () => document.documentElement.dir === 'rtl'; - const defineJQueryPlugin = plugin => { - onDOMContentLoaded(() => { - const $ = getjQuery(); - /* istanbul ignore if */ - if ($) { - const name = plugin.NAME; - const JQUERY_NO_CONFLICT = $.fn[name]; - $.fn[name] = plugin.jQueryInterface; - $.fn[name].Constructor = plugin; - $.fn[name].noConflict = () => { - $.fn[name] = JQUERY_NO_CONFLICT; - return plugin.jQueryInterface; - }; - } - }); - }; - const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { - return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; - }; - const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { - if (!waitForTransition) { - execute(callback); + } + + // Can find the shadow root otherwise it'll return the document + if (typeof element.getRootNode === 'function') { + const root = element.getRootNode(); + return root instanceof ShadowRoot ? root : null; + } + if (element instanceof ShadowRoot) { + return element; + } + + // when we don't find a shadow root + if (!element.parentNode) { + return null; + } + return findShadowRoot(element.parentNode); +}; +const noop = () => {}; + +/** + * Trick to restart an element's animation + * + * @param {HTMLElement} element + * @return void + * + * @see https://www.harrytheo.com/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation + */ +const reflow = element => { + element.offsetHeight; // eslint-disable-line no-unused-expressions +}; +const DOMContentLoadedCallbacks = []; +const onDOMContentLoaded = callback => { + if (document.readyState === 'loading') { + // add listener on the first call when the document is in loading state + if (!DOMContentLoadedCallbacks.length) { + document.addEventListener('DOMContentLoaded', () => { + for (const callback of DOMContentLoadedCallbacks) { + callback(); + } + }); + } + DOMContentLoadedCallbacks.push(callback); + } else { + callback(); + } +}; +const isRTL = () => document.documentElement.dir === 'rtl'; +const execute = (possibleCallback, args = [], defaultValue = possibleCallback) => { + return typeof possibleCallback === 'function' ? possibleCallback.call(...args) : defaultValue; +}; +const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => { + if (!waitForTransition) { + execute(callback); + return; + } + const durationPadding = 5; + const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; + let called = false; + const handler = ({ + target + }) => { + if (target !== transitionElement) { return; } - const durationPadding = 5; - const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding; - let called = false; - const handler = ({ - target - }) => { - if (target !== transitionElement) { - return; - } - called = true; - transitionElement.removeEventListener(TRANSITION_END, handler); - execute(callback); - }; - transitionElement.addEventListener(TRANSITION_END, handler); - setTimeout(() => { - if (!called) { - triggerTransitionEnd(transitionElement); - } - }, emulatedDuration); - }; - - /** - * Return the previous/next element of a list. - * - * @param {array} list The list of elements - * @param activeElement The active element - * @param shouldGetNext Choose to get next or previous element - * @param isCycleAllowed - * @return {Element|elem} The proper element - */ - const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { - const listLength = list.length; - let index = list.indexOf(activeElement); - - // if the element does not exist in the list return an element - // depending on the direction and if cycle is allowed - if (index === -1) { - return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; - } - index += shouldGetNext ? 1 : -1; - if (isCycleAllowed) { - index = (index + listLength) % listLength; - } - return list[Math.max(0, Math.min(index, listLength - 1))]; - }; - - exports.defineJQueryPlugin = defineJQueryPlugin; - exports.execute = execute; - exports.executeAfterTransition = executeAfterTransition; - exports.findShadowRoot = findShadowRoot; - exports.getElement = getElement; - exports.getNextActiveElement = getNextActiveElement; - exports.getTransitionDurationFromElement = getTransitionDurationFromElement; - exports.getUID = getUID; - exports.getjQuery = getjQuery; - exports.isDisabled = isDisabled; - exports.isElement = isElement; - exports.isRTL = isRTL; - exports.isVisible = isVisible; - exports.noop = noop; - exports.onDOMContentLoaded = onDOMContentLoaded; - exports.parseSelector = parseSelector; - exports.reflow = reflow; - exports.toType = toType; - exports.triggerTransitionEnd = triggerTransitionEnd; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); + called = true; + transitionElement.removeEventListener(TRANSITION_END, handler); + execute(callback); + }; + transitionElement.addEventListener(TRANSITION_END, handler); + setTimeout(() => { + if (!called) { + triggerTransitionEnd(transitionElement); + } + }, emulatedDuration); +}; + +/** + * Return the previous/next element of a list. + * + * @param {array} list The list of elements + * @param activeElement The active element + * @param shouldGetNext Choose to get next or previous element + * @param isCycleAllowed + * @return {Element|elem} The proper element + */ +const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => { + const listLength = list.length; + let index = list.indexOf(activeElement); + + // if the element does not exist in the list return an element + // depending on the direction and if cycle is allowed + if (index === -1) { + return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]; + } + index += shouldGetNext ? 1 : -1; + if (isCycleAllowed) { + index = (index + listLength) % listLength; + } + return list[Math.max(0, Math.min(index, listLength - 1))]; +}; + +export { execute, executeAfterTransition, findShadowRoot, getElement, getNextActiveElement, getTransitionDurationFromElement, getUID, isDisabled, isElement, isRTL, isVisible, noop, onDOMContentLoaded, parseSelector, reflow, toType, triggerTransitionEnd }; diff --git a/assets/javascripts/bootstrap/util/sanitizer.js b/assets/javascripts/bootstrap/util/sanitizer.js index cf6a02d8..82320432 100644 --- a/assets/javascripts/bootstrap/util/sanitizer.js +++ b/assets/javascripts/bootstrap/util/sanitizer.js @@ -1,112 +1,109 @@ /*! - * Bootstrap sanitizer.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap sanitizer.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Sanitizer = {})); -})(this, (function (exports) { 'use strict'; +/** + * -------------------------------------------------------------------------- + * Bootstrap util/sanitizer.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * -------------------------------------------------------------------------- - * Bootstrap util/sanitizer.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +// js-docs-start allow-list +const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; +const DefaultAllowlist = { + // Global attributes allowed on any supplied element below. + '*': ['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: [] +}; +// js-docs-end allow-list - // js-docs-start allow-list - const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i; - const DefaultAllowlist = { - // Global attributes allowed on any supplied element below. - '*': ['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: [] - }; - // 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']); +/** + * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation + * contexts. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 + */ +const SAFE_URL_PATTERN = /^(?!(?:javascript|data|vbscript):)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - /** - * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation - * contexts. - * - * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38 - */ - const SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i; - const allowedAttribute = (attribute, allowedAttributeList) => { - const attributeName = attribute.nodeName.toLowerCase(); - if (allowedAttributeList.includes(attributeName)) { - if (uriAttributes.has(attributeName)) { - return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue)); - } - return true; +/** + * A pattern that matches safe data URLs. Only matches image, video and audio + * types — notably NOT `data:text/html`, which is an XSS vector. + * + * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L49 + */ +const DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z=]+$/i; +const allowedAttribute = (attribute, allowedAttributeList) => { + const attributeName = attribute.nodeName.toLowerCase(); + if (allowedAttributeList.includes(attributeName)) { + if (uriAttributes.has(attributeName)) { + return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue) || DATA_URL_PATTERN.test(attribute.nodeValue)); } + return true; + } - // Check if a regular expression validates the attribute. - return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); - }; - function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { - if (!unsafeHtml.length) { - return unsafeHtml; - } - if (sanitizeFunction && typeof sanitizeFunction === 'function') { - return sanitizeFunction(unsafeHtml); + // Check if a regular expression validates the attribute. + return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName)); +}; +function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { + if (!unsafeHtml.length) { + return unsafeHtml; + } + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml); + } + const domParser = new window.DOMParser(); + const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); + const elements = [...createdDocument.body.querySelectorAll('*')]; + for (const element of elements) { + const elementName = element.nodeName.toLowerCase(); + if (!Object.keys(allowList).includes(elementName)) { + element.remove(); + continue; } - const domParser = new window.DOMParser(); - const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html'); - const elements = [].concat(...createdDocument.body.querySelectorAll('*')); - for (const element of elements) { - const elementName = element.nodeName.toLowerCase(); - if (!Object.keys(allowList).includes(elementName)) { - element.remove(); - continue; - } - const attributeList = [].concat(...element.attributes); - const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []); - for (const attribute of attributeList) { - if (!allowedAttribute(attribute, allowedAttributes)) { - element.removeAttribute(attribute.nodeName); - } + const attributeList = [...element.attributes]; + const allowedAttributes = [...(allowList['*'] || []), ...(allowList[elementName] || [])]; + for (const attribute of attributeList) { + if (!allowedAttribute(attribute, allowedAttributes)) { + element.removeAttribute(attribute.nodeName); } } - return createdDocument.body.innerHTML; } + return createdDocument.body.innerHTML; +} - exports.DefaultAllowlist = DefaultAllowlist; - exports.sanitizeHtml = sanitizeHtml; - - Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); - -})); +export { DefaultAllowlist, sanitizeHtml }; diff --git a/assets/javascripts/bootstrap/util/scrollbar.js b/assets/javascripts/bootstrap/util/scrollbar.js deleted file mode 100644 index f99e3dc7..00000000 --- a/assets/javascripts/bootstrap/util/scrollbar.js +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * Bootstrap scrollbar.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/manipulator.js'), require('../dom/selector-engine.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/manipulator', '../dom/selector-engine', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollbar = factory(global.Manipulator, global.SelectorEngine, global.Index)); -})(this, (function (Manipulator, SelectorEngine, index_js) { 'use strict'; - - /** - * -------------------------------------------------------------------------- - * Bootstrap util/scrollBar.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - - - /** - * Constants - */ - - const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'; - const SELECTOR_STICKY_CONTENT = '.sticky-top'; - const PROPERTY_PADDING = 'padding-right'; - const PROPERTY_MARGIN = 'margin-right'; - - /** - * Class definition - */ - - class ScrollBarHelper { - constructor() { - this._element = document.body; - } - - // Public - getWidth() { - // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes - const documentWidth = document.documentElement.clientWidth; - return Math.abs(window.innerWidth - documentWidth); - } - hide() { - const width = this.getWidth(); - this._disableOverFlow(); - // give padding to element to balance the hidden scrollbar width - this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth - this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width); - this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width); - } - reset() { - this._resetElementAttributes(this._element, 'overflow'); - this._resetElementAttributes(this._element, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING); - this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN); - } - isOverflowing() { - return this.getWidth() > 0; - } - - // Private - _disableOverFlow() { - this._saveInitialAttribute(this._element, 'overflow'); - this._element.style.overflow = 'hidden'; - } - _setElementAttributes(selector, styleProperty, callback) { - const scrollbarWidth = this.getWidth(); - const manipulationCallBack = element => { - if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { - return; - } - this._saveInitialAttribute(element, styleProperty); - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty); - element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`); - }; - this._applyManipulationCallback(selector, manipulationCallBack); - } - _saveInitialAttribute(element, styleProperty) { - const actualValue = element.style.getPropertyValue(styleProperty); - if (actualValue) { - Manipulator.setDataAttribute(element, styleProperty, actualValue); - } - } - _resetElementAttributes(selector, styleProperty) { - const manipulationCallBack = element => { - const value = Manipulator.getDataAttribute(element, styleProperty); - // We only want to remove the property if the value is `null`; the value can also be zero - if (value === null) { - element.style.removeProperty(styleProperty); - return; - } - Manipulator.removeDataAttribute(element, styleProperty); - element.style.setProperty(styleProperty, value); - }; - this._applyManipulationCallback(selector, manipulationCallBack); - } - _applyManipulationCallback(selector, callBack) { - if (index_js.isElement(selector)) { - callBack(selector); - return; - } - for (const sel of SelectorEngine.find(selector, this._element)) { - callBack(sel); - } - } - } - - return ScrollBarHelper; - -})); diff --git a/assets/javascripts/bootstrap/util/swipe.js b/assets/javascripts/bootstrap/util/swipe.js index 6c296425..f02c778f 100644 --- a/assets/javascripts/bootstrap/util/swipe.js +++ b/assets/javascripts/bootstrap/util/swipe.js @@ -1,134 +1,159 @@ /*! - * Bootstrap swipe.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap swipe.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/event-handler.js'), require('./config.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/event-handler', './config', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Swipe = factory(global.EventHandler, global.Config, global.Index)); -})(this, (function (EventHandler, Config, index_js) { 'use strict'; +import EventHandler from '../dom/event-handler.js'; +import Config from './config.js'; +import { execute } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/swipe.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/swipe.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'swipe'; - const EVENT_KEY = '.bs.swipe'; - const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`; - const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`; - const EVENT_TOUCHEND = `touchend${EVENT_KEY}`; - const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; - const EVENT_POINTERUP = `pointerup${EVENT_KEY}`; - const POINTER_TYPE_TOUCH = 'touch'; - const POINTER_TYPE_PEN = 'pen'; - const CLASS_NAME_POINTER_EVENT = 'pointer-event'; - const SWIPE_THRESHOLD = 40; - const Default = { - endCallback: null, - leftCallback: null, - rightCallback: null - }; - const DefaultType = { - endCallback: '(function|null)', - leftCallback: '(function|null)', - rightCallback: '(function|null)' - }; +const NAME = 'swipe'; +const EVENT_KEY = '.bs.swipe'; +const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`; +const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`; +const EVENT_TOUCHEND = `touchend${EVENT_KEY}`; +const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`; +const EVENT_POINTERUP = `pointerup${EVENT_KEY}`; +const POINTER_TYPE_TOUCH = 'touch'; +const POINTER_TYPE_PEN = 'pen'; +const CLASS_NAME_POINTER_EVENT = 'pointer-event'; +const SWIPE_THRESHOLD = 40; +const Default = { + endCallback: null, + leftCallback: null, + rightCallback: null, + upCallback: null, + downCallback: null +}; +const DefaultType = { + endCallback: '(function|null)', + leftCallback: '(function|null)', + rightCallback: '(function|null)', + upCallback: '(function|null)', + downCallback: '(function|null)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class Swipe extends Config { - constructor(element, config) { - super(); - this._element = element; - if (!element || !Swipe.isSupported()) { - return; - } - this._config = this._getConfig(config); - this._deltaX = 0; - this._supportPointerEvents = Boolean(window.PointerEvent); - this._initEvents(); +class Swipe extends Config { + constructor(element, config) { + super(); + this._element = element; + if (!element || !Swipe.isSupported()) { + return; } + this._config = this._getConfig(config); + this._deltaX = 0; + this._deltaY = 0; + this._supportPointerEvents = Boolean(window.PointerEvent); + this._initEvents(); + } + + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Getters - static get Default() { - return Default; + // Public + dispose() { + EventHandler.off(this._element, EVENT_KEY); + } + + // Private + _start(event) { + if (!this._supportPointerEvents) { + this._deltaX = event.touches[0].clientX; + this._deltaY = event.touches[0].clientY; + return; } - static get DefaultType() { - return DefaultType; + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX; + this._deltaY = event.clientY; } - static get NAME() { - return NAME; + } + _end(event) { + if (this._eventIsPointerPenTouch(event)) { + this._deltaX = event.clientX - this._deltaX; + this._deltaY = event.clientY - this._deltaY; } - - // Public - dispose() { - EventHandler.off(this._element, EVENT_KEY); + this._handleSwipe(); + execute(this._config.endCallback); + } + _move(event) { + if (event.touches && event.touches.length > 1) { + this._deltaX = 0; + this._deltaY = 0; + return; } + this._deltaX = event.touches[0].clientX - this._deltaX; + this._deltaY = event.touches[0].clientY - this._deltaY; + } + _handleSwipe() { + const absDeltaX = Math.abs(this._deltaX); + const absDeltaY = Math.abs(this._deltaY); - // Private - _start(event) { - if (!this._supportPointerEvents) { - this._deltaX = event.touches[0].clientX; - return; - } - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX; - } - } - _end(event) { - if (this._eventIsPointerPenTouch(event)) { - this._deltaX = event.clientX - this._deltaX; - } - this._handleSwipe(); - index_js.execute(this._config.endCallback); - } - _move(event) { - this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX; + // Determine primary axis: whichever has greater movement wins + if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) { + // Vertical swipe + const direction = this._deltaY > 0 ? 'down' : 'up'; + this._deltaX = 0; + this._deltaY = 0; + execute(direction === 'down' ? this._config.downCallback : this._config.upCallback); + return; } - _handleSwipe() { - const absDeltaX = Math.abs(this._deltaX); - if (absDeltaX <= SWIPE_THRESHOLD) { - return; - } + if (absDeltaX > SWIPE_THRESHOLD) { + // Horizontal swipe const direction = absDeltaX / this._deltaX; this._deltaX = 0; + this._deltaY = 0; if (!direction) { return; } - index_js.execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback); + return; } - _initEvents() { - if (this._supportPointerEvents) { - EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); - EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); - this._element.classList.add(CLASS_NAME_POINTER_EVENT); - } else { - EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); - EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); - EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); - } - } - _eventIsPointerPenTouch(event) { - return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); - } - - // Static - static isSupported() { - return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + this._deltaX = 0; + this._deltaY = 0; + } + _initEvents() { + if (this._supportPointerEvents) { + EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event)); + EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event)); + this._element.classList.add(CLASS_NAME_POINTER_EVENT); + } else { + EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event)); + EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event)); + EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event)); } } + _eventIsPointerPenTouch(event) { + return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH); + } - return Swipe; + // Static + static isSupported() { + return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0; + } +} -})); +export { Swipe as default }; diff --git a/assets/javascripts/bootstrap/util/template-factory.js b/assets/javascripts/bootstrap/util/template-factory.js index b15e31d1..36ca1c04 100644 --- a/assets/javascripts/bootstrap/util/template-factory.js +++ b/assets/javascripts/bootstrap/util/template-factory.js @@ -1,150 +1,147 @@ /*! - * Bootstrap template-factory.js v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap template-factory.js v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('../dom/selector-engine.js'), require('./config.js'), require('./sanitizer.js'), require('./index.js')) : - typeof define === 'function' && define.amd ? define(['../dom/selector-engine', './config', './sanitizer', './index'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TemplateFactory = factory(global.SelectorEngine, global.Config, global.Sanitizer, global.Index)); -})(this, (function (SelectorEngine, Config, sanitizer_js, index_js) { 'use strict'; +import SelectorEngine from '../dom/selector-engine.js'; +import Config from './config.js'; +import { DefaultAllowlist, sanitizeHtml } from './sanitizer.js'; +import { isElement, getElement, execute } from './index.js'; - /** - * -------------------------------------------------------------------------- - * Bootstrap util/template-factory.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ +/** + * -------------------------------------------------------------------------- + * Bootstrap util/template-factory.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + * -------------------------------------------------------------------------- + */ - /** - * Constants - */ +/** + * Constants + */ - const NAME = 'TemplateFactory'; - const Default = { - allowList: sanitizer_js.DefaultAllowlist, - content: {}, - // { selector : text , selector2 : text2 , } - extraClass: '', - html: false, - sanitize: true, - sanitizeFn: null, - template: '' - }; - const DefaultType = { - allowList: 'object', - content: 'object', - extraClass: '(string|function)', - html: 'boolean', - sanitize: 'boolean', - sanitizeFn: '(null|function)', - template: 'string' - }; - const DefaultContentType = { - entry: '(string|element|function|null)', - selector: '(string|element)' - }; +const NAME = 'TemplateFactory'; +const Default = { + allowList: DefaultAllowlist, + content: {}, + // { selector : text , selector2 : text2 , } + extraClass: '', + html: false, + sanitize: true, + sanitizeFn: null, + template: '' +}; +const DefaultType = { + allowList: 'object', + content: 'object', + extraClass: '(string|function)', + html: 'boolean', + sanitize: 'boolean', + sanitizeFn: '(null|function)', + template: 'string' +}; +const DefaultContentType = { + entry: '(string|element|function|null)', + selector: '(string|element)' +}; - /** - * Class definition - */ +/** + * Class definition + */ - class TemplateFactory extends Config { - constructor(config) { - super(); - this._config = this._getConfig(config); - } +class TemplateFactory extends Config { + constructor(config) { + super(); + this._config = this._getConfig(config); + } - // Getters - static get Default() { - return Default; - } - static get DefaultType() { - return DefaultType; - } - static get NAME() { - return NAME; - } + // Getters + static get Default() { + return Default; + } + static get DefaultType() { + return DefaultType; + } + static get NAME() { + return NAME; + } - // Public - getContent() { - return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); - } - hasContent() { - return this.getContent().length > 0; - } - changeContent(content) { - this._checkContent(content); - this._config.content = { - ...this._config.content, - ...content - }; - return this; + // Public + getContent() { + return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean); + } + hasContent() { + return this.getContent().length > 0; + } + changeContent(content) { + this._checkContent(content); + this._config.content = { + ...this._config.content, + ...content + }; + return this; + } + toHtml() { + const templateWrapper = document.createElement('div'); + templateWrapper.innerHTML = this._maybeSanitize(this._config.template); + for (const [selector, text] of Object.entries(this._config.content)) { + this._setContent(templateWrapper, text, selector); } - toHtml() { - const templateWrapper = document.createElement('div'); - templateWrapper.innerHTML = this._maybeSanitize(this._config.template); - for (const [selector, text] of Object.entries(this._config.content)) { - this._setContent(templateWrapper, text, selector); - } - const template = templateWrapper.children[0]; - const extraClass = this._resolvePossibleFunction(this._config.extraClass); - if (extraClass) { - template.classList.add(...extraClass.split(' ')); - } - return template; + const template = templateWrapper.children[0]; + const extraClass = this._resolvePossibleFunction(this._config.extraClass); + if (extraClass) { + template.classList.add(...extraClass.split(' ')); } + return template; + } - // Private - _typeCheckConfig(config) { - super._typeCheckConfig(config); - this._checkContent(config.content); + // Private + _typeCheckConfig(config) { + super._typeCheckConfig(config); + this._checkContent(config.content); + } + _checkContent(arg) { + for (const [selector, content] of Object.entries(arg)) { + super._typeCheckConfig({ + selector, + entry: content + }, DefaultContentType); } - _checkContent(arg) { - for (const [selector, content] of Object.entries(arg)) { - super._typeCheckConfig({ - selector, - entry: content - }, DefaultContentType); - } + } + _setContent(template, content, selector) { + const templateElement = SelectorEngine.findOne(selector, template); + if (!templateElement) { + return; } - _setContent(template, content, selector) { - const templateElement = SelectorEngine.findOne(selector, template); - if (!templateElement) { - return; - } - content = this._resolvePossibleFunction(content); - if (!content) { - templateElement.remove(); - return; - } - if (index_js.isElement(content)) { - this._putElementInTemplate(index_js.getElement(content), templateElement); - return; - } - if (this._config.html) { - templateElement.innerHTML = this._maybeSanitize(content); - return; - } - templateElement.textContent = content; + content = this._resolvePossibleFunction(content); + if (!content) { + templateElement.remove(); + return; } - _maybeSanitize(arg) { - return this._config.sanitize ? sanitizer_js.sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + if (isElement(content)) { + this._putElementInTemplate(getElement(content), templateElement); + return; } - _resolvePossibleFunction(arg) { - return index_js.execute(arg, [undefined, this]); + if (this._config.html) { + templateElement.innerHTML = this._maybeSanitize(content); + return; } - _putElementInTemplate(element, templateElement) { - if (this._config.html) { - templateElement.innerHTML = ''; - templateElement.append(element); - return; - } - templateElement.textContent = element.textContent; + templateElement.textContent = content; + } + _maybeSanitize(arg) { + return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg; + } + _resolvePossibleFunction(arg) { + return execute(arg, [undefined, this]); + } + _putElementInTemplate(element, templateElement) { + if (this._config.html) { + templateElement.innerHTML = ''; + templateElement.append(element); + return; } + templateElement.textContent = element.textContent; } +} - return TemplateFactory; - -})); +export { TemplateFactory as default }; diff --git a/assets/javascripts/floating-ui.js b/assets/javascripts/floating-ui.js new file mode 100644 index 00000000..1310f438 --- /dev/null +++ b/assets/javascripts/floating-ui.js @@ -0,0 +1,3 @@ +/* esm.sh - @floating-ui/dom@1.7.6 */ +var yt=["top","right","bottom","left"],St=["start","end"],vt=yt.reduce((t,e)=>t.concat(e,e+"-"+St[0],e+"-"+St[1]),[]),W=Math.min,P=Math.max,st=Math.round,rt=Math.floor,X=t=>({x:t,y:t}),ce={left:"right",right:"left",bottom:"top",top:"bottom"};function ft(t,e,n){return P(t,W(e,n))}function $(t,e){return typeof t=="function"?t(e):t}function D(t){return t.split("-")[0]}function H(t){return t.split("-")[1]}function at(t){return t==="x"?"y":"x"}function ut(t){return t==="y"?"height":"width"}function V(t){let e=t[0];return e==="t"||e==="b"?"y":"x"}function dt(t){return at(V(t))}function bt(t,e,n){n===void 0&&(n=!1);let o=H(t),i=dt(t),s=ut(i),r=i==="x"?o===(n?"end":"start")?"right":"left":o==="start"?"bottom":"top";return e.reference[s]>e.floating[s]&&(r=it(r)),[r,it(r)]}function Lt(t){let e=it(t);return[ot(t),e,ot(e)]}function ot(t){return t.includes("start")?t.replace("start","end"):t.replace("end","start")}var Pt=["left","right"],Tt=["right","left"],le=["top","bottom"],fe=["bottom","top"];function ae(t,e,n){switch(t){case"top":case"bottom":return n?e?Tt:Pt:e?Pt:Tt;case"left":case"right":return e?le:fe;default:return[]}}function Et(t,e,n,o){let i=H(t),s=ae(D(t),n==="start",o);return i&&(s=s.map(r=>r+"-"+i),e&&(s=s.concat(s.map(ot)))),s}function it(t){let e=D(t);return ce[e]+t.slice(e.length)}function ue(t){return{top:0,right:0,bottom:0,left:0,...t}}function mt(t){return typeof t!="number"?ue(t):{top:t,right:t,bottom:t,left:t}}function q(t){let{x:e,y:n,width:o,height:i}=t;return{width:o,height:i,top:n,left:e,right:e+o,bottom:n+i,x:e,y:n}}function Dt(t,e,n){let{reference:o,floating:i}=t,s=V(e),r=dt(e),c=ut(r),a=D(e),d=s==="y",u=o.x+o.width/2-i.width/2,l=o.y+o.height/2-i.height/2,m=o[c]/2-i[c]/2,f;switch(a){case"top":f={x:u,y:o.y-i.height};break;case"bottom":f={x:u,y:o.y+o.height};break;case"right":f={x:o.x+o.width,y:l};break;case"left":f={x:o.x-i.width,y:l};break;default:f={x:o.x,y:o.y}}switch(H(e)){case"start":f[r]-=m*(n&&d?-1:1);break;case"end":f[r]+=m*(n&&d?-1:1);break}return f}async function At(t,e){var n;e===void 0&&(e={});let{x:o,y:i,platform:s,rects:r,elements:c,strategy:a}=t,{boundary:d="clippingAncestors",rootBoundary:u="viewport",elementContext:l="floating",altBoundary:m=!1,padding:f=0}=$(e,t),g=mt(f),p=c[m?l==="floating"?"reference":"floating":l],w=q(await s.getClippingRect({element:(n=await(s.isElement==null?void 0:s.isElement(p)))==null||n?p:p.contextElement||await(s.getDocumentElement==null?void 0:s.getDocumentElement(c.floating)),boundary:d,rootBoundary:u,strategy:a})),x=l==="floating"?{x:o,y:i,width:r.floating.width,height:r.floating.height}:r.reference,y=await(s.getOffsetParent==null?void 0:s.getOffsetParent(c.floating)),v=await(s.isElement==null?void 0:s.isElement(y))?await(s.getScale==null?void 0:s.getScale(y))||{x:1,y:1}:{x:1,y:1},A=q(s.convertOffsetParentRelativeRectToViewportRelativeRect?await s.convertOffsetParentRelativeRectToViewportRelativeRect({elements:c,rect:x,offsetParent:y,strategy:a}):x);return{top:(w.top-A.top+g.top)/v.y,bottom:(A.bottom-w.bottom+g.bottom)/v.y,left:(w.left-A.left+g.left)/v.x,right:(A.right-w.right+g.right)/v.x}}var de=50,Bt=async(t,e,n)=>{let{placement:o="bottom",strategy:i="absolute",middleware:s=[],platform:r}=n,c=r.detectOverflow?r:{...r,detectOverflow:At},a=await(r.isRTL==null?void 0:r.isRTL(e)),d=await r.getElementRects({reference:t,floating:e,strategy:i}),{x:u,y:l}=Dt(d,o,a),m=o,f=0,g={};for(let h=0;h({name:"arrow",options:t,async fn(e){let{x:n,y:o,placement:i,rects:s,platform:r,elements:c,middlewareData:a}=e,{element:d,padding:u=0}=$(t,e)||{};if(d==null)return{};let l=mt(u),m={x:n,y:o},f=dt(i),g=ut(f),h=await r.getDimensions(d),p=f==="y",w=p?"top":"left",x=p?"bottom":"right",y=p?"clientHeight":"clientWidth",v=s.reference[g]+s.reference[f]-m[f]-s.floating[g],A=m[f]-s.reference[f],O=await(r.getOffsetParent==null?void 0:r.getOffsetParent(d)),R=O?O[y]:0;(!R||!await(r.isElement==null?void 0:r.isElement(O)))&&(R=c.floating[y]||s.floating[g]);let k=v/2-A/2,T=R/2-h[g]/2-1,b=W(l[w],T),C=W(l[x],T),L=b,E=R-h[g]-C,S=R/2-h[g]/2+k,I=ft(L,S,E),N=!a.arrow&&H(i)!=null&&S!==I&&s.reference[g]/2-(SH(i)===t),...n.filter(i=>H(i)!==t)]:n.filter(i=>D(i)===i)).filter(i=>t?H(i)===t||(e?ot(i)!==i:!1):!0)}var kt=function(t){return t===void 0&&(t={}),{name:"autoPlacement",options:t,async fn(e){var n,o,i;let{rects:s,middlewareData:r,placement:c,platform:a,elements:d}=e,{crossAxis:u=!1,alignment:l,allowedPlacements:m=vt,autoAlignment:f=!0,...g}=$(t,e),h=l!==void 0||m===vt?me(l||null,f,m):m,p=await a.detectOverflow(e,g),w=((n=r.autoPlacement)==null?void 0:n.index)||0,x=h[w];if(x==null)return{};let y=bt(x,s,await(a.isRTL==null?void 0:a.isRTL(d.floating)));if(c!==x)return{reset:{placement:h[0]}};let v=[p[D(x)],p[y[0]],p[y[1]]],A=[...((o=r.autoPlacement)==null?void 0:o.overflows)||[],{placement:x,overflows:v}],O=h[w+1];if(O)return{data:{index:w+1,overflows:A},reset:{placement:O}};let R=A.map(b=>{let C=H(b.placement);return[b.placement,C&&u?b.overflows.slice(0,2).reduce((L,E)=>L+E,0):b.overflows[0],b.overflows]}).sort((b,C)=>b[1]-C[1]),T=((i=R.filter(b=>b[2].slice(0,H(b[0])?2:3).every(C=>C<=0))[0])==null?void 0:i[0])||R[0][0];return T!==c?{data:{index:w+1,overflows:A},reset:{placement:T}}:{}}}},Nt=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,o;let{placement:i,middlewareData:s,rects:r,initialPlacement:c,platform:a,elements:d}=e,{mainAxis:u=!0,crossAxis:l=!0,fallbackPlacements:m,fallbackStrategy:f="bestFit",fallbackAxisSideDirection:g="none",flipAlignment:h=!0,...p}=$(t,e);if((n=s.arrow)!=null&&n.alignmentOffset)return{};let w=D(i),x=V(c),y=D(c)===c,v=await(a.isRTL==null?void 0:a.isRTL(d.floating)),A=m||(y||!h?[it(c)]:Lt(c)),O=g!=="none";!m&&O&&A.push(...Et(c,h,g,v));let R=[c,...A],k=await a.detectOverflow(e,p),T=[],b=((o=s.flip)==null?void 0:o.overflows)||[];if(u&&T.push(k[w]),l){let S=bt(i,r,v);T.push(k[S[0]],k[S[1]])}if(b=[...b,{placement:i,overflows:T}],!T.every(S=>S<=0)){var C,L;let S=(((C=s.flip)==null?void 0:C.index)||0)+1,I=R[S];if(I&&(!(l==="alignment"?x!==V(I):!1)||b.every(B=>V(B.placement)===x?B.overflows[0]>0:!0)))return{data:{index:S,overflows:b},reset:{placement:I}};let N=(L=b.filter(F=>F.overflows[0]<=0).sort((F,B)=>F.overflows[1]-B.overflows[1])[0])==null?void 0:L.placement;if(!N)switch(f){case"bestFit":{var E;let F=(E=b.filter(B=>{if(O){let U=V(B.placement);return U===x||U==="y"}return!0}).map(B=>[B.placement,B.overflows.filter(U=>U>0).reduce((U,re)=>U+re,0)]).sort((B,U)=>B[1]-U[1])[0])==null?void 0:E[0];F&&(N=F);break}case"initialPlacement":N=c;break}if(i!==N)return{reset:{placement:N}}}return{}}}};function Mt(t,e){return{top:t.top-e.height,right:t.right-e.width,bottom:t.bottom-e.height,left:t.left-e.width}}function Ft(t){return yt.some(e=>t[e]>=0)}var $t=function(t){return t===void 0&&(t={}),{name:"hide",options:t,async fn(e){let{rects:n,platform:o}=e,{strategy:i="referenceHidden",...s}=$(t,e);switch(i){case"referenceHidden":{let r=await o.detectOverflow(e,{...s,elementContext:"reference"}),c=Mt(r,n.reference);return{data:{referenceHiddenOffsets:c,referenceHidden:Ft(c)}}}case"escaped":{let r=await o.detectOverflow(e,{...s,altBoundary:!0}),c=Mt(r,n.floating);return{data:{escapedOffsets:c,escaped:Ft(c)}}}default:return{}}}}};function Ht(t){let e=W(...t.map(s=>s.left)),n=W(...t.map(s=>s.top)),o=P(...t.map(s=>s.right)),i=P(...t.map(s=>s.bottom));return{x:e,y:n,width:o-e,height:i-n}}function ge(t){let e=t.slice().sort((i,s)=>i.y-s.y),n=[],o=null;for(let i=0;io.height/2?n.push([s]):n[n.length-1].push(s),o=s}return n.map(i=>q(Ht(i)))}var Vt=function(t){return t===void 0&&(t={}),{name:"inline",options:t,async fn(e){let{placement:n,elements:o,rects:i,platform:s,strategy:r}=e,{padding:c=2,x:a,y:d}=$(t,e),u=Array.from(await(s.getClientRects==null?void 0:s.getClientRects(o.reference))||[]),l=ge(u),m=q(Ht(u)),f=mt(c);function g(){if(l.length===2&&l[0].left>l[1].right&&a!=null&&d!=null)return l.find(p=>a>p.left-f.left&&ap.top-f.top&&d=2){if(V(n)==="y"){let b=l[0],C=l[l.length-1],L=D(n)==="top",E=b.top,S=C.bottom,I=L?b.left:C.left,N=L?b.right:C.right,F=N-I,B=S-E;return{top:E,bottom:S,left:I,right:N,width:F,height:B,x:I,y:E}}let p=D(n)==="left",w=P(...l.map(b=>b.right)),x=W(...l.map(b=>b.left)),y=l.filter(b=>p?b.left===x:b.right===w),v=y[0].top,A=y[y.length-1].bottom,O=x,R=w,k=R-O,T=A-v;return{top:v,bottom:A,left:O,right:R,width:k,height:T,x:O,y:v}}return m}let h=await s.getElementRects({reference:{getBoundingClientRect:g},floating:o.floating,strategy:r});return i.reference.x!==h.reference.x||i.reference.y!==h.reference.y||i.reference.width!==h.reference.width||i.reference.height!==h.reference.height?{reset:{rects:h}}:{}}}},_t=new Set(["left","top"]);async function he(t,e){let{placement:n,platform:o,elements:i}=t,s=await(o.isRTL==null?void 0:o.isRTL(i.floating)),r=D(n),c=H(n),a=V(n)==="y",d=_t.has(r)?-1:1,u=s&&a?-1:1,l=$(e,t),{mainAxis:m,crossAxis:f,alignmentAxis:g}=typeof l=="number"?{mainAxis:l,crossAxis:0,alignmentAxis:null}:{mainAxis:l.mainAxis||0,crossAxis:l.crossAxis||0,alignmentAxis:l.alignmentAxis};return c&&typeof g=="number"&&(f=c==="end"?g*-1:g),a?{x:f*u,y:m*d}:{x:m*d,y:f*u}}var zt=function(t){return t===void 0&&(t=0),{name:"offset",options:t,async fn(e){var n,o;let{x:i,y:s,placement:r,middlewareData:c}=e,a=await he(e,t);return r===((n=c.offset)==null?void 0:n.placement)&&(o=c.arrow)!=null&&o.alignmentOffset?{}:{x:i+a.x,y:s+a.y,data:{...a,placement:r}}}}},It=function(t){return t===void 0&&(t={}),{name:"shift",options:t,async fn(e){let{x:n,y:o,placement:i,platform:s}=e,{mainAxis:r=!0,crossAxis:c=!1,limiter:a={fn:w=>{let{x,y}=w;return{x,y}}},...d}=$(t,e),u={x:n,y:o},l=await s.detectOverflow(e,d),m=V(D(i)),f=at(m),g=u[f],h=u[m];if(r){let w=f==="y"?"top":"left",x=f==="y"?"bottom":"right",y=g+l[w],v=g-l[x];g=ft(y,g,v)}if(c){let w=m==="y"?"top":"left",x=m==="y"?"bottom":"right",y=h+l[w],v=h-l[x];h=ft(y,h,v)}let p=a.fn({...e,[f]:g,[m]:h});return{...p,data:{x:p.x-n,y:p.y-o,enabled:{[f]:r,[m]:c}}}}}},Xt=function(t){return t===void 0&&(t={}),{options:t,fn(e){let{x:n,y:o,placement:i,rects:s,middlewareData:r}=e,{offset:c=0,mainAxis:a=!0,crossAxis:d=!0}=$(t,e),u={x:n,y:o},l=V(i),m=at(l),f=u[m],g=u[l],h=$(c,e),p=typeof h=="number"?{mainAxis:h,crossAxis:0}:{mainAxis:0,crossAxis:0,...h};if(a){let y=m==="y"?"height":"width",v=s.reference[m]-s.floating[y]+p.mainAxis,A=s.reference[m]+s.reference[y]-p.mainAxis;fA&&(f=A)}if(d){var w,x;let y=m==="y"?"width":"height",v=_t.has(D(i)),A=s.reference[l]-s.floating[y]+(v&&((w=r.offset)==null?void 0:w[l])||0)+(v?0:p.crossAxis),O=s.reference[l]+s.reference[y]+(v?0:((x=r.offset)==null?void 0:x[l])||0)-(v?p.crossAxis:0);gO&&(g=O)}return{[m]:f,[l]:g}}}},jt=function(t){return t===void 0&&(t={}),{name:"size",options:t,async fn(e){var n,o;let{placement:i,rects:s,platform:r,elements:c}=e,{apply:a=()=>{},...d}=$(t,e),u=await r.detectOverflow(e,d),l=D(i),m=H(i),f=V(i)==="y",{width:g,height:h}=s.floating,p,w;l==="top"||l==="bottom"?(p=l,w=m===(await(r.isRTL==null?void 0:r.isRTL(c.floating))?"start":"end")?"left":"right"):(w=l,p=m==="end"?"top":"bottom");let x=h-u.top-u.bottom,y=g-u.left-u.right,v=W(h-u[p],x),A=W(g-u[w],y),O=!e.middlewareData.shift,R=v,k=A;if((n=e.middlewareData.shift)!=null&&n.enabled.x&&(k=y),(o=e.middlewareData.shift)!=null&&o.enabled.y&&(R=x),O&&!m){let b=P(u.left,0),C=P(u.right,0),L=P(u.top,0),E=P(u.bottom,0);f?k=g-2*(b!==0||C!==0?b+C:P(u.left,u.right)):R=h-2*(L!==0||E!==0?L+E:P(u.top,u.bottom))}await a({...e,availableWidth:k,availableHeight:R});let T=await r.getDimensions(c.floating);return g!==T.width||h!==T.height?{reset:{rects:!0}}:{}}}};function gt(){return typeof window<"u"}function Q(t){return qt(t)?(t.nodeName||"").toLowerCase():"#document"}function M(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function j(t){var e;return(e=(qt(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function qt(t){return gt()?t instanceof Node||t instanceof M(t).Node:!1}function _(t){return gt()?t instanceof Element||t instanceof M(t).Element:!1}function Y(t){return gt()?t instanceof HTMLElement||t instanceof M(t).HTMLElement:!1}function Yt(t){return!gt()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof M(t).ShadowRoot}function et(t){let{overflow:e,overflowX:n,overflowY:o,display:i}=z(t);return/auto|scroll|overlay|hidden|clip/.test(e+o+n)&&i!=="inline"&&i!=="contents"}function Kt(t){return/^(table|td|th)$/.test(Q(t))}function ct(t){try{if(t.matches(":popover-open"))return!0}catch{}try{return t.matches(":modal")}catch{return!1}}var pe=/transform|translate|scale|rotate|perspective|filter/,we=/paint|layout|strict|content/,G=t=>!!t&&t!=="none",Ot;function ht(t){let e=_(t)?z(t):t;return G(e.transform)||G(e.translate)||G(e.scale)||G(e.rotate)||G(e.perspective)||!pt()&&(G(e.backdropFilter)||G(e.filter))||pe.test(e.willChange||"")||we.test(e.contain||"")}function Ut(t){let e=K(t);for(;Y(e)&&!Z(e);){if(ht(e))return e;if(ct(e))return null;e=K(e)}return null}function pt(){return Ot==null&&(Ot=typeof CSS<"u"&&CSS.supports&&CSS.supports("-webkit-backdrop-filter","none")),Ot}function Z(t){return/^(html|body|#document)$/.test(Q(t))}function z(t){return M(t).getComputedStyle(t)}function lt(t){return _(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function K(t){if(Q(t)==="html")return t;let e=t.assignedSlot||t.parentNode||Yt(t)&&t.host||j(t);return Yt(e)?e.host:e}function Gt(t){let e=K(t);return Z(e)?t.ownerDocument?t.ownerDocument.body:t.body:Y(e)&&et(e)?e:Gt(e)}function J(t,e,n){var o;e===void 0&&(e=[]),n===void 0&&(n=!0);let i=Gt(t),s=i===((o=t.ownerDocument)==null?void 0:o.body),r=M(i);if(s){let c=wt(r);return e.concat(r,r.visualViewport||[],et(i)?i:[],c&&n?J(c):[])}else return e.concat(i,J(i,[],n))}function wt(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function te(t){let e=z(t),n=parseFloat(e.width)||0,o=parseFloat(e.height)||0,i=Y(t),s=i?t.offsetWidth:n,r=i?t.offsetHeight:o,c=st(n)!==s||st(o)!==r;return c&&(n=s,o=r),{width:n,height:o,$:c}}function Ct(t){return _(t)?t:t.contextElement}function nt(t){let e=Ct(t);if(!Y(e))return X(1);let n=e.getBoundingClientRect(),{width:o,height:i,$:s}=te(e),r=(s?st(n.width):n.width)/o,c=(s?st(n.height):n.height)/i;return(!r||!Number.isFinite(r))&&(r=1),(!c||!Number.isFinite(c))&&(c=1),{x:r,y:c}}var xe=X(0);function ee(t){let e=M(t);return!pt()||!e.visualViewport?xe:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function ye(t,e,n){return e===void 0&&(e=!1),!n||e&&n!==M(t)?!1:e}function tt(t,e,n,o){e===void 0&&(e=!1),n===void 0&&(n=!1);let i=t.getBoundingClientRect(),s=Ct(t),r=X(1);e&&(o?_(o)&&(r=nt(o)):r=nt(t));let c=ye(s,n,o)?ee(s):X(0),a=(i.left+c.x)/r.x,d=(i.top+c.y)/r.y,u=i.width/r.x,l=i.height/r.y;if(s){let m=M(s),f=o&&_(o)?M(o):o,g=m,h=wt(g);for(;h&&o&&f!==g;){let p=nt(h),w=h.getBoundingClientRect(),x=z(h),y=w.left+(h.clientLeft+parseFloat(x.paddingLeft))*p.x,v=w.top+(h.clientTop+parseFloat(x.paddingTop))*p.y;a*=p.x,d*=p.y,u*=p.x,l*=p.y,a+=y,d+=v,g=M(h),h=wt(g)}}return q({width:u,height:l,x:a,y:d})}function xt(t,e){let n=lt(t).scrollLeft;return e?e.left+n:tt(j(t)).left+n}function ne(t,e){let n=t.getBoundingClientRect(),o=n.left+e.scrollLeft-xt(t,n),i=n.top+e.scrollTop;return{x:o,y:i}}function ve(t){let{elements:e,rect:n,offsetParent:o,strategy:i}=t,s=i==="fixed",r=j(o),c=e?ct(e.floating):!1;if(o===r||c&&s)return n;let a={scrollLeft:0,scrollTop:0},d=X(1),u=X(0),l=Y(o);if((l||!l&&!s)&&((Q(o)!=="body"||et(r))&&(a=lt(o)),l)){let f=tt(o);d=nt(o),u.x=f.x+o.clientLeft,u.y=f.y+o.clientTop}let m=r&&!l&&!s?ne(r,a):X(0);return{width:n.width*d.x,height:n.height*d.y,x:n.x*d.x-a.scrollLeft*d.x+u.x+m.x,y:n.y*d.y-a.scrollTop*d.y+u.y+m.y}}function be(t){return Array.from(t.getClientRects())}function Ae(t){let e=j(t),n=lt(t),o=t.ownerDocument.body,i=P(e.scrollWidth,e.clientWidth,o.scrollWidth,o.clientWidth),s=P(e.scrollHeight,e.clientHeight,o.scrollHeight,o.clientHeight),r=-n.scrollLeft+xt(t),c=-n.scrollTop;return z(o).direction==="rtl"&&(r+=P(e.clientWidth,o.clientWidth)-i),{width:i,height:s,x:r,y:c}}var Jt=25;function Oe(t,e){let n=M(t),o=j(t),i=n.visualViewport,s=o.clientWidth,r=o.clientHeight,c=0,a=0;if(i){s=i.width,r=i.height;let u=pt();(!u||u&&e==="fixed")&&(c=i.offsetLeft,a=i.offsetTop)}let d=xt(o);if(d<=0){let u=o.ownerDocument,l=u.body,m=getComputedStyle(l),f=u.compatMode==="CSS1Compat"&&parseFloat(m.marginLeft)+parseFloat(m.marginRight)||0,g=Math.abs(o.clientWidth-l.clientWidth-f);g<=Jt&&(s-=g)}else d<=Jt&&(s+=d);return{width:s,height:r,x:c,y:a}}function Re(t,e){let n=tt(t,!0,e==="fixed"),o=n.top+t.clientTop,i=n.left+t.clientLeft,s=Y(t)?nt(t):X(1),r=t.clientWidth*s.x,c=t.clientHeight*s.y,a=i*s.x,d=o*s.y;return{width:r,height:c,x:a,y:d}}function Qt(t,e,n){let o;if(e==="viewport")o=Oe(t,n);else if(e==="document")o=Ae(j(t));else if(_(e))o=Re(e,n);else{let i=ee(t);o={x:e.x-i.x,y:e.y-i.y,width:e.width,height:e.height}}return q(o)}function oe(t,e){let n=K(t);return n===e||!_(n)||Z(n)?!1:z(n).position==="fixed"||oe(n,e)}function Ce(t,e){let n=e.get(t);if(n)return n;let o=J(t,[],!1).filter(c=>_(c)&&Q(c)!=="body"),i=null,s=z(t).position==="fixed",r=s?K(t):t;for(;_(r)&&!Z(r);){let c=z(r),a=ht(r);!a&&c.position==="fixed"&&(i=null),(s?!a&&!i:!a&&c.position==="static"&&!!i&&(i.position==="absolute"||i.position==="fixed")||et(r)&&!a&&oe(t,r))?o=o.filter(u=>u!==r):i=c,r=K(r)}return e.set(t,o),o}function Se(t){let{element:e,boundary:n,rootBoundary:o,strategy:i}=t,r=[...n==="clippingAncestors"?ct(e)?[]:Ce(e,this._c):[].concat(n),o],c=Qt(e,r[0],i),a=c.top,d=c.right,u=c.bottom,l=c.left;for(let m=1;m{r(!1,1e-7)},1e3)}R===1&&!se(d,t.getBoundingClientRect())&&r(),v=!1}try{n=new IntersectionObserver(A,{...y,root:i.ownerDocument})}catch{n=new IntersectionObserver(A,y)}n.observe(t)}return r(!0),s}function _e(t,e,n,o){o===void 0&&(o={});let{ancestorScroll:i=!0,ancestorResize:s=!0,elementResize:r=typeof ResizeObserver=="function",layoutShift:c=typeof IntersectionObserver=="function",animationFrame:a=!1}=o,d=Ct(t),u=i||s?[...d?J(d):[],...e?J(e):[]]:[];u.forEach(w=>{i&&w.addEventListener("scroll",n,{passive:!0}),s&&w.addEventListener("resize",n)});let l=d&&c?Me(d,n):null,m=-1,f=null;r&&(f=new ResizeObserver(w=>{let[x]=w;x&&x.target===d&&f&&e&&(f.unobserve(e),cancelAnimationFrame(m),m=requestAnimationFrame(()=>{var y;(y=f)==null||y.observe(e)})),n()}),d&&!a&&f.observe(d),e&&f.observe(e));let g,h=a?tt(t):null;a&&p();function p(){let w=tt(t);h&&!se(h,w)&&n(),h=w,g=requestAnimationFrame(p)}return n(),()=>{var w;u.forEach(x=>{i&&x.removeEventListener("scroll",n),s&&x.removeEventListener("resize",n)}),l?.(),(w=f)==null||w.disconnect(),f=null,a&&cancelAnimationFrame(g)}}var ze=At,Ie=zt,Xe=kt,je=It,Ye=Nt,qe=jt,Ke=$t,Ue=Wt,Ge=Vt,Je=Xt,Qe=(t,e,n)=>{let o=new Map,i={platform:De,...n},s={...i.platform,_c:o};return Bt(t,e,{...i,platform:s})};export{Ue as arrow,Xe as autoPlacement,_e as autoUpdate,Qe as computePosition,ze as detectOverflow,Ye as flip,J as getOverflowAncestors,Ke as hide,Ge as inline,Je as limitShift,Ie as offset,De as platform,je as shift,qe as size}; +//# sourceMappingURL=dom.bundle.mjs.map \ No newline at end of file diff --git a/assets/stylesheets/_bootstrap-grid.scss b/assets/stylesheets/_bootstrap-grid.scss deleted file mode 100644 index 5185c78f..00000000 --- a/assets/stylesheets/_bootstrap-grid.scss +++ /dev/null @@ -1,62 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Grid); - -$include-column-box-sizing: true !default; - -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; - -@import "bootstrap/mixins/breakpoints"; -@import "bootstrap/mixins/container"; -@import "bootstrap/mixins/grid"; -@import "bootstrap/mixins/utilities"; - -@import "bootstrap/vendor/rfs"; - -@import "bootstrap/containers"; -@import "bootstrap/grid"; - -@import "bootstrap/utilities"; -// Only use the utilities we need -// stylelint-disable-next-line scss/dollar-variable-default -$utilities: map-get-multiple( - $utilities, - ( - "bootstrap/display", - "bootstrap/order", - "bootstrap/flex", - "bootstrap/flex-direction", - "bootstrap/flex-grow", - "bootstrap/flex-shrink", - "bootstrap/flex-wrap", - "bootstrap/justify-content", - "bootstrap/align-items", - "bootstrap/align-content", - "bootstrap/align-self", - "bootstrap/margin", - "bootstrap/margin-x", - "bootstrap/margin-y", - "bootstrap/margin-top", - "bootstrap/margin-end", - "bootstrap/margin-bottom", - "bootstrap/margin-start", - "bootstrap/negative-margin", - "bootstrap/negative-margin-x", - "bootstrap/negative-margin-y", - "bootstrap/negative-margin-top", - "bootstrap/negative-margin-end", - "bootstrap/negative-margin-bottom", - "bootstrap/negative-margin-start", - "bootstrap/padding", - "bootstrap/padding-x", - "bootstrap/padding-y", - "bootstrap/padding-top", - "bootstrap/padding-end", - "bootstrap/padding-bottom", - "bootstrap/padding-start", - ) -); - -@import "bootstrap/utilities/api"; diff --git a/assets/stylesheets/_bootstrap-reboot.scss b/assets/stylesheets/_bootstrap-reboot.scss deleted file mode 100644 index 9d4266ed..00000000 --- a/assets/stylesheets/_bootstrap-reboot.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Reboot); - -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/root"; -@import "bootstrap/reboot"; diff --git a/assets/stylesheets/_bootstrap-utilities.scss b/assets/stylesheets/_bootstrap-utilities.scss deleted file mode 100644 index 475783b8..00000000 --- a/assets/stylesheets/_bootstrap-utilities.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(Utilities); - -// Configuration -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/utilities"; - -// Layout & components -@import "bootstrap/root"; - -// Helpers -@import "bootstrap/helpers"; - -// Utilities -@import "bootstrap/utilities/api"; diff --git a/assets/stylesheets/_bootstrap.scss b/assets/stylesheets/_bootstrap.scss index c2dfdf3a..f21610bc 100644 --- a/assets/stylesheets/_bootstrap.scss +++ b/assets/stylesheets/_bootstrap.scss @@ -1,52 +1,47 @@ -@import "bootstrap/mixins/banner"; -@include bsBanner(""); - +@forward "bootstrap/banner"; // scss-docs-start import-stack -// Configuration -@import "bootstrap/functions"; -@import "bootstrap/variables"; -@import "bootstrap/variables-dark"; -@import "bootstrap/maps"; -@import "bootstrap/mixins"; -@import "bootstrap/utilities"; +@forward "bootstrap/colors"; + +// Global CSS variables, layer definitions, and configuration +@forward "bootstrap/root"; + +// Subdir imports +@forward "bootstrap/content"; +@forward "bootstrap/layout"; +@forward "bootstrap/forms"; +@forward "bootstrap/buttons"; -// Layout & components -@import "bootstrap/root"; -@import "bootstrap/reboot"; -@import "bootstrap/type"; -@import "bootstrap/images"; -@import "bootstrap/containers"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; -@import "bootstrap/transitions"; -@import "bootstrap/dropdown"; -@import "bootstrap/button-group"; -@import "bootstrap/nav"; -@import "bootstrap/navbar"; -@import "bootstrap/card"; -@import "bootstrap/accordion"; -@import "bootstrap/breadcrumb"; -@import "bootstrap/pagination"; -@import "bootstrap/badge"; -@import "bootstrap/alert"; -@import "bootstrap/progress"; -@import "bootstrap/list-group"; -@import "bootstrap/close"; -@import "bootstrap/toasts"; -@import "bootstrap/modal"; -@import "bootstrap/tooltip"; -@import "bootstrap/popover"; -@import "bootstrap/carousel"; -@import "bootstrap/spinners"; -@import "bootstrap/offcanvas"; -@import "bootstrap/placeholders"; +// Components +@forward "bootstrap/accordion"; +@forward "bootstrap/alert"; +@forward "bootstrap/avatar"; +@forward "bootstrap/badge"; +@forward "bootstrap/breadcrumb"; +@forward "bootstrap/chip"; +@forward "bootstrap/card"; +@forward "bootstrap/carousel"; +@forward "bootstrap/datepicker"; +@forward "bootstrap/dialog"; +@forward "bootstrap/menu"; +@forward "bootstrap/list-group"; +@forward "bootstrap/nav"; +@forward "bootstrap/nav-overflow"; +@forward "bootstrap/navbar"; +@forward "bootstrap/drawer"; +@forward "bootstrap/pagination"; +@forward "bootstrap/placeholder"; +@forward "bootstrap/popover"; +@forward "bootstrap/progress"; +@forward "bootstrap/spinner"; +@forward "bootstrap/stepper"; +@forward "bootstrap/toasts"; +@forward "bootstrap/tooltip"; +@forward "bootstrap/transitions"; // Helpers -@import "bootstrap/helpers"; +@forward "bootstrap/helpers"; // Utilities -@import "bootstrap/utilities/api"; +@forward "bootstrap/utilities/api"; // scss-docs-end import-stack diff --git a/assets/stylesheets/bootstrap/_accordion.scss b/assets/stylesheets/bootstrap/_accordion.scss index e9f267fb..6a9d7d56 100644 --- a/assets/stylesheets/bootstrap/_accordion.scss +++ b/assets/stylesheets/bootstrap/_accordion.scss @@ -1,153 +1,171 @@ -// -// Base styles -// - -.accordion { - // scss-docs-start accordion-css-vars - --#{$prefix}accordion-color: #{$accordion-color}; - --#{$prefix}accordion-bg: #{$accordion-bg}; - --#{$prefix}accordion-transition: #{$accordion-transition}; - --#{$prefix}accordion-border-color: #{$accordion-border-color}; - --#{$prefix}accordion-border-width: #{$accordion-border-width}; - --#{$prefix}accordion-border-radius: #{$accordion-border-radius}; - --#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius}; - --#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x}; - --#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y}; - --#{$prefix}accordion-btn-color: #{$accordion-button-color}; - --#{$prefix}accordion-btn-bg: #{$accordion-button-bg}; - --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)}; - --#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width}; - --#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform}; - --#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition}; - --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)}; - --#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow}; - --#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x}; - --#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y}; - --#{$prefix}accordion-active-color: #{$accordion-button-active-color}; - --#{$prefix}accordion-active-bg: #{$accordion-button-active-bg}; - // scss-docs-end accordion-css-vars -} +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/focus-ring" as *; +@use "mixins/tokens" as *; + +$accordion-tokens: () !default; + +// scss-docs-start accordion-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$accordion-tokens: defaults( + ( + --accordion-padding-x: 1.25rem, + --accordion-padding-y: 1rem, + --accordion-color: var(--fg-body), + --accordion-bg: var(--bg-body), + --accordion-transition-property: "color, background-color, border-radius", + --accordion-transition-timing: ".15s ease-in-out", + --accordion-transition: var(--accordion-transition-property) var(--accordion-timing), + --accordion-border-color: var(--border-color), + --accordion-border-width: var(--border-width), + --accordion-border-radius: var(--accordion-radius, var(--radius-7)), + --accordion-btn-color: var(--fg-2), + --accordion-btn-bg: var(--bg-body), + --accordion-btn-icon-width: 1rem, + --accordion-btn-icon-transform: rotate(-180deg), + --accordion-btn-icon-transition: transform .2s ease-in-out, + --accordion-active-color: var(--fg), + --accordion-active-bg: var(--bg-2), + ), + $accordion-tokens +); +// scss-docs-end accordion-tokens + +@layer components { + .accordion { + @include tokens($accordion-tokens); + } -.accordion-button { - position: relative; - display: flex; - align-items: center; - width: 100%; - padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x); - @include font-size($font-size-base); - color: var(--#{$prefix}accordion-btn-color); - text-align: left; // Reset button style - background-color: var(--#{$prefix}accordion-btn-bg); - border: 0; - @include border-radius(0); - overflow-anchor: none; - @include transition(var(--#{$prefix}accordion-transition)); - - &:not(.collapsed) { - color: var(--#{$prefix}accordion-active-color); - background-color: var(--#{$prefix}accordion-active-bg); - box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list - - &::after { - background-image: var(--#{$prefix}accordion-btn-active-icon); - transform: var(--#{$prefix}accordion-btn-icon-transform); + .accordion-header { + display: flex; + align-items: center; + width: 100%; + padding: var(--accordion-btn-padding-y, var(--accordion-padding-y)) var(--accordion-btn-padding-x, var(--accordion-padding-x)); + font-size: var(--accordion-font-size, var(--font-size-base)); + color: var(--accordion-btn-color); + text-align: start; + list-style: none; // Remove default marker + cursor: pointer; + background-color: var(--accordion-btn-bg); + @include transition(var(--accordion-transition)); + + &::-webkit-details-marker { + display: none; } - } - // Accordion icon - &::after { - flex-shrink: 0; - width: var(--#{$prefix}accordion-btn-icon-width); - height: var(--#{$prefix}accordion-btn-icon-width); - margin-left: auto; - content: ""; - background-image: var(--#{$prefix}accordion-btn-icon); - background-repeat: no-repeat; - background-size: var(--#{$prefix}accordion-btn-icon-width); - @include transition(var(--#{$prefix}accordion-btn-icon-transition)); - } + .accordion-icon { + flex-shrink: 0; + width: var(--accordion-btn-icon-width); + height: var(--accordion-btn-icon-width); + margin-inline-start: auto; + color: currentcolor; + @include transition(var(--accordion-btn-icon-transition)); + } - &:hover { - z-index: 2; - } + &:hover { + z-index: 2; + } - &:focus { - z-index: 3; - outline: 0; - box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow); + &:focus-visible { + position: relative; + z-index: 3; + @include focus-ring(true); + outline-offset: -1px; + } } -} -.accordion-header { - margin-bottom: 0; -} + .accordion-item { + color: var(--accordion-color); + background-color: var(--accordion-bg); + border: var(--accordion-border-width) solid var(--accordion-border-color); -.accordion-item { - color: var(--#{$prefix}accordion-color); - background-color: var(--#{$prefix}accordion-bg); - border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color); + @media (prefers-reduced-motion: no-preference) { + interpolate-size: allow-keywords; + } + + &::details-content { + block-size: 0; + overflow-y: clip; + @include transition(content-visibility .2s allow-discrete, block-size .2s); + } - &:first-of-type { - @include border-top-radius(var(--#{$prefix}accordion-border-radius)); + &:first-of-type { + @include border-top-radius(var(--accordion-border-radius)); - > .accordion-header .accordion-button { - @include border-top-radius(var(--#{$prefix}accordion-inner-border-radius)); + > .accordion-header { + @include border-top-radius(calc(var(--accordion-border-radius) - var(--accordion-border-width))); + } } - } - &:not(:first-of-type) { - border-top: 0; - } + &:not(:first-of-type) { + border-block-start: 0; + } - // Only set a border-radius on the last item if the accordion is collapsed - &:last-of-type { - @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius(var(--accordion-border-radius)); + + > .accordion-header { + @include border-bottom-radius(calc(var(--accordion-border-radius) - var(--accordion-border-width))); + } - > .accordion-header .accordion-button { - &.collapsed { - @include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius)); + > .accordion-body { + @include border-bottom-radius(var(--accordion-border-radius)); } } - > .accordion-collapse { - @include border-bottom-radius(var(--#{$prefix}accordion-border-radius)); + // Open state - details[open] applies these styles + &[open] { + + border-color: var(--theme-border, var(--accordion-border-color)); + &::details-content { + block-size: auto; + } + + > .accordion-header { + color: var(--theme-fg, var(--accordion-active-color)); + background-color: var(--theme-bg-subtle, var(--accordion-active-bg)); + box-shadow: inset 0 calc(-1 * var(--accordion-border-width)) 0 var(--theme-border, var(--accordion-border-color)); + + .accordion-icon { + transform: var(--accordion-btn-icon-transform); + } + } + + // Remove bottom radius from header when open (even on last item) + &:last-of-type > .accordion-header { + @include border-bottom-radius(0); + } } } -} -.accordion-body { - padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x); -} + .accordion-body { + padding: var(--accordion-body-padding-y, var(--accordion-padding-y)) var(--accordion-body-padding-x, var(--accordion-padding-x)); + } -// Flush accordion items -// -// Remove borders and border-radius to keep accordion items edge-to-edge. + // Flush accordion items + // + // Remove borders and border-radius to keep accordion items edge-to-edge. -.accordion-flush { - > .accordion-item { - border-right: 0; - border-left: 0; - @include border-radius(0); + .accordion-flush { + > .accordion-item { + border-inline: 0; + @include border-radius(0); - &:first-child { border-top: 0; } - &:last-child { border-bottom: 0; } + &:first-child { + border-block-start: 0; + } - // stylelint-disable selector-max-class - > .accordion-collapse, - > .accordion-header .accordion-button, - > .accordion-header .accordion-button.collapsed { - @include border-radius(0); - } - // stylelint-enable selector-max-class - } -} + &:last-child { + border-block-end: 0; + } -@if $enable-dark-mode { - @include color-mode(dark) { - .accordion-button::after { - --#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)}; - --#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)}; + > .accordion-header, + > .accordion-body { + @include border-radius(0); + } } } } diff --git a/assets/stylesheets/bootstrap/_alert.scss b/assets/stylesheets/bootstrap/_alert.scss index b8cff9b7..10077c8b 100644 --- a/assets/stylesheets/bootstrap/_alert.scss +++ b/assets/stylesheets/bootstrap/_alert.scss @@ -1,68 +1,55 @@ -// -// Base styles -// - -.alert { - // scss-docs-start alert-css-vars - --#{$prefix}alert-bg: transparent; - --#{$prefix}alert-padding-x: #{$alert-padding-x}; - --#{$prefix}alert-padding-y: #{$alert-padding-y}; - --#{$prefix}alert-margin-bottom: #{$alert-margin-bottom}; - --#{$prefix}alert-color: inherit; - --#{$prefix}alert-border-color: transparent; - --#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color); - --#{$prefix}alert-border-radius: #{$alert-border-radius}; - --#{$prefix}alert-link-color: inherit; - // scss-docs-end alert-css-vars - - position: relative; - padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x); - margin-bottom: var(--#{$prefix}alert-margin-bottom); - color: var(--#{$prefix}alert-color); - background-color: var(--#{$prefix}alert-bg); - border: var(--#{$prefix}alert-border); - @include border-radius(var(--#{$prefix}alert-border-radius)); -} - -// Headings for larger alerts -.alert-heading { - // Specified to prevent conflicts of changing $headings-color - color: inherit; -} - -// Provide class for links that match alerts -.alert-link { - font-weight: $alert-link-font-weight; - color: var(--#{$prefix}alert-link-color); -} - - -// Dismissible alerts -// -// Expand the right padding and account for the close button's positioning. - -.alert-dismissible { - padding-right: $alert-dismissible-padding-r; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$alert-tokens: () !default; + +// scss-docs-start alert-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$alert-tokens: defaults( + ( + --alert-gap: var(--spacer-3), + --alert-bg: var(--theme-bg-subtle, var(--bg-1)), + --alert-padding-x: var(--spacer), + --alert-padding-y: var(--spacer), + --alert-color: var(--theme-fg, inherit), + --alert-border-color: var(--theme-border, var(--border-color)), + --alert-border: var(--border-width) solid var(--alert-border-color), + --alert-border-radius: var(--radius-5), + --alert-link-color: inherit, + --hr-border-color: var(--theme-border, var(--border-color)), + ), + $alert-tokens +); +// scss-docs-end alert-tokens + +@layer components { + .alert { + @include tokens($alert-tokens); + + display: flex; + gap: var(--alert-gap); + align-items: start; + padding: var(--alert-padding-y) var(--alert-padding-x); + color: var(--alert-color); + background-color: var(--alert-bg); + border: var(--alert-border); + @include border-radius(var(--alert-border-radius)); + } - // Adjust close link position - .btn-close { - position: absolute; - top: 0; - right: 0; - z-index: $stretched-link-z-index + 1; - padding: $alert-padding-y * 1.25 $alert-padding-x; + .alert > p { + margin-bottom: 0; } -} + .alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; + } -// scss-docs-start alert-modifiers -// Generate contextual modifier classes for colorizing the alert -@each $state in map-keys($theme-colors) { - .alert-#{$state} { - --#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis); + // Provide class for links that match alerts + .alert-link { + font-weight: var(--font-weight-semibold); + color: var(--alert-link-color); } } -// scss-docs-end alert-modifiers diff --git a/assets/stylesheets/bootstrap/_avatar.scss b/assets/stylesheets/bootstrap/_avatar.scss new file mode 100644 index 00000000..14326ace --- /dev/null +++ b/assets/stylesheets/bootstrap/_avatar.scss @@ -0,0 +1,159 @@ +@use "sass:map"; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +$avatar-tokens: () !default; + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start avatar-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$avatar-tokens: defaults( + ( + --avatar-size: 2.5rem, + --avatar-border-radius: 50%, + --avatar-border-width: 2px, + --avatar-border-color: var(--bg-body), + --avatar-bg: var(--bg-2), + --avatar-color: var(--fg-body), + // --avatar-font-weight: var(--font-weight-medium), // Defaults to fallback + --avatar-status-size: .75rem, + --avatar-status-border-width: 2px, + --avatar-status-border-color: var(--bg-body), + --avatar-stack-spacing: -.3, + --avatar-stack-transition: "transform .2s ease-in-out", + ), + $avatar-tokens +); +// scss-docs-end avatar-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start avatar-sizes +$avatar-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$avatar-sizes: defaults( + ( + "xs": ( + size: 1.5rem, + status-size: .625rem, + ), + "sm": ( + size: 2rem, + ), + "lg": ( + size: 3rem, + status-size: 1rem, + border-width: 3px, + ), + "xl": ( + size: 4rem, + status-size: 1.25rem, + border-width: 4px, + ), + ), + $avatar-sizes +); +// scss-docs-end avatar-sizes + +@layer components { + .avatar { + @include tokens($avatar-tokens); + + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--avatar-size); + height: var(--avatar-size); + font-size: calc(var(--avatar-size) * .4); + font-weight: var(--avatar-font-weight, var(--font-weight-medium)); + line-height: 1; + color: var(--theme-contrast, var(--avatar-color)); + text-transform: uppercase; + vertical-align: middle; + background-color: var(--theme-bg, var(--avatar-bg)); + @include border-radius(var(--avatar-border-radius)); + + > .avatar-img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .avatar-subtle { + color: var(--theme-fg, var(--avatar-color)); + background-color: var(--theme-bg-subtle, var(--avatar-bg)); + } + + .avatar-img { + @include border-radius(var(--avatar-border-radius, 50%)); + } + + .avatar-status { + position: absolute; + right: calc(var(--avatar-status-border-width) * -1); + bottom: calc(var(--avatar-status-border-width) * -1); + width: var(--avatar-status-size); + height: var(--avatar-status-size); + background-color: var(--gray-400); + border: var(--avatar-status-border-width) solid var(--avatar-status-border-color); + @include border-radius(50%); + + &.status-online { + background-color: var(--green-500); + } + + &.status-offline { + background-color: var(--gray-400); + @include border-radius(20%); + } + + &.status-busy { + background-color: var(--red-500); + @include border-radius(20%); + } + + &.status-away { + background-color: var(--yellow-500); + } + } + + .avatar-stack { + display: inline-flex; + flex-direction: row-reverse; + + .avatar { + // Stack spacing is calculated as a percentage of avatar size + margin-left: calc(var(--avatar-size) * var(--avatar-stack-spacing)); + border: var(--avatar-border-width) solid var(--avatar-border-color); + mask-image: none; + @include transition(var(--avatar-stack-transition)); + + &:last-child { + margin-left: 0; + } + + &:hover { + z-index: 1; + transform: translateY(-2px); + } + } + } + + @each $size, $tokens in $avatar-sizes { + .avatar-#{$size}, + .avatar-stack-#{$size} > .avatar { + --avatar-size: #{map.get($tokens, size)}; + + @if map.has-key($tokens, status-size) { + --avatar-status-size: #{map.get($tokens, status-size)}; + } + + @if map.has-key($tokens, border-width) { + --avatar-border-width: #{map.get($tokens, border-width)}; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/_badge.scss b/assets/stylesheets/bootstrap/_badge.scss index cc3d2695..7fdb38d8 100644 --- a/assets/stylesheets/bootstrap/_badge.scss +++ b/assets/stylesheets/bootstrap/_badge.scss @@ -1,38 +1,90 @@ -// Base class -// -// Requires one of the contextual, color modifier classes for `color` and -// `background-color`. - -.badge { - // scss-docs-start badge-css-vars - --#{$prefix}badge-padding-x: #{$badge-padding-x}; - --#{$prefix}badge-padding-y: #{$badge-padding-y}; - @include rfs($badge-font-size, --#{$prefix}badge-font-size); - --#{$prefix}badge-font-weight: #{$badge-font-weight}; - --#{$prefix}badge-color: #{$badge-color}; - --#{$prefix}badge-border-radius: #{$badge-border-radius}; - // scss-docs-end badge-css-vars - - display: inline-block; - padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x); - @include font-size(var(--#{$prefix}badge-font-size)); - font-weight: var(--#{$prefix}badge-font-weight); - line-height: 1; - color: var(--#{$prefix}badge-color); - text-align: center; - white-space: nowrap; - vertical-align: baseline; - @include border-radius(var(--#{$prefix}badge-border-radius)); - @include gradient-bg(); - - // Empty badges collapse automatically - &:empty { - display: none; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$badge-tokens: () !default; + +// scss-docs-start badge-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$badge-tokens: defaults( + ( + --badge-padding-x: .625em, + --badge-padding-y: .25em, + --badge-font-size: clamp(12px, .75em, .75em), + --badge-font-weight: var(--font-weight-semibold), + --badge-color: inherit, + --badge-bg: var(--bg-2), + --badge-border-width: var(--border-width), + --badge-border-color: transparent, + --badge-border-radius: var(--radius-7), + ), + $badge-tokens +); +// scss-docs-end badge-tokens + +// scss-docs-start badge-variants +$badge-variants: ( + "subtle": ( + "color": "fg", + "bg": "bg-subtle", + "border-color": "transparent" + ), + "outline": ( + "color": "fg", + "bg": "transparent", + "border-color": "border" + ) +) !default; +// scss-docs-end badge-variants + +@layer components { + .badge { + @include tokens($badge-tokens); + + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.375rem; + padding: var(--badge-padding-y) var(--badge-padding-x); + font-size: var(--badge-font-size); + font-weight: var(--badge-font-weight); + line-height: 1; + color: var(--theme-contrast, var(--badge-color)); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: var(--theme-bg, var(--badge-bg)); + border: var(--badge-border-width) solid var(--badge-border-color); + @include border-radius(var(--badge-border-radius)); + // @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } + } + + // Quick fix for badges in buttons + .btn .badge { + position: relative; + top: -1px; } -} -// Quick fix for badges in buttons -.btn .badge { - position: relative; - top: -1px; + // scss-docs-start badge-variant-loop + @each $variant, $properties in $badge-variants { + .badge-#{$variant} { + @each $property, $value in $properties { + @if $value == "transparent" { + --badge-#{$property}: transparent; + } @else { + --badge-#{$property}: var(--theme-#{$value}); + } + } + + color: var(--badge-color); + background-color: var(--badge-bg); + border-color: var(--badge-border-color); + } + } + // scss-docs-end badge-variant-loop } diff --git a/assets/stylesheets/bootstrap/_banner.scss b/assets/stylesheets/bootstrap/_banner.scss new file mode 100644 index 00000000..c96aa3ca --- /dev/null +++ b/assets/stylesheets/bootstrap/_banner.scss @@ -0,0 +1,7 @@ +$file: "" !default; + +/*! + * Bootstrap #{$file} v6.0.0-dev (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/assets/stylesheets/bootstrap/_breadcrumb.scss b/assets/stylesheets/bootstrap/_breadcrumb.scss index b8252ff2..430999d6 100644 --- a/assets/stylesheets/bootstrap/_breadcrumb.scss +++ b/assets/stylesheets/bootstrap/_breadcrumb.scss @@ -1,40 +1,90 @@ -.breadcrumb { - // scss-docs-start breadcrumb-css-vars - --#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x}; - --#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y}; - --#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom}; - @include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size); - --#{$prefix}breadcrumb-bg: #{$breadcrumb-bg}; - --#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius}; - --#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color}; - --#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x}; - --#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color}; - // scss-docs-end breadcrumb-css-vars - - display: flex; - flex-wrap: wrap; - padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x); - margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom); - @include font-size(var(--#{$prefix}breadcrumb-font-size)); - list-style: none; - background-color: var(--#{$prefix}breadcrumb-bg); - @include border-radius(var(--#{$prefix}breadcrumb-border-radius)); -} +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/mask-icon" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +$breadcrumb-tokens: () !default; + +// scss-docs-start breadcrumb-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$breadcrumb-tokens: defaults( + ( + --breadcrumb-margin-bottom: 1rem, + --breadcrumb-font-size: inherit, + --breadcrumb-bg: transparent, + --breadcrumb-border-radius: var(--radius-5), + --breadcrumb-divider-color: var(--fg-4), + --breadcrumb-divider-icon: #{escape-svg(url("data:image/svg+xml,"))}, + --breadcrumb-divider-width: .375rem, + --breadcrumb-divider-height: .75rem, + --breadcrumb-link-padding-x: .75rem, + --breadcrumb-link-padding-y: .25rem, + --breadcrumb-link-color: var(--fg-3), + --breadcrumb-link-hover-color: var(--fg-2), + --breadcrumb-link-hover-bg: var(--bg-1), + --breadcrumb-link-active-color: var(--fg-1), + --breadcrumb-link-border-radius: var(--radius-7), + ), + $breadcrumb-tokens +); +// scss-docs-end breadcrumb-tokens + +@layer components { + .breadcrumb { + @include tokens($breadcrumb-tokens); + + display: flex; + flex-wrap: wrap; + align-items: center; + padding: var(--breadcrumb-padding-y, 0) var(--breadcrumb-padding-x, 0); + font-size: var(--breadcrumb-font-size); + list-style-type: ""; + background-color: var(--breadcrumb-bg); + @include border-radius(var(--breadcrumb-border-radius)); + } -.breadcrumb-item { - // The separator between breadcrumbs (by default, a forward-slash: "/") - + .breadcrumb-item { - padding-left: var(--#{$prefix}breadcrumb-item-padding-x); + .breadcrumb-item { + display: flex; + } + + .breadcrumb-divider { + margin-inline: calc(var(--breadcrumb-link-padding-x) / 4); + color: var(--breadcrumb-divider-color); - &::before { - float: left; // Suppress inline spacings and underlining of the separator - padding-right: var(--#{$prefix}breadcrumb-item-padding-x); - color: var(--#{$prefix}breadcrumb-divider-color); - content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; + // Render a default chevron, painted with `currentcolor` via a mask, when the + // divider has no explicit content. Any content (an inline SVG, a text + // character, etc.) added to the element overrides this default. + &:empty::before { + display: block; + width: var(--breadcrumb-divider-width); + height: var(--breadcrumb-divider-height); + content: ""; + background-color: currentcolor; + @include mask-icon(var(--breadcrumb-divider-icon)); } } - &.active { - color: var(--#{$prefix}breadcrumb-item-active-color); + .breadcrumb-link { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 2.25rem; + padding: var(--breadcrumb-link-padding-y) var(--breadcrumb-link-padding-x); + color: var(--breadcrumb-link-color); + text-decoration: none; + @include border-radius(var(--breadcrumb-link-border-radius)); + @include transition(.1s text-decoration-color ease-in-out); + + &:hover { + z-index: 2; + color: var(--breadcrumb-link-hover-color); + background-color: var(--breadcrumb-link-hover-bg); + } + + &.active { + color: var(--breadcrumb-link-active-color); + } } } diff --git a/assets/stylesheets/bootstrap/_button-group.scss b/assets/stylesheets/bootstrap/_button-group.scss deleted file mode 100644 index 78e12522..00000000 --- a/assets/stylesheets/bootstrap/_button-group.scss +++ /dev/null @@ -1,147 +0,0 @@ -// Make the div behave like a button -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-flex; - vertical-align: middle; // match .btn alignment given font-size hack above - - > .btn { - position: relative; - flex: 1 1 auto; - } - - // Bring the hover, focused, and "active" buttons to the front to overlay - // the borders properly - > .btn-check:checked + .btn, - > .btn-check:focus + .btn, - > .btn:hover, - > .btn:focus, - > .btn:active, - > .btn.active { - z-index: 1; - } -} - -// Optional: Group multiple button groups together for a toolbar -.btn-toolbar { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - - .input-group { - width: auto; - } -} - -.btn-group { - @include border-radius($btn-border-radius); - - // Prevent double borders when buttons are next to each other - > :not(.btn-check:first-child) + .btn, - > .btn-group:not(:first-child) { - margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list - } - - // Reset rounded corners - > .btn:not(:last-child):not(.dropdown-toggle), - > .btn.dropdown-toggle-split:first-child, - > .btn-group:not(:last-child) > .btn { - @include border-end-radius(0); - } - - // The left radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, - > .btn-group:not(:first-child) > .btn { - @include border-start-radius(0); - } -} - -// Sizing -// -// Remix the default button sizing classes into new ones for easier manipulation. - -.btn-group-sm > .btn { @extend .btn-sm; } -.btn-group-lg > .btn { @extend .btn-lg; } - - -// -// Split button dropdowns -// - -.dropdown-toggle-split { - padding-right: $btn-padding-x * .75; - padding-left: $btn-padding-x * .75; - - &::after, - .dropup &::after, - .dropend &::after { - margin-left: 0; - } - - .dropstart &::before { - margin-right: 0; - } -} - -.btn-sm + .dropdown-toggle-split { - padding-right: $btn-padding-x-sm * .75; - padding-left: $btn-padding-x-sm * .75; -} - -.btn-lg + .dropdown-toggle-split { - padding-right: $btn-padding-x-lg * .75; - padding-left: $btn-padding-x-lg * .75; -} - - -// The clickable button for toggling the menu -// Set the same inset shadow as the :active state -.btn-group.show .dropdown-toggle { - @include box-shadow($btn-active-box-shadow); - - // Show no shadow for `.btn-link` since it has no other button styles. - &.btn-link { - @include box-shadow(none); - } -} - - -// -// Vertical button groups -// - -.btn-group-vertical { - flex-direction: column; - align-items: flex-start; - justify-content: center; - - > .btn, - > .btn-group { - width: 100%; - } - - > .btn:not(:first-child), - > .btn-group:not(:first-child) { - margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list - } - - // Reset rounded corners - > .btn:not(:last-child):not(.dropdown-toggle), - > .btn-group:not(:last-child) > .btn { - @include border-bottom-radius(0); - } - - // The top radius should be 0 if the button is: - // - the "third or more" child - // - the second child and the previous element isn't `.btn-check` (making it the first child visually) - // - part of a btn-group which isn't the first child - > .btn:nth-child(n + 3), - > :not(.btn-check) + .btn, - > .btn-group:not(:first-child) > .btn { - @include border-top-radius(0); - } -} diff --git a/assets/stylesheets/bootstrap/_buttons.scss b/assets/stylesheets/bootstrap/_buttons.scss deleted file mode 100644 index caa4518a..00000000 --- a/assets/stylesheets/bootstrap/_buttons.scss +++ /dev/null @@ -1,216 +0,0 @@ -// -// Base styles -// - -.btn { - // scss-docs-start btn-css-vars - --#{$prefix}btn-padding-x: #{$btn-padding-x}; - --#{$prefix}btn-padding-y: #{$btn-padding-y}; - --#{$prefix}btn-font-family: #{$btn-font-family}; - @include rfs($btn-font-size, --#{$prefix}btn-font-size); - --#{$prefix}btn-font-weight: #{$btn-font-weight}; - --#{$prefix}btn-line-height: #{$btn-line-height}; - --#{$prefix}btn-color: #{$btn-color}; - --#{$prefix}btn-bg: transparent; - --#{$prefix}btn-border-width: #{$btn-border-width}; - --#{$prefix}btn-border-color: transparent; - --#{$prefix}btn-border-radius: #{$btn-border-radius}; - --#{$prefix}btn-hover-border-color: transparent; - --#{$prefix}btn-box-shadow: #{$btn-box-shadow}; - --#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity}; - --#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5); - // scss-docs-end btn-css-vars - - display: inline-block; - padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x); - font-family: var(--#{$prefix}btn-font-family); - @include font-size(var(--#{$prefix}btn-font-size)); - font-weight: var(--#{$prefix}btn-font-weight); - line-height: var(--#{$prefix}btn-line-height); - color: var(--#{$prefix}btn-color); - text-align: center; - text-decoration: if($link-decoration == none, null, none); - white-space: $btn-white-space; - vertical-align: middle; - cursor: if($enable-button-pointers, pointer, null); - user-select: none; - border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color); - @include border-radius(var(--#{$prefix}btn-border-radius)); - @include gradient-bg(var(--#{$prefix}btn-bg)); - @include box-shadow(var(--#{$prefix}btn-box-shadow)); - @include transition($btn-transition); - - &:hover { - color: var(--#{$prefix}btn-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - background-color: var(--#{$prefix}btn-hover-bg); - border-color: var(--#{$prefix}btn-hover-border-color); - } - - .btn-check + &:hover { - // override for the checkbox/radio buttons - color: var(--#{$prefix}btn-color); - background-color: var(--#{$prefix}btn-bg); - border-color: var(--#{$prefix}btn-border-color); - } - - &:focus-visible { - color: var(--#{$prefix}btn-hover-color); - @include gradient-bg(var(--#{$prefix}btn-hover-bg)); - border-color: var(--#{$prefix}btn-hover-border-color); - outline: 0; - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - .btn-check:focus-visible + & { - border-color: var(--#{$prefix}btn-hover-border-color); - outline: 0; - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - .btn-check:checked + &, - :not(.btn-check) + &:active, - &:first-child:active, - &.active, - &.show { - color: var(--#{$prefix}btn-active-color); - background-color: var(--#{$prefix}btn-active-bg); - // Remove CSS gradients if they're enabled - background-image: if($enable-gradients, none, null); - border-color: var(--#{$prefix}btn-active-border-color); - @include box-shadow(var(--#{$prefix}btn-active-shadow)); - - &:focus-visible { - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - } - - .btn-check:checked:focus-visible + & { - // Avoid using mixin so we can pass custom focus shadow properly - @if $enable-shadows { - box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow); - } @else { - box-shadow: var(--#{$prefix}btn-focus-box-shadow); - } - } - - &:disabled, - &.disabled, - fieldset:disabled & { - color: var(--#{$prefix}btn-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}btn-disabled-bg); - background-image: if($enable-gradients, none, null); - border-color: var(--#{$prefix}btn-disabled-border-color); - opacity: var(--#{$prefix}btn-disabled-opacity); - @include box-shadow(none); - } -} - - -// -// Alternate buttons -// - -// scss-docs-start btn-variant-loops -@each $color, $value in $theme-colors { - .btn-#{$color} { - @if $color == "light" { - @include button-variant( - $value, - $value, - $hover-background: shade-color($value, $btn-hover-bg-shade-amount), - $hover-border: shade-color($value, $btn-hover-border-shade-amount), - $active-background: shade-color($value, $btn-active-bg-shade-amount), - $active-border: shade-color($value, $btn-active-border-shade-amount) - ); - } @else if $color == "dark" { - @include button-variant( - $value, - $value, - $hover-background: tint-color($value, $btn-hover-bg-tint-amount), - $hover-border: tint-color($value, $btn-hover-border-tint-amount), - $active-background: tint-color($value, $btn-active-bg-tint-amount), - $active-border: tint-color($value, $btn-active-border-tint-amount) - ); - } @else { - @include button-variant($value, $value); - } - } -} - -@each $color, $value in $theme-colors { - .btn-outline-#{$color} { - @include button-outline-variant($value); - } -} -// scss-docs-end btn-variant-loops - - -// -// Link buttons -// - -// Make a button look and behave like a link -.btn-link { - --#{$prefix}btn-font-weight: #{$font-weight-normal}; - --#{$prefix}btn-color: #{$btn-link-color}; - --#{$prefix}btn-bg: transparent; - --#{$prefix}btn-border-color: transparent; - --#{$prefix}btn-hover-color: #{$btn-link-hover-color}; - --#{$prefix}btn-hover-border-color: transparent; - --#{$prefix}btn-active-color: #{$btn-link-hover-color}; - --#{$prefix}btn-active-border-color: transparent; - --#{$prefix}btn-disabled-color: #{$btn-link-disabled-color}; - --#{$prefix}btn-disabled-border-color: transparent; - --#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows - --#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb}; - - text-decoration: $link-decoration; - @if $enable-gradients { - background-image: none; - } - - &:hover, - &:focus-visible { - text-decoration: $link-hover-decoration; - } - - &:focus-visible { - color: var(--#{$prefix}btn-color); - } - - &:hover { - color: var(--#{$prefix}btn-hover-color); - } - - // No need for an active state here -} - - -// -// Button Sizes -// - -.btn-lg { - @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); -} - -.btn-sm { - @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); -} diff --git a/assets/stylesheets/bootstrap/_card.scss b/assets/stylesheets/bootstrap/_card.scss index dcebe6ac..ee99de75 100644 --- a/assets/stylesheets/bootstrap/_card.scss +++ b/assets/stylesheets/bootstrap/_card.scss @@ -1,235 +1,306 @@ -// -// Base styles -// - -.card { - // scss-docs-start card-css-vars - --#{$prefix}card-spacer-y: #{$card-spacer-y}; - --#{$prefix}card-spacer-x: #{$card-spacer-x}; - --#{$prefix}card-title-spacer-y: #{$card-title-spacer-y}; - --#{$prefix}card-title-color: #{$card-title-color}; - --#{$prefix}card-subtitle-color: #{$card-subtitle-color}; - --#{$prefix}card-border-width: #{$card-border-width}; - --#{$prefix}card-border-color: #{$card-border-color}; - --#{$prefix}card-border-radius: #{$card-border-radius}; - --#{$prefix}card-box-shadow: #{$card-box-shadow}; - --#{$prefix}card-inner-border-radius: #{$card-inner-border-radius}; - --#{$prefix}card-cap-padding-y: #{$card-cap-padding-y}; - --#{$prefix}card-cap-padding-x: #{$card-cap-padding-x}; - --#{$prefix}card-cap-bg: #{$card-cap-bg}; - --#{$prefix}card-cap-color: #{$card-cap-color}; - --#{$prefix}card-height: #{$card-height}; - --#{$prefix}card-color: #{$card-color}; - --#{$prefix}card-bg: #{$card-bg}; - --#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding}; - --#{$prefix}card-group-margin: #{$card-group-margin}; - // scss-docs-end card-css-vars - - position: relative; - display: flex; - flex-direction: column; - min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 - height: var(--#{$prefix}card-height); - color: var(--#{$prefix}body-color); - word-wrap: break-word; - background-color: var(--#{$prefix}card-bg); - background-clip: border-box; - border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); - @include border-radius(var(--#{$prefix}card-border-radius)); - @include box-shadow(var(--#{$prefix}card-box-shadow)); - - > hr { - margin-right: 0; - margin-left: 0; - } - - > .list-group { - border-top: inherit; - border-bottom: inherit; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; +@use "layout/breakpoints" as *; + +$card-tokens: () !default; + +// scss-docs-start card-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$card-tokens: defaults( + ( + --card-spacer-y: var(--spacer-5), + --card-spacer-x: var(--spacer-5), + --card-subtitle-color: inherit, + --card-border-width: var(--border-width), + --card-border-color: var(--border-color-translucent), + --card-border-radius: var(--radius-7), + --card-box-shadow: none, + --card-inner-border-radius: calc(var(--radius-7) - var(--border-width)), + --card-cap-padding-y: var(--spacer-3), + --card-cap-padding-x: var(--spacer), + --card-cap-bg: var(--bg-1), + --card-cap-color: inherit, + --card-height: auto, + --card-color: inherit, + --card-bg: var(--bg-body), + --card-img-overlay-padding: var(--card-spacer-y), + --card-group-margin: #{$grid-gutter-x * .5}, + --card-body-gap: calc(var(--card-spacer-y) * .5), + ), + $card-tokens +); +// scss-docs-end card-tokens + +@layer components { + .card { + @include tokens($card-tokens); + + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: var(--card-height); + color: var(--fg-body); + word-wrap: break-word; + background-color: var(--card-bg); + // border: var(--card-border-width) solid var(--card-border-color); + @include border-radius(var(--card-border-radius)); + @include box-shadow(var(--card-box-shadow)); + + > hr { + margin-inline: 0; + } + } + + .card-body { + display: flex; + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + flex-direction: column; + gap: var(--card-body-gap); + align-items: flex-start; + padding: var(--card-spacer-y) var(--card-spacer-x); + color: var(--card-color); + border: solid var(--theme-bg, var(--card-border-color)); + border-width: 0 var(--card-border-width); + + > * { + margin-block: 0; + } + } + + .card-body, + .card-list { + border: solid var(--theme-bg, var(--card-border-color)); + border-width: 0 var(--card-border-width); &:first-child { - border-top-width: 0; - @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); + @include border-top-radius(var(--card-border-radius)); + border-top-width: var(--card-border-width); } - &:last-child { - border-bottom-width: 0; - @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); + &:last-child { + @include border-bottom-radius(var(--card-border-radius)); + border-bottom-width: var(--card-border-width); + } + + &:not(:first-child, :last-child) { + border-block-end-width: var(--card-border-width); + } + + // The footer draws a full border (including its top edge), so a body/list + // segment that precedes it must not also draw a bottom border or the seam + // doubles up. + &:has(+ .card-footer) { + border-block-end-width: 0; } } - // Due to specificity of the above selector (`.card > .list-group`), we must - // use a child selector here to prevent double borders. - > .card-header + .list-group, - > .list-group + .card-footer { - border-top: 0; + .card-title, + .card-subtitle, + .card-text { + align-self: stretch; } -} -.card-body { - // Enable `flex-grow: 1` for decks and groups so that card blocks take up - // as much space as possible, ensuring footers are aligned to the bottom. - flex: 1 1 auto; - padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x); - color: var(--#{$prefix}card-color); -} + .card-subtitle { + margin-top: calc(var(--card-body-gap) * -.5); + } -.card-title { - margin-bottom: var(--#{$prefix}card-title-spacer-y); - color: var(--#{$prefix}card-title-color); -} + .card-header { + padding: var(--card-cap-padding-y) var(--card-cap-padding-x); + margin-bottom: 0; // Removes the default margin-bottom of + color: var(--theme-contrast, var(--card-cap-color)); + background-color: var(--theme-bg, var(--card-cap-bg)); + border: var(--card-border-width) solid var(--theme-bg, var(--card-border-color)); -.card-subtitle { - margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list - margin-bottom: 0; - color: var(--#{$prefix}card-subtitle-color); -} + &:first-child { + @include border-radius(var(--card-inner-border-radius) var(--card-inner-border-radius) 0 0); + } + } -.card-text:last-child { - margin-bottom: 0; -} + .card-footer { + padding: var(--card-cap-padding-y) var(--card-cap-padding-x); + color: var(--card-cap-color); + background-color: var(--theme-bg, var(--card-cap-bg)); + border: var(--card-border-width) solid var(--theme-bg, var(--card-border-color)); -.card-link { - &:hover { - text-decoration: if($link-hover-decoration == underline, none, null); + &:last-child { + @include border-radius(0 0 var(--card-inner-border-radius) var(--card-inner-border-radius)); + } } - + .card-link { - margin-left: var(--#{$prefix}card-spacer-x); + .card-translucent { + background-color: color-mix(in oklch, var(--card-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + + .card-header, + .card-footer { + background-color: color-mix(in oklch, var(--card-cap-bg) 60%, transparent); + } } -} -// -// Optional textual caps -// + .card-subtle { + border-color: var(--theme-border, var(--card-border-color)); -.card-header { - padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); - margin-bottom: 0; // Removes the default margin-bottom of - color: var(--#{$prefix}card-cap-color); - background-color: var(--#{$prefix}card-cap-bg); - border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + .card-header { + color: var(--theme-fg-emphasis, currentcolor); + background-color: var(--theme-bg-subtle, var(--card-cap-bg)); + border-color: var(--theme-border, var(--card-border-color)); + } - &:first-child { - @include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0); + .card-footer { + color: var(--theme-fg-emphasis, currentcolor); + background-color: var(--theme-bg-subtle, var(--card-cap-bg)); + border-color: var(--theme-border, var(--card-border-color)); + } + + .card-body, + .card-list { + border-color: var(--theme-border, var(--card-border-color)); + } } -} -.card-footer { - padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x); - color: var(--#{$prefix}card-cap-color); - background-color: var(--#{$prefix}card-cap-bg); - border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color); + // + // Header navs + // - &:last-child { - @include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius)); - } -} + // Combined selector because of specificity match with `.nav` base class + .nav.card-header-tabs { + margin-inline: calc(-.5 * var(--card-cap-padding-x)); + margin-bottom: calc(-1 * var(--card-cap-padding-y) - var(--nav-tabs-border-width)); + border-block-end: 0; + .nav-link.active { + background-color: var(--card-bg); + border-block-end-color: var(--card-bg); + } + } -// -// Header navs -// + // Card image + .card-img-overlay { + position: absolute; + inset: 0; + padding: var(--card-img-overlay-padding); + @include border-radius(var(--card-inner-border-radius)); + } -.card-header-tabs { - margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - border-bottom: 0; + .card-img, + .card-img-top, + .card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch + outline: var(--card-border-width) solid var(--card-border-color); + outline-offset: calc(var(--card-border-width) * -1); + } - .nav-link.active { - background-color: var(--#{$prefix}card-bg); - border-bottom-color: var(--#{$prefix}card-bg); + .card-img, + .card-img-top { + @include border-top-radius(var(--card-inner-border-radius)); } -} -.card-header-pills { - margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list -} + .card-img, + .card-img-bottom { + @include border-bottom-radius(var(--card-inner-border-radius)); + } -// Card image -.card-img-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: var(--#{$prefix}card-img-overlay-padding); - @include border-radius(var(--#{$prefix}card-inner-border-radius)); -} + .card-row { + flex-direction: row; -.card-img, -.card-img-top, -.card-img-bottom { - width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch -} + .card-body, + .card-list { + border-width: var(--card-border-width) 0; + @include border-radius(0); -.card-img, -.card-img-top { - @include border-top-radius(var(--#{$prefix}card-inner-border-radius)); -} + &:first-child { + @include border-start-radius(var(--card-inner-border-radius)); + border-inline-start-width: var(--card-border-width); + } -.card-img, -.card-img-bottom { - @include border-bottom-radius(var(--#{$prefix}card-inner-border-radius)); -} + &:last-child { + @include border-end-radius(var(--card-inner-border-radius)); + border-inline-end-width: var(--card-border-width); + } + &:not(:first-child, :last-child) { + border-inline-end-width: var(--card-border-width); + } + } + } -// -// Card groups -// + .card-img-start { + @include border-start-radius(var(--card-inner-border-radius)); + } -.card-group { - // The child selector allows nested `.card` within `.card-group` - // to display properly. - > .card { - margin-bottom: var(--#{$prefix}card-group-margin); + .card-img-end { + @include border-end-radius(var(--card-inner-border-radius)); } - @include media-breakpoint-up(sm) { - display: flex; - flex-flow: row wrap; + // + // Card groups + // + + // Card groups lay out their cards in a row using a container query, so wrap the + // group in a query container (e.g., the `.contains-inline` utility) for the row + // layout to take effect. Without a query container the cards remain stacked. + .card-group { // The child selector allows nested `.card` within `.card-group` // to display properly. > .card { - flex: 1 0 0; - margin-bottom: 0; - - + .card { - margin-left: 0; - border-left: 0; - } - - // Handle rounded corners - @if $enable-rounded { - &:not(:last-child) { - @include border-end-radius(0); + margin-bottom: var(--card-group-margin); + } - > .card-img-top, - > .card-header { - // stylelint-disable-next-line property-disallowed-list - border-top-right-radius: 0; - } - > .card-img-bottom, - > .card-footer { - // stylelint-disable-next-line property-disallowed-list - border-bottom-right-radius: 0; - } + @include container-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + flex: 1 0 0; + margin-bottom: 0; + + // Borders now live on the inner segments (header, body, list, footer) + // and the card images use outlines, so adjacent cards would otherwise + // render a doubled-up border at each seam. Pull subsequent cards back by + // one border width so their leading edges overlap the previous card's + // trailing edge, collapsing the seam into a single line. Gap can't be + // negative, so this relies on a negative margin. + + .card { + margin-inline-start: calc(-1 * var(--card-border-width)); } - &:not(:first-child) { - @include border-start-radius(0); - - > .card-img-top, - > .card-header { - // stylelint-disable-next-line property-disallowed-list - border-top-left-radius: 0; + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + > .card-img-top, + > .card-header, + > .card-body { + border-start-end-radius: 0; + } + > .card-img-bottom, + > .card-footer, + > .card-body { + border-end-end-radius: 0; + } } - > .card-img-bottom, - > .card-footer { - // stylelint-disable-next-line property-disallowed-list - border-bottom-left-radius: 0; + + &:not(:first-child) { + @include border-start-radius(0); + + > .card-img-top, + > .card-header, + > .card-body { + border-start-start-radius: 0; + } + > .card-img-bottom, + > .card-footer, + > .card-body { + border-end-start-radius: 0; + } } } } diff --git a/assets/stylesheets/bootstrap/_carousel.scss b/assets/stylesheets/bootstrap/_carousel.scss index 5ebf6b15..2cbeab7b 100644 --- a/assets/stylesheets/bootstrap/_carousel.scss +++ b/assets/stylesheets/bootstrap/_carousel.scss @@ -1,226 +1,263 @@ -// Notes on the classes: -// -// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) -// even when their scroll action started on a carousel, but for compatibility (with Firefox) -// we're preventing all actions instead -// 2. The .carousel-item-start and .carousel-item-end is used to indicate where -// the active slide is heading. -// 3. .active.carousel-item is the current slide. -// 4. .active.carousel-item-start and .active.carousel-item-end is the current -// slide in its in-transition state. Only one of these occurs at a time. -// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end -// is the upcoming slide in transition. - -.carousel { - position: relative; -} - -.carousel.pointer-event { - touch-action: pan-y; -} +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/transition" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; - @include clearfix(); -} +$carousel-tokens: () !default; -.carousel-item { - position: relative; - display: none; - float: left; - width: 100%; - margin-right: -100%; - backface-visibility: hidden; - @include transition($carousel-transition); -} +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start carousel-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$carousel-tokens: defaults( + ( + --carousel-gap: .75rem, + --carousel-indicator-bg: var(--fg-3), + --carousel-indicator-width: .75rem, + --carousel-indicator-height: .75rem, + --carousel-indicator-spacer: .25rem, + --carousel-indicator-transition: "opacity .6s ease, width .3s ease", + --carousel-indicator-progress-bg: var(--carousel-indicator-bg), + --carousel-control-icon-width: 1rem, + --carousel-control-prev-icon: url("data:image/svg+xml,"), + --carousel-control-next-icon: url("data:image/svg+xml,"), + --carousel-control-pause-icon: url("data:image/svg+xml,"), + --carousel-control-play-icon: url("data:image/svg+xml,"), + // Scroll-snap engine. `gap` must carry a length unit: it feeds the + // `.carousel-item` flex-basis `calc()`, and subtracting a unitless `0` from a + // percentage is invalid CSS (it would drop the whole declaration and collapse + // every slide to its content width). `peek` only feeds `padding-inline`/ + // `scroll-padding-inline`, so a bare `0` would be valid there, but we keep it + // unit-bearing for consistency. + --carousel-items: 1, + --carousel-items-gap: 0px, + --carousel-items-peek: 0px, + --carousel-fade-duration: .6s, + ), + $carousel-tokens +); +// scss-docs-end carousel-tokens +// stylelint-enable custom-property-no-missing-var-function -.carousel-item.active, -.carousel-item-next, -.carousel-item-prev { - display: block; -} +@layer components { + .carousel { + @include tokens($carousel-tokens); -.carousel-item-next:not(.carousel-item-start), -.active.carousel-item-end { - transform: translateX(100%); -} + position: relative; + display: flex; + flex-direction: column; + gap: var(--carousel-gap); + } -.carousel-item-prev:not(.carousel-item-end), -.active.carousel-item-start { - transform: translateX(-100%); -} + // The scroll viewport + .carousel-inner { + display: flex; + gap: var(--carousel-items-gap); + width: 100%; + padding-inline: var(--carousel-items-peek); + overflow-x: auto; + overscroll-behavior-x: contain; + scroll-snap-type: x mandatory; + scroll-padding-inline: var(--carousel-items-peek); + scrollbar-width: none; // Hide the scrollbar without losing scrollability + &::-webkit-scrollbar { + display: none; + } + } -// -// Alternate transitions -// + // Smooth programmatic/keyboard scrolling, disabled under reduced-motion + @media (prefers-reduced-motion: no-preference) { + .carousel-inner { + scroll-behavior: smooth; + } + } -.carousel-fade { .carousel-item { - opacity: 0; - transition-property: opacity; - transform: none; + // `100%` here is `.carousel-inner`'s content box, which `padding-inline` + // has already inset by the peek on each side, so the peek must NOT be + // subtracted again — doing so makes every slide `2 * peek` too narrow and + // the peek lopsided. Only the inter-slide gaps need removing. + flex: 0 0 calc((100% - (var(--carousel-items) - 1) * var(--carousel-items-gap)) / var(--carousel-items)); + min-width: 0; + scroll-snap-align: start; + scroll-snap-stop: always; } - .carousel-item.active, - .carousel-item-next.carousel-item-start, - .carousel-item-prev.carousel-item-end { - z-index: 1; - opacity: 1; + // + // Layout variants + // + + // Center the active slide in the viewport (pairs well with `--carousel-items-peek`) + .carousel-center { + .carousel-item { + scroll-snap-align: center; + } } - .active.carousel-item-start, - .active.carousel-item-end { - z-index: 0; - opacity: 0; - @include transition(opacity 0s $carousel-transition-duration); + // Let each slide size itself; snap points still land on every item + .carousel-auto { + .carousel-item { + flex-basis: auto; + } } -} + // + // Alternate transitions + // + + // Fade can't ride scroll-snap (it stacks slides instead of scrolling), so it + // becomes a JavaScript-driven mode: every slide is stacked and the active one + // is faded in via a CSS opacity transition. + .carousel-fade { + .carousel-inner { + display: grid; + overflow: hidden; + scroll-snap-type: none; + } -// -// Left/right controls for nav -// - -.carousel-control-prev, -.carousel-control-next { - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - // Use flex for alignment (1-3) - display: flex; // 1. allow flex styles - align-items: center; // 2. vertically center contents - justify-content: center; // 3. horizontally center contents - width: $carousel-control-width; - padding: 0; - color: $carousel-control-color; - text-align: center; - background: none; - filter: var(--#{$prefix}carousel-control-icon-filter); - border: 0; - opacity: $carousel-control-opacity; - @include transition($carousel-control-transition); - - // Hover/focus state - &:hover, - &:focus { - color: $carousel-control-color; - text-decoration: none; - outline: 0; - opacity: $carousel-control-hover-opacity; + .carousel-item { + grid-area: 1 / 1; + width: 100%; + visibility: hidden; + opacity: 0; + @include transition(opacity var(--carousel-fade-duration) ease, visibility 0s linear var(--carousel-fade-duration)); + } + + .carousel-item.active { + visibility: visible; + opacity: 1; + @include transition(opacity var(--carousel-fade-duration) ease); + } } -} -.carousel-control-prev { - left: 0; - background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); -} -.carousel-control-next { - right: 0; - background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); -} -// Icons for within -.carousel-control-prev-icon, -.carousel-control-next-icon { - display: inline-block; - width: $carousel-control-icon-width; - height: $carousel-control-icon-width; - background-repeat: no-repeat; - background-position: 50%; - background-size: 100% 100%; -} + // Icons for within, rendered via CSS mask so they inherit the current text + // color (white on the overlay controls, the button color inside `.btn-*`). + .carousel-icon-prev, + .carousel-icon-next, + .carousel-icon-pause, + .carousel-icon-play { + display: inline-block; + width: var(--carousel-control-icon-width); + height: var(--carousel-control-icon-width); + background-color: currentcolor; + @include mask-icon($size: 100% 100%, $position: 50%); + } -.carousel-control-prev-icon { - background-image: escape-svg($carousel-control-prev-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-next-icon-bg) + "*/"}; -} -.carousel-control-next-icon { - background-image: escape-svg($carousel-control-next-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-prev-icon-bg) + "*/"}; -} + .carousel-icon-prev { + mask-image: var(--carousel-control-prev-icon); + } -// Optional indicator pips/controls -// -// Add a container (such as a list) with the following class and add an item (ideally a focusable control, -// like a button) with data-bs-target for each slide your carousel holds. - -.carousel-indicators { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - display: flex; - justify-content: center; - padding: 0; - // Use the .carousel-control's width as margin so we don't overlay those - margin-right: $carousel-control-width; - margin-bottom: 1rem; - margin-left: $carousel-control-width; - - [data-bs-target] { - box-sizing: content-box; - flex: 0 1 auto; - width: $carousel-indicator-width; - height: $carousel-indicator-height; - padding: 0; - margin-right: $carousel-indicator-spacer; - margin-left: $carousel-indicator-spacer; - text-indent: -999px; - cursor: pointer; - background-color: var(--#{$prefix}carousel-indicator-active-bg); - background-clip: padding-box; - border: 0; - // Use transparent borders to increase the hit area by 10px on top and bottom. - border-top: $carousel-indicator-hit-area-height solid transparent; - border-bottom: $carousel-indicator-hit-area-height solid transparent; - opacity: $carousel-indicator-opacity; - @include transition($carousel-indicator-transition); - } - - .active { - opacity: $carousel-indicator-active-opacity; + .carousel-icon-next { + mask-image: var(--carousel-control-next-icon); } -} + [dir="rtl"] .carousel-icon-prev, + [dir="rtl"] .carousel-icon-next { + transform: scaleX(-1); + } -// Optional captions -// -// + .carousel-icon-pause { + mask-image: var(--carousel-control-pause-icon); + } -.carousel-caption { - position: absolute; - right: (100% - $carousel-caption-width) * .5; - bottom: $carousel-caption-spacer; - left: (100% - $carousel-caption-width) * .5; - padding-top: $carousel-caption-padding-y; - padding-bottom: $carousel-caption-padding-y; - color: var(--#{$prefix}carousel-caption-color); - text-align: center; -} + .carousel-icon-play { + mask-image: var(--carousel-control-play-icon); + } -// Dark mode carousel + // Optional play/pause control + // + // A discoverable toggle so users can stop an autoplaying carousel, as required + // by WCAG 2.2.2 (Pause, Stop, Hide). `.carousel-control-play-pause` is only a + // behavior hook—JS toggles `.paused` on it and its appearance comes from the + // wrapping button (e.g. `.btn-icon`). The button holds both glyphs and we show + // whichever `.carousel-icon-*` matches the current state. + .carousel-control-play-pause .carousel-icon-play { + display: none; + } -@mixin carousel-dark() { - --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark}; - --#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark}; - --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark}; -} + .carousel-control-play-pause.paused { + .carousel-icon-pause { + display: none; + } -.carousel-dark { - @include carousel-dark(); -} + .carousel-icon-play { + display: inline-block; + } + } -:root, -[data-bs-theme="light"] { - --#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg}; - --#{$prefix}carousel-caption-color: #{$carousel-caption-color}; - --#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter}; -} + .carousel-indicators { + display: flex; + gap: var(--carousel-indicator-spacer); + justify-content: center; + + [data-bs-target] { + flex: 0 1 auto; + width: var(--carousel-indicator-width); + height: var(--carousel-indicator-height); + padding: 0; + cursor: pointer; + background-color: transparent; + border: 1px solid var(--carousel-indicator-bg); + @include border-radius(var(--carousel-indicator-width)); + @include transition(var(--carousel-indicator-transition)); + } + + .active { + width: calc(var(--carousel-indicator-width) * 2.5); + background-color: var(--carousel-indicator-bg); + border-color: var(--carousel-indicator-bg); + } + } + + // Autoplay progress: fill the active indicator like a progress bar over the + // current slide's interval. The JS adds `.carousel-playing` and sets + // `--carousel-interval` (shipped as `--bs-carousel-interval`) while autoplay is + // running. The fill restarts on its own each slide because `.active` moves to a + // fresh indicator, so its `::after` animation begins from scratch. + @if $enable-transitions { + @keyframes carousel-indicator-progress { + from { inline-size: 0; } + to { inline-size: 100%; } + } + + .carousel-playing .carousel-indicators .active { + @media (prefers-reduced-motion: no-preference) { + position: relative; + overflow: hidden; + // Empty the pill so it reads as a track that the fill grows across. + background-color: transparent; + + &::after { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + inline-size: 0; + content: ""; + background-color: var(--carousel-indicator-progress-bg); + animation: carousel-indicator-progress var(--carousel-interval, 5000ms) linear forwards; + } + } + } + } + + // Overlay layout + // + // Overlays the prev/next controls, play/pause button, and indicators on top of + // the slides (the classic carousel look) instead of stacking them in the flow. + + .carousel-overlay { + --carousel-indicator-bg: light-dark(var(--white), var(--black)); -@if $enable-dark-mode { - @include color-mode(dark, true) { - @include carousel-dark(); + .carousel-overlay-controls { + position: absolute; + inset-block-end: 1rem; + inset-inline: 1rem; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + } } } diff --git a/assets/stylesheets/bootstrap/_chip.scss b/assets/stylesheets/bootstrap/_chip.scss new file mode 100644 index 00000000..9f618ba1 --- /dev/null +++ b/assets/stylesheets/bootstrap/_chip.scss @@ -0,0 +1,148 @@ +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/tokens" as *; + +$chip-tokens: () !default; + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start chip-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$chip-tokens: defaults( + ( + --chip-height: 1.75rem, + --chip-padding-x: .625rem, + --chip-gap: .3125rem, + --chip-border-radius: var(--radius-pill), + --chip-img-size: 1.25rem, + --chip-icon-size: 1rem, + --chip-dismiss-size: 1rem, + --chip-dismiss-opacity: .65, + --chip-dismiss-hover-opacity: 1, + --chip-color: var(--theme-fg, var(--fg-body)), + --chip-bg: var(--theme-bg-subtle, var(--bg-2)), + --chip-border-color: transparent, + --chip-selected-color: var(--theme-contrast, var(--primary-contrast)), + --chip-selected-bg: var(--theme-bg, var(--primary-bg)), + --chip-selected-border-color: var(--theme-bg, var(--primary-bg)), + ), + $chip-tokens +); +// scss-docs-end chip-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + .chip { + @include tokens($chip-tokens); + + display: inline-flex; + gap: var(--chip-gap); + align-items: center; + height: var(--chip-height); + padding-inline: var(--chip-padding-x); + font-size: var(--chip-font-size, var(--font-size-sm)); + font-weight: var(--chip-font-weight, var(--font-weight-base)); + line-height: var(--chip-line-height, 1.25rem); + color: var(--chip-color); + text-decoration: none; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + background-color: var(--chip-bg); + border: var(--border-width) solid var(--chip-border-color); + @include border-radius(var(--chip-border-radius)); + + &:hover { + --chip-bg: var(--theme-bg-muted, var(--bg-3)); + } + + &:focus-visible { + outline: 0; + // @include focus-ring(); + } + + &.active { + --chip-color: var(--chip-selected-color); + --chip-bg: var(--chip-selected-bg); + --chip-border-color: var(--chip-selected-border-color); + + &:hover { + --chip-bg: var(--chip-selected-bg); + opacity: .9; + } + } + + &.disabled, + &:disabled { + pointer-events: none; + opacity: .65; + } + } + + .chip-img { + width: var(--chip-img-size); + height: var(--chip-img-size); + @include border-radius(50%); + + &:first-child { + margin-inline-start: -.375rem; + } + } + + // Chip icon (left side) + .chip-icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + margin-inline-start: calc(var(--chip-gap) * -.25); + + > svg { + display: block; // Prevents baseline alignment issues + width: var(--chip-icon-size); + height: var(--chip-icon-size); + } + + > img { + width: var(--chip-icon-size); + height: var(--chip-icon-size); + object-fit: cover; + @include border-radius(50%); + } + } + + // Dismiss button (right side) + .chip-dismiss { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: var(--chip-min-height); + height: var(--chip-min-height); + padding: 0; + // margin-inline-start: calc(var(--chip-padding-x) * -.5); + margin-inline-end: calc(var(--chip-padding-x) * -.25); + color: inherit; + cursor: pointer; + background: transparent; + border: 0; + opacity: var(--chip-dismiss-opacity); + // @include transition(opacity .15s ease-in-out); + + &:hover { + opacity: var(--chip-dismiss-hover-opacity); + } + + &:focus-visible { + outline: 0; + opacity: 1; + @include focus-ring(); + } + + > svg { + display: block; // Prevents baseline alignment issues + width: var(--chip-dismiss-size); + height: var(--chip-dismiss-size); + } + } +} diff --git a/assets/stylesheets/bootstrap/_close.scss b/assets/stylesheets/bootstrap/_close.scss deleted file mode 100644 index d53c96fb..00000000 --- a/assets/stylesheets/bootstrap/_close.scss +++ /dev/null @@ -1,66 +0,0 @@ -// Transparent background and border properties included for button version. -// iOS requires the button element instead of an anchor tag. -// If you want the anchor version, it requires `href="#"`. -// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - -.btn-close { - // scss-docs-start close-css-vars - --#{$prefix}btn-close-color: #{$btn-close-color}; - --#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) }; - --#{$prefix}btn-close-opacity: #{$btn-close-opacity}; - --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity}; - --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow}; - --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity}; - --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity}; - // scss-docs-end close-css-vars - - box-sizing: content-box; - width: $btn-close-width; - height: $btn-close-height; - padding: $btn-close-padding-y $btn-close-padding-x; - color: var(--#{$prefix}btn-close-color); - background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements - filter: var(--#{$prefix}btn-close-filter); - border: 0; // for button elements - @include border-radius(); - opacity: var(--#{$prefix}btn-close-opacity); - - // Override 's hover style - &:hover { - color: var(--#{$prefix}btn-close-color); - text-decoration: none; - opacity: var(--#{$prefix}btn-close-hover-opacity); - } - - &:focus { - outline: 0; - box-shadow: var(--#{$prefix}btn-close-focus-shadow); - opacity: var(--#{$prefix}btn-close-focus-opacity); - } - - &:disabled, - &.disabled { - pointer-events: none; - user-select: none; - opacity: var(--#{$prefix}btn-close-disabled-opacity); - } -} - -@mixin btn-close-white() { - --#{$prefix}btn-close-filter: #{$btn-close-filter-dark}; -} - -.btn-close-white { - @include btn-close-white(); -} - -:root, -[data-bs-theme="light"] { - --#{$prefix}btn-close-filter: #{$btn-close-filter}; -} - -@if $enable-dark-mode { - @include color-mode(dark, true) { - @include btn-close-white(); - } -} diff --git a/assets/stylesheets/bootstrap/_colors.scss b/assets/stylesheets/bootstrap/_colors.scss new file mode 100644 index 00000000..cba537ae --- /dev/null +++ b/assets/stylesheets/bootstrap/_colors.scss @@ -0,0 +1,102 @@ +// stylelint-disable hue-degree-notation, @stylistic/number-leading-zero + +@use "sass:map"; +@use "functions" as *; +@use "mixins/tokens" as *; + +// Easily convert colors to oklch() with https://oklch.com/ + +$white: #fff !default; +$black: #000 !default; + +// scss-docs-start colors-list +$blue: oklch(60% 0.24 240) !default; +$indigo: oklch(56% 0.26 288) !default; +$violet: oklch(56% 0.24 300) !default; +$purple: oklch(56% 0.24 320) !default; +$pink: oklch(60% 0.22 4) !default; +$red: oklch(60% 0.22 20) !default; +$orange: oklch(70% 0.22 52) !default; +$amber: oklch(79% 0.2 78) !default; +$yellow: oklch(88% 0.24 88) !default; +$lime: oklch(65% 0.24 135) !default; +$green: oklch(64% 0.22 160) !default; +$teal: oklch(68% 0.22 190) !default; +$cyan: oklch(69% 0.22 220) !default; +$brown: oklch(60% 0.12 54) !default; +$gray: oklch(60% 0.02 245) !default; +$pewter: oklch(65% 0.01 290) !default; +// scss-docs-end colors-list + +// scss-docs-start colors-map +$colors: () !default; + +// stylelint-disable-next-line scss/dollar-variable-default +$colors: defaults( + ( + "blue": $blue, + "indigo": $indigo, + "violet": $violet, + "purple": $purple, + "pink": $pink, + "red": $red, + "orange": $orange, + "amber": $amber, + "yellow": $yellow, + "lime": $lime, + "green": $green, + "teal": $teal, + "cyan": $cyan, + "brown": $brown, + "gray": $gray, + "pewter": $pewter, + ), + $colors +); +// scss-docs-end colors-map + +// scss-docs-start color-mix-options +$color-mix-space: lab !default; +$tint-color: var(--white) !default; +$shade-color: var(--black) !default; + +$color-tints: ( + "025": 94%, + "050": 90%, + "100": 80%, + "200": 60%, + "300": 40%, + "400": 20%, +) !default; + +$color-shades: ( + "600": 16%, + "700": 32%, + "800": 48%, + "900": 64%, + "950": 76%, + "975": 88%, +) !default; +// scss-docs-end color-mix-options + +// scss-docs-start color-tokens +$color-tokens: () !default; + +$-color-defaults: () !default; +@each $color, $value in $colors { + @each $stop, $percent in $color-tints { + $-color-defaults: map.set($-color-defaults, --#{$color}-#{$stop}, color-mix(in #{$color-mix-space}, #{$tint-color} #{$percent}, #{$value})); + } + $-color-defaults: map.set($-color-defaults, --#{$color}-500, #{$value}); + @each $stop, $percent in $color-shades { + $-color-defaults: map.set($-color-defaults, --#{$color}-#{$stop}, color-mix(in #{$color-mix-space}, #{$shade-color} #{$percent}, #{$value})); + } +} + +// stylelint-disable-next-line scss/dollar-variable-default +$color-tokens: defaults($-color-defaults, $color-tokens); +// scss-docs-end color-tokens + +:root { + @include tokens($color-tokens); +} diff --git a/assets/stylesheets/bootstrap/_config.scss b/assets/stylesheets/bootstrap/_config.scss new file mode 100644 index 00000000..580f0cc1 --- /dev/null +++ b/assets/stylesheets/bootstrap/_config.scss @@ -0,0 +1,348 @@ +@use "sass:map"; +@use "sass:meta"; + +// Configuration +// +// Variables and settings not related to theme, components, and more go here. It does include layout. + +// Merge overrides on top of defaults, stripping null entries. +// Null values let users remove map keys via @use ... with(). +// Accepts a list as $defaults (converted to a map with `true` values). +@function defaults($defaults, $overrides) { + @if meta.type-of($defaults) == "list" { + $map: (); + @each $key in $defaults { + $map: map.merge($map, ($key: true)); + } + $defaults: $map; + } + $merged: map.merge($defaults, $overrides); + @each $key, $value in $merged { + @if $value == null { + $merged: map.remove($merged, $key); + } + } + @return $merged; +} + +$enable-caret: true !default; +$enable-rounded: true !default; +$enable-shadows: true !default; +$enable-gradients: true !default; +$enable-transitions: true !default; +$enable-reduced-motion: true !default; +$enable-smooth-scroll: false !default; +$enable-grid-classes: true !default; +$enable-container-classes: true !default; +$enable-cssgrid: true !default; +$enable-button-pointers: true !default; +// $enable-negative-margins: false !default; +$enable-deprecation-messages: true !default; + +$color-mode-type: "media-query" !default; +$color-contrast-dark: #000 !default; +$color-contrast-light: #fff !default; +$min-contrast-ratio: 4.5 !default; + +// scss-docs-start spacer-variables-maps +$spacer: 1rem !default; +$spacers: ( + 0: 0, + 1: $spacer * .25, + 2: $spacer * .5, + 3: $spacer * .75, + 4: $spacer, + 5: $spacer * 1.25, + 6: $spacer * 1.5, + 7: $spacer * 2, + 8: $spacer * 2.5, + 9: $spacer * 3, +) !default; + +$negative-spacers: ( + "-1": $spacer * -.25, + "-2": $spacer * -.5, +) !default; +// scss-docs-end spacer-variables-maps + +$sizes: ( + 1: $spacer, + 2: $spacer * 2, + 3: $spacer * 3, + 4: $spacer * 4, + 5: $spacer * 5, + 6: $spacer * 6, + 7: $spacer * 7, + 8: $spacer * 8, + 9: $spacer * 9, + 10: $spacer * 10, + 11: $spacer * 11, + 12: $spacer * 12, +) !default; + +$radius: .5rem !default; +$radii: ( + 0: 0, + 1: $radius * .25, + 2: $radius * .375, + 3: $radius * .5, + 4: $radius * .75, + 5: $radius, + 6: $radius * 1.25, + 7: $radius * 1.5, + 8: $radius * 2, + 9: $radius * 3, +) !default; + +// Breakpoints +// +// Define the minimum dimensions at which your layout will change, +// adapting to different screen sizes, for use in media queries. + +// scss-docs-start breakpoints +$breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 1024px, + xl: 1280px, + 2xl: 1536px +) !default; +// scss-docs-end breakpoints + +// @include _assert-ascending($breakpoints, "$breakpoints"); +// @include _assert-starts-at-zero($breakpoints, "$breakpoints"); + +// Grid columns +// +// Set the number of columns and specify the width of the gutters. + +$grid-columns: 12 !default; +$grid-gutter-x: 1.5rem !default; +$grid-gutter-y: 0 !default; +$grid-row-columns: 6 !default; + +$gutters: $spacers !default; + +// Grid containers +// +// Define the maximum width of `.container` for different screen sizes. + +// scss-docs-start container-max-widths +$container-max-widths: ( + sm: 540px, + md: 720px, + lg: 960px, + xl: 1200px, + 2xl: 1440px +) !default; +// scss-docs-end container-max-widths + +$container-padding-x: $grid-gutter-x !default; + +$utilities: () !default; + +// Characters which are escaped by the escape-svg function +$escaped-characters: ( + ("<", "%3c"), + (">", "%3e"), + ("#", "%23"), + ("(", "%28"), + (")", "%29"), +) !default; + +// Gradient +// +// The gradient which is added to components if `$enable-gradients` is `true` +// This gradient is also added to elements with `.bg-gradient` +// scss-docs-start variable-gradient +$gradient: linear-gradient(180deg, color-mix(var(--white) 15%, transparent), color-mix(var(--white) 0%, transparent)) !default; +// scss-docs-end variable-gradient + +// Position +// +// Define the edge positioning anchors of the position utilities. + +// scss-docs-start position-map +$position-values: ( + 0: 0, + 50: 50%, + 100: 100% +) !default; +// scss-docs-end position-map + +// Links +// +// Style anchor elements. + +$link-decoration: underline !default; +$link-underline-offset: .2em !default; + +$stretched-link-pseudo-element: after !default; +$stretched-link-z-index: 1 !default; + +// Icon links +// scss-docs-start icon-link-variables +$icon-link-gap: .375rem !default; +$icon-link-underline-offset: .25em !default; +$icon-link-icon-size: 1em !default; +$icon-link-icon-transition: .2s ease-in-out transform !default; +$icon-link-icon-transform: translate3d(.25em, 0, 0) !default; +// scss-docs-end icon-link-variables + +// Paragraphs +// +// Style p element. + +$paragraph-margin-bottom: 1rem !default; + +// Components +// +// Define common padding and border radius sizes and more. + +// scss-docs-start border-variables +$border-width: 1px !default; +$border-widths: ( + 1: 1px, + 2: 2px, + 3: 3px, + 4: 4px, + 5: 5px +) !default; +$border-style: solid !default; +$border-color: color-mix(in oklch, var(--gray-100), var(--gray-200)) !default; +// scss-docs-end border-variables + +$transition-base: all .2s ease-in-out !default; +$transition-fade: opacity .15s linear !default; + +// scss-docs-start collapse-transition +$transition-collapse: height .35s ease !default; +$transition-collapse-width: width .35s ease !default; +// scss-docs-end collapse-transition + +// scss-docs-start aspect-ratios +$aspect-ratios: ( + "auto": auto, + "1x1": #{"1 / 1"}, + "4x3": #{"4 / 3"}, + "16x9": #{"16 / 9"}, + "21x9": #{"21 / 9"} +) !default; +// scss-docs-end aspect-ratios + +// Typography +// +// Font, line-height, and color for body text, headings, and more. + +// scss-docs-start font-variables +$font-weight-lighter: lighter !default; +$font-weight-light: 300 !default; +$font-weight-normal: 400 !default; +$font-weight-medium: 500 !default; +$font-weight-semibold: 600 !default; +$font-weight-bold: 700 !default; +$font-weight-bolder: bolder !default; + +$font-weight-base: $font-weight-normal !default; + +$line-height-base: 1.5 !default; +$line-height-sm: 1.25 !default; +$line-height-lg: 2 !default; +// scss-docs-end font-variables + +// scss-docs-start font-sizes +$font-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$font-sizes: defaults( + ( + "xs": ( + "font-size": .75rem, + "line-height": 1.25 + ), + "sm": ( + "font-size": .875rem, + "line-height": 1.5 + ), + "md": ( + "font-size": 1rem, + "line-height": 1.5 + ), + "lg": ( + "font-size": clamp(1.25rem, 1rem + .625vw, 1.5rem), + "line-height": 1.5 + ), + "xl": ( + "font-size": clamp(1.5rem, 1.1rem + .75vw, 1.75rem), + "line-height": calc(2.5 / 1.75) + ), + "2xl": ( + "font-size": clamp(1.75rem, 1.3rem + 1vw, 2rem), + "line-height": calc(3 / 2.25) + ), + "3xl": ( + "font-size": clamp(2rem, 1.5rem + 1.875vw, 2.5rem), + "line-height": 1.2 + ), + "4xl": ( + "font-size": clamp(2.25rem, 1.75rem + 2.5vw, 3rem), + "line-height": 1.1 + ), + "5xl": ( + "font-size": clamp(3rem, 2rem + 5vw, 4rem), + "line-height": 1.1 + ), + "6xl": ( + "font-size": clamp(3.75rem, 2.5rem + 6.25vw, 5rem), + "line-height": 1 + ), + ), + $font-sizes +); +// scss-docs-end font-sizes + +// scss-docs-start headings-variables +$headings-margin-bottom: var(--spacer-2) !default; +$headings-font-family: null !default; +$headings-font-style: null !default; +$headings-font-weight: 500 !default; +$headings-line-height: 1.2 !default; +$headings-color: inherit !default; +// scss-docs-end headings-variables + +// scss-docs-start type-variables + +$legend-margin-bottom: .5rem !default; +$legend-font-size: 1.5rem !default; +$legend-font-weight: null !default; + +$dt-font-weight: $font-weight-bold !default; + +// scss-docs-end type-variables + +// Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. + +// scss-docs-start zindex-stack +$zindex-menu: 1000 !default; +$zindex-sticky: 1020 !default; +$zindex-fixed: 1030 !default; +// $zindex-drawer-backdrop: 1040 !default; +$zindex-drawer: 1045 !default; +$zindex-dialog: 1055 !default; +$zindex-popover: 1070 !default; +$zindex-tooltip: 1080 !default; +$zindex-toast: 1090 !default; +// scss-docs-end zindex-stack + +// scss-docs-start zindex-levels-map +$zindex-levels: ( + n1: -1, + 0: 0, + 1: 1, + 2: 2, + 3: 3 +) !default; +// scss-docs-end zindex-levels-map diff --git a/assets/stylesheets/bootstrap/_containers.scss b/assets/stylesheets/bootstrap/_containers.scss deleted file mode 100644 index 83b31381..00000000 --- a/assets/stylesheets/bootstrap/_containers.scss +++ /dev/null @@ -1,41 +0,0 @@ -// Container widths -// -// Set the container width, and override it for fixed navbars in media queries. - -@if $enable-container-classes { - // Single container class with breakpoint max-widths - .container, - // 100% wide container at all breakpoints - .container-fluid { - @include make-container(); - } - - // Responsive containers that are 100% wide until a breakpoint - @each $breakpoint, $container-max-width in $container-max-widths { - .container-#{$breakpoint} { - @extend .container-fluid; - } - - @include media-breakpoint-up($breakpoint, $grid-breakpoints) { - %responsive-container-#{$breakpoint} { - max-width: $container-max-width; - } - - // Extend each breakpoint which is smaller or equal to the current breakpoint - $extend-breakpoint: true; - - @each $name, $width in $grid-breakpoints { - @if ($extend-breakpoint) { - .container#{breakpoint-infix($name, $grid-breakpoints)} { - @extend %responsive-container-#{$breakpoint}; - } - - // Once the current breakpoint is reached, stop extending - @if ($breakpoint == $name) { - $extend-breakpoint: false; - } - } - } - } - } -} diff --git a/assets/stylesheets/bootstrap/_datepicker.scss b/assets/stylesheets/bootstrap/_datepicker.scss new file mode 100644 index 00000000..d45f1e39 --- /dev/null +++ b/assets/stylesheets/bootstrap/_datepicker.scss @@ -0,0 +1,415 @@ +// stylelint-disable selector-max-attribute, property-disallowed-list, selector-no-qualifying-type -- VCP uses extensive data attributes and requires direct border-radius properties for range selection + +@use "functions" as *; +@use "config" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; + +$datepicker-tokens: () !default; + +// scss-docs-start datepicker-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$datepicker-tokens: defaults( + ( + --datepicker-padding: 1rem, + --datepicker-bg: var(--bg-body), + --datepicker-color: var(--fg-body), + --datepicker-border-color: var(--border-color-translucent), + --datepicker-border-width: var(--border-width), + --datepicker-border-radius: var(--radius-7), + --datepicker-box-shadow: var(--box-shadow), + --datepicker-font-size: var(--font-size-sm), + --datepicker-min-width: 280px, + --datepicker-zindex: #{$zindex-menu}, + --datepicker-header-font-weight: 600, + --datepicker-weekday-color: var(--fg-3), + --datepicker-day-hover-bg: var(--bg-1), + --datepicker-day-selected-bg: var(--primary-bg), + --datepicker-day-selected-color: var(--primary-contrast), + --datepicker-day-today-bg: var(--bg-2), + --datepicker-day-today-color: var(--fg-1), + --datepicker-day-disabled-color: var(--fg-4), + ), + $datepicker-tokens +); +// scss-docs-end datepicker-tokens + +@layer components { + [data-vc="calendar"] { + @include tokens($datepicker-tokens); + + position: absolute; + z-index: var(--datepicker-zindex); + box-sizing: border-box; + display: flex; + flex-direction: column; + min-width: var(--datepicker-min-width); + padding: var(--datepicker-padding); + font-family: var(--font-sans-serif); + font-size: var(--datepicker-font-size); + color: var(--datepicker-color); + color-scheme: light dark; + background-color: var(--datepicker-bg); + border: var(--datepicker-border-width) solid var(--datepicker-border-color); + box-shadow: var(--datepicker-box-shadow); + opacity: 1; + @include border-radius(var(--datepicker-border-radius)); + + // Respond to Bootstrap's color mode system + &[data-bs-theme="light"] { + color-scheme: light; + } + + &[data-bs-theme="dark"] { + color-scheme: dark; + } + + // Catch-all for focus styles + button:focus-visible { + position: relative; + z-index: 1; + @include focus-ring(); + } + } + + [data-vc-calendar-hidden] { + pointer-events: none; + opacity: 0; + } + + // Inline calendars + // + // Remove popover styling for more neutral styling + [data-vc="calendar"]:not([data-vc-input]) { + position: relative; + width: fit-content; + padding: 0; + border: 0; + box-shadow: none; + } + + [data-vc-position="bottom"] { + margin-block-start: .25rem; + } + + [data-vc-position="top"] { + margin-block-end: -.25rem; + } + + [data-vc-arrow] { + position: relative; + display: block; + width: 2rem; + height: 2rem; + color: var(--datepicker-color); + pointer-events: auto; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &::before { + position: absolute; + inset: .25rem; + content: ""; + background-color: var(--datepicker-color); + @include mask-icon(url("data:image/svg+xml,"), $size: null); + } + + &:hover { + background-color: var(--datepicker-day-hover-bg); + } + } + + [data-vc-arrow="prev"]::before { + transform: rotate(90deg); + } + + [data-vc-arrow="next"]::before { + transform: rotate(-90deg); + } + + // Grid layout + [data-vc="controls"] { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 1rem; + padding-right: 1rem; + padding-left: 1rem; + pointer-events: none; + } + + [data-vc="grid"] { + display: flex; + flex-grow: 1; + flex-wrap: wrap; + gap: 1.75rem; + } + + [data-vc="column"] { + display: flex; + flex-grow: 1; + flex-direction: column; + min-width: 240px; + } + + // + // Header + // + + [data-vc="header"] { + position: relative; + display: flex; + align-items: center; + margin-bottom: .75rem; + } + + // Month and year + [data-vc-header="content"] { + display: inline-flex; + flex-grow: 1; + align-items: center; + justify-content: center; + white-space: pre-wrap; + } + + [data-vc="month"], + [data-vc="year"] { + padding: .25rem .5rem; + margin-inline: -.125rem; + font-size: 1rem; + font-weight: var(--datepicker-header-font-weight); + color: var(--datepicker-color); + // cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--datepicker-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--datepicker-day-hover-bg); + } + } + + [data-vc="content"] { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + // Month/Year grids + [data-vc="months"], + [data-vc="years"] { + display: grid; + flex-grow: 1; + grid-template-columns: repeat(var(--vc-columns, 4), minmax(0, 1fr)); + row-gap: 1rem; + column-gap: .25rem; + align-items: center; + } + + [data-vc="years"] { + --vc-columns: 5; + } + + [data-vc-months-month], + [data-vc-years-year] { + display: flex; + align-items: center; + justify-content: center; + height: 2.5rem; + padding: .25rem; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--datepicker-weekday-color); + text-align: center; + word-break: break-all; + cursor: pointer; + background-color: transparent; + border: 0; + @include border-radius(var(--radius-5)); + + &:disabled { + color: var(--datepicker-day-disabled-color); + pointer-events: none; + } + + &:hover:not(:disabled) { + background-color: var(--datepicker-day-hover-bg); + } + + &[data-vc-months-month-selected], + &[data-vc-years-year-selected] { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + + &:hover { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + } + } + } + + // Week days header + [data-vc="week"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + justify-items: center; + margin-bottom: .5rem; + } + + [data-vc-week-day] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + padding: 0; + margin: 0; + font-size: .75rem; + font-weight: 600; + line-height: 1rem; + color: var(--datepicker-weekday-color); + background-color: transparent; + border: 0; + } + + button[data-vc-week-day] { + cursor: pointer; + } + + // Dates grid + [data-vc="dates"] { + pointer-events: none; + } + + [data-vc-dates="row"] { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + justify-items: center; + width: 100%; + } + + [data-vc-date] { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding-top: .125rem; + padding-bottom: .125rem; + pointer-events: auto; + + &:not(:has([data-vc-date-btn])), + &[data-vc-date-disabled], + &[data-vc-date-disabled] [data-vc-date-btn] { + pointer-events: none; + } + } + + // Date button + [data-vc-date-btn] { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-width: 1.875rem; + height: 100%; + min-height: 1.875rem; + padding: 0; + font-size: .75rem; + font-weight: 400; + line-height: 1rem; + color: var(--datepicker-color); + cursor: pointer; + background-color: transparent; + border: 0; + border-radius: var(--radius-5); + + &:hover { + background-color: var(--datepicker-day-hover-bg); + } + } + + // Today + [data-vc-date-today] [data-vc-date-btn] { + font-weight: 600; + color: var(--datepicker-day-today-color); + background-color: var(--datepicker-day-today-bg); + } + + // Outside month + [data-vc-date-month="next"] [data-vc-date-btn], + [data-vc-date-month="prev"] [data-vc-date-btn] { + opacity: .5; + } + + // Disabled + [data-vc-date-disabled] [data-vc-date-btn] { + color: var(--datepicker-day-disabled-color); + } + + // Range selection styles + [data-vc-date-hover] [data-vc-date-btn] { + background-color: var(--datepicker-day-hover-bg); + border-radius: 0; + } + + [data-vc-date-hover="first"] [data-vc-date-btn] { + border-start-start-radius: var(--radius-5); + border-end-start-radius: var(--radius-5); + } + + [data-vc-date-hover="last"] [data-vc-date-btn] { + border-start-end-radius: var(--radius-5); + border-end-end-radius: var(--radius-5); + } + + [data-vc-date-hover="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } + + [data-vc-date-selected="middle"] [data-vc-date-btn] { + border-radius: 0; + opacity: .8; + } + + // Selected + [data-vc-date-selected] [data-vc-date-btn] { + color: var(--datepicker-day-selected-color); + background-color: var(--datepicker-day-selected-bg); + + } + + [data-vc-date-selected="first"] [data-vc-date-btn] { + border-top-left-radius: var(--radius-5); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: var(--radius-5); + } + + [data-vc-date-selected="last"] [data-vc-date-btn] { + border-top-left-radius: 0; + border-top-right-radius: var(--radius-5); + border-bottom-right-radius: var(--radius-5); + border-bottom-left-radius: 0; + } + + [data-vc-date-selected="first-and-last"] [data-vc-date-btn] { + border-radius: var(--radius-5); + } +} diff --git a/assets/stylesheets/bootstrap/_dialog.scss b/assets/stylesheets/bootstrap/_dialog.scss new file mode 100644 index 00000000..69bada97 --- /dev/null +++ b/assets/stylesheets/bootstrap/_dialog.scss @@ -0,0 +1,289 @@ +@use "sass:map"; +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/dialog-shared" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; + +// Native component +// Uses the browser's native dialog element with showModal()/show()/close() APIs +// Leverages native [open] attribute and ::backdrop pseudo-element + +// stylelint-disable custom-property-no-missing-var-function +$dialog-tokens: () !default; + +// scss-docs-start dialog-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$dialog-tokens: defaults( + ( + --dialog-padding: 1rem, + --dialog-width: 500px, + --dialog-margin: 1.75rem, + --dialog-color: var(--fg-body), + --dialog-bg: var(--bg-body), + --dialog-border-color: var(--border-color-translucent), + --dialog-border-width: var(--border-width), + --dialog-border-radius: var(--radius-7), + --dialog-box-shadow: var(--box-shadow-lg), + --dialog-transition-duration: .3s, + --dialog-transition-timing: cubic-bezier(.22, 1, .36, 1), + --dialog-backdrop-bg: light-dark(rgb(0 0 0 / 50%), rgb(0 0 0 / 65%)), + --dialog-backdrop-blur: 8px, + --dialog-header-padding: 1rem, + --dialog-header-border-color: var(--border-color-translucent), + --dialog-header-border-width: var(--border-width), + --dialog-footer-padding: 1rem, + --dialog-footer-border-color: var(--border-color-translucent), + --dialog-footer-border-width: var(--border-width), + --dialog-footer-gap: .5rem, + ), + $dialog-tokens +); +// scss-docs-end dialog-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start dialog-sizes +$dialog-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$dialog-sizes: defaults( + ( + sm: 280px, + lg: 800px, + xl: 1140px, + ), + $dialog-sizes +); +// scss-docs-end dialog-sizes + +@layer components { + // Prevent page scroll when a dialog is open. Applied to the root element so + // `overflow: hidden` sits on the same element as `scrollbar-gutter: stable` + // (see _root.scss): the gutter stays reserved while the scrollbar is hidden, + // so the page doesn't shift when a dialog opens. + :root.dialog-open { + overflow: hidden; + } + + .dialog { + @include tokens($dialog-tokens); + + // Override UA display:none so visibility controls the hidden state, + // enabling reliable cross-browser exit animations after close(). + display: flex; + flex-direction: column; + width: var(--dialog-width); + max-width: calc(100% - var(--dialog-margin) * 2); + max-height: calc(100% - var(--dialog-margin) * 2); + padding: 0; + margin: auto; + overflow: visible; + color: var(--dialog-color); + visibility: hidden; + background-color: var(--dialog-bg); + background-clip: padding-box; + border: var(--dialog-border-width) solid var(--dialog-border-color); + @include border-radius(var(--dialog-border-radius)); + @include box-shadow(var(--dialog-box-shadow)); + + // Animated variant (default) — transitions, opacity fade, slide transforms. + // Adding .dialog-instant skips all animations (instant show/hide). + &:not(.dialog-instant) { + // Exit state: faded out + opacity: 0; + + // Exit transition: opacity and transform animate out, then visibility + // flips hidden after the animation completes (via the delay). + @include transition( + opacity var(--dialog-transition-duration) var(--dialog-transition-timing), + transform var(--dialog-transition-duration) var(--dialog-transition-timing), + visibility 0s var(--dialog-transition-duration) + ); + + // Slide-down variant: enters from above sliding down, exits by reversing + // back up. Base value is the entry-from / exit-to position so the + // animation works on every open (not just the first, which is the only + // time @starting-style applies for a persistent element). + &.dialog-slide-down { + transform: translateY(-3rem); + } + + // Slide-up variant: enters from below sliding up, exits by reversing + // back down. See note above re: base value choice. + &.dialog-slide-up { + transform: translateY(3rem); + } + + // Open state: visible and faded in. + // Entry transition: visibility flips visible immediately (0s, no delay), + // then opacity and transform animate in. + // The :not(.hiding) qualifier lets the exit transition fall back to the + // base "exit" state above while [open] is still present (the JS keeps + // the dialog in the top layer during the exit so the ::backdrop and + // the browser's modal centering remain intact). + &[open]:not(.hiding) { + overflow: visible; + visibility: visible; + opacity: 1; + @include transition( + opacity var(--dialog-transition-duration) var(--dialog-transition-timing), + transform var(--dialog-transition-duration) var(--dialog-transition-timing), + visibility 0s + ); + transform: none; + } + + // Static backdrop "bounce" animation (modal dialogs only). Qualified + // with [open] (to outrank the open-state `transform: none` selector + // which now also includes `:not(.hiding)`) and `:not(.hiding)` (so + // a backdrop click while the dialog is mid-exit doesn't fight the + // slide-out transform). + &[open].dialog-static:not(.hiding) { + transform: scale(1.02); + } + + // Native backdrop styling with transitions + &::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + @include backdrop-transitions(var(--dialog-transition-duration), var(--dialog-transition-timing)); + } + + // Exit: fade the native backdrop out alongside the dialog. The dialog + // is kept in the top layer (and thus the ::backdrop is still rendered) + // for the duration of the exit transition. + &.hiding::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + } + + // Instant variant — no transitions, just snap visibility + &.dialog-instant { + &::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + } + } + + // Open state base (always applies, regardless of animation mode). + // Excluded while .hiding is present so the animated exit (above) can + // fall through to the base "exit" state — for instant dialogs, .hiding + // is removed synchronously after close() so this still applies normally. + &[open]:not(.hiding) { + overflow: visible; + visibility: visible; + opacity: 1; + transform: none; + } + + // Non-modal dialog positioning + // show() doesn't use the top layer, so we need explicit positioning and z-index + &.dialog-nonmodal { + position: fixed; + inset-block-start: 50%; + inset-inline-start: 50%; + z-index: $zindex-dialog; + margin-inline: 0; + transform: translate(-50%, -50%); + } + + // Scrollable dialog body (header/footer stay fixed) + &.dialog-scrollable[open] { + max-height: calc(100% - var(--dialog-margin) * 2); + + .dialog-body { + overflow-y: auto; + } + } + } + + // Entry animation for ::backdrop via @starting-style. The backdrop only + // exists while the dialog is in the top layer, so its starting state can't + // be expressed on the base selector. + // Default dialog (fade only) and the slide variants do NOT need + // @starting-style — the base opacity: 0 (and base transform for slides) + // serves as the entry-from state with the visibility trick. + @starting-style { + .dialog:not(.dialog-instant)::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + + // Swap entry: when this dialog is opened as the target of a swap, the + // outgoing dialog's ::backdrop is being removed synchronously in the same + // JS tick. To avoid any flicker (either a dip from a fade-in over nothing, + // or double-darkening from two stacked backdrops), start this backdrop + // already-opaque so it takes over from the outgoing one seamlessly. + .dialog.dialog-swap-in:not(.dialog-instant)::backdrop { + background-color: var(--dialog-backdrop-bg); + backdrop-filter: blur(var(--dialog-backdrop-blur)); + } + } + + // Dialog sizes + @each $size, $value in $dialog-sizes { + .dialog-#{$size} { --dialog-width: #{$value}; } + } + + // Fullscreen dialog + .dialog-fullscreen { + --dialog-width: 100vw; + --dialog-margin: 0; + --dialog-border-radius: 0; + + width: 100%; + max-width: none; + height: 100%; + max-height: none; + } + + // Responsive fullscreen dialogs + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + @if $prefix != "" { + @include media-breakpoint-down($breakpoint) { + .#{css-escape-ident($breakpoint)}-down\:dialog-fullscreen { + --dialog-width: 100vw; + --dialog-margin: 0; + --dialog-border-radius: 0; + + width: 100%; + max-width: none; + height: 100%; + max-height: none; + } + } + } + } + + // Dialog header + .dialog-header { + @include dialog-header(var(--dialog-header-padding)); + border-block-end: var(--dialog-header-border-width) solid var(--dialog-header-border-color); + + .btn-close { + margin-inline-start: auto; + } + } + + // Dialog title + .dialog-title { + @include dialog-title(); + font-size: var(--font-size-md); + } + + // Dialog body + .dialog-body { + position: relative; + @include dialog-body(var(--dialog-padding)); + } + + // Dialog footer + .dialog-footer { + @include dialog-footer(var(--dialog-footer-padding), var(--dialog-footer-gap), var(--dialog-footer-border-width), var(--dialog-footer-border-color)); + } +} diff --git a/assets/stylesheets/bootstrap/_drawer.scss b/assets/stylesheets/bootstrap/_drawer.scss new file mode 100644 index 00000000..fd5f7846 --- /dev/null +++ b/assets/stylesheets/bootstrap/_drawer.scss @@ -0,0 +1,302 @@ +@use "functions" as *; +@use "config" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/dialog-shared" as *; +@use "mixins/transition" as *; +@use "layout/breakpoints" as *; +@use "mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$drawer-tokens: () !default; + +// scss-docs-start drawer-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$drawer-tokens: defaults( + ( + --drawer-inset: var(--spacer), + --drawer-zindex: #{$zindex-drawer}, + --drawer-width: 400px, + --drawer-height: 30vh, + --drawer-padding-x: var(--spacer), + --drawer-padding-y: var(--spacer), + --drawer-color: var(--fg-body), + --drawer-bg: var(--bg-body), + --drawer-border-width: var(--border-width), + --drawer-border-color: var(--border-color-translucent), + --drawer-border-radius: var(--radius-7), + --drawer-box-shadow: var(--box-shadow-lg), + --drawer-transition-duration: .3s, + --drawer-transition-timing: cubic-bezier(.22, 1, .36, 1), + --drawer-title-line-height: 1.5, + --drawer-backdrop-bg: color-mix(in oklch, var(--bg-body) 25%, transparent), + --drawer-backdrop-blur: 8px, + ), + $drawer-tokens +); +// scss-docs-end drawer-tokens +// stylelint-enable custom-property-no-missing-var-function + +$drawer-backdrop-tokens: () !default; + +// scss-docs-start drawer-backdrop-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$drawer-backdrop-tokens: defaults( + ( + --drawer-backdrop-bg: var(--bg-body), + --drawer-backdrop-opacity: 25%, + --drawer-backdrop-blur: 8px, + ), + $drawer-backdrop-tokens +); +// scss-docs-end drawer-backdrop-tokens + +%drawer-css-vars { + @include tokens($drawer-tokens); +} + +@layer components { + // Apply CSS vars to all drawer responsive variants + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer { + @extend %drawer-css-vars; + } + } + + // Responsive drawer styles + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer { + @include media-breakpoint-down($next) { + // Reset native UA defaults (fit-content sizing, inset, margins) + // and override display:none so visibility controls the hidden state. + position: fixed; + inset: auto; + z-index: var(--drawer-zindex); + display: flex; + flex-direction: column; + width: auto; + max-width: calc(100% - var(--drawer-inset) * 2); + height: auto; + max-height: calc(100% - var(--drawer-inset) * 2); + padding: 0; + margin: 0; + color: var(--drawer-color); + visibility: hidden; + background-color: var(--drawer-bg); + background-clip: padding-box; + border: var(--drawer-border-width) solid var(--drawer-border-color); + outline: 0; + + @include border-radius(var(--drawer-border-radius)); + @include box-shadow(var(--drawer-box-shadow)); + + // Placement positioning and sizing — always applied regardless of animation mode. + &:where(.drawer-start) { + inset-block: var(--drawer-inset); + inset-inline-start: var(--drawer-inset); + width: var(--drawer-width); + } + + &:where(.drawer-end) { + inset-block: var(--drawer-inset); + inset-inline-end: var(--drawer-inset); + width: var(--drawer-width); + } + + &:where(.drawer-top) { + inset: var(--drawer-inset) var(--drawer-inset) auto; + height: var(--drawer-height); + } + + &:where(.drawer-bottom) { + inset: auto var(--drawer-inset) var(--drawer-inset); + height: var(--drawer-height); + } + + &:where(.drawer-fullscreen) { + inset: var(--drawer-inset); + width: auto; + max-width: none; + height: auto; + max-height: none; + } + + // Animated variant (default) — transitions + off-screen transforms. + // Adding .drawer-instant skips all animations. + &:not(.drawer-instant) { + @include transition(transform var(--drawer-transition-duration) var(--drawer-transition-timing), visibility 0s var(--drawer-transition-duration)); + + // Off-screen transforms per placement + &:where(.drawer-start) { + transform: translateX(calc(-100% - var(--drawer-inset))); + + :root:dir(rtl) & { + transform: translateX(calc(100% + var(--drawer-inset))); + } + } + + &:where(.drawer-end) { + transform: translateX(calc(100% + var(--drawer-inset))); + + :root:dir(rtl) & { + transform: translateX(calc(-100% - var(--drawer-inset))); + } + } + + &:where(.drawer-top) { + transform: translateY(calc(-100% - var(--drawer-inset))); + } + + &:where(.drawer-bottom) { + transform: translateY(calc(100% + var(--drawer-inset))); + } + + &:where(.drawer-fullscreen) { + transform: translateY(calc(100% + var(--drawer-inset))); + } + + // Open state: slide in with transition + &[open] { + visibility: visible; + @include transition(transform var(--drawer-transition-duration) var(--drawer-transition-timing), visibility 0s); + transform: none; + } + } + + // Open state base (always applies, regardless of animation mode) + &[open] { + visibility: visible; + transform: none; + } + } + + // Above breakpoint - show content inline (for responsive drawer) + // Above breakpoint - show content inline (for responsive drawer). + // Must fully reset all drawer styles so the element behaves as an + // inline flex container within its parent (e.g., a navbar). + @if not ($prefix == "") { + @include media-breakpoint-up($next) { + // stylelint-disable declaration-no-important + --drawer-height: auto; + --drawer-border-width: 0; + // Reset native UA styles + position: static !important; + inset: auto; + z-index: auto; + display: flex !important; + flex-grow: 1; + width: auto !important; + max-width: none; + height: auto !important; + max-height: none; + padding: 0; + margin: 0; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + @include transition(none !important); + // stylelint-enable declaration-no-important + + .drawer-header { + display: none; + } + + .drawer-body { + display: flex; + flex-grow: 0; + flex-direction: row; + width: 100%; + padding: 0; + overflow-y: visible; + // stylelint-disable-next-line declaration-no-important + background-color: transparent !important; + } + @include border-radius(0); + @include box-shadow(none); + } + } + } + } + + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer::backdrop { + background-color: var(--drawer-backdrop-bg); + backdrop-filter: blur(var(--drawer-backdrop-blur)); + @include backdrop-transitions(var(--drawer-transition-duration), var(--drawer-transition-timing)); + } + } + + // Backdrop entry animation — ::backdrop can safely use @starting-style + // since it only exists when the dialog is in the top layer (no responsive issue). + @starting-style { + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + .#{$prefix}drawer::backdrop { + background-color: transparent; + backdrop-filter: blur(0); + } + } + } + + // Static backdrop transition ("bounce") + .drawer-static { + transform: scale(1.02); + } + + .drawer-translucent { + background-color: color-mix(in oklch, var(--drawer-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + } + + // Sheet variant: flush-to-edge panel with no inset, border-radius, or shadow. + // Overrides tokens so placement transforms (which use calc() with --drawer-inset) + // automatically position the drawer at the viewport edge. + .drawer-sheet { + right: 0; + bottom: 0; + left: 0; + width: 100vw; + margin-inline: auto; + margin-bottom: calc(-1 * var(--drawer-border-width)); + border-end-start-radius: 0; + border-end-end-radius: 0; + + @include media-breakpoint-up(lg) { + max-width: var(--drawer-sheet-width, 760px); + } + } + + // Header with close button + .drawer-header { + @include dialog-header(var(--drawer-padding-y) var(--drawer-padding-x)); + + .btn-close { + margin-block: calc(-.5 * var(--drawer-padding-y)); + margin-inline-start: auto; + } + } + + // Title + .drawer-title { + @include dialog-title(var(--drawer-title-line-height)); + } + + // Scrollable body + .drawer-body { + display: flex; + flex-direction: column; + gap: var(--drawer-padding-y); + @include dialog-body(var(--drawer-padding-y) var(--drawer-padding-x)); + overflow-y: auto; + } + + // Optional footer + .drawer-footer { + @include dialog-footer(var(--drawer-padding-y) var(--drawer-padding-x), .5rem, var(--drawer-border-width), var(--drawer-border-color)); + } + + .drawer-fit-content { + inset-block-end: auto; + } +} diff --git a/assets/stylesheets/bootstrap/_dropdown.scss b/assets/stylesheets/bootstrap/_dropdown.scss deleted file mode 100644 index 587ebb48..00000000 --- a/assets/stylesheets/bootstrap/_dropdown.scss +++ /dev/null @@ -1,250 +0,0 @@ -// The dropdown wrapper (``) -.dropup, -.dropend, -.dropdown, -.dropstart, -.dropup-center, -.dropdown-center { - position: relative; -} - -.dropdown-toggle { - white-space: nowrap; - - // Generate the caret automatically - @include caret(); -} - -// The dropdown menu -.dropdown-menu { - // scss-docs-start dropdown-css-vars - --#{$prefix}dropdown-zindex: #{$zindex-dropdown}; - --#{$prefix}dropdown-min-width: #{$dropdown-min-width}; - --#{$prefix}dropdown-padding-x: #{$dropdown-padding-x}; - --#{$prefix}dropdown-padding-y: #{$dropdown-padding-y}; - --#{$prefix}dropdown-spacer: #{$dropdown-spacer}; - @include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size); - --#{$prefix}dropdown-color: #{$dropdown-color}; - --#{$prefix}dropdown-bg: #{$dropdown-bg}; - --#{$prefix}dropdown-border-color: #{$dropdown-border-color}; - --#{$prefix}dropdown-border-radius: #{$dropdown-border-radius}; - --#{$prefix}dropdown-border-width: #{$dropdown-border-width}; - --#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius}; - --#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg}; - --#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y}; - --#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow}; - --#{$prefix}dropdown-link-color: #{$dropdown-link-color}; - --#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color}; - --#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg}; - --#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color}; - --#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg}; - --#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color}; - --#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x}; - --#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y}; - --#{$prefix}dropdown-header-color: #{$dropdown-header-color}; - --#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x}; - --#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y}; - // scss-docs-end dropdown-css-vars - - position: absolute; - z-index: var(--#{$prefix}dropdown-zindex); - display: none; // none by default, but block on "open" of the menu - min-width: var(--#{$prefix}dropdown-min-width); - padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x); - margin: 0; // Override default margin of ul - @include font-size(var(--#{$prefix}dropdown-font-size)); - color: var(--#{$prefix}dropdown-color); - text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) - list-style: none; - background-color: var(--#{$prefix}dropdown-bg); - background-clip: padding-box; - border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color); - @include border-radius(var(--#{$prefix}dropdown-border-radius)); - @include box-shadow(var(--#{$prefix}dropdown-box-shadow)); - - &[data-bs-popper] { - top: 100%; - left: 0; - margin-top: var(--#{$prefix}dropdown-spacer); - } - - @if $dropdown-padding-y == 0 { - > .dropdown-item:first-child, - > li:first-child .dropdown-item { - @include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius)); - } - > .dropdown-item:last-child, - > li:last-child .dropdown-item { - @include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius)); - } - - } -} - -// scss-docs-start responsive-breakpoints -// We deliberately hardcode the `bs-` prefix because we check -// this custom property in JS to determine Popper's positioning - -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - .dropdown-menu#{$infix}-start { - --bs-position: start; - - &[data-bs-popper] { - right: auto; - left: 0; - } - } - - .dropdown-menu#{$infix}-end { - --bs-position: end; - - &[data-bs-popper] { - right: 0; - left: auto; - } - } - } -} -// scss-docs-end responsive-breakpoints - -// Allow for dropdowns to go bottom up (aka, dropup-menu) -// Just add .dropup after the standard .dropdown class and you're set. -.dropup { - .dropdown-menu[data-bs-popper] { - top: auto; - bottom: 100%; - margin-top: 0; - margin-bottom: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(up); - } -} - -.dropend { - .dropdown-menu[data-bs-popper] { - top: 0; - right: auto; - left: 100%; - margin-top: 0; - margin-left: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(end); - &::after { - vertical-align: 0; - } - } -} - -.dropstart { - .dropdown-menu[data-bs-popper] { - top: 0; - right: 100%; - left: auto; - margin-top: 0; - margin-right: var(--#{$prefix}dropdown-spacer); - } - - .dropdown-toggle { - @include caret(start); - &::before { - vertical-align: 0; - } - } -} - - -// Dividers (basically an ``) within the dropdown -.dropdown-divider { - height: 0; - margin: var(--#{$prefix}dropdown-divider-margin-y) 0; - overflow: hidden; - border-top: 1px solid var(--#{$prefix}dropdown-divider-bg); - opacity: 1; // Revisit in v6 to de-dupe styles that conflict with element -} - -// Links, buttons, and more within the dropdown menu -// -// ``-specific styles are denoted with `// For s` -.dropdown-item { - display: block; - width: 100%; // For ``s - padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x); - clear: both; - font-weight: $font-weight-normal; - color: var(--#{$prefix}dropdown-link-color); - text-align: inherit; // For ``s - text-decoration: if($link-decoration == none, null, none); - white-space: nowrap; // prevent links from randomly breaking onto new lines - background-color: transparent; // For ``s - border: 0; // For ``s - @include border-radius(var(--#{$prefix}dropdown-item-border-radius, 0)); - - &:hover, - &:focus { - color: var(--#{$prefix}dropdown-link-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - @include gradient-bg(var(--#{$prefix}dropdown-link-hover-bg)); - } - - &.active, - &:active { - color: var(--#{$prefix}dropdown-link-active-color); - text-decoration: none; - @include gradient-bg(var(--#{$prefix}dropdown-link-active-bg)); - } - - &.disabled, - &:disabled { - color: var(--#{$prefix}dropdown-link-disabled-color); - pointer-events: none; - background-color: transparent; - // Remove CSS gradients if they're enabled - background-image: if($enable-gradients, none, null); - } -} - -.dropdown-menu.show { - display: block; -} - -// Dropdown section headers -.dropdown-header { - display: block; - padding: var(--#{$prefix}dropdown-header-padding-y) var(--#{$prefix}dropdown-header-padding-x); - margin-bottom: 0; // for use with heading elements - @include font-size($font-size-sm); - color: var(--#{$prefix}dropdown-header-color); - white-space: nowrap; // as with > li > a -} - -// Dropdown text -.dropdown-item-text { - display: block; - padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x); - color: var(--#{$prefix}dropdown-link-color); -} - -// Dark dropdowns -.dropdown-menu-dark { - // scss-docs-start dropdown-dark-css-vars - --#{$prefix}dropdown-color: #{$dropdown-dark-color}; - --#{$prefix}dropdown-bg: #{$dropdown-dark-bg}; - --#{$prefix}dropdown-border-color: #{$dropdown-dark-border-color}; - --#{$prefix}dropdown-box-shadow: #{$dropdown-dark-box-shadow}; - --#{$prefix}dropdown-link-color: #{$dropdown-dark-link-color}; - --#{$prefix}dropdown-link-hover-color: #{$dropdown-dark-link-hover-color}; - --#{$prefix}dropdown-divider-bg: #{$dropdown-dark-divider-bg}; - --#{$prefix}dropdown-link-hover-bg: #{$dropdown-dark-link-hover-bg}; - --#{$prefix}dropdown-link-active-color: #{$dropdown-dark-link-active-color}; - --#{$prefix}dropdown-link-active-bg: #{$dropdown-dark-link-active-bg}; - --#{$prefix}dropdown-link-disabled-color: #{$dropdown-dark-link-disabled-color}; - --#{$prefix}dropdown-header-color: #{$dropdown-dark-header-color}; - // scss-docs-end dropdown-dark-css-vars -} diff --git a/assets/stylesheets/bootstrap/_forms.scss b/assets/stylesheets/bootstrap/_forms.scss deleted file mode 100644 index 7b17d849..00000000 --- a/assets/stylesheets/bootstrap/_forms.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import "forms/labels"; -@import "forms/form-text"; -@import "forms/form-control"; -@import "forms/form-select"; -@import "forms/form-check"; -@import "forms/form-range"; -@import "forms/floating-labels"; -@import "forms/input-group"; -@import "forms/validation"; diff --git a/assets/stylesheets/bootstrap/_functions.scss b/assets/stylesheets/bootstrap/_functions.scss index 59d431a1..9c9b1ac7 100644 --- a/assets/stylesheets/bootstrap/_functions.scss +++ b/assets/stylesheets/bootstrap/_functions.scss @@ -1,3 +1,12 @@ +@use "sass:color"; +@use "sass:list"; +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +@use "sass:string"; +@forward "config" show defaults; +@use "config" as *; + // Bootstrap functions // // Utility mixins and functions for evaluating source code across our variables, maps, and mixins. @@ -8,9 +17,9 @@ $prev-key: null; $prev-num: null; @each $key, $num in $map { - @if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" { + @if $prev-num == null or math.unit($num) == "%" or math.unit($prev-num) == "%" { // Do nothing - } @else if not comparable($prev-num, $num) { + } @else if not math.compatible($prev-num, $num) { @warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !"; } @else if $prev-num >= $num { @warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !"; @@ -22,64 +31,23 @@ // Starts at zero // Used to ensure the min-width of the lowest breakpoint starts at 0. -@mixin _assert-starts-at-zero($map, $map-name: "$grid-breakpoints") { - @if length($map) > 0 { - $values: map-values($map); - $first-value: nth($values, 1); +@mixin _assert-starts-at-zero($map, $map-name: "$breakpoints") { + @if list.length($map) > 0 { + $values: map.values($map); + $first-value: list.nth($values, 1); @if $first-value != 0 { @warn "First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}."; } } } -// Colors -@function to-rgb($value) { - @return red($value), green($value), blue($value); -} - -// stylelint-disable scss/dollar-variable-pattern -@function rgba-css-var($identifier, $target) { - @if $identifier == "body" and $target == "bg" { - @return rgba(var(--#{$prefix}#{$identifier}-bg-rgb), var(--#{$prefix}#{$target}-opacity)); - } @if $identifier == "body" and $target == "text" { - @return rgba(var(--#{$prefix}#{$identifier}-color-rgb), var(--#{$prefix}#{$target}-opacity)); - } @else { - @return rgba(var(--#{$prefix}#{$identifier}-rgb), var(--#{$prefix}#{$target}-opacity)); - } -} - -@function map-loop($map, $func, $args...) { - $_map: (); - - @each $key, $value in $map { - // allow to pass the $key and $value of the map as an function argument - $_args: (); - @each $arg in $args { - $_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg))); - } - - $_map: map-merge($_map, ($key: call(get-function($func), $_args...))); - } - - @return $_map; -} -// stylelint-enable scss/dollar-variable-pattern - -@function varify($list) { - $result: null; - @each $entry in $list { - $result: append($result, var(--#{$prefix}#{$entry}), space); - } - @return $result; -} - // Internal Bootstrap function to turn maps into its negative variant. // It prefixes the keys with `n` and makes the value negative. @function negativify-map($map) { $result: (); @each $key, $value in $map { @if $key != 0 { - $result: map-merge($result, ("n" + $key: (-$value))); + $result: map.merge($result, ("n" + $key: (-$value))); } } @return $result; @@ -89,8 +57,25 @@ @function map-get-multiple($map, $values) { $result: (); @each $key, $value in $map { - @if (index($values, $key) != null) { - $result: map-merge($result, ($key: $value)); + @if (list.index($values, $key) != null) { + $result: map.merge($result, ($key: $value)); + } + } + @return $result; +} + +// Extract a specific nested property from all items in a map +// Useful for extracting a single property from nested map structures +// Example: map-get-nested($font-sizes, "font-size") +// Returns: ("xs": clamp(...), "sm": clamp(...), ...) +@function map-get-nested($map, $nested-key) { + $result: (); + @each $key, $value in $map { + @if meta.type-of($value) == "map" { + $nested-value: map.get($value, $nested-key); + @if $nested-value != null { + $result: map.merge($result, ($key: $nested-value)); + } } } @return $result; @@ -101,7 +86,7 @@ $merged-maps: (); @each $map in $maps { - $merged-maps: map-merge($merged-maps, $map); + $merged-maps: map.merge($merged-maps, $map); } @return $merged-maps; } @@ -115,10 +100,10 @@ // @param {String} $replace ('') - New value // @return {String} - Updated string @function str-replace($string, $search, $replace: "") { - $index: str-index($string, $search); + $index: string.index($string, $search); @if $index { - @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); + @return string.slice($string, 1, $index - 1) + $replace + str-replace(string.slice($string, $index + string.length($search)), $search, $replace); } @return $string; @@ -129,11 +114,11 @@ // Requires the use of quotes around data URIs. @function escape-svg($string) { - @if str-index($string, "data:image/svg+xml") { + @if string.index($string, "data:image/svg+xml") { @each $char, $encoded in $escaped-characters { // Do not escape the url brackets - @if str-index($string, "url(") == 1 { - $string: url("#{str-replace(str-slice($string, 6, -3), $char, $encoded)}"); + @if string.index($string, "url(") == 1 { + $string: url("#{str-replace(string.slice($string, 6, -3), $char, $encoded)}"); } @else { $string: str-replace($string, $char, $encoded); } @@ -151,7 +136,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 .0033 .0037 .004 .0044 .0048 .0052 .0056 .006 .0065 .007 .0075 .008 .0086 .0091 .0097 .0103 .011 .0116 .0123 .013 .0137 .0144 .0152 .016 .0168 .0176 .0185 .0194 .0203 .0212 .0222 .0232 .0242 .0252 .0262 .0273 .0284 .0296 .0307 .0319 .0331 .0343 .0356 .0369 .0382 .0395 .0409 .0423 .0437 .0452 .0467 .0482 .0497 .0513 .0529 .0545 .0561 .0578 .0595 .0612 .063 .0648 .0666 .0685 .0704 .0723 .0742 .0762 .0782 .0802 .0823 .0844 .0865 .0887 .0908 .0931 .0953 .0976 .0999 .1022 .1046 .107 .1095 .1119 .1144 .117 .1195 .1221 .1248 .1274 .1301 .1329 .1356 .1384 .1413 .1441 .147 .15 .1529 .1559 .159 .162 .1651 .1683 .1714 .1746 .1779 .1812 .1845 .1878 .1912 .1946 .1981 .2016 .2051 .2086 .2122 .2159 .2195 .2232 .227 .2307 .2346 .2384 .2423 .2462 .2502 .2542 .2582 .2623 .2664 .2705 .2747 .2789 .2831 .2874 .2918 .2961 .3005 .305 .3095 .314 .3185 .3231 .3278 .3325 .3372 .3419 .3467 .3515 .3564 .3613 .3663 .3712 .3763 .3813 .3864 .3916 .3968 .402 .4072 .4125 .4179 .4233 .4287 .4342 .4397 .4452 .4508 .4564 .4621 .4678 .4735 .4793 .4851 .491 .4969 .5029 .5089 .5149 .521 .5271 .5333 .5395 .5457 .552 .5583 .5647 .5711 .5776 .5841 .5906 .5972 .6038 .6105 .6172 .624 .6308 .6376 .6445 .6514 .6584 .6654 .6724 .6795 .6867 .6939 .7011 .7084 .7157 .7231 .7305 .7379 .7454 .7529 .7605 .7682 .7758 .7835 .7913 .7991 .807 .8148 .8228 .8308 .8388 .8469 .855 .8632 .8714 .8796 .8879 .8963 .9047 .9131 .9216 .9301 .9387 .9473 .956 .9647 .9734 .9823 .9911 1; @function color-contrast($background, $color-contrast-dark: $color-contrast-dark, $color-contrast-light: $color-contrast-light, $min-contrast-ratio: $min-contrast-ratio) { - $foregrounds: $color-contrast-light, $color-contrast-dark, $white, $black; + $foregrounds: $color-contrast-light, $color-contrast-dark, #fff, #000; $max-ratio: 0; $max-ratio-color: null; @@ -174,7 +159,7 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 $l1: luminance($background); $l2: luminance(opaque($background, $foreground)); - @return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05)); + @return if(sass($l1 > $l2): math.div($l1 + .05, $l2 + .05); else: math.div($l2 + .05, $l1 + .05)); } // Return WCAG2.2 relative luminance @@ -182,121 +167,22 @@ $_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 // See https://www.w3.org/TR/WCAG/#dfn-contrast-ratio @function luminance($color) { $rgb: ( - "r": red($color), - "g": green($color), - "b": blue($color) + "r": color.channel($color, "red"), + "g": color.channel($color, "green"), + "b": color.channel($color, "blue") ); @each $name, $value in $rgb { - $value: if(divide($value, 255) < .04045, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1)); - $rgb: map-merge($rgb, ($name: $value)); + // stylelint-disable-next-line scss/at-function-named-arguments, @stylistic/function-whitespace-after + $value: if(sass(math.div($value, 255) < .04045): math.div(math.div($value, 255), 12.92); else: list.nth($_luminance-list, math.round($value + 1))); + $rgb: map.merge($rgb, ($name: $value)); } - @return (map-get($rgb, "r") * .2126) + (map-get($rgb, "g") * .7152) + (map-get($rgb, "b") * .0722); + @return (map.get($rgb, "r") * .2126) + (map.get($rgb, "g") * .7152) + (map.get($rgb, "b") * .0722); } // Return opaque color -// opaque(#fff, rgba(0, 0, 0, .5)) => #808080 +// opaque(#fff, rgb(0 0 0 / .5)) => #808080 @function opaque($background, $foreground) { - @return mix(rgba($foreground, 1), $background, opacity($foreground) * 100%); -} - -// scss-docs-start color-functions -// Tint a color: mix a color with white -@function tint-color($color, $weight) { - @return mix(white, $color, $weight); -} - -// Shade a color: mix a color with black -@function shade-color($color, $weight) { - @return mix(black, $color, $weight); -} - -// Shade the color if the weight is positive, else tint it -@function shift-color($color, $weight) { - @return if($weight > 0, shade-color($color, $weight), tint-color($color, -$weight)); -} -// scss-docs-end color-functions - -// Return valid calc -@function add($value1, $value2, $return-calc: true) { - @if $value1 == null { - @return $value2; - } - - @if $value2 == null { - @return $value1; - } - - @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) { - @return $value1 + $value2; - } - - @return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2); -} - -@function subtract($value1, $value2, $return-calc: true) { - @if $value1 == null and $value2 == null { - @return null; - } - - @if $value1 == null { - @return -$value2; - } - - @if $value2 == null { - @return $value1; - } - - @if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) { - @return $value1 - $value2; - } - - @if type-of($value2) != number { - $value2: unquote("(") + $value2 + unquote(")"); - } - - @return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(" - ") + $value2); -} - -@function divide($dividend, $divisor, $precision: 10) { - $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1); - $dividend: abs($dividend); - $divisor: abs($divisor); - @if $dividend == 0 { - @return 0; - } - @if $divisor == 0 { - @error "Cannot divide by 0"; - } - $remainder: $dividend; - $result: 0; - $factor: 10; - @while ($remainder > 0 and $precision >= 0) { - $quotient: 0; - @while ($remainder >= $divisor) { - $remainder: $remainder - $divisor; - $quotient: $quotient + 1; - } - $result: $result * 10 + $quotient; - $factor: $factor * .1; - $remainder: $remainder * 10; - $precision: $precision - 1; - @if ($precision < 0 and $remainder >= $divisor * 5) { - $result: $result + 1; - } - } - $result: $result * $factor * $sign; - $dividend-unit: unit($dividend); - $divisor-unit: unit($divisor); - $unit-map: ( - "px": 1px, - "rem": 1rem, - "em": 1em, - "%": 1% - ); - @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) { - $result: $result * map-get($unit-map, $dividend-unit); - } - @return $result; + @return color-mix(in srgb, rgba($foreground, 1), $background, color.opacity($foreground) * 100%); } diff --git a/assets/stylesheets/bootstrap/_grid.scss b/assets/stylesheets/bootstrap/_grid.scss deleted file mode 100644 index 048f8009..00000000 --- a/assets/stylesheets/bootstrap/_grid.scss +++ /dev/null @@ -1,39 +0,0 @@ -// Row -// -// Rows contain your columns. - -:root { - @each $name, $value in $grid-breakpoints { - --#{$prefix}breakpoint-#{$name}: #{$value}; - } -} - -@if $enable-grid-classes { - .row { - @include make-row(); - - > * { - @include make-col-ready(); - } - } -} - -@if $enable-cssgrid { - .grid { - display: grid; - grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr); - grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr); - gap: var(--#{$prefix}gap, #{$grid-gutter-width}); - - @include make-cssgrid(); - } -} - - -// Columns -// -// Common styles for small and large grid columns - -@if $enable-grid-classes { - @include make-grid-columns(); -} diff --git a/assets/stylesheets/bootstrap/_helpers.scss b/assets/stylesheets/bootstrap/_helpers.scss deleted file mode 100644 index 13f2752c..00000000 --- a/assets/stylesheets/bootstrap/_helpers.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import "helpers/clearfix"; -@import "helpers/color-bg"; -@import "helpers/colored-links"; -@import "helpers/focus-ring"; -@import "helpers/icon-link"; -@import "helpers/ratio"; -@import "helpers/position"; -@import "helpers/stacks"; -@import "helpers/visually-hidden"; -@import "helpers/stretched-link"; -@import "helpers/text-truncation"; -@import "helpers/vr"; diff --git a/assets/stylesheets/bootstrap/_images.scss b/assets/stylesheets/bootstrap/_images.scss deleted file mode 100644 index 3d6a1014..00000000 --- a/assets/stylesheets/bootstrap/_images.scss +++ /dev/null @@ -1,42 +0,0 @@ -// Responsive images (ensure images don't scale beyond their parents) -// -// This is purposefully opt-in via an explicit class rather than being the default for all ``s. -// We previously tried the "images are responsive by default" approach in Bootstrap v2, -// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps) -// which weren't expecting the images within themselves to be involuntarily resized. -// See also https://github.com/twbs/bootstrap/issues/18178 -.img-fluid { - @include img-fluid(); -} - - -// Image thumbnails -.img-thumbnail { - padding: $thumbnail-padding; - background-color: $thumbnail-bg; - border: $thumbnail-border-width solid $thumbnail-border-color; - @include border-radius($thumbnail-border-radius); - @include box-shadow($thumbnail-box-shadow); - - // Keep them at most 100% wide - @include img-fluid(); -} - -// -// Figures -// - -.figure { - // Ensures the caption's text aligns with the image. - display: inline-block; -} - -.figure-img { - margin-bottom: $spacer * .5; - line-height: 1; -} - -.figure-caption { - @include font-size($figure-caption-font-size); - color: $figure-caption-color; -} diff --git a/assets/stylesheets/bootstrap/_list-group.scss b/assets/stylesheets/bootstrap/_list-group.scss index 3bdff679..6860abd7 100644 --- a/assets/stylesheets/bootstrap/_list-group.scss +++ b/assets/stylesheets/bootstrap/_list-group.scss @@ -1,199 +1,191 @@ -// Base class -// -// Easily usable on , , or . - -.list-group { - // scss-docs-start list-group-css-vars - --#{$prefix}list-group-color: #{$list-group-color}; - --#{$prefix}list-group-bg: #{$list-group-bg}; - --#{$prefix}list-group-border-color: #{$list-group-border-color}; - --#{$prefix}list-group-border-width: #{$list-group-border-width}; - --#{$prefix}list-group-border-radius: #{$list-group-border-radius}; - --#{$prefix}list-group-item-padding-x: #{$list-group-item-padding-x}; - --#{$prefix}list-group-item-padding-y: #{$list-group-item-padding-y}; - --#{$prefix}list-group-action-color: #{$list-group-action-color}; - --#{$prefix}list-group-action-hover-color: #{$list-group-action-hover-color}; - --#{$prefix}list-group-action-hover-bg: #{$list-group-hover-bg}; - --#{$prefix}list-group-action-active-color: #{$list-group-action-active-color}; - --#{$prefix}list-group-action-active-bg: #{$list-group-action-active-bg}; - --#{$prefix}list-group-disabled-color: #{$list-group-disabled-color}; - --#{$prefix}list-group-disabled-bg: #{$list-group-disabled-bg}; - --#{$prefix}list-group-active-color: #{$list-group-active-color}; - --#{$prefix}list-group-active-bg: #{$list-group-active-bg}; - --#{$prefix}list-group-active-border-color: #{$list-group-active-border-color}; - // scss-docs-end list-group-css-vars - - display: flex; - flex-direction: column; - - // No need to set list-style: none; since .list-group-item is block level - padding-left: 0; // reset padding because ul and ol - margin-bottom: 0; - @include border-radius(var(--#{$prefix}list-group-border-radius)); -} - -.list-group-numbered { - list-style-type: none; - counter-reset: section; - - > .list-group-item::before { - // Increments only this instance of the section counter - content: counters(section, ".") ". "; - counter-increment: section; +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "layout/breakpoints" as *; +@use "mixins/tokens" as *; + +$list-group-tokens: () !default; + +// scss-docs-start list-group-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$list-group-tokens: defaults( + ( + --list-group-color: var(--fg-body), + --list-group-bg: var(--bg-body), + --list-group-border-color: var(--border-color), + --list-group-border-width: var(--border-width), + --list-group-border-radius: var(--radius-5), + --list-group-item-padding-x: var(--spacer), + --list-group-item-padding-y: var(--spacer-2), + --list-group-action-color: var(--fg-2), + --list-group-action-hover-color: var(--fg-1), + --list-group-action-hover-bg: var(--bg-1), + --list-group-action-active-color: var(--fg-body), + --list-group-action-active-bg: var(--bg-2), + --list-group-disabled-color: var(--fg-3), + --list-group-disabled-bg: var(--bg-body), + --list-group-active-color: var(--primary-contrast), + --list-group-active-bg: var(--primary-bg), + --list-group-active-border-color: var(--primary-bg), + ), + $list-group-tokens +); +// scss-docs-end list-group-tokens + +@layer components { + .list-group { + @include tokens($list-group-tokens); + + display: flex; + flex-direction: column; + + // No need to set list-style-type: ""; since .list-group-item is block level + padding-inline-start: 0; // reset padding because ul and ol + margin-bottom: 0; + @include border-radius(var(--list-group-border-radius)); } -} -// Individual list items -// -// Use on `li`s or `div`s within the `.list-group` parent. - -.list-group-item { - position: relative; - display: block; - padding: var(--#{$prefix}list-group-item-padding-y) var(--#{$prefix}list-group-item-padding-x); - color: var(--#{$prefix}list-group-color); - text-decoration: if($link-decoration == none, null, none); - background-color: var(--#{$prefix}list-group-bg); - border: var(--#{$prefix}list-group-border-width) solid var(--#{$prefix}list-group-border-color); - - &:first-child { - @include border-top-radius(inherit); - } + .list-group-numbered { + list-style-type: none; + counter-reset: section; - &:last-child { - @include border-bottom-radius(inherit); + > .list-group-item::before { + // Increments only this instance of the section counter + content: counters(section, ".") ". "; + counter-increment: section; + } } - &.disabled, - &:disabled { - color: var(--#{$prefix}list-group-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}list-group-disabled-bg); - } + // Individual list items + // + // Use on `li`s or `div`s within the `.list-group` parent. + + .list-group-item { + position: relative; + display: block; + padding: var(--list-group-item-padding-y) var(--list-group-item-padding-x); + color: var(--theme-fg, var(--list-group-color)); + // stylelint-disable-next-line scss/at-function-named-arguments + text-decoration: if(sass($link-decoration == none): null); + background-color: var(--theme-bg-subtle, var(--list-group-bg)); + border: var(--list-group-border-width) solid var(--theme-border, var(--list-group-border-color)); + + &:first-child { + @include border-top-radius(inherit); + } - // Include both here for ``s and ``s - &.active { - z-index: 2; // Place active items above their siblings for proper border styling - color: var(--#{$prefix}list-group-active-color); - background-color: var(--#{$prefix}list-group-active-bg); - border-color: var(--#{$prefix}list-group-active-border-color); - } + &:last-child { + @include border-bottom-radius(inherit); + } - // stylelint-disable-next-line scss/selector-no-redundant-nesting-selector - & + .list-group-item { - border-top-width: 0; + &.disabled, + &:disabled { + color: var(--list-group-disabled-color); + pointer-events: none; + background-color: var(--list-group-disabled-bg); + } + // Include both here for ``s and ``s &.active { - margin-top: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list - border-top-width: var(--#{$prefix}list-group-border-width); + z-index: 2; // Place active items above their siblings for proper border styling + color: var(--list-group-active-color); + background-color: var(--list-group-active-bg); + border-color: var(--list-group-active-border-color); } - } -} -// Interactive list items -// -// Use anchor or button elements instead of `li`s or `div`s to create interactive -// list items. Includes an extra `.active` modifier class for selected items. - -.list-group-item-action { - width: 100%; // For ``s (anchors become 100% by default though) - color: var(--#{$prefix}list-group-action-color); - text-align: inherit; // For ``s (anchors inherit) - - &:not(.active) { - // Hover state - &:hover, - &:focus { - z-index: 1; // Place hover/focus items above their siblings for proper border styling - color: var(--#{$prefix}list-group-action-hover-color); - text-decoration: none; - background-color: var(--#{$prefix}list-group-action-hover-bg); - } + // stylelint-disable-next-line scss/selector-no-redundant-nesting-selector + & + .list-group-item { + border-block-start-width: 0; - &:active { - color: var(--#{$prefix}list-group-action-active-color); - background-color: var(--#{$prefix}list-group-action-active-bg); + &.active { + margin-top: calc(-1 * var(--list-group-border-width)); + border-block-start-width: var(--list-group-border-width); + } } } -} -// Horizontal -// -// Change the layout of list group items from vertical (default) to horizontal. - -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + // Interactive list items + // + // Use anchor or button elements instead of `li`s or `div`s to create interactive + // list items. Includes an extra `.active` modifier class for selected items. + + .list-group-item-action { + width: 100%; // For ``s (anchors become 100% by default though) + color: var(--theme-fg, var(--list-group-action-color)); + text-align: inherit; // For ``s (anchors inherit) + text-decoration: none; + + &:not(.active) { + // Hover state + &:hover, + &:focus { + z-index: 1; // Place hover/focus items above their siblings for proper border styling + color: var(--theme-fg-emphasis, var(--list-group-action-hover-color)); + text-decoration: none; + background-color: var(--theme-bg-muted, var(--list-group-action-hover-bg)); + } - .list-group-horizontal#{$infix} { - flex-direction: row; + &:active { + color: var(--theme-fg-emphasis, var(--list-group-action-active-color)); + background-color: var(--theme-bg-muted, var(--list-group-action-active-bg)); + } + } + } - > .list-group-item { - &:first-child:not(:last-child) { - @include border-bottom-start-radius(var(--#{$prefix}list-group-border-radius)); - @include border-top-end-radius(0); - } + // Horizontal + // + // Change the layout of list group items from vertical (default) to horizontal. + // The responsive variants use container queries, so wrap the list group in a + // query container (e.g., the `.contains-inline` utility) for them to take effect. + + @include loop-breakpoints-up() using ($breakpoint, $prefix) { + .#{$prefix}list-group-horizontal { + @include container-breakpoint-up($breakpoint) { + flex-direction: row; + + > .list-group-item { + &:first-child:not(:last-child) { + @include border-bottom-start-radius(var(--list-group-border-radius)); + @include border-top-end-radius(0); + } - &:last-child:not(:first-child) { - @include border-top-end-radius(var(--#{$prefix}list-group-border-radius)); - @include border-bottom-start-radius(0); - } + &:last-child:not(:first-child) { + @include border-top-end-radius(var(--list-group-border-radius)); + @include border-bottom-start-radius(0); + } - &.active { - margin-top: 0; - } + &.active { + margin-top: 0; + } - + .list-group-item { - border-top-width: var(--#{$prefix}list-group-border-width); - border-left-width: 0; + + .list-group-item { + border-block-start-width: var(--list-group-border-width); + border-inline-start-width: 0; - &.active { - margin-left: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list - border-left-width: var(--#{$prefix}list-group-border-width); + &.active { + margin-inline-start: calc(-1 * var(--list-group-border-width)); + border-inline-start-width: var(--list-group-border-width); + } } } } } } -} - -// Flush list items -// -// Remove borders and border-radius to keep list group items edge-to-edge. Most -// useful within other components (e.g., cards). + // Flush list items + // + // Remove borders and border-radius to keep list group items edge-to-edge. Most + // useful within other components (e.g., cards). -.list-group-flush { - @include border-radius(0); + .list-group-flush { + @include border-radius(0); - > .list-group-item { - border-width: 0 0 var(--#{$prefix}list-group-border-width); + > .list-group-item { + border-width: 0 0 var(--list-group-border-width); - &:last-child { - border-bottom-width: 0; + &:last-child { + border-block-end-width: 0; + } } } } - - -// scss-docs-start list-group-modifiers -// List group contextual variants -// -// Add modifier classes to change text and background color on individual items. -// Organizationally, this must come after the `:hover` states. - -@each $state in map-keys($theme-colors) { - .list-group-item-#{$state} { - --#{$prefix}list-group-color: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}list-group-bg: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}list-group-border-color: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-action-hover-color: var(--#{$prefix}emphasis-color); - --#{$prefix}list-group-action-hover-bg: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-action-active-color: var(--#{$prefix}emphasis-color); - --#{$prefix}list-group-action-active-bg: var(--#{$prefix}#{$state}-border-subtle); - --#{$prefix}list-group-active-color: var(--#{$prefix}#{$state}-bg-subtle); - --#{$prefix}list-group-active-bg: var(--#{$prefix}#{$state}-text-emphasis); - --#{$prefix}list-group-active-border-color: var(--#{$prefix}#{$state}-text-emphasis); - } -} -// scss-docs-end list-group-modifiers diff --git a/assets/stylesheets/bootstrap/_maps.scss b/assets/stylesheets/bootstrap/_maps.scss deleted file mode 100644 index 68ee421c..00000000 --- a/assets/stylesheets/bootstrap/_maps.scss +++ /dev/null @@ -1,174 +0,0 @@ -// Re-assigned maps -// -// Placed here so that others can override the default Sass maps and see automatic updates to utilities and more. - -// scss-docs-start theme-colors-rgb -$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value") !default; -// scss-docs-end theme-colors-rgb - -// scss-docs-start theme-text-map -$theme-colors-text: ( - "primary": $primary-text-emphasis, - "secondary": $secondary-text-emphasis, - "success": $success-text-emphasis, - "info": $info-text-emphasis, - "warning": $warning-text-emphasis, - "danger": $danger-text-emphasis, - "light": $light-text-emphasis, - "dark": $dark-text-emphasis, -) !default; -// scss-docs-end theme-text-map - -// scss-docs-start theme-bg-subtle-map -$theme-colors-bg-subtle: ( - "primary": $primary-bg-subtle, - "secondary": $secondary-bg-subtle, - "success": $success-bg-subtle, - "info": $info-bg-subtle, - "warning": $warning-bg-subtle, - "danger": $danger-bg-subtle, - "light": $light-bg-subtle, - "dark": $dark-bg-subtle, -) !default; -// scss-docs-end theme-bg-subtle-map - -// scss-docs-start theme-border-subtle-map -$theme-colors-border-subtle: ( - "primary": $primary-border-subtle, - "secondary": $secondary-border-subtle, - "success": $success-border-subtle, - "info": $info-border-subtle, - "warning": $warning-border-subtle, - "danger": $danger-border-subtle, - "light": $light-border-subtle, - "dark": $dark-border-subtle, -) !default; -// scss-docs-end theme-border-subtle-map - -$theme-colors-text-dark: null !default; -$theme-colors-bg-subtle-dark: null !default; -$theme-colors-border-subtle-dark: null !default; - -@if $enable-dark-mode { - // scss-docs-start theme-text-dark-map - $theme-colors-text-dark: ( - "primary": $primary-text-emphasis-dark, - "secondary": $secondary-text-emphasis-dark, - "success": $success-text-emphasis-dark, - "info": $info-text-emphasis-dark, - "warning": $warning-text-emphasis-dark, - "danger": $danger-text-emphasis-dark, - "light": $light-text-emphasis-dark, - "dark": $dark-text-emphasis-dark, - ) !default; - // scss-docs-end theme-text-dark-map - - // scss-docs-start theme-bg-subtle-dark-map - $theme-colors-bg-subtle-dark: ( - "primary": $primary-bg-subtle-dark, - "secondary": $secondary-bg-subtle-dark, - "success": $success-bg-subtle-dark, - "info": $info-bg-subtle-dark, - "warning": $warning-bg-subtle-dark, - "danger": $danger-bg-subtle-dark, - "light": $light-bg-subtle-dark, - "dark": $dark-bg-subtle-dark, - ) !default; - // scss-docs-end theme-bg-subtle-dark-map - - // scss-docs-start theme-border-subtle-dark-map - $theme-colors-border-subtle-dark: ( - "primary": $primary-border-subtle-dark, - "secondary": $secondary-border-subtle-dark, - "success": $success-border-subtle-dark, - "info": $info-border-subtle-dark, - "warning": $warning-border-subtle-dark, - "danger": $danger-border-subtle-dark, - "light": $light-border-subtle-dark, - "dark": $dark-border-subtle-dark, - ) !default; - // scss-docs-end theme-border-subtle-dark-map -} - -// Utilities maps -// -// Extends the default `$theme-colors` maps to help create our utilities. - -// Come v6, we'll de-dupe these variables. Until then, for backward compatibility, we keep them to reassign. -// scss-docs-start utilities-colors -$utilities-colors: $theme-colors-rgb !default; -// scss-docs-end utilities-colors - -// scss-docs-start utilities-text-colors -$utilities-text: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white), - "body": to-rgb($body-color) - ) -) !default; -$utilities-text-colors: map-loop($utilities-text, rgba-css-var, "$key", "text") !default; - -$utilities-text-emphasis-colors: ( - "primary-emphasis": var(--#{$prefix}primary-text-emphasis), - "secondary-emphasis": var(--#{$prefix}secondary-text-emphasis), - "success-emphasis": var(--#{$prefix}success-text-emphasis), - "info-emphasis": var(--#{$prefix}info-text-emphasis), - "warning-emphasis": var(--#{$prefix}warning-text-emphasis), - "danger-emphasis": var(--#{$prefix}danger-text-emphasis), - "light-emphasis": var(--#{$prefix}light-text-emphasis), - "dark-emphasis": var(--#{$prefix}dark-text-emphasis) -) !default; -// scss-docs-end utilities-text-colors - -// scss-docs-start utilities-bg-colors -$utilities-bg: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white), - "body": to-rgb($body-bg) - ) -) !default; -$utilities-bg-colors: map-loop($utilities-bg, rgba-css-var, "$key", "bg") !default; - -$utilities-bg-subtle: ( - "primary-subtle": var(--#{$prefix}primary-bg-subtle), - "secondary-subtle": var(--#{$prefix}secondary-bg-subtle), - "success-subtle": var(--#{$prefix}success-bg-subtle), - "info-subtle": var(--#{$prefix}info-bg-subtle), - "warning-subtle": var(--#{$prefix}warning-bg-subtle), - "danger-subtle": var(--#{$prefix}danger-bg-subtle), - "light-subtle": var(--#{$prefix}light-bg-subtle), - "dark-subtle": var(--#{$prefix}dark-bg-subtle) -) !default; -// scss-docs-end utilities-bg-colors - -// scss-docs-start utilities-border-colors -$utilities-border: map-merge( - $utilities-colors, - ( - "black": to-rgb($black), - "white": to-rgb($white) - ) -) !default; -$utilities-border-colors: map-loop($utilities-border, rgba-css-var, "$key", "border") !default; - -$utilities-border-subtle: ( - "primary-subtle": var(--#{$prefix}primary-border-subtle), - "secondary-subtle": var(--#{$prefix}secondary-border-subtle), - "success-subtle": var(--#{$prefix}success-border-subtle), - "info-subtle": var(--#{$prefix}info-border-subtle), - "warning-subtle": var(--#{$prefix}warning-border-subtle), - "danger-subtle": var(--#{$prefix}danger-border-subtle), - "light-subtle": var(--#{$prefix}light-border-subtle), - "dark-subtle": var(--#{$prefix}dark-border-subtle) -) !default; -// scss-docs-end utilities-border-colors - -$utilities-links-underline: map-loop($utilities-colors, rgba-css-var, "$key", "link-underline") !default; - -$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default; - -$gutters: $spacers !default; diff --git a/assets/stylesheets/bootstrap/_menu.scss b/assets/stylesheets/bootstrap/_menu.scss new file mode 100644 index 00000000..cddc5ab7 --- /dev/null +++ b/assets/stylesheets/bootstrap/_menu.scss @@ -0,0 +1,289 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +// stylelint-disable scss/dollar-variable-default, custom-property-no-missing-var-function +$menu-tokens: () !default; + +// scss-docs-start menu-tokens +$menu-tokens: defaults( + ( + --menu-zindex: #{$zindex-menu}, + --menu-gap: .125rem, + --menu-min-width: 10rem, + --menu-padding-x: .25rem, + --menu-padding-y: .25rem, + --menu-spacer: .125rem, + --menu-font-size: var(--font-size-sm), + --menu-color: var(--fg-body), + --menu-bg: var(--bg-body), + // --menu-border-color: var(--border-color-translucent), + // --menu-border-radius: var(--radius-7), + // --menu-border-width: var(--border-width), + --menu-box-shadow: var(--box-shadow), + // --menu-max-height: none, + --menu-divider-bg: var(--border-color-translucent), + --menu-divider-margin-y: .125rem, + --menu-divider-margin-x: .25rem, + --menu-item-color: var(--menu-color, var(--fg-body)), + --menu-item-hover-color: var(--menu-color, var(--fg-body)), + --menu-item-hover-bg: var(--bg-1), + --menu-item-active-color: var(--primary-contrast), + --menu-item-active-bg: var(--primary-bg), + --menu-item-disabled-color: var(--fg-3), + --menu-item-gap: .5rem, + --menu-item-padding-x: .75rem, + --menu-item-padding-y: .25rem, + --menu-item-border-radius: var(--radius-5), + --menu-icon-size: 1rem, + --menu-description-font-size: var(--font-size-xs), + --menu-check-color: currentcolor, + --menu-header-color: var(--fg-3), + --menu-header-padding-x: .75rem, + --menu-header-padding-y: .25rem, + --menu-transition-duration: .15s, + --menu-transition-timing: cubic-bezier(.22, 1, .36, 1), + ), + $menu-tokens +); +// scss-docs-end menu-tokens + +// stylelint-enable custom-property-no-missing-var-function, scss/dollar-variable-default + +@layer components { + .menu { + @include tokens($menu-tokens); + + position: absolute; + z-index: var(--menu-zindex); + display: none; + flex-direction: column; + gap: var(--menu-gap); + min-width: var(--menu-min-width); + max-height: var(--menu-max-height, none); + padding: var(--menu-padding-y) var(--menu-padding-x); + margin: 0; + overflow-y: var(--menu-overflow-y, initial); + overscroll-behavior: contain; + font-size: var(--menu-font-size); + color: var(--menu-color); + text-align: start; + list-style-type: ""; + background-color: var(--menu-bg); + background-clip: padding-box; + border: var(--menu-border-width, var(--border-width)) solid var(--menu-border-color, var(--border-color-translucent)); + @include border-radius(var(--menu-border-radius, var(--radius-7))); + @include box-shadow(var(--menu-box-shadow)); + opacity: 0; + transform: scale(.95); + transform-origin: top start; + + &[data-bs-placement^="top"] { + transform-origin: bottom start; + } + + &[data-bs-placement="bottom-end"] { + transform-origin: top end; + } + + &[data-bs-placement="top-end"] { + transform-origin: bottom end; + } + + &[data-bs-placement^="left"] { + transform-origin: top end; + } + + @include transition( + opacity var(--menu-transition-duration) var(--menu-transition-timing), + transform var(--menu-transition-duration) var(--menu-transition-timing), + display var(--menu-transition-duration) allow-discrete + ); + + &.show { + display: flex; + opacity: 1; + transform: none; + } + } + + @starting-style { + .menu.show { + opacity: 0; + transform: scale(.95); + } + } + + .menu-scrollable { + --menu-max-height: 80dvh; + --menu-overflow-y: auto; + } + + .menu-translucent { + --menu-item-hover-bg-light: color-mix(in oklch, var(--bg-1) 90%, transparent); + --menu-item-hover-bg-dark: color-mix(in oklch, var(--bg-1) 80%, transparent); + + --menu-item-active-bg-light: color-mix(in oklch, var(--primary-bg) 80%, transparent); + --menu-item-active-bg-dark: color-mix(in oklch, var(--primary-bg) 70%, transparent); + + --menu-item-active-bg: light-dark(var(--menu-item-active-bg-light), var(--menu-item-active-bg-dark)); + --menu-item-hover-bg: light-dark(var(--menu-item-hover-bg-light), var(--menu-item-hover-bg-dark)); + + background-color: color-mix(in oklch, var(--menu-bg) 80%, transparent); + backdrop-filter: blur(5px) saturate(180%); + } + + .menu-divider { + height: 0; + margin: var(--menu-divider-margin-y) var(--menu-divider-margin-x); + overflow: hidden; + border-block-start: 1px solid var(--menu-divider-bg); + opacity: 1; + } + + .menu-item { + display: flex; + gap: var(--menu-item-gap); + align-items: center; + width: 100%; + padding: var(--menu-item-padding-y) var(--menu-item-padding-x); + font-weight: var(--menu-item-font-weight, var(--font-weight-normal)); + color: var(--theme-fg, var(--menu-item-color)); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + background-color: transparent; + border: 0; + outline: 0; + @include border-radius(var(--menu-item-border-radius, 0)); + + &:hover, + &:focus { + color: var(--theme-fg-emphasis, var(--menu-item-hover-color)); + background-color: var(--theme-bg-subtle, var(--menu-item-hover-bg)); + // @include gradient-bg(var(--theme-bg-subtle, var(--menu-item-hover-bg))); + } + + &.active, + &:active { + color: var(--theme-contrast, var(--menu-item-active-color)); + background-color: var(--theme-bg, var(--menu-item-active-bg)); + // @include gradient-bg(var(--theme-bg, var(--menu-item-active-bg))); + + .menu-item-icon { + color: inherit !important; // stylelint-disable-line declaration-no-important + } + } + + &.selected { + font-weight: $font-weight-semibold; + } + + &.disabled, + &:disabled { + color: var(--menu-item-disabled-color); + pointer-events: none; + background-color: transparent; + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + } + } + + .menu-item-icon { + flex-shrink: 0; + align-self: flex-start; + width: var(--menu-icon-size); + height: auto; + margin-top: .125rem; + } + + .menu-item-content { + display: flex; + flex: 1; + flex-direction: column; + min-width: fit-content; + } + + .menu-item-description { + font-size: var(--menu-description-font-size); + font-weight: var(--font-weight-normal); + color: color-mix(in oklch, currentcolor 65%, transparent); + } + + .menu-item-check { + flex-shrink: 0; + align-self: flex-start; + margin-block-start: .125rem; + margin-inline-start: auto; + color: var(--menu-check-color); + visibility: hidden; + + .selected > & { + visibility: visible; + } + } + + .menu-header { + display: block; + padding: var(--menu-header-padding-y) var(--menu-header-padding-x); + margin-bottom: 0; + font-size: var(--font-size-sm); + color: var(--menu-header-color); + white-space: nowrap; + } + + .menu-text { + display: block; + padding: var(--menu-item-padding-y) var(--menu-item-padding-x); + color: var(--fg-2); + } + + // scss-docs-start submenu + .submenu { + position: relative; + + > .menu-item { + display: flex; + align-items: center; + justify-content: space-between; + } + + > .menu-item::after { + display: inline-block; + flex-shrink: 0; + width: .375em; + height: .375em; + margin-inline-start: auto; + content: ""; + border-color: currentcolor; + border-style: solid; + border-width: 0 .125em .125em 0; + transform: rotate(-45deg); + + [dir="rtl"] & { + transform: rotate(135deg); + } + } + + > .menu { + top: 0; + margin-top: calc(-1 * var(--menu-padding-y)); + } + + &:hover > .menu-item, + &:focus-within > .menu-item { + color: var(--menu-item-hover-color); + background-color: var(--menu-item-hover-bg); + } + + &.show > .menu-item { + color: var(--menu-item-hover-color); + background-color: var(--menu-item-hover-bg); + } + } + // scss-docs-end submenu +} diff --git a/assets/stylesheets/bootstrap/_mixins.scss b/assets/stylesheets/bootstrap/_mixins.scss deleted file mode 100644 index e1e130b1..00000000 --- a/assets/stylesheets/bootstrap/_mixins.scss +++ /dev/null @@ -1,42 +0,0 @@ -// Toggles -// -// Used in conjunction with global variables to enable certain theme features. - -// Vendor -@import "vendor/rfs"; - -// Deprecate -@import "mixins/deprecate"; - -// Helpers -@import "mixins/breakpoints"; -@import "mixins/color-mode"; -@import "mixins/color-scheme"; -@import "mixins/image"; -@import "mixins/resize"; -@import "mixins/visually-hidden"; -@import "mixins/reset-text"; -@import "mixins/text-truncate"; - -// Utilities -@import "mixins/utilities"; - -// Components -@import "mixins/backdrop"; -@import "mixins/buttons"; -@import "mixins/caret"; -@import "mixins/pagination"; -@import "mixins/lists"; -@import "mixins/forms"; -@import "mixins/table-variants"; - -// Skins -@import "mixins/border-radius"; -@import "mixins/box-shadow"; -@import "mixins/gradients"; -@import "mixins/transition"; - -// Layout -@import "mixins/clearfix"; -@import "mixins/container"; -@import "mixins/grid"; diff --git a/assets/stylesheets/bootstrap/_modal.scss b/assets/stylesheets/bootstrap/_modal.scss deleted file mode 100644 index a3492c17..00000000 --- a/assets/stylesheets/bootstrap/_modal.scss +++ /dev/null @@ -1,240 +0,0 @@ -// stylelint-disable function-disallowed-list - -// .modal-open - body class for killing the scroll -// .modal - container to scroll within -// .modal-dialog - positioning shell for the actual modal -// .modal-content - actual modal w/ bg and corners and stuff - - -// Container that the modal scrolls within -.modal { - // scss-docs-start modal-css-vars - --#{$prefix}modal-zindex: #{$zindex-modal}; - --#{$prefix}modal-width: #{$modal-md}; - --#{$prefix}modal-padding: #{$modal-inner-padding}; - --#{$prefix}modal-margin: #{$modal-dialog-margin}; - --#{$prefix}modal-color: #{$modal-content-color}; - --#{$prefix}modal-bg: #{$modal-content-bg}; - --#{$prefix}modal-border-color: #{$modal-content-border-color}; - --#{$prefix}modal-border-width: #{$modal-content-border-width}; - --#{$prefix}modal-border-radius: #{$modal-content-border-radius}; - --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-xs}; - --#{$prefix}modal-inner-border-radius: #{$modal-content-inner-border-radius}; - --#{$prefix}modal-header-padding-x: #{$modal-header-padding-x}; - --#{$prefix}modal-header-padding-y: #{$modal-header-padding-y}; - --#{$prefix}modal-header-padding: #{$modal-header-padding}; // Todo in v6: Split this padding into x and y - --#{$prefix}modal-header-border-color: #{$modal-header-border-color}; - --#{$prefix}modal-header-border-width: #{$modal-header-border-width}; - --#{$prefix}modal-title-line-height: #{$modal-title-line-height}; - --#{$prefix}modal-footer-gap: #{$modal-footer-margin-between}; - --#{$prefix}modal-footer-bg: #{$modal-footer-bg}; - --#{$prefix}modal-footer-border-color: #{$modal-footer-border-color}; - --#{$prefix}modal-footer-border-width: #{$modal-footer-border-width}; - // scss-docs-end modal-css-vars - - position: fixed; - top: 0; - left: 0; - z-index: var(--#{$prefix}modal-zindex); - display: none; - width: 100%; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - // Prevent Chrome on Windows from adding a focus outline. For details, see - // https://github.com/twbs/bootstrap/pull/10951. - outline: 0; - // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a - // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342 - // See also https://github.com/twbs/bootstrap/issues/17695 -} - -// Shell div to position the modal with bottom padding -.modal-dialog { - position: relative; - width: auto; - margin: var(--#{$prefix}modal-margin); - // allow clicks to pass through for custom click handling to close modal - pointer-events: none; - - // When fading in the modal, animate it to slide down - .modal.fade & { - transform: $modal-fade-transform; - @include transition($modal-transition); - } - .modal.show & { - transform: $modal-show-transform; - } - - // When trying to close, animate focus to scale - .modal.modal-static & { - transform: $modal-scale-transform; - } -} - -.modal-dialog-scrollable { - height: calc(100% - var(--#{$prefix}modal-margin) * 2); - - .modal-content { - max-height: 100%; - overflow: hidden; - } - - .modal-body { - overflow-y: auto; - } -} - -.modal-dialog-centered { - display: flex; - align-items: center; - min-height: calc(100% - var(--#{$prefix}modal-margin) * 2); -} - -// Actual modal -.modal-content { - position: relative; - display: flex; - flex-direction: column; - width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog` - // counteract the pointer-events: none; in the .modal-dialog - color: var(--#{$prefix}modal-color); - pointer-events: auto; - background-color: var(--#{$prefix}modal-bg); - background-clip: padding-box; - border: var(--#{$prefix}modal-border-width) solid var(--#{$prefix}modal-border-color); - @include border-radius(var(--#{$prefix}modal-border-radius)); - @include box-shadow(var(--#{$prefix}modal-box-shadow)); - // Remove focus outline from opened modal - outline: 0; -} - -// Modal background -.modal-backdrop { - // scss-docs-start modal-backdrop-css-vars - --#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop}; - --#{$prefix}backdrop-bg: #{$modal-backdrop-bg}; - --#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity}; - // scss-docs-end modal-backdrop-css-vars - - @include overlay-backdrop(var(--#{$prefix}backdrop-zindex), var(--#{$prefix}backdrop-bg), var(--#{$prefix}backdrop-opacity)); -} - -// Modal header -// Top section of the modal w/ title and dismiss -.modal-header { - display: flex; - flex-shrink: 0; - align-items: center; - padding: var(--#{$prefix}modal-header-padding); - border-bottom: var(--#{$prefix}modal-header-border-width) solid var(--#{$prefix}modal-header-border-color); - @include border-top-radius(var(--#{$prefix}modal-inner-border-radius)); - - .btn-close { - padding: calc(var(--#{$prefix}modal-header-padding-y) * .5) calc(var(--#{$prefix}modal-header-padding-x) * .5); - // Split properties to avoid invalid calc() function if value is 0 - margin-top: calc(-.5 * var(--#{$prefix}modal-header-padding-y)); - margin-right: calc(-.5 * var(--#{$prefix}modal-header-padding-x)); - margin-bottom: calc(-.5 * var(--#{$prefix}modal-header-padding-y)); - margin-left: auto; - } -} - -// Title text within header -.modal-title { - margin-bottom: 0; - line-height: var(--#{$prefix}modal-title-line-height); -} - -// Modal body -// Where all modal content resides (sibling of .modal-header and .modal-footer) -.modal-body { - position: relative; - // Enable `flex-grow: 1` so that the body take up as much space as possible - // when there should be a fixed height on `.modal-dialog`. - flex: 1 1 auto; - padding: var(--#{$prefix}modal-padding); -} - -// Footer (for actions) -.modal-footer { - display: flex; - flex-shrink: 0; - flex-wrap: wrap; - align-items: center; // vertically center - justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items - padding: calc(var(--#{$prefix}modal-padding) - var(--#{$prefix}modal-footer-gap) * .5); - background-color: var(--#{$prefix}modal-footer-bg); - border-top: var(--#{$prefix}modal-footer-border-width) solid var(--#{$prefix}modal-footer-border-color); - @include border-bottom-radius(var(--#{$prefix}modal-inner-border-radius)); - - // Place margin between footer elements - // This solution is far from ideal because of the universal selector usage, - // but is needed to fix https://github.com/twbs/bootstrap/issues/24800 - > * { - margin: calc(var(--#{$prefix}modal-footer-gap) * .5); // Todo in v6: replace with gap on parent class - } -} - -// Scale up the modal -@include media-breakpoint-up(sm) { - .modal { - --#{$prefix}modal-margin: #{$modal-dialog-margin-y-sm-up}; - --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-sm-up}; - } - - // Automatically set modal's width for larger viewports - .modal-dialog { - max-width: var(--#{$prefix}modal-width); - margin-right: auto; - margin-left: auto; - } - - .modal-sm { - --#{$prefix}modal-width: #{$modal-sm}; - } -} - -@include media-breakpoint-up(lg) { - .modal-lg, - .modal-xl { - --#{$prefix}modal-width: #{$modal-lg}; - } -} - -@include media-breakpoint-up(xl) { - .modal-xl { - --#{$prefix}modal-width: #{$modal-xl}; - } -} - -// scss-docs-start modal-fullscreen-loop -@each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - $postfix: if($infix != "", $infix + "-down", ""); - - @include media-breakpoint-down($breakpoint) { - .modal-fullscreen#{$postfix} { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - - .modal-content { - height: 100%; - border: 0; - @include border-radius(0); - } - - .modal-header, - .modal-footer { - @include border-radius(0); - } - - .modal-body { - overflow-y: auto; - } - } - } -} -// scss-docs-end modal-fullscreen-loop diff --git a/assets/stylesheets/bootstrap/_nav-overflow.scss b/assets/stylesheets/bootstrap/_nav-overflow.scss new file mode 100644 index 00000000..0b810a35 --- /dev/null +++ b/assets/stylesheets/bootstrap/_nav-overflow.scss @@ -0,0 +1,39 @@ +// Nav Overflow (Priority+ Pattern) +// +// A responsive navigation pattern that automatically moves items +// to an overflow menu when space is limited. + +@layer components { + .nav-overflow { + flex-wrap: nowrap; + min-width: 0; // Allow flex child to shrink below content width + } + + // Pills use inline-flex by default; override so the nav fills its container + // and the ResizeObserver can detect width changes. + .nav-pills.nav-overflow { + display: flex; + } + + // Inside a navbar the nav is a flex child that sizes to content by default; + // grow it so it fills remaining space and shrinks with the container. + .navbar-nav.nav-overflow { + flex: 1 1 0; + } + + // Container item for overflow + .nav-overflow-item { + flex-shrink: 0; + margin-inline-start: auto; + } + + // Hide items that have been moved to overflow + .nav-overflow [data-bs-nav-overflow="true"] { + display: none; + } + + // Preserve items that should never overflow + .nav-overflow-keep { + flex-shrink: 0; + } +} diff --git a/assets/stylesheets/bootstrap/_nav.scss b/assets/stylesheets/bootstrap/_nav.scss index 96fa5289..90ac39d0 100644 --- a/assets/stylesheets/bootstrap/_nav.scss +++ b/assets/stylesheets/bootstrap/_nav.scss @@ -1,197 +1,289 @@ +@use "functions" as *; +@use "config" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/gradients" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +$nav-tokens: () !default; + +// scss-docs-start nav-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-tokens: defaults( + ( + --nav-gap: .125rem, + --nav-link-gap: .5rem, + --nav-link-align: center, + --nav-link-justify: center, + --nav-link-padding-x: .75rem, + --nav-link-padding-y: .375rem, + --nav-link-color: var(--fg-2), + --nav-link-hover-color: var(--fg-1), + --nav-link-hover-bg: var(--bg-1), + --nav-link-active-color: var(--fg-body), + --nav-link-active-bg: var(--bg-2), + --nav-link-disabled-color: var(--fg-4), + --nav-link-border-width: var(--border-width), + --nav-link-transition-property: "color, background-color, border-color", + --nav-link-transition-timing: .15s ease-in-out, + --nav-link-transition: var(--nav-link-transition-property) var(--nav-link-transition-timing), + ), + $nav-tokens +); +// scss-docs-end nav-tokens + +$nav-tabs-tokens: () !default; + +// scss-docs-start nav-tabs-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-tabs-tokens: defaults( + ( + --nav-tabs-border-width: var(--border-width), + --nav-tabs-border-color: var(--border-color), + --nav-tabs-border-radius: var(--radius-5), + --nav-tabs-link-hover-border-color: var(--border-subtle), + --nav-tabs-link-active-color: var(--fg-color), + --nav-tabs-link-active-bg: var(--bg-body), + --nav-tabs-link-active-border-color: var(--border-color) var(--border-color) var(--bg-body), + ), + $nav-tabs-tokens +); +// scss-docs-end nav-tabs-tokens + +$nav-pills-tokens: () !default; + +// scss-docs-start nav-pills-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-pills-tokens: defaults( + ( + --nav-pills-bg: var(--bg-1), + --nav-pills-padding: .25rem, + --nav-pills-border-radius: var(--radius-9), + --nav-pills-link-active-color: var(--primary-contrast), + --nav-pills-link-active-bg: var(--primary-bg), + --nav-pills-link-border-radius: var(--radius-9), + ), + $nav-pills-tokens +); +// scss-docs-end nav-pills-tokens + +$nav-underline-tokens: () !default; + +// scss-docs-start nav-underline-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$nav-underline-tokens: defaults( + ( + --nav-gap: 1rem, + --nav-link-active-bg: transparent, + --nav-underline-border-width: .125rem, + --nav-underline-link-active-color: var(--fg-color), + ), + $nav-underline-tokens +); +// scss-docs-end nav-underline-tokens + // Base class // // Kickstart any navigation component with a set of style resets. Works with // ``s, ``s or ``s. -.nav { - // scss-docs-start nav-css-vars - --#{$prefix}nav-link-padding-x: #{$nav-link-padding-x}; - --#{$prefix}nav-link-padding-y: #{$nav-link-padding-y}; - @include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size); - --#{$prefix}nav-link-font-weight: #{$nav-link-font-weight}; - --#{$prefix}nav-link-color: #{$nav-link-color}; - --#{$prefix}nav-link-hover-color: #{$nav-link-hover-color}; - --#{$prefix}nav-link-disabled-color: #{$nav-link-disabled-color}; - // scss-docs-end nav-css-vars - - display: flex; - flex-wrap: wrap; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} - -.nav-link { - display: block; - padding: var(--#{$prefix}nav-link-padding-y) var(--#{$prefix}nav-link-padding-x); - @include font-size(var(--#{$prefix}nav-link-font-size)); - font-weight: var(--#{$prefix}nav-link-font-weight); - color: var(--#{$prefix}nav-link-color); - text-decoration: if($link-decoration == none, null, none); - background: none; - border: 0; - @include transition($nav-link-transition); - - &:hover, - &:focus { - color: var(--#{$prefix}nav-link-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - } +@layer components { + .nav { + @include tokens($nav-tokens); - &:focus-visible { - outline: 0; - box-shadow: $nav-link-focus-box-shadow; + display: flex; + flex-wrap: wrap; + gap: var(--nav-gap); + padding-inline-start: 0; + margin-bottom: 0; + list-style-type: ""; } - // Disabled state lightens text - &.disabled, - &:disabled { - color: var(--#{$prefix}nav-link-disabled-color); - pointer-events: none; - cursor: default; + .nav-item { + display: flex; } -} - -// -// Tabs -// - -.nav-tabs { - // scss-docs-start nav-tabs-css-vars - --#{$prefix}nav-tabs-border-width: #{$nav-tabs-border-width}; - --#{$prefix}nav-tabs-border-color: #{$nav-tabs-border-color}; - --#{$prefix}nav-tabs-border-radius: #{$nav-tabs-border-radius}; - --#{$prefix}nav-tabs-link-hover-border-color: #{$nav-tabs-link-hover-border-color}; - --#{$prefix}nav-tabs-link-active-color: #{$nav-tabs-link-active-color}; - --#{$prefix}nav-tabs-link-active-bg: #{$nav-tabs-link-active-bg}; - --#{$prefix}nav-tabs-link-active-border-color: #{$nav-tabs-link-active-border-color}; - // scss-docs-end nav-tabs-css-vars - - border-bottom: var(--#{$prefix}nav-tabs-border-width) solid var(--#{$prefix}nav-tabs-border-color); .nav-link { - margin-bottom: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list - border: var(--#{$prefix}nav-tabs-border-width) solid transparent; - @include border-top-radius(var(--#{$prefix}nav-tabs-border-radius)); + display: flex; + gap: var(--nav-link-gap); + align-items: var(--nav-link-align); + justify-content: var(--nav-link-justify); + padding: var(--nav-link-padding-y) var(--nav-link-padding-x); + font-weight: var(--nav-link-font-weight); + color: var(--nav-link-color); + text-decoration: none; + white-space: nowrap; + background: none; + border: var(--nav-link-border-width) solid transparent; + @include border-radius(var(--radius-5)); + @include transition(var(--nav-link-transition)); &:hover, &:focus { - // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link - isolation: isolate; - border-color: var(--#{$prefix}nav-tabs-link-hover-border-color); + color: var(--nav-link-hover-color); + background-color: var(--nav-link-hover-bg); } - } - .nav-link.active, - .nav-item.show .nav-link { - color: var(--#{$prefix}nav-tabs-link-active-color); - background-color: var(--#{$prefix}nav-tabs-link-active-bg); - border-color: var(--#{$prefix}nav-tabs-link-active-border-color); - } + &:focus-visible { + --focus-ring-offset: 1px; + color: var(--nav-link-hover-color); + @include focus-ring(true); + } - .dropdown-menu { - // Make dropdown border overlap tab border - margin-top: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list - // Remove the top rounded corners here since there is a hard edge above the menu - @include border-top-radius(0); + &.active, + &:active { + color: var(--nav-link-active-color); + background-color: var(--nav-link-active-bg); + } + + // Disabled state lightens text + &.disabled, + &:disabled { + color: var(--nav-link-disabled-color); + pointer-events: none; + cursor: default; + } } -} + // + // Tabs + // -// -// Pills -// + .nav-tabs { + // scss-docs-start nav-tabs-css-vars + @include tokens($nav-tabs-tokens); + // scss-docs-end nav-tabs-css-vars -.nav-pills { - // scss-docs-start nav-pills-css-vars - --#{$prefix}nav-pills-border-radius: #{$nav-pills-border-radius}; - --#{$prefix}nav-pills-link-active-color: #{$nav-pills-link-active-color}; - --#{$prefix}nav-pills-link-active-bg: #{$nav-pills-link-active-bg}; - // scss-docs-end nav-pills-css-vars + box-shadow: inset 0 calc(-1 * var(--nav-tabs-border-width)) 0 var(--nav-tabs-border-color); - .nav-link { - @include border-radius(var(--#{$prefix}nav-pills-border-radius)); - } + .nav-link { + border: var(--nav-tabs-border-width) solid transparent; + border-bottom-color: var(--nav-tabs-border-color); + @include border-bottom-radius(0); - .nav-link.active, - .show > .nav-link { - color: var(--#{$prefix}nav-pills-link-active-color); - @include gradient-bg(var(--#{$prefix}nav-pills-link-active-bg)); - } -} + &:hover { + // Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link + isolation: isolate; + border-color: var(--nav-tabs-link-hover-border-color); + border-bottom-color: var(--nav-tabs-border-color); + } + } + .nav-link.active, + .nav-item.show .nav-link { + color: var(--nav-tabs-link-active-color); + background-color: var(--nav-tabs-link-active-bg); + border-color: var(--nav-tabs-link-active-border-color); + border-bottom-color: var(--nav-tabs-link-active-bg); + } -// -// Underline -// + .menu { + margin-top: calc(-1 * var(--nav-tabs-border-width)); + @include border-top-radius(0); + } + } -.nav-underline { - // scss-docs-start nav-underline-css-vars - --#{$prefix}nav-underline-gap: #{$nav-underline-gap}; - --#{$prefix}nav-underline-border-width: #{$nav-underline-border-width}; - --#{$prefix}nav-underline-link-active-color: #{$nav-underline-link-active-color}; - // scss-docs-end nav-underline-css-vars + // + // Pills + // - gap: var(--#{$prefix}nav-underline-gap); + .nav-pills { + @include tokens($nav-pills-tokens); - .nav-link { - padding-right: 0; - padding-left: 0; - border-bottom: var(--#{$prefix}nav-underline-border-width) solid transparent; + display: inline-flex; + padding: var(--nav-pills-padding); + background-color: var(--nav-pills-bg); + @include border-radius(var(--nav-pills-border-radius)); - &:hover, - &:focus { - border-bottom-color: currentcolor; + .nav-link { + @include border-radius(var(--nav-pills-link-border-radius)); } - } - .nav-link.active, - .show > .nav-link { - font-weight: $font-weight-bold; - color: var(--#{$prefix}nav-underline-link-active-color); - border-bottom-color: currentcolor; + .nav-link.active, + .show > .nav-link { + color: var(--nav-pills-link-active-color); + @include gradient-bg(var(--nav-pills-link-active-bg)); + } } -} + .nav-pills-vertical { + flex-direction: column; + align-items: stretch; -// -// Justified variants -// + .nav-item, + .nav-link { + width: 100%; + } + } -.nav-fill { - > .nav-link, - .nav-item { - flex: 1 1 auto; - text-align: center; + // + // Underline + // + + .nav-underline { + // scss-docs-start nav-underline-css-vars + @include tokens($nav-underline-tokens); + // scss-docs-end nav-underline-css-vars + + .nav-link { + padding-inline: 0; + border: 0; + border-block-end: var(--nav-underline-border-width) solid transparent; + @include border-radius(0); + + &:hover, + &:focus { + border-block-end-color: currentcolor; + } + } + + .nav-link.active, + .show > .nav-link { + font-weight: $font-weight-bold; + color: var(--nav-underline-link-active-color); + border-block-end-color: currentcolor; + } } -} -.nav-justified { - > .nav-link, - .nav-item { - flex-grow: 1; - flex-basis: 0; - text-align: center; + // + // Justified variants + // + + .nav-fill { + > .nav-link, + .nav-item { + flex: 1 1 auto; + text-align: center; + } } -} -.nav-fill, -.nav-justified { - .nav-item .nav-link { - width: 100%; // Make sure button will grow + .nav-justified { + > .nav-link, + .nav-item { + flex-grow: 1; + flex-basis: 0; + text-align: center; + } } -} + .nav-fill, + .nav-justified { + .nav-item .nav-link { + width: 100%; // Make sure button will grow + } + } -// Tabbable tabs -// -// Hide tabbable panes to start, show them when `.active` + // Tabbable tabs + // + // Hide tabbable panes to start, show them when `.active` -.tab-content { - > .tab-pane { - display: none; - } - > .active { - display: block; + .tab-content { + > .tab-pane { + display: none; + } + > .active { + display: block; + } } } diff --git a/assets/stylesheets/bootstrap/_navbar.scss b/assets/stylesheets/bootstrap/_navbar.scss index 86aa441e..3dc4bebf 100644 --- a/assets/stylesheets/bootstrap/_navbar.scss +++ b/assets/stylesheets/bootstrap/_navbar.scss @@ -1,289 +1,312 @@ -// Navbar -// -// Provide a static navbar from which we expand to create full-width, fixed, and -// other navbar variations. - -.navbar { - // scss-docs-start navbar-css-vars - --#{$prefix}navbar-padding-x: #{if($navbar-padding-x == null, 0, $navbar-padding-x)}; - --#{$prefix}navbar-padding-y: #{$navbar-padding-y}; - --#{$prefix}navbar-color: #{$navbar-light-color}; - --#{$prefix}navbar-hover-color: #{$navbar-light-hover-color}; - --#{$prefix}navbar-disabled-color: #{$navbar-light-disabled-color}; - --#{$prefix}navbar-active-color: #{$navbar-light-active-color}; - --#{$prefix}navbar-brand-padding-y: #{$navbar-brand-padding-y}; - --#{$prefix}navbar-brand-margin-end: #{$navbar-brand-margin-end}; - --#{$prefix}navbar-brand-font-size: #{$navbar-brand-font-size}; - --#{$prefix}navbar-brand-color: #{$navbar-light-brand-color}; - --#{$prefix}navbar-brand-hover-color: #{$navbar-light-brand-hover-color}; - --#{$prefix}navbar-nav-link-padding-x: #{$navbar-nav-link-padding-x}; - --#{$prefix}navbar-toggler-padding-y: #{$navbar-toggler-padding-y}; - --#{$prefix}navbar-toggler-padding-x: #{$navbar-toggler-padding-x}; - --#{$prefix}navbar-toggler-font-size: #{$navbar-toggler-font-size}; - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-light-toggler-icon-bg)}; - --#{$prefix}navbar-toggler-border-color: #{$navbar-light-toggler-border-color}; - --#{$prefix}navbar-toggler-border-radius: #{$navbar-toggler-border-radius}; - --#{$prefix}navbar-toggler-focus-width: #{$navbar-toggler-focus-width}; - --#{$prefix}navbar-toggler-transition: #{$navbar-toggler-transition}; - // scss-docs-end navbar-css-vars - - position: relative; - display: flex; - flex-wrap: wrap; // allow us to do the line break for collapsing content - align-items: center; - justify-content: space-between; // space out brand from logo - padding: var(--#{$prefix}navbar-padding-y) var(--#{$prefix}navbar-padding-x); - @include gradient-bg(); - - // Because flex properties aren't inherited, we need to redeclare these first - // few properties so that content nested within behave properly. - // The `flex-wrap` property is inherited to simplify the expanded navbars - %container-flex-properties { +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/box-shadow" as *; +@use "mixins/mask-icon" as *; +@use "mixins/tokens" as *; +@use "mixins/transition" as *; + +// mdo-do: fix nav-link-height and navbar-brand-height, which we previously calculated with font-size, line-height, and block padding + +// stylelint-disable custom-property-no-missing-var-function +// scss-docs-start navbar-breakpoints +$navbar-breakpoints: $breakpoints !default; +// scss-docs-end navbar-breakpoints + +$navbar-tokens: () !default; +$navbar-dark-tokens: () !default; +$navbar-nav-tokens: () !default; + +// scss-docs-start navbar-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-tokens: defaults( + ( + --navbar-padding-x: 0, + --navbar-padding-y: .5rem, + --navbar-color: var(--fg-2), + --navbar-hover-color: var(--fg-1), + --navbar-disabled-color: var(--fg-3), + --navbar-active-color: var(--fg-body), + --navbar-brand-padding-y: .75rem, + --navbar-brand-margin-end: 1rem, + --navbar-brand-font-size: var(--font-size-md), + --navbar-brand-font-weight: var(--font-weight-medium), + --navbar-brand-color: var(--fg-body), + --navbar-brand-hover-color: var(--fg-body), + --navbar-nav-link-padding-x: .75rem, + --navbar-toggler-width: 2rem, + --navbar-toggler-padding-y: .25rem, + --navbar-toggler-padding-x: .75rem, + --navbar-toggler-font-size: var(--font-size-lg), + --navbar-toggler-border-color: color-mix(in oklch, var(--fg-body) 15%, transparent), + --navbar-toggler-border-radius: var(--radius-5), + --navbar-toggler-transition: box-shadow .15s ease-in-out, + --navbar-toggler-icon-size: 1.25rem, + --navbar-toggler-icon: #{escape-svg(url("data:image/svg+xml,"))}, + ), + $navbar-tokens +); +// scss-docs-end navbar-tokens + +// scss-docs-start navbar-dark-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-dark-tokens: defaults( + ( + --navbar-color: color-mix(in oklch, var(--white) .55, transparent), + --navbar-hover-color: color-mix(in oklch, var(--white) .75, transparent), + --navbar-disabled-color: color-mix(in oklch, var(--white) .25, transparent), + --navbar-active-color: var(--white), + --navbar-brand-color: var(--white), + --navbar-brand-hover-color: var(--white), + --navbar-toggler-border-color: color-mix(in oklch, var(--white) .1, transparent), + ), + $navbar-dark-tokens +); +// scss-docs-end navbar-dark-tokens + +// scss-docs-start navbar-nav-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$navbar-nav-tokens: defaults( + ( + --nav-gap: .25rem, + --nav-link-gap: .5rem, + --nav-link-padding-x: .5rem, + --nav-link-padding-y: .375rem, + --nav-link-color: var(--navbar-color), + --nav-link-border-width: var(--border-width), + //--nav-link-border-color: var(--border-color), + --nav-link-hover-color: var(--navbar-hover-color), + --nav-link-hover-bg: transparent, + --nav-link-active-color: var(--navbar-active-color), + --nav-link-active-bg: transparent, + --nav-link-disabled-color: var(--navbar-disabled-color), + ), + $navbar-nav-tokens +); +// scss-docs-end navbar-nav-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + // Base navbar + .navbar { + @include tokens($navbar-tokens); + + position: relative; display: flex; - flex-wrap: inherit; + flex-wrap: wrap; align-items: center; justify-content: space-between; - } - - > .container, - > .container-fluid { - @extend %container-flex-properties; - } + padding: var(--navbar-padding-y) var(--navbar-padding-x); + @include set-container(); + color: var(--navbar-color, var(--fg-body)); + background-color: var(--navbar-bg, var(--bg-body)); + // @include gradient-bg(var(--navbar-bg, var(--bg-body))); + + // Container properties for nested containers + %container-flex-properties { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; + } - @each $breakpoint, $container-max-width in $container-max-widths { - > .container#{breakpoint-infix($breakpoint, $container-max-widths)} { + > .container, + > .container-fluid { @extend %container-flex-properties; } - } -} - -// Navbar brand -// -// Used for brand, project, or site names. + @each $breakpoint, $container-max-width in $container-max-widths { + > .#{breakpoint-prefix($breakpoint, $container-max-widths)}container { + @extend %container-flex-properties; + } + } + } -.navbar-brand { - padding-top: var(--#{$prefix}navbar-brand-padding-y); - padding-bottom: var(--#{$prefix}navbar-brand-padding-y); - margin-right: var(--#{$prefix}navbar-brand-margin-end); - @include font-size(var(--#{$prefix}navbar-brand-font-size)); - color: var(--#{$prefix}navbar-brand-color); - text-decoration: if($link-decoration == none, null, none); - white-space: nowrap; + // Navbar brand + // + // Used for brand, project, or site names. + .navbar-brand { + padding-top: var(--navbar-brand-padding-y); + padding-bottom: var(--navbar-brand-padding-y); + margin-inline-end: var(--navbar-brand-margin-end); + font-size: var(--navbar-brand-font-size); + font-weight: var(--navbar-brand-font-weight); + color: var(--navbar-brand-color); + text-decoration: none; + white-space: nowrap; - &:hover, - &:focus { - color: var(--#{$prefix}navbar-brand-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); + &:hover, + &:focus { + color: var(--navbar-brand-hover-color); + } } -} + // Navigation within navbars. Sets all nav-link CSS variables needed for + // proper styling. + // + // Relies on `.nav` base class. + .navbar-nav { + @include tokens($navbar-nav-tokens); -// Navbar nav -// -// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`). - -.navbar-nav { - // scss-docs-start navbar-nav-css-vars - --#{$prefix}nav-link-padding-x: 0; - --#{$prefix}nav-link-padding-y: #{$nav-link-padding-y}; - @include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size); - --#{$prefix}nav-link-font-weight: #{$nav-link-font-weight}; - --#{$prefix}nav-link-color: var(--#{$prefix}navbar-color); - --#{$prefix}nav-link-hover-color: var(--#{$prefix}navbar-hover-color); - --#{$prefix}nav-link-disabled-color: var(--#{$prefix}navbar-disabled-color); - // scss-docs-end navbar-nav-css-vars - - display: flex; - flex-direction: column; // cannot use `inherit` to get the `.navbar`s value - padding-left: 0; - margin-bottom: 0; - list-style: none; - - .nav-link { - &.active, - &.show { - color: var(--#{$prefix}navbar-active-color); + display: flex; + flex-direction: column; + gap: var(--nav-gap); + padding-inline-start: 0; + margin-bottom: 0; + list-style-type: ""; + + .nav-link { + &.active, + &.show { + color: var(--navbar-active-color); + border: var(--nav-link-border-width) solid var(--nav-link-border-color, transparent); + } } } - .dropdown-menu { - position: static; + // Navbar text + // + // For adding text or inline elements to the navbar + .navbar-text { + padding-top: var(--navbar-brand-padding-y); + padding-bottom: var(--navbar-brand-padding-y); + color: var(--navbar-color); + + a, + a:hover, + a:focus { + color: var(--navbar-active-color); + } } -} - - -// Navbar text -// -// - -.navbar-text { - padding-top: $nav-link-padding-y; - padding-bottom: $nav-link-padding-y; - color: var(--#{$prefix}navbar-color); - a, - a:hover, - a:focus { - color: var(--#{$prefix}navbar-active-color); + // Button for toggling the navbar when in its collapsed state + .navbar-toggler { + --btn-bg: transparent; + --btn-hover-bg: var(--bg-2); } -} - - -// Responsive navbar -// -// Custom styles for responsive collapsing and toggling of navbar contents. -// Powered by the collapse Bootstrap JavaScript plugin. - -// When collapsed, prevent the toggleable navbar contents from appearing in -// the default flexbox row orientation. Requires the use of `flex-wrap: wrap` -// on the `.navbar` parent. -.navbar-collapse { - flex-grow: 1; - flex-basis: 100%; - // For always expanded or extra full navbars, ensure content aligns itself - // properly vertically. Can be easily overridden with flex utilities. - align-items: center; -} -// Button for toggling the navbar when in its collapsed state -.navbar-toggler { - padding: var(--#{$prefix}navbar-toggler-padding-y) var(--#{$prefix}navbar-toggler-padding-x); - @include font-size(var(--#{$prefix}navbar-toggler-font-size)); - line-height: 1; - color: var(--#{$prefix}navbar-color); - background-color: transparent; // remove default button style - border: var(--#{$prefix}border-width) solid var(--#{$prefix}navbar-toggler-border-color); // remove default button style - @include border-radius(var(--#{$prefix}navbar-toggler-border-radius)); - @include transition(var(--#{$prefix}navbar-toggler-transition)); - - &:hover { - text-decoration: none; + // Hamburger icon, rendered via CSS mask so it inherits the navbar color + .navbar-toggler-icon { + display: inline-block; + width: var(--navbar-toggler-icon-size); + height: var(--navbar-toggler-icon-size); + background-color: currentcolor; + @include mask-icon(var(--navbar-toggler-icon)); } - &:focus { - text-decoration: none; - outline: 0; - box-shadow: 0 0 0 var(--#{$prefix}navbar-toggler-focus-width); - } -} + // scss-docs-start navbar-expand-loop + // Generate series of responsive `.navbar-expand` classes for configuring + // where your navbar collapses and expands. Uses container queries so the + // navbar responds to its own width, not the viewport width. + + // Mixin for expanded state styles (applied to descendants) + @mixin navbar-expanded { + // Style the inner container since we can't style .navbar itself with container queries + > .container, + > .container-fluid, + %navbar-expand-container { + flex-wrap: nowrap; + justify-content: flex-start; + } -// Keep as a separate element so folks can easily override it with another icon -// or image file as needed. -.navbar-toggler-icon { - display: inline-block; - width: 1.5em; - height: 1.5em; - vertical-align: middle; - background-image: var(--#{$prefix}navbar-toggler-icon-bg); - background-repeat: no-repeat; - background-position: center; - background-size: 100%; -} + .navbar-nav { + --nav-link-padding-x: var(--navbar-nav-link-padding-x); + flex-direction: row; + } -.navbar-nav-scroll { - max-height: var(--#{$prefix}scroll-height, 75vh); - overflow-y: auto; -} + .navbar-toggler { + display: none !important; // stylelint-disable-line declaration-no-important + } -// scss-docs-start navbar-expand-loop -// Generate series of `.navbar-expand-*` responsive classes for configuring -// where your navbar collapses. -.navbar-expand { - @each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - // stylelint-disable-next-line scss/selector-no-union-class-name - {$infix} { - @include media-breakpoint-up($next) { - flex-wrap: nowrap; - justify-content: flex-start; - - .navbar-nav { - flex-direction: row; - - .dropdown-menu { - position: absolute; - } - - .nav-link { - padding-right: var(--#{$prefix}navbar-nav-link-padding-x); - padding-left: var(--#{$prefix}navbar-nav-link-padding-x); - } - } + [class*="drawer"] { + // stylelint-disable declaration-no-important + // Reset native UA styles and below-breakpoint drawer styles. + // Must use !important to override both UA defaults and the + // responsive drawer styles from media-breakpoint-down(). + position: static !important; + inset: auto !important; + z-index: auto; + display: flex !important; + flex-grow: 1; + width: auto !important; + max-width: none !important; + height: auto !important; + max-height: none !important; + padding: 0; + margin: 0; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + @include box-shadow(none); + @include transition(none); + // stylelint-enable declaration-no-important + + .drawer-header { + display: none !important; // stylelint-disable-line declaration-no-important + } - .navbar-nav-scroll { - overflow: visible; - } + .drawer-body { + display: flex; + flex-grow: 1; + flex-direction: row; + align-items: center; + padding: 0; + overflow-y: visible; + } + } + } - .navbar-collapse { - display: flex !important; // stylelint-disable-line declaration-no-important - flex-basis: auto; - } + // Always expanded (no responsive behavior) + .navbar-expand { + @include navbar-expanded(); - .navbar-toggler { - display: none; - } + // Also set on navbar itself for non-responsive case + flex-wrap: nowrap; + justify-content: flex-start; + } - .offcanvas { - // stylelint-disable declaration-no-important - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - @include box-shadow(none); - @include transition(none); - // stylelint-enable declaration-no-important - - .offcanvas-header { - display: none; - } - - .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } + // Responsive navbar expand classes using container queries + @include loop-breakpoints-down($navbar-breakpoints) using ($breakpoint, $next, $prefix) { + @if $next { + .#{$prefix}navbar-expand { + @include container-breakpoint-up($next) { + @include navbar-expanded(); } } } } -} -// scss-docs-end navbar-expand-loop - -// Navbar themes -// -// Styles for switching between navbars with light or dark background. - -.navbar-light { - @include deprecate("`.navbar-light`", "v5.2.0", "v6.0.0", true); -} - -.navbar-dark, -.navbar[data-bs-theme="dark"] { - // scss-docs-start navbar-dark-css-vars - --#{$prefix}navbar-color: #{$navbar-dark-color}; - --#{$prefix}navbar-hover-color: #{$navbar-dark-hover-color}; - --#{$prefix}navbar-disabled-color: #{$navbar-dark-disabled-color}; - --#{$prefix}navbar-active-color: #{$navbar-dark-active-color}; - --#{$prefix}navbar-brand-color: #{$navbar-dark-brand-color}; - --#{$prefix}navbar-brand-hover-color: #{$navbar-dark-brand-hover-color}; - --#{$prefix}navbar-toggler-border-color: #{$navbar-dark-toggler-border-color}; - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; - // scss-docs-end navbar-dark-css-vars -} + // scss-docs-end navbar-expand-loop + + // Prevent drawer flash on breakpoint crossing. + // When the navbar crosses from expanded (inline) to collapsed (drawer), + // the drawer transitions from visibility:visible to visibility:hidden. + // Without this override, the slide transition plays — briefly showing the + // panel sliding away. Disabling transitions when not [open] ensures only + // intentional show/hide actions animate. + // stylelint-disable-next-line no-duplicate-selectors + .navbar { + [class*="drawer"]:not([open], .hiding) { + @include transition(none !important); + } + } -@if $enable-dark-mode { - @include color-mode(dark) { - .navbar-toggler-icon { - --#{$prefix}navbar-toggler-icon-bg: #{escape-svg($navbar-dark-toggler-icon-bg)}; + .navbar-translucent { + position: relative; + background-color: transparent; + + &::before { + position: absolute; + inset: 0; + z-index: -1; + content: ""; + background-color: color-mix(in oklch, var(--navbar-bg, var(--bg-body)) 80%, transparent); + background-image: none; + backdrop-filter: blur(5px) saturate(180%); } } + + .navbar[data-bs-theme="dark"] { + @include tokens($navbar-dark-tokens); + } } diff --git a/assets/stylesheets/bootstrap/_offcanvas.scss b/assets/stylesheets/bootstrap/_offcanvas.scss deleted file mode 100644 index b40b2cd9..00000000 --- a/assets/stylesheets/bootstrap/_offcanvas.scss +++ /dev/null @@ -1,147 +0,0 @@ -// stylelint-disable function-disallowed-list - -%offcanvas-css-vars { - // scss-docs-start offcanvas-css-vars - --#{$prefix}offcanvas-zindex: #{$zindex-offcanvas}; - --#{$prefix}offcanvas-width: #{$offcanvas-horizontal-width}; - --#{$prefix}offcanvas-height: #{$offcanvas-vertical-height}; - --#{$prefix}offcanvas-padding-x: #{$offcanvas-padding-x}; - --#{$prefix}offcanvas-padding-y: #{$offcanvas-padding-y}; - --#{$prefix}offcanvas-color: #{$offcanvas-color}; - --#{$prefix}offcanvas-bg: #{$offcanvas-bg-color}; - --#{$prefix}offcanvas-border-width: #{$offcanvas-border-width}; - --#{$prefix}offcanvas-border-color: #{$offcanvas-border-color}; - --#{$prefix}offcanvas-box-shadow: #{$offcanvas-box-shadow}; - --#{$prefix}offcanvas-transition: #{transform $offcanvas-transition-duration ease-in-out}; - --#{$prefix}offcanvas-title-line-height: #{$offcanvas-title-line-height}; - // scss-docs-end offcanvas-css-vars -} - -@each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - .offcanvas#{$infix} { - @extend %offcanvas-css-vars; - } -} - -@each $breakpoint in map-keys($grid-breakpoints) { - $next: breakpoint-next($breakpoint, $grid-breakpoints); - $infix: breakpoint-infix($next, $grid-breakpoints); - - .offcanvas#{$infix} { - @include media-breakpoint-down($next) { - position: fixed; - bottom: 0; - z-index: var(--#{$prefix}offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--#{$prefix}offcanvas-color); - visibility: hidden; - background-color: var(--#{$prefix}offcanvas-bg); - background-clip: padding-box; - outline: 0; - @include box-shadow(var(--#{$prefix}offcanvas-box-shadow)); - @include transition(var(--#{$prefix}offcanvas-transition)); - - &.offcanvas-start { - top: 0; - left: 0; - width: var(--#{$prefix}offcanvas-width); - border-right: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateX(-100%); - } - - &.offcanvas-end { - top: 0; - right: 0; - width: var(--#{$prefix}offcanvas-width); - border-left: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateX(100%); - } - - &.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--#{$prefix}offcanvas-height); - max-height: 100%; - border-bottom: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateY(-100%); - } - - &.offcanvas-bottom { - right: 0; - left: 0; - height: var(--#{$prefix}offcanvas-height); - max-height: 100%; - border-top: var(--#{$prefix}offcanvas-border-width) solid var(--#{$prefix}offcanvas-border-color); - transform: translateY(100%); - } - - &.showing, - &.show:not(.hiding) { - transform: none; - } - - &.showing, - &.hiding, - &.show { - visibility: visible; - } - } - - @if not ($infix == "") { - @include media-breakpoint-up($next) { - --#{$prefix}offcanvas-height: auto; - --#{$prefix}offcanvas-border-width: 0; - background-color: transparent !important; // stylelint-disable-line declaration-no-important - - .offcanvas-header { - display: none; - } - - .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - // Reset `background-color` in case `.bg-*` classes are used in offcanvas - background-color: transparent !important; // stylelint-disable-line declaration-no-important - } - } - } - } -} - -.offcanvas-backdrop { - @include overlay-backdrop($zindex-offcanvas-backdrop, $offcanvas-backdrop-bg, $offcanvas-backdrop-opacity); -} - -.offcanvas-header { - display: flex; - align-items: center; - padding: var(--#{$prefix}offcanvas-padding-y) var(--#{$prefix}offcanvas-padding-x); - - .btn-close { - padding: calc(var(--#{$prefix}offcanvas-padding-y) * .5) calc(var(--#{$prefix}offcanvas-padding-x) * .5); - // Split properties to avoid invalid calc() function if value is 0 - margin-top: calc(-.5 * var(--#{$prefix}offcanvas-padding-y)); - margin-right: calc(-.5 * var(--#{$prefix}offcanvas-padding-x)); - margin-bottom: calc(-.5 * var(--#{$prefix}offcanvas-padding-y)); - margin-left: auto; - } -} - -.offcanvas-title { - margin-bottom: 0; - line-height: var(--#{$prefix}offcanvas-title-line-height); -} - -.offcanvas-body { - flex-grow: 1; - padding: var(--#{$prefix}offcanvas-padding-y) var(--#{$prefix}offcanvas-padding-x); - overflow-y: auto; -} diff --git a/assets/stylesheets/bootstrap/_pagination.scss b/assets/stylesheets/bootstrap/_pagination.scss index 9f09694c..9ddeed2a 100644 --- a/assets/stylesheets/bootstrap/_pagination.scss +++ b/assets/stylesheets/bootstrap/_pagination.scss @@ -1,109 +1,139 @@ -.pagination { - // scss-docs-start pagination-css-vars - --#{$prefix}pagination-padding-x: #{$pagination-padding-x}; - --#{$prefix}pagination-padding-y: #{$pagination-padding-y}; - @include rfs($pagination-font-size, --#{$prefix}pagination-font-size); - --#{$prefix}pagination-color: #{$pagination-color}; - --#{$prefix}pagination-bg: #{$pagination-bg}; - --#{$prefix}pagination-border-width: #{$pagination-border-width}; - --#{$prefix}pagination-border-color: #{$pagination-border-color}; - --#{$prefix}pagination-border-radius: #{$pagination-border-radius}; - --#{$prefix}pagination-hover-color: #{$pagination-hover-color}; - --#{$prefix}pagination-hover-bg: #{$pagination-hover-bg}; - --#{$prefix}pagination-hover-border-color: #{$pagination-hover-border-color}; - --#{$prefix}pagination-focus-color: #{$pagination-focus-color}; - --#{$prefix}pagination-focus-bg: #{$pagination-focus-bg}; - --#{$prefix}pagination-focus-box-shadow: #{$pagination-focus-box-shadow}; - --#{$prefix}pagination-active-color: #{$pagination-active-color}; - --#{$prefix}pagination-active-bg: #{$pagination-active-bg}; - --#{$prefix}pagination-active-border-color: #{$pagination-active-border-color}; - --#{$prefix}pagination-disabled-color: #{$pagination-disabled-color}; - --#{$prefix}pagination-disabled-bg: #{$pagination-disabled-bg}; - --#{$prefix}pagination-disabled-border-color: #{$pagination-disabled-border-color}; - // scss-docs-end pagination-css-vars - - display: flex; - @include list-unstyled(); -} +@use "functions" as *; +@use "mixins/lists" as *; +@use "mixins/border-radius" as *; +@use "mixins/focus-ring" as *; +@use "mixins/gradients" as *; +@use "mixins/transition" as *; +@use "mixins/tokens" as *; -.page-link { - position: relative; - display: block; - padding: var(--#{$prefix}pagination-padding-y) var(--#{$prefix}pagination-padding-x); - @include font-size(var(--#{$prefix}pagination-font-size)); - color: var(--#{$prefix}pagination-color); - text-decoration: if($link-decoration == none, null, none); - background-color: var(--#{$prefix}pagination-bg); - border: var(--#{$prefix}pagination-border-width) solid var(--#{$prefix}pagination-border-color); - @include transition($pagination-transition); - - &:hover { - z-index: 2; - color: var(--#{$prefix}pagination-hover-color); - text-decoration: if($link-hover-decoration == underline, none, null); - background-color: var(--#{$prefix}pagination-hover-bg); - border-color: var(--#{$prefix}pagination-hover-border-color); - } +// mdo-do: Update pagination to support variant themes - &:focus { - z-index: 3; - color: var(--#{$prefix}pagination-focus-color); - background-color: var(--#{$prefix}pagination-focus-bg); - outline: $pagination-focus-outline; - box-shadow: var(--#{$prefix}pagination-focus-box-shadow); - } +// stylelint-disable custom-property-no-missing-var-function +$pagination-tokens: () !default; - &.active, - .active > & { - z-index: 3; - color: var(--#{$prefix}pagination-active-color); - @include gradient-bg(var(--#{$prefix}pagination-active-bg)); - border-color: var(--#{$prefix}pagination-active-border-color); - } +// scss-docs-start pagination-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$pagination-tokens: defaults( + ( + --pagination-min-height: var(--btn-input-min-height), + --pagination-padding-x: var(--btn-input-padding-x), + --pagination-padding-y: var(--btn-input-padding-y), + --pagination-font-size: var(--btn-input-font-size), + --pagination-color: var(--link-color), + --pagination-bg: var(--bg-body), + --pagination-border-width: var(--border-width), + --pagination-border-color: var(--border-color), + --pagination-border-radius: var(--btn-input-border-radius), + --pagination-hover-color: var(--link-hover-color), + --pagination-hover-bg: var(--bg-1), + --pagination-hover-border-color: var(--border-color), + --pagination-focus-color: var(--link-hover-color), + --pagination-focus-bg: var(--bg-2), + --pagination-active-color: var(--primary-contrast), + --pagination-active-bg: var(--primary-bg), + --pagination-active-border-color: var(--primary-bg), + --pagination-disabled-color: var(--fg-3), + --pagination-disabled-bg: var(--bg-2), + --pagination-disabled-border-color: var(--border-color), + ), + $pagination-tokens +); +// scss-docs-end pagination-tokens + +// scss-docs-start pagination-sizes +$pagination-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$pagination-sizes: defaults( + ("sm", "lg"), + $pagination-sizes +); +// scss-docs-end pagination-sizes +// stylelint-enable custom-property-no-missing-var-function + +@layer components { + .pagination { + @include tokens($pagination-tokens); - &.disabled, - .disabled > & { - color: var(--#{$prefix}pagination-disabled-color); - pointer-events: none; - background-color: var(--#{$prefix}pagination-disabled-bg); - border-color: var(--#{$prefix}pagination-disabled-border-color); + display: flex; + @include list-unstyled(); } -} -.page-item { - &:not(:first-child) .page-link { - margin-left: $pagination-margin-start; + .page-link { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: var(--pagination-min-height); + padding: var(--pagination-padding-y) var(--pagination-padding-x); + font-size: var(--pagination-font-size); + color: var(--pagination-color); + text-decoration: none; + background-color: var(--pagination-bg); + border: var(--pagination-border-width) solid var(--pagination-border-color); + @include transition(color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out); + + &:hover { + z-index: 2; + color: var(--pagination-hover-color); + background-color: var(--pagination-hover-bg); + border-color: var(--pagination-hover-border-color); + } + + &:focus-visible { + z-index: 3; + color: var(--pagination-focus-color); + background-color: var(--pagination-focus-bg); + @include focus-ring(true); + } + + &.active, + .active > & { + z-index: 3; + color: var(--pagination-active-color); + @include gradient-bg(var(--pagination-active-bg)); + border-color: var(--pagination-active-border-color); + } + + &.disabled, + .disabled > & { + color: var(--pagination-disabled-color); + pointer-events: none; + background-color: var(--pagination-disabled-bg); + border-color: var(--pagination-disabled-border-color); + } } - @if $pagination-margin-start == calc(-1 * #{$pagination-border-width}) { + .page-item { + &:not(:first-child) .page-link { + margin-inline-start: calc(-1 * var(--pagination-border-width)); + } + &:first-child { .page-link { - @include border-start-radius(var(--#{$prefix}pagination-border-radius)); + @include border-start-radius(var(--pagination-border-radius)); } } &:last-child { .page-link { - @include border-end-radius(var(--#{$prefix}pagination-border-radius)); + @include border-end-radius(var(--pagination-border-radius)); } } - } @else { - // Add border-radius to all pageLinks in case they have left margin - .page-link { - @include border-radius(var(--#{$prefix}pagination-border-radius)); - } } -} - - -// -// Sizing -// -.pagination-lg { - @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $pagination-border-radius-lg); -} + // + // Sizing + // -.pagination-sm { - @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $pagination-border-radius-sm); + // scss-docs-start pagination-sizes-loop + @each $size, $_ in $pagination-sizes { + .pagination-#{$size} { + --pagination-min-height: var(--bs-btn-input-#{$size}-min-height); + --pagination-padding-y: var(--btn-input-#{$size}-padding-y); + --pagination-padding-x: var(--btn-input-#{$size}-padding-x); + --pagination-font-size: var(--btn-input-#{$size}-font-size); + --pagination-border-radius: var(--btn-input-#{$size}-border-radius); + } + } + // scss-docs-end pagination-sizes-loop } diff --git a/assets/stylesheets/bootstrap/_placeholder.scss b/assets/stylesheets/bootstrap/_placeholder.scss new file mode 100644 index 00000000..7242dcb0 --- /dev/null +++ b/assets/stylesheets/bootstrap/_placeholder.scss @@ -0,0 +1,72 @@ +@use "colors" as *; +@use "functions" as *; +@use "mixins/tokens" as *; + +$placeholder-tokens: () !default; + +// scss-docs-start placeholder-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$placeholder-tokens: defaults( + ( + --placeholder-opacity-max: .5, + --placeholder-opacity-min: .2, + ), + $placeholder-tokens +); +// scss-docs-end placeholder-tokens + +@layer components { + .placeholder { + @include tokens($placeholder-tokens); + + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentcolor; + opacity: var(--placeholder-opacity-max); + + &.btn::before { + display: inline-block; + content: ""; + } + } + + // Sizing + .placeholder-xs { + min-height: .6em; + } + + .placeholder-sm { + min-height: .8em; + } + + .placeholder-lg { + min-height: 1.2em; + } + + // Animation + .placeholder-glow { + .placeholder { + animation: placeholder-glow 2s ease-in-out infinite; + } + } + + @keyframes placeholder-glow { + 50% { + opacity: var(--placeholder-opacity-min); + } + } + + .placeholder-wave { + mask-image: linear-gradient(130deg, $black 55%, rgb(0 0 0 / calc(1 - var(--placeholder-opacity-min))) 75%, $black 95%); + mask-size: 200% 100%; + animation: placeholder-wave 2s linear infinite; + } + + @keyframes placeholder-wave { + 100% { + mask-position: -200% 0%; + } + } +} diff --git a/assets/stylesheets/bootstrap/_placeholders.scss b/assets/stylesheets/bootstrap/_placeholders.scss deleted file mode 100644 index 6e32e1cd..00000000 --- a/assets/stylesheets/bootstrap/_placeholders.scss +++ /dev/null @@ -1,51 +0,0 @@ -.placeholder { - display: inline-block; - min-height: 1em; - vertical-align: middle; - cursor: wait; - background-color: currentcolor; - opacity: $placeholder-opacity-max; - - &.btn::before { - display: inline-block; - content: ""; - } -} - -// Sizing -.placeholder-xs { - min-height: .6em; -} - -.placeholder-sm { - min-height: .8em; -} - -.placeholder-lg { - min-height: 1.2em; -} - -// Animation -.placeholder-glow { - .placeholder { - animation: placeholder-glow 2s ease-in-out infinite; - } -} - -@keyframes placeholder-glow { - 50% { - opacity: $placeholder-opacity-min; - } -} - -.placeholder-wave { - mask-image: linear-gradient(130deg, $black 55%, rgba(0, 0, 0, (1 - $placeholder-opacity-min)) 75%, $black 95%); - mask-size: 200% 100%; - animation: placeholder-wave 2s linear infinite; -} - -@keyframes placeholder-wave { - 100% { - mask-position: -200% 0%; - } -} diff --git a/assets/stylesheets/bootstrap/_popover.scss b/assets/stylesheets/bootstrap/_popover.scss index 7b69f623..0060ff3c 100644 --- a/assets/stylesheets/bootstrap/_popover.scss +++ b/assets/stylesheets/bootstrap/_popover.scss @@ -1,196 +1,217 @@ -.popover { - // scss-docs-start popover-css-vars - --#{$prefix}popover-zindex: #{$zindex-popover}; - --#{$prefix}popover-max-width: #{$popover-max-width}; - @include rfs($popover-font-size, --#{$prefix}popover-font-size); - --#{$prefix}popover-bg: #{$popover-bg}; - --#{$prefix}popover-border-width: #{$popover-border-width}; - --#{$prefix}popover-border-color: #{$popover-border-color}; - --#{$prefix}popover-border-radius: #{$popover-border-radius}; - --#{$prefix}popover-inner-border-radius: #{$popover-inner-border-radius}; - --#{$prefix}popover-box-shadow: #{$popover-box-shadow}; - --#{$prefix}popover-header-padding-x: #{$popover-header-padding-x}; - --#{$prefix}popover-header-padding-y: #{$popover-header-padding-y}; - @include rfs($popover-header-font-size, --#{$prefix}popover-header-font-size); - --#{$prefix}popover-header-color: #{$popover-header-color}; - --#{$prefix}popover-header-bg: #{$popover-header-bg}; - --#{$prefix}popover-body-padding-x: #{$popover-body-padding-x}; - --#{$prefix}popover-body-padding-y: #{$popover-body-padding-y}; - --#{$prefix}popover-body-color: #{$popover-body-color}; - --#{$prefix}popover-arrow-width: #{$popover-arrow-width}; - --#{$prefix}popover-arrow-height: #{$popover-arrow-height}; - --#{$prefix}popover-arrow-border: var(--#{$prefix}popover-border-color); - // scss-docs-end popover-css-vars - - z-index: var(--#{$prefix}popover-zindex); - display: block; - max-width: var(--#{$prefix}popover-max-width); - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - @include reset-text(); - @include font-size(var(--#{$prefix}popover-font-size)); - // Allow breaking very long words so they don't overflow the popover's bounds - word-wrap: break-word; - background-color: var(--#{$prefix}popover-bg); - background-clip: padding-box; - border: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-border-color); - @include border-radius(var(--#{$prefix}popover-border-radius)); - @include box-shadow(var(--#{$prefix}popover-box-shadow)); - - .popover-arrow { +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/reset-text" as *; +@use "mixins/tokens" as *; + +$popover-tokens: () !default; + +// scss-docs-start popover-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$popover-tokens: defaults( + ( + --popover-zindex: #{$zindex-popover}, + --popover-max-width: 280px, + --popover-font-size: var(--font-size-sm), + --popover-bg: var(--bg-body), + --popover-border-width: var(--border-width), + --popover-border-color: var(--border-color-translucent), + --popover-border-radius: var(--radius-7), + --popover-inner-border-radius: calc(var(--radius-7) - var(--border-width)), + --popover-box-shadow: var(--box-shadow), + --popover-header-padding-x: var(--spacer), + --popover-header-padding-y: var(--spacer-3), + --popover-header-font-size: var(--font-size-sm), + --popover-header-color: #{$headings-color}, + --popover-header-bg: var(--bg-1), + --popover-body-padding-x: var(--spacer), + --popover-body-padding-y: var(--spacer-3), + --popover-body-color: var(--fg-body), + --popover-arrow-width: 1rem, + --popover-arrow-height: .5rem, + --popover-arrow-border: var(--popover-border-color), + ), + $popover-tokens +); +// scss-docs-end popover-tokens + +@layer components { + .popover { + // scss-docs-start popover-css-vars + @include tokens($popover-tokens); + // scss-docs-end popover-css-vars + + z-index: var(--popover-zindex); display: block; - width: var(--#{$prefix}popover-arrow-width); - height: var(--#{$prefix}popover-arrow-height); - - &::before, - &::after { - position: absolute; + max-width: var(--popover-max-width); + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + @include reset-text(); + font-size: var(--popover-font-size); + // Allow breaking very long words so they don't overflow the popover's bounds + word-wrap: break-word; + background-color: var(--popover-bg); + background-clip: padding-box; + border: var(--popover-border-width) solid var(--popover-border-color); + @include border-radius(var(--popover-border-radius)); + @include box-shadow(var(--popover-box-shadow)); + + .popover-arrow { display: block; - content: ""; - border-color: transparent; - border-style: solid; - border-width: 0; + width: var(--popover-arrow-width); + height: var(--popover-arrow-height); + + &::before, + &::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; + border-width: 0; + } } } -} -.bs-popover-top { - > .popover-arrow { - bottom: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list + .bs-popover-top { + > .popover-arrow { + bottom: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); - &::before, - &::after { - border-width: var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - } + &::before, + &::after { + border-width: var(--popover-arrow-height) calc(var(--popover-arrow-width) * .5) 0; + } - &::before { - bottom: 0; - border-top-color: var(--#{$prefix}popover-arrow-border); - } + &::before { + bottom: 0; + border-block-start-color: var(--popover-arrow-border); + } - &::after { - bottom: var(--#{$prefix}popover-border-width); - border-top-color: var(--#{$prefix}popover-bg); + &::after { + bottom: var(--popover-border-width); + border-block-start-color: var(--popover-bg); + } } } -} - -/* rtl:begin:ignore */ -.bs-popover-end { - > .popover-arrow { - left: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}popover-arrow-height); - height: var(--#{$prefix}popover-arrow-width); - - &::before, - &::after { - border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - } - - &::before { - left: 0; - border-right-color: var(--#{$prefix}popover-arrow-border); - } - &::after { - left: var(--#{$prefix}popover-border-width); - border-right-color: var(--#{$prefix}popover-bg); + .bs-popover-end { + > .popover-arrow { + left: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + width: var(--popover-arrow-height); + height: var(--popover-arrow-width); + + &::before, + &::after { + border-width: calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height) calc(var(--popover-arrow-width) * .5) 0; + } + + &::before { + left: 0; + border-inline-end-color: var(--popover-arrow-border); + } + + &::after { + left: var(--popover-border-width); + border-inline-end-color: var(--popover-bg); + } } } -} - -/* rtl:end:ignore */ -.bs-popover-bottom { - > .popover-arrow { - top: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - - &::before, - &::after { - border-width: 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height); // stylelint-disable-line function-disallowed-list + .bs-popover-bottom { + > .popover-arrow { + top: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + + &::before, + &::after { + border-width: 0 calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height); + } + + &::before { + top: 0; + border-block-end-color: var(--popover-arrow-border); + } + + &::after { + top: var(--popover-border-width); + border-block-end-color: var(--popover-bg); + } + + // When the popover has a header, the bottom arrow points into the header, + // so its fill should match the header background, not the body background. + &:has(+ .popover-header)::after { + border-block-end-color: var(--popover-header-bg); + } } - &::before { + // This will remove the popover-header's border just below the arrow + .popover-header::before { + position: absolute; top: 0; - border-bottom-color: var(--#{$prefix}popover-arrow-border); - } - - &::after { - top: var(--#{$prefix}popover-border-width); - border-bottom-color: var(--#{$prefix}popover-bg); + left: 50%; + display: block; + width: var(--popover-arrow-width); + margin-inline-start: calc(-.5 * var(--popover-arrow-width)); + content: ""; + border-block-end: var(--popover-border-width) solid var(--popover-header-bg); } } - // This will remove the popover-header's border just below the arrow - .popover-header::before { - position: absolute; - top: 0; - left: 50%; - display: block; - width: var(--#{$prefix}popover-arrow-width); - margin-left: calc(-.5 * var(--#{$prefix}popover-arrow-width)); // stylelint-disable-line function-disallowed-list - content: ""; - border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-header-bg); + .bs-popover-start { + > .popover-arrow { + right: calc(-1 * (var(--popover-arrow-height)) - var(--popover-border-width)); + width: var(--popover-arrow-height); + height: var(--popover-arrow-width); + + &::before, + &::after { + border-width: calc(var(--popover-arrow-width) * .5) 0 calc(var(--popover-arrow-width) * .5) var(--popover-arrow-height); + } + + &::before { + right: 0; + border-inline-start-color: var(--popover-arrow-border); + } + + &::after { + right: var(--popover-border-width); + border-inline-start-color: var(--popover-bg); + } + } } -} -/* rtl:begin:ignore */ -.bs-popover-start { - > .popover-arrow { - right: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}popover-arrow-height); - height: var(--#{$prefix}popover-arrow-width); - - &::before, - &::after { - border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height); // stylelint-disable-line function-disallowed-list + .bs-popover-auto { + &[data-bs-placement^="top"] { + @extend .bs-popover-top; } - - &::before { - right: 0; - border-left-color: var(--#{$prefix}popover-arrow-border); + &[data-bs-placement^="right"] { + @extend .bs-popover-end; } - - &::after { - right: var(--#{$prefix}popover-border-width); - border-left-color: var(--#{$prefix}popover-bg); + &[data-bs-placement^="bottom"] { + @extend .bs-popover-bottom; + } + &[data-bs-placement^="left"] { + @extend .bs-popover-start; } } -} - -/* rtl:end:ignore */ -.bs-popover-auto { - &[data-popper-placement^="top"] { - @extend .bs-popover-top; - } - &[data-popper-placement^="right"] { - @extend .bs-popover-end; - } - &[data-popper-placement^="bottom"] { - @extend .bs-popover-bottom; - } - &[data-popper-placement^="left"] { - @extend .bs-popover-start; + // Offset the popover to account for the popover arrow + .popover-header { + padding: var(--popover-header-padding-y) var(--popover-header-padding-x); + margin-bottom: 0; // Reset the default from Reboot + font-size: var(--popover-header-font-size); + color: var(--popover-header-color); + background-color: var(--popover-header-bg); + border-block-end: var(--popover-border-width) solid var(--popover-border-color); + @include border-top-radius(var(--popover-inner-border-radius)); + + &:empty { + display: none; + } } -} -// Offset the popover to account for the popover arrow -.popover-header { - padding: var(--#{$prefix}popover-header-padding-y) var(--#{$prefix}popover-header-padding-x); - margin-bottom: 0; // Reset the default from Reboot - @include font-size(var(--#{$prefix}popover-header-font-size)); - color: var(--#{$prefix}popover-header-color); - background-color: var(--#{$prefix}popover-header-bg); - border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-border-color); - @include border-top-radius(var(--#{$prefix}popover-inner-border-radius)); - - &:empty { - display: none; + .popover-body { + padding: var(--popover-body-padding-y) var(--popover-body-padding-x); + color: var(--popover-body-color); } } - -.popover-body { - padding: var(--#{$prefix}popover-body-padding-y) var(--#{$prefix}popover-body-padding-x); - color: var(--#{$prefix}popover-body-color); -} diff --git a/assets/stylesheets/bootstrap/_progress.scss b/assets/stylesheets/bootstrap/_progress.scss index 732365c5..4d042e23 100644 --- a/assets/stylesheets/bootstrap/_progress.scss +++ b/assets/stylesheets/bootstrap/_progress.scss @@ -1,67 +1,88 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/transition" as *; +@use "mixins/gradients" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/tokens" as *; + +$progress-tokens: () !default; + +// scss-docs-start progress-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$progress-tokens: defaults( + ( + --progress-height: 1rem, + --progress-font-size: var(--font-size-sm), + --progress-bg: var(--bg-2), + --progress-border-radius: var(--radius-5), + --progress-box-shadow: var(--box-shadow-inset), + --progress-bar-color: var(--white), + --progress-bar-bg: var(--primary-bg), + --progress-bar-transition: width .6s ease, + --progress-bar-animation: progress-bar-stripes 1s linear infinite, + ), + $progress-tokens +); +// scss-docs-end progress-tokens + // Disable animation if transitions are disabled -// scss-docs-start progress-keyframes -@if $enable-transitions { - @keyframes progress-bar-stripes { - 0% { background-position-x: var(--#{$prefix}progress-height); } +@layer components { + // scss-docs-start progress-keyframes + @if $enable-transitions { + @keyframes progress-bar-stripes { + 0% { background-position-x: var(--progress-height); } + } } -} -// scss-docs-end progress-keyframes + // scss-docs-end progress-keyframes -.progress, -.progress-stacked { - // scss-docs-start progress-css-vars - --#{$prefix}progress-height: #{$progress-height}; - @include rfs($progress-font-size, --#{$prefix}progress-font-size); - --#{$prefix}progress-bg: #{$progress-bg}; - --#{$prefix}progress-border-radius: #{$progress-border-radius}; - --#{$prefix}progress-box-shadow: #{$progress-box-shadow}; - --#{$prefix}progress-bar-color: #{$progress-bar-color}; - --#{$prefix}progress-bar-bg: #{$progress-bar-bg}; - --#{$prefix}progress-bar-transition: #{$progress-bar-transition}; - // scss-docs-end progress-css-vars + .progress, + .progress-stacked { + @include tokens($progress-tokens); - display: flex; - height: var(--#{$prefix}progress-height); - overflow: hidden; // force rounded corners by cropping it - @include font-size(var(--#{$prefix}progress-font-size)); - background-color: var(--#{$prefix}progress-bg); - @include border-radius(var(--#{$prefix}progress-border-radius)); - @include box-shadow(var(--#{$prefix}progress-box-shadow)); -} + display: flex; + height: var(--progress-height); + overflow: hidden; + font-size: var(--progress-font-size); + background-color: var(--progress-bg); + @include border-radius(var(--progress-border-radius)); + @include box-shadow(var(--progress-box-shadow)); + } -.progress-bar { - display: flex; - flex-direction: column; - justify-content: center; - overflow: hidden; - color: var(--#{$prefix}progress-bar-color); - text-align: center; - white-space: nowrap; - background-color: var(--#{$prefix}progress-bar-bg); - @include transition(var(--#{$prefix}progress-bar-transition)); -} + .progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: var(--theme-contrast, var(--progress-bar-color)); + text-align: center; + white-space: nowrap; + background-color: var(--theme-bg, var(--progress-bar-bg)); + @include transition(var(--progress-bar-transition)); + } -.progress-bar-striped { - @include gradient-striped(); - background-size: var(--#{$prefix}progress-height) var(--#{$prefix}progress-height); -} + .progress-bar-striped { + @include gradient-striped(); + background-size: var(--progress-height) var(--progress-height); + } -.progress-stacked > .progress { - overflow: visible; -} + .progress-stacked > .progress { + overflow: visible; + } -.progress-stacked > .progress > .progress-bar { - width: 100%; -} + .progress-stacked > .progress > .progress-bar { + width: 100%; + } -@if $enable-transitions { - .progress-bar-animated { - animation: $progress-bar-animation-timing progress-bar-stripes; + @if $enable-transitions { + .progress-bar-animated { + animation: var(--progress-bar-animation); - @if $enable-reduced-motion { - @media (prefers-reduced-motion: reduce) { - animation: none; + @if $enable-reduced-motion { + @media (prefers-reduced-motion: reduce) { + animation: none; + } } } } diff --git a/assets/stylesheets/bootstrap/_reboot.scss b/assets/stylesheets/bootstrap/_reboot.scss deleted file mode 100644 index 524645fb..00000000 --- a/assets/stylesheets/bootstrap/_reboot.scss +++ /dev/null @@ -1,617 +0,0 @@ -// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix - - -// Reboot -// -// Normalization of HTML elements, manually forked from Normalize.css to remove -// styles targeting irrelevant browsers while applying new styles. -// -// Normalize is licensed MIT. https://github.com/necolas/normalize.css - - -// Document -// -// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. - -*, -*::before, -*::after { - box-sizing: border-box; -} - - -// Root -// -// Ability to the value of the root font sizes, affecting the value of `rem`. -// null by default, thus nothing is generated. - -:root { - @if $font-size-root != null { - @include font-size(var(--#{$prefix}root-font-size)); - } - - @if $enable-smooth-scroll { - @media (prefers-reduced-motion: no-preference) { - scroll-behavior: smooth; - } - } -} - - -// Body -// -// 1. Remove the margin in all browsers. -// 2. As a best practice, apply a default `background-color`. -// 3. Prevent adjustments of font size after orientation changes in iOS. -// 4. Change the default tap highlight to be completely transparent in iOS. - -// scss-docs-start reboot-body-rules -body { - margin: 0; // 1 - font-family: var(--#{$prefix}body-font-family); - @include font-size(var(--#{$prefix}body-font-size)); - font-weight: var(--#{$prefix}body-font-weight); - line-height: var(--#{$prefix}body-line-height); - color: var(--#{$prefix}body-color); - text-align: var(--#{$prefix}body-text-align); - background-color: var(--#{$prefix}body-bg); // 2 - -webkit-text-size-adjust: 100%; // 3 - -webkit-tap-highlight-color: rgba($black, 0); // 4 -} -// scss-docs-end reboot-body-rules - - -// Content grouping -// -// 1. Reset Firefox's gray color - -hr { - margin: $hr-margin-y 0; - color: $hr-color; // 1 - border: 0; - border-top: $hr-border-width solid $hr-border-color; - opacity: $hr-opacity; -} - - -// Typography -// -// 1. Remove top margins from headings -// By default, ``-`` all receive top and bottom margins. We nuke the top -// margin for easier control within type scales as it avoids margin collapsing. - -%heading { - margin-top: 0; // 1 - margin-bottom: $headings-margin-bottom; - font-family: $headings-font-family; - font-style: $headings-font-style; - font-weight: $headings-font-weight; - line-height: $headings-line-height; - color: var(--#{$prefix}heading-color); -} - -h1 { - @extend %heading; - @include font-size($h1-font-size); -} - -h2 { - @extend %heading; - @include font-size($h2-font-size); -} - -h3 { - @extend %heading; - @include font-size($h3-font-size); -} - -h4 { - @extend %heading; - @include font-size($h4-font-size); -} - -h5 { - @extend %heading; - @include font-size($h5-font-size); -} - -h6 { - @extend %heading; - @include font-size($h6-font-size); -} - - -// Reset margins on paragraphs -// -// Similarly, the top margin on ``s get reset. However, we also reset the -// bottom margin to use `rem` units instead of `em`. - -p { - margin-top: 0; - margin-bottom: $paragraph-margin-bottom; -} - - -// Abbreviations -// -// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. -// 2. Add explicit cursor to indicate changed behavior. -// 3. Prevent the text-decoration to be skipped. - -abbr[title] { - text-decoration: underline dotted; // 1 - cursor: help; // 2 - text-decoration-skip-ink: none; // 3 -} - - -// Address - -address { - margin-bottom: 1rem; - font-style: normal; - line-height: inherit; -} - - -// Lists - -ol, -ul { - padding-left: 2rem; -} - -ol, -ul, -dl { - margin-top: 0; - margin-bottom: 1rem; -} - -ol ol, -ul ul, -ol ul, -ul ol { - margin-bottom: 0; -} - -dt { - font-weight: $dt-font-weight; -} - -// 1. Undo browser default - -dd { - margin-bottom: .5rem; - margin-left: 0; // 1 -} - - -// Blockquote - -blockquote { - margin: 0 0 1rem; -} - - -// Strong -// -// Add the correct font weight in Chrome, Edge, and Safari - -b, -strong { - font-weight: $font-weight-bolder; -} - - -// Small -// -// Add the correct font size in all browsers - -small { - @include font-size($small-font-size); -} - - -// Mark - -mark { - padding: $mark-padding; - color: var(--#{$prefix}highlight-color); - background-color: var(--#{$prefix}highlight-bg); -} - - -// Sub and Sup -// -// Prevent `sub` and `sup` elements from affecting the line height in -// all browsers. - -sub, -sup { - position: relative; - @include font-size($sub-sup-font-size); - line-height: 0; - vertical-align: baseline; -} - -sub { bottom: -.25em; } -sup { top: -.5em; } - - -// Links - -a { - color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1)); - text-decoration: $link-decoration; - - &:hover { - --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb); - text-decoration: $link-hover-decoration; - } -} - -// And undo these styles for placeholder links/named anchors (without href). -// It would be more straightforward to just use a[href] in previous block, but that -// causes specificity issues in many other styles that are too complex to fix. -// See https://github.com/twbs/bootstrap/issues/19402 - -a:not([href]):not([class]) { - &, - &:hover { - color: inherit; - text-decoration: none; - } -} - - -// Code - -pre, -code, -kbd, -samp { - font-family: $font-family-code; - @include font-size(1em); // Correct the odd `em` font sizing in all browsers. -} - -// 1. Remove browser default top margin -// 2. Reset browser default of `1em` to use `rem`s -// 3. Don't allow content to break outside - -pre { - display: block; - margin-top: 0; // 1 - margin-bottom: 1rem; // 2 - overflow: auto; // 3 - @include font-size($code-font-size); - color: $pre-color; - - // Account for some code outputs that place code tags in pre tags - code { - @include font-size(inherit); - color: inherit; - word-break: normal; - } -} - -code { - @include font-size($code-font-size); - color: var(--#{$prefix}code-color); - word-wrap: break-word; - - // Streamline the style when inside anchors to avoid broken underline and more - a > & { - color: inherit; - } -} - -kbd { - padding: $kbd-padding-y $kbd-padding-x; - @include font-size($kbd-font-size); - color: $kbd-color; - background-color: $kbd-bg; - @include border-radius($border-radius-sm); - - kbd { - padding: 0; - @include font-size(1em); - font-weight: $nested-kbd-font-weight; - } -} - - -// Figures -// -// Apply a consistent margin strategy (matches our type styles). - -figure { - margin: 0 0 1rem; -} - - -// Images and content - -img, -svg { - vertical-align: middle; -} - - -// Tables -// -// Prevent double borders - -table { - caption-side: bottom; - border-collapse: collapse; -} - -caption { - padding-top: $table-cell-padding-y; - padding-bottom: $table-cell-padding-y; - color: $table-caption-color; - text-align: left; -} - -// 1. Removes font-weight bold by inheriting -// 2. Matches default `` alignment by inheriting `text-align`. -// 3. Fix alignment for Safari - -th { - font-weight: $table-th-font-weight; // 1 - text-align: inherit; // 2 - text-align: -webkit-match-parent; // 3 -} - -thead, -tbody, -tfoot, -tr, -td, -th { - border-color: inherit; - border-style: solid; - border-width: 0; -} - - -// Forms -// -// 1. Allow labels to use `margin` for spacing. - -label { - display: inline-block; // 1 -} - -// Remove the default `border-radius` that macOS Chrome adds. -// See https://github.com/twbs/bootstrap/issues/24093 - -button { - // stylelint-disable-next-line property-disallowed-list - border-radius: 0; -} - -// Explicitly remove focus outline in Chromium when it shouldn't be -// visible (e.g. as result of mouse click or touch tap). It already -// should be doing this automatically, but seems to currently be -// confused and applies its very visible two-tone outline anyway. - -button:focus:not(:focus-visible) { - outline: 0; -} - -// 1. Remove the margin in Firefox and Safari - -input, -button, -select, -optgroup, -textarea { - margin: 0; // 1 - font-family: inherit; - @include font-size(inherit); - line-height: inherit; -} - -// Remove the inheritance of text transform in Firefox -button, -select { - text-transform: none; -} -// Set the cursor for non-`` buttons -// -// Details at https://github.com/twbs/bootstrap/pull/30562 -[role="button"] { - cursor: pointer; -} - -select { - // Remove the inheritance of word-wrap in Safari. - // See https://github.com/twbs/bootstrap/issues/24990 - word-wrap: normal; - - // Undo the opacity change from Chrome - &:disabled { - opacity: 1; - } -} - -// Remove the dropdown arrow only from text type inputs built with datalists in Chrome. -// See https://stackoverflow.com/a/54997118 - -[list]:not([type="date"]):not([type="datetime-local"]):not([type="month"]):not([type="week"]):not([type="time"])::-webkit-calendar-picker-indicator { - display: none !important; -} - -// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` -// controls in Android 4. -// 2. Correct the inability to style clickable types in iOS and Safari. -// 3. Opinionated: add "hand" cursor to non-disabled button elements. - -button, -[type="button"], // 1 -[type="reset"], -[type="submit"] { - -webkit-appearance: button; // 2 - - @if $enable-button-pointers { - &:not(:disabled) { - cursor: pointer; // 3 - } - } -} - -// Remove inner border and padding from Firefox, but don't restore the outline like Normalize. - -::-moz-focus-inner { - padding: 0; - border-style: none; -} - -// 1. Textareas should really only resize vertically so they don't break their (horizontal) containers. - -textarea { - resize: vertical; // 1 -} - -// 1. Browsers set a default `min-width: min-content;` on fieldsets, -// unlike e.g. ``s, which have `min-width: 0;` by default. -// So we reset that to ensure fieldsets behave more like a standard block element. -// See https://github.com/twbs/bootstrap/issues/12359 -// and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements -// 2. Reset the default outline behavior of fieldsets so they don't affect page layout. - -fieldset { - min-width: 0; // 1 - padding: 0; // 2 - margin: 0; // 2 - border: 0; // 2 -} - -// 1. By using `float: left`, the legend will behave like a block element. -// This way the border of a fieldset wraps around the legend if present. -// 2. Fix wrapping bug. -// See https://github.com/twbs/bootstrap/issues/29712 - -legend { - float: left; // 1 - width: 100%; - padding: 0; - margin-bottom: $legend-margin-bottom; - font-weight: $legend-font-weight; - line-height: inherit; - @include font-size($legend-font-size); - - + * { - clear: left; // 2 - } -} - -// Fix height of inputs with a type of datetime-local, date, month, week, or time -// See https://github.com/twbs/bootstrap/issues/18842 - -::-webkit-datetime-edit-fields-wrapper, -::-webkit-datetime-edit-text, -::-webkit-datetime-edit-minute, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-year-field { - padding: 0; -} - -::-webkit-inner-spin-button { - height: auto; -} - -// 1. This overrides the extra rounded corners on search inputs in iOS so that our -// `.form-control` class can properly style them. Note that this cannot simply -// be added to `.form-control` as it's not specific enough. For details, see -// https://github.com/twbs/bootstrap/issues/11586. -// 2. Correct the outline style in Safari. - -[type="search"] { - -webkit-appearance: textfield; // 1 - outline-offset: -2px; // 2 - - // 3. Better affordance and consistent appearance for search cancel button - &::-webkit-search-cancel-button { - cursor: pointer; - filter: grayscale(1); - } -} - -// 1. A few input types should stay LTR -// See https://rtlstyling.com/posts/rtl-styling#form-inputs -// 2. RTL only output -// See https://rtlcss.com/learn/usage-guide/control-directives/#raw - -/* rtl:raw: -[type="tel"], -[type="url"], -[type="email"], -[type="number"] { - direction: ltr; -} -*/ - -// Remove the inner padding in Chrome and Safari on macOS. - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -// Remove padding around color pickers in webkit browsers - -::-webkit-color-swatch-wrapper { - padding: 0; -} - - -// 1. Inherit font family and line height for file input buttons -// 2. Correct the inability to style clickable types in iOS and Safari. - -::file-selector-button { - font: inherit; // 1 - -webkit-appearance: button; // 2 -} - -// Correct element displays - -output { - display: inline-block; -} - -// Remove border from iframe - -iframe { - border: 0; -} - -// Summary -// -// 1. Add the correct display in all browsers - -summary { - display: list-item; // 1 - cursor: pointer; -} - - -// Progress -// -// Add the correct vertical alignment in Chrome, Firefox, and Opera. - -progress { - vertical-align: baseline; -} - - -// Hidden attribute -// -// Always hide an element with the `hidden` HTML attribute. - -[hidden] { - display: none !important; -} diff --git a/assets/stylesheets/bootstrap/_root.scss b/assets/stylesheets/bootstrap/_root.scss index becddf14..b35cb1c5 100644 --- a/assets/stylesheets/bootstrap/_root.scss +++ b/assets/stylesheets/bootstrap/_root.scss @@ -1,187 +1,187 @@ -:root, -[data-bs-theme="light"] { - // Note: Custom variable values only support SassScript inside `#{}`. - - // Colors - // - // Generate palettes for full colors, grays, and theme colors. - - @each $color, $value in $colors { - --#{$prefix}#{$color}: #{$value}; - } - - @each $color, $value in $grays { - --#{$prefix}gray-#{$color}: #{$value}; - } - - @each $color, $value in $theme-colors { - --#{$prefix}#{$color}: #{$value}; - } - - @each $color, $value in $theme-colors-rgb { - --#{$prefix}#{$color}-rgb: #{$value}; - } - - @each $color, $value in $theme-colors-text { - --#{$prefix}#{$color}-text-emphasis: #{$value}; - } - - @each $color, $value in $theme-colors-bg-subtle { - --#{$prefix}#{$color}-bg-subtle: #{$value}; - } - - @each $color, $value in $theme-colors-border-subtle { - --#{$prefix}#{$color}-border-subtle: #{$value}; - } - - --#{$prefix}white-rgb: #{to-rgb($white)}; - --#{$prefix}black-rgb: #{to-rgb($black)}; - - // Fonts - - // Note: Use `inspect` for lists so that quoted items keep the quotes. - // See https://github.com/sass/sass/issues/2383#issuecomment-336349172 - --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)}; - --#{$prefix}font-monospace: #{inspect($font-family-monospace)}; - --#{$prefix}gradient: #{$gradient}; - - // Root and body - // scss-docs-start root-body-variables - @if $font-size-root != null { - --#{$prefix}root-font-size: #{$font-size-root}; - } - --#{$prefix}body-font-family: #{inspect($font-family-base)}; - @include rfs($font-size-base, --#{$prefix}body-font-size); - --#{$prefix}body-font-weight: #{$font-weight-base}; - --#{$prefix}body-line-height: #{$line-height-base}; - @if $body-text-align != null { - --#{$prefix}body-text-align: #{$body-text-align}; - } - - --#{$prefix}body-color: #{$body-color}; - --#{$prefix}body-color-rgb: #{to-rgb($body-color)}; - --#{$prefix}body-bg: #{$body-bg}; - --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)}; - - --#{$prefix}emphasis-color: #{$body-emphasis-color}; - --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color)}; - - --#{$prefix}secondary-color: #{$body-secondary-color}; - --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color)}; - --#{$prefix}secondary-bg: #{$body-secondary-bg}; - --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg)}; - - --#{$prefix}tertiary-color: #{$body-tertiary-color}; - --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color)}; - --#{$prefix}tertiary-bg: #{$body-tertiary-bg}; - --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg)}; - // scss-docs-end root-body-variables - - --#{$prefix}heading-color: #{$headings-color}; - - --#{$prefix}link-color: #{$link-color}; - --#{$prefix}link-color-rgb: #{to-rgb($link-color)}; - --#{$prefix}link-decoration: #{$link-decoration}; - - --#{$prefix}link-hover-color: #{$link-hover-color}; - --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color)}; +@use "sass:map"; +@use "colors" as *; +@use "config" as *; +@use "functions" as *; +@use "theme" as *; +@use "mixins/tokens" as *; +// mdo-do: do we need theme? +@layer colors, theme, config, root, reboot, layout, content, forms, components, custom, helpers, utilities; + +$root-tokens: () !default; + +// scss-docs-start root-tokens +// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-disable-next-line scss/dollar-variable-default +$root-tokens: defaults( + ( + --black: #{$black}, + --white: #{$white}, + + --gradient: #{$gradient}, + + // scss-docs-start root-font-weight-variables + --font-weight-lighter: lighter, + --font-weight-light: 300, + --font-weight-normal: 400, + --font-weight-medium: 500, + --font-weight-semibold: 600, + --font-weight-bold: 700, + --font-weight-bolder: bolder, + // scss-docs-end root-font-weight-variables + + // scss-docs-start root-body-variables + --body-font-family: system-ui, + --body-font-size: var(--font-size-base), + --body-font-weight: #{$font-weight-base}, + --body-line-height: #{$line-height-base}, + + --heading-color: #{$headings-color}, + + --hr-border-color: var(--border-color), + + --link-color: light-dark(var(--primary-base), var(--primary-fg)), + --link-decoration: #{$link-decoration}, + --link-hover-color: color-mix(in oklch, var(--link-color) 90%, #000), + + --font-mono: "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, 'Cascadia Mono', Consolas, 'Liberation Mono', monospace;", + --code-font-size: 95%, + --code-color: var(--fg-2), + + // scss-docs-start root-border-var + --border-width: #{$border-width}, + --border-style: #{$border-style}, + --border-color: light-dark(var(--gray-200), var(--gray-700)), + --border-color-translucent: color-mix(in oklch, var(--fg-body) 15%, transparent), + // scss-docs-end root-border-var + + // scss-docs-start root-box-shadow-variables + --box-shadow-xs: 0 .0625rem .1875rem rgb(0 0 0 / 7.5%), + --box-shadow-sm: 0 .125rem .25rem rgb(0 0 0 / 7.5%), + --box-shadow: 0 .5rem 1rem rgb(0 0 0 / 15%), + --box-shadow-lg: 0 1rem 3rem rgb(0 0 0 / 17.5%), + --box-shadow-inset: inset 0 1px 2px rgb(0 0 0 / 7.5%), + // scss-docs-end root-box-shadow-variables + + --spacer: 1rem, + + // scss-docs-start root-focus-variables + --focus-ring-width: 3px, + --focus-ring-offset: 1px, + --focus-ring-color: var(--primary-focus-ring), + --focus-ring: var(--focus-ring-width) solid var(--focus-ring-color), + // scss-docs-end root-focus-variables + + // scss-docs-start root-form-variables + --control-checked-bg: var(--primary-base), + --control-checked-border-color: var(--control-checked-bg), + --control-active-bg: var(--primary-base), + --control-active-border-color: var(--control-active-bg), + --control-disabled-bg: var(--bg-3), + --control-disabled-opacity: .65, + + --btn-input-fg: var(--fg-body), + --btn-input-bg: var(--bg-body), + + --btn-input-min-height: 2.375rem, + --btn-input-padding-y: .375rem, + --btn-input-padding-x: .75rem, + --btn-input-font-size: var(--font-size-base), + --btn-input-line-height: var(--line-height-base), + --btn-input-border-radius: var(--radius-5), + + --btn-input-xs-min-height: 1.5rem, + --btn-input-xs-padding-y: .125rem, + --btn-input-xs-padding-x: .5rem, + --btn-input-xs-font-size: var(--font-size-xs), + --btn-input-xs-line-height: 1.125, + --btn-input-xs-border-radius: var(--radius-5), + + --btn-input-sm-min-height: 2rem, + --btn-input-sm-padding-y: .25rem, + --btn-input-sm-padding-x: .625rem, + --btn-input-sm-font-size: var(--font-size-sm), + --btn-input-sm-line-height: var(--line-height-sm), + --btn-input-sm-border-radius: var(--radius-5), + + --btn-input-lg-min-height: 2.75rem, + --btn-input-lg-padding-y: .5rem, + --btn-input-lg-padding-x: 1rem, + --btn-input-lg-font-size: var(--font-size-md), + --btn-input-lg-line-height: var(--line-height-md), + --btn-input-lg-border-radius: var(--radius-7), + // scss-docs-end root-form-variables + ), + $root-tokens +); +// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// scss-docs-end root-tokens + +// scss-docs-start root-font-size-loop +// Generate font-size and line-height tokens +@each $name, $props in $font-sizes { + $root-tokens: map.set($root-tokens, --font-size-#{$name}, map.get($props, "font-size")); + $root-tokens: map.set($root-tokens, --line-height-#{$name}, map.get($props, "line-height")); +} +// scss-docs-end root-font-size-loop - @if $link-hover-decoration != null { - --#{$prefix}link-hover-decoration: #{$link-hover-decoration}; +// scss-docs-start root-theme-tokens +// Generate semantic theme colors +@each $color-name, $color-map in $theme-colors { + @each $key, $value in $color-map { + $root-tokens: map.set($root-tokens, --#{$color-name}-#{$key}, $value); } - - --#{$prefix}code-color: #{$code-color}; - --#{$prefix}highlight-color: #{$mark-color}; - --#{$prefix}highlight-bg: #{$mark-bg}; - - // scss-docs-start root-border-var - --#{$prefix}border-width: #{$border-width}; - --#{$prefix}border-style: #{$border-style}; - --#{$prefix}border-color: #{$border-color}; - --#{$prefix}border-color-translucent: #{$border-color-translucent}; - - --#{$prefix}border-radius: #{$border-radius}; - --#{$prefix}border-radius-sm: #{$border-radius-sm}; - --#{$prefix}border-radius-lg: #{$border-radius-lg}; - --#{$prefix}border-radius-xl: #{$border-radius-xl}; - --#{$prefix}border-radius-xxl: #{$border-radius-xxl}; - --#{$prefix}border-radius-2xl: var(--#{$prefix}border-radius-xxl); // Deprecated in v5.3.0 for consistency - --#{$prefix}border-radius-pill: #{$border-radius-pill}; - // scss-docs-end root-border-var - - --#{$prefix}box-shadow: #{$box-shadow}; - --#{$prefix}box-shadow-sm: #{$box-shadow-sm}; - --#{$prefix}box-shadow-lg: #{$box-shadow-lg}; - --#{$prefix}box-shadow-inset: #{$box-shadow-inset}; - - // Focus styles - // scss-docs-start root-focus-variables - --#{$prefix}focus-ring-width: #{$focus-ring-width}; - --#{$prefix}focus-ring-opacity: #{$focus-ring-opacity}; - --#{$prefix}focus-ring-color: #{$focus-ring-color}; - // scss-docs-end root-focus-variables - - // scss-docs-start root-form-validation-variables - --#{$prefix}form-valid-color: #{$form-valid-color}; - --#{$prefix}form-valid-border-color: #{$form-valid-border-color}; - --#{$prefix}form-invalid-color: #{$form-invalid-color}; - --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color}; - // scss-docs-end root-form-validation-variables } -@if $enable-dark-mode { - @include color-mode(dark, true) { - color-scheme: dark; - - // scss-docs-start root-dark-mode-vars - --#{$prefix}body-color: #{$body-color-dark}; - --#{$prefix}body-color-rgb: #{to-rgb($body-color-dark)}; - --#{$prefix}body-bg: #{$body-bg-dark}; - --#{$prefix}body-bg-rgb: #{to-rgb($body-bg-dark)}; - - --#{$prefix}emphasis-color: #{$body-emphasis-color-dark}; - --#{$prefix}emphasis-color-rgb: #{to-rgb($body-emphasis-color-dark)}; - - --#{$prefix}secondary-color: #{$body-secondary-color-dark}; - --#{$prefix}secondary-color-rgb: #{to-rgb($body-secondary-color-dark)}; - --#{$prefix}secondary-bg: #{$body-secondary-bg-dark}; - --#{$prefix}secondary-bg-rgb: #{to-rgb($body-secondary-bg-dark)}; +// Generate background tokens +@each $key, $value in $theme-bgs { + $root-tokens: map.set($root-tokens, --bg-#{$key}, $value); +} - --#{$prefix}tertiary-color: #{$body-tertiary-color-dark}; - --#{$prefix}tertiary-color-rgb: #{to-rgb($body-tertiary-color-dark)}; - --#{$prefix}tertiary-bg: #{$body-tertiary-bg-dark}; - --#{$prefix}tertiary-bg-rgb: #{to-rgb($body-tertiary-bg-dark)}; +// Generate foreground tokens +@each $key, $value in $theme-fgs { + $root-tokens: map.set($root-tokens, --fg-#{$key}, $value); +} - @each $color, $value in $theme-colors-text-dark { - --#{$prefix}#{$color}-text-emphasis: #{$value}; - } +// Generate border tokens +@each $key, $value in $theme-borders { + $root-tokens: map.set($root-tokens, --border-#{$key}, $value); +} +// scss-docs-end root-theme-tokens - @each $color, $value in $theme-colors-bg-subtle-dark { - --#{$prefix}#{$color}-bg-subtle: #{$value}; - } +// Generate breakpoint tokens +@each $name, $value in $breakpoints { + $root-tokens: map.set($root-tokens, --breakpoint-#{$name}, $value); +} - @each $color, $value in $theme-colors-border-subtle-dark { - --#{$prefix}#{$color}-border-subtle: #{$value}; - } +// Generate spacer tokens +// scss-docs-start root-spacer-loop +@each $key, $value in $spacers { + $root-tokens: map.set($root-tokens, --spacer-#{$key}, $value); +} +// scss-docs-end root-spacer-loop - --#{$prefix}heading-color: #{$headings-color-dark}; +// Generate radius tokens +// scss-docs-start root-radius-loop +@each $key, $value in $radii { + $root-tokens: map.set($root-tokens, --radius-#{$key}, $value); +} +// stylelint-disable-next-line scss/dollar-variable-default +$root-tokens: map.set($root-tokens, --radius-pill, 50rem); +// scss-docs-end root-radius-loop - --#{$prefix}link-color: #{$link-color-dark}; - --#{$prefix}link-hover-color: #{$link-hover-color-dark}; - --#{$prefix}link-color-rgb: #{to-rgb($link-color-dark)}; - --#{$prefix}link-hover-color-rgb: #{to-rgb($link-hover-color-dark)}; +:root { + @include tokens($root-tokens); - --#{$prefix}code-color: #{$code-color-dark}; - --#{$prefix}highlight-color: #{$mark-color-dark}; - --#{$prefix}highlight-bg: #{$mark-bg-dark}; + color-scheme: light dark; + // Always reserve the viewport scrollbar gutter so layout doesn't shift + // when overflow: hidden is applied (e.g. when a dialog opens on Windows). + scrollbar-gutter: stable; +} - --#{$prefix}border-color: #{$border-color-dark}; - --#{$prefix}border-color-translucent: #{$border-color-translucent-dark}; +[data-bs-theme="dark"] { + color-scheme: dark; +} - --#{$prefix}form-valid-color: #{$form-valid-color-dark}; - --#{$prefix}form-valid-border-color: #{$form-valid-border-color-dark}; - --#{$prefix}form-invalid-color: #{$form-invalid-color-dark}; - --#{$prefix}form-invalid-border-color: #{$form-invalid-border-color-dark}; - // scss-docs-end root-dark-mode-vars - } +[data-bs-theme="light"] { + color-scheme: light; } diff --git a/assets/stylesheets/bootstrap/_spinner.scss b/assets/stylesheets/bootstrap/_spinner.scss new file mode 100644 index 00000000..5520c469 --- /dev/null +++ b/assets/stylesheets/bootstrap/_spinner.scss @@ -0,0 +1,118 @@ +@use "config" as *; +@use "functions" as *; +@use "mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$spinner-border-tokens: () !default; + +// scss-docs-start spinner-border-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$spinner-border-tokens: defaults( + ( + --spinner-width: 2rem, + --spinner-height: 2rem, + --spinner-vertical-align: -.125em, + --spinner-border-width: .25em, + --spinner-animation-speed: .75s, + --spinner-animation-name: spinner-border, + ), + $spinner-border-tokens +); +// scss-docs-end spinner-border-tokens + +$spinner-grow-tokens: () !default; + +// scss-docs-start spinner-grow-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$spinner-grow-tokens: defaults( + ( + --spinner-width: 2rem, + --spinner-height: 2rem, + --spinner-vertical-align: -.125em, + --spinner-animation-speed: .75s, + --spinner-animation-name: spinner-grow, + ), + $spinner-grow-tokens +); +// scss-docs-end spinner-grow-tokens + +// stylelint-enable custom-property-no-missing-var-function + +// +// Rotating border +// + +@layer components { + // mdo-do: Refactor this to assume flex parent and remove `vertical-align` + .spinner-grow, + .spinner-border { + display: inline-block; + flex-shrink: 0; + width: var(--spinner-width); + height: var(--spinner-height); + vertical-align: var(--spinner-vertical-align); + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + animation: var(--spinner-animation-speed) linear infinite var(--spinner-animation-name); + } + + // scss-docs-start spinner-border-keyframes + @keyframes spinner-border { + to { transform: rotate(360deg); } + } + // scss-docs-end spinner-border-keyframes + + .spinner-border { + @include tokens($spinner-border-tokens); + + border: var(--spinner-border-width) solid currentcolor; + border-inline-end-color: transparent; + } + + .spinner-border-sm { + // scss-docs-start spinner-border-sm-css-vars + --spinner-width: 1rem; + --spinner-height: 1rem; + --spinner-border-width: .2em; + // scss-docs-end spinner-border-sm-css-vars + } + + // + // Growing circle + // + + // scss-docs-start spinner-grow-keyframes + @keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } + } + // scss-docs-end spinner-grow-keyframes + + .spinner-grow { + @include tokens($spinner-grow-tokens); + + background-color: currentcolor; + opacity: 0; + } + + .spinner-grow-sm { + // scss-docs-start spinner-grow-sm-css-vars + --spinner-width: 1rem; + --spinner-height: 1rem; + // scss-docs-end spinner-grow-sm-css-vars + } + + @if $enable-reduced-motion { + @media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + --spinner-animation-speed: 1.5s; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/_spinners.scss b/assets/stylesheets/bootstrap/_spinners.scss deleted file mode 100644 index 9dff2892..00000000 --- a/assets/stylesheets/bootstrap/_spinners.scss +++ /dev/null @@ -1,86 +0,0 @@ -// -// Rotating border -// - -.spinner-grow, -.spinner-border { - display: inline-block; - flex-shrink: 0; - width: var(--#{$prefix}spinner-width); - height: var(--#{$prefix}spinner-height); - vertical-align: var(--#{$prefix}spinner-vertical-align); - // stylelint-disable-next-line property-disallowed-list - border-radius: 50%; - animation: var(--#{$prefix}spinner-animation-speed) linear infinite var(--#{$prefix}spinner-animation-name); -} - -// scss-docs-start spinner-border-keyframes -@keyframes spinner-border { - to { transform: rotate(360deg) #{"/* rtl:ignore */"}; } -} -// scss-docs-end spinner-border-keyframes - -.spinner-border { - // scss-docs-start spinner-border-css-vars - --#{$prefix}spinner-width: #{$spinner-width}; - --#{$prefix}spinner-height: #{$spinner-height}; - --#{$prefix}spinner-vertical-align: #{$spinner-vertical-align}; - --#{$prefix}spinner-border-width: #{$spinner-border-width}; - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed}; - --#{$prefix}spinner-animation-name: spinner-border; - // scss-docs-end spinner-border-css-vars - - border: var(--#{$prefix}spinner-border-width) solid currentcolor; - border-right-color: transparent; -} - -.spinner-border-sm { - // scss-docs-start spinner-border-sm-css-vars - --#{$prefix}spinner-width: #{$spinner-width-sm}; - --#{$prefix}spinner-height: #{$spinner-height-sm}; - --#{$prefix}spinner-border-width: #{$spinner-border-width-sm}; - // scss-docs-end spinner-border-sm-css-vars -} - -// -// Growing circle -// - -// scss-docs-start spinner-grow-keyframes -@keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} -// scss-docs-end spinner-grow-keyframes - -.spinner-grow { - // scss-docs-start spinner-grow-css-vars - --#{$prefix}spinner-width: #{$spinner-width}; - --#{$prefix}spinner-height: #{$spinner-height}; - --#{$prefix}spinner-vertical-align: #{$spinner-vertical-align}; - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed}; - --#{$prefix}spinner-animation-name: spinner-grow; - // scss-docs-end spinner-grow-css-vars - - background-color: currentcolor; - opacity: 0; -} - -.spinner-grow-sm { - --#{$prefix}spinner-width: #{$spinner-width-sm}; - --#{$prefix}spinner-height: #{$spinner-height-sm}; -} - -@if $enable-reduced-motion { - @media (prefers-reduced-motion: reduce) { - .spinner-border, - .spinner-grow { - --#{$prefix}spinner-animation-speed: #{$spinner-animation-speed * 2}; - } - } -} diff --git a/assets/stylesheets/bootstrap/_stepper.scss b/assets/stylesheets/bootstrap/_stepper.scss new file mode 100644 index 00000000..26f32294 --- /dev/null +++ b/assets/stylesheets/bootstrap/_stepper.scss @@ -0,0 +1,156 @@ +@use "config" as *; +@use "functions" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; + +$stepper-tokens: () !default; + +// scss-docs-start stepper-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$stepper-tokens: defaults( + ( + --stepper-size: 2rem, + --stepper-gap: 1rem, + --stepper-font-size: var(--font-size-sm), + --stepper-text-gap: .5rem, + --stepper-track-size: .125rem, + --stepper-bg: var(--bg-2), + --stepper-active-color: var(--primary-contrast), + --stepper-active-bg: var(--primary-bg), + ), + $stepper-tokens +); +// scss-docs-end stepper-tokens + +// scss-docs-start stepper-horizontal-mixin +@mixin stepper-horizontal() { + display: inline-grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + + .stepper-item { + grid-template-rows: var(--stepper-size) auto; + grid-template-columns: auto; + align-items: start; + justify-items: center; + text-align: center; + + &::after { + inset-block-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + inset-block-end: auto; + inset-inline-start: 50%; + inset-inline-end: 100%; + width: calc(100% + var(--stepper-gap)); + height: var(--stepper-track-size); + } + + &:last-child::after { + right: 100%; + } + } +} +// scss-docs-end stepper-horizontal-mixin + +@layer components { + .stepper { + @include tokens($stepper-tokens); + + display: grid; + grid-auto-rows: 1fr; + grid-auto-flow: row; + gap: var(--stepper-gap); + padding-inline-start: 0; + list-style-type: ""; + counter-reset: stepper; + } + + .stepper-item { + position: relative; + display: grid; + grid-template-rows: auto; + grid-template-columns: var(--stepper-size) auto; + gap: var(--stepper-text-gap); + align-items: var(--stepper-align-items, center); + text-decoration: none; + + // The counter + &::before { + position: relative; + z-index: 1; + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: var(--stepper-size); + height: var(--stepper-size); + padding: .5rem; + font-size: var(--stepper-font-size); + font-weight: 600; + line-height: 1; + text-align: center; + content: counter(stepper); + counter-increment: stepper; + background-color: var(--stepper-bg); + @include border-radius(50%); + } + + // Connecting lines + &::after { + position: absolute; + inset-block-start: 50%; + inset-block-end: 100%; + inset-inline-start: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + width: var(--stepper-track-size); + height: calc(100% + var(--stepper-gap)); + content: ""; + background-color: var(--stepper-bg); + } + + // Avoid sibling selector for easier CSS overrides + &:last-child::after { + display: none; + } + + &.active { + &::before, + &::after { + color: var(--theme-contrast, var(--stepper-active-color)); + background-color: var(--theme-bg, var(--stepper-active-bg)); + } + } + } + + // Targets the last .active element from a sequence of active elements + .stepper-item.active:not(:has(+ .stepper-item.active))::after { + background-color: var(--stepper-bg); + } + + .stepper-horizontal { + @include stepper-horizontal(); + } + + @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { + @if $next { + .#{$prefix}stepper-horizontal { + @include container-breakpoint-up($next) { + @include stepper-horizontal(); + } + } + } + } + + // scss-docs-start stepper-overflow + .stepper-overflow { + container-type: inline-size; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + + > .stepper { + width: max-content; + min-width: 100%; + } + } + // scss-docs-end stepper-overflow +} diff --git a/assets/stylesheets/bootstrap/_tables.scss b/assets/stylesheets/bootstrap/_tables.scss deleted file mode 100644 index 276521a3..00000000 --- a/assets/stylesheets/bootstrap/_tables.scss +++ /dev/null @@ -1,171 +0,0 @@ -// -// Basic Bootstrap table -// - -.table { - // Reset needed for nesting tables - --#{$prefix}table-color-type: initial; - --#{$prefix}table-bg-type: initial; - --#{$prefix}table-color-state: initial; - --#{$prefix}table-bg-state: initial; - // End of reset - --#{$prefix}table-color: #{$table-color}; - --#{$prefix}table-bg: #{$table-bg}; - --#{$prefix}table-border-color: #{$table-border-color}; - --#{$prefix}table-accent-bg: #{$table-accent-bg}; - --#{$prefix}table-striped-color: #{$table-striped-color}; - --#{$prefix}table-striped-bg: #{$table-striped-bg}; - --#{$prefix}table-active-color: #{$table-active-color}; - --#{$prefix}table-active-bg: #{$table-active-bg}; - --#{$prefix}table-hover-color: #{$table-hover-color}; - --#{$prefix}table-hover-bg: #{$table-hover-bg}; - - width: 100%; - margin-bottom: $spacer; - vertical-align: $table-cell-vertical-align; - border-color: var(--#{$prefix}table-border-color); - - // Target th & td - // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class. - // We use the universal selectors here to simplify the selector (else we would need 6 different selectors). - // Another advantage is that this generates less code and makes the selector less specific making it easier to override. - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - padding: $table-cell-padding-y $table-cell-padding-x; - // Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb - color: var(--#{$prefix}table-color-state, var(--#{$prefix}table-color-type, var(--#{$prefix}table-color))); - background-color: var(--#{$prefix}table-bg); - border-bottom-width: $table-border-width; - box-shadow: inset 0 0 0 9999px var(--#{$prefix}table-bg-state, var(--#{$prefix}table-bg-type, var(--#{$prefix}table-accent-bg))); - } - - > tbody { - vertical-align: inherit; - } - - > thead { - vertical-align: bottom; - } -} - -.table-group-divider { - border-top: calc(#{$table-border-width} * 2) solid $table-group-separator-color; // stylelint-disable-line function-disallowed-list -} - -// -// Change placement of captions with a class -// - -.caption-top { - caption-side: top; -} - - -// -// Condensed table w/ half padding -// - -.table-sm { - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - padding: $table-cell-padding-y-sm $table-cell-padding-x-sm; - } -} - - -// Border versions -// -// Add or remove borders all around the table and between all the columns. -// -// When borders are added on all sides of the cells, the corners can render odd when -// these borders do not have the same color or if they are semi-transparent. -// Therefore we add top and border bottoms to the `tr`s and left and right borders -// to the `td`s or `th`s - -.table-bordered { - > :not(caption) > * { - border-width: $table-border-width 0; - - // stylelint-disable-next-line selector-max-universal - > * { - border-width: 0 $table-border-width; - } - } -} - -.table-borderless { - // stylelint-disable-next-line selector-max-universal - > :not(caption) > * > * { - border-bottom-width: 0; - } - - > :not(:first-child) { - border-top-width: 0; - } -} - -// Zebra-striping -// -// Default zebra-stripe styles (alternating gray and transparent backgrounds) - -// For rows -.table-striped { - > tbody > tr:nth-of-type(#{$table-striped-order}) > * { - --#{$prefix}table-color-type: var(--#{$prefix}table-striped-color); - --#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg); - } -} - -// For columns -.table-striped-columns { - > :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) { - --#{$prefix}table-color-type: var(--#{$prefix}table-striped-color); - --#{$prefix}table-bg-type: var(--#{$prefix}table-striped-bg); - } -} - -// Active table -// -// The `.table-active` class can be added to highlight rows or cells - -.table-active { - --#{$prefix}table-color-state: var(--#{$prefix}table-active-color); - --#{$prefix}table-bg-state: var(--#{$prefix}table-active-bg); -} - -// Hover effect -// -// Placed here since it has to come after the potential zebra striping - -.table-hover { - > tbody > tr:hover > * { - --#{$prefix}table-color-state: var(--#{$prefix}table-hover-color); - --#{$prefix}table-bg-state: var(--#{$prefix}table-hover-bg); - } -} - - -// Table variants -// -// Table variants set the table cell backgrounds, border colors -// and the colors of the striped, hovered & active tables - -@each $color, $value in $table-variants { - @include table-variant($color, $value); -} - -// Responsive tables -// -// Generate series of `.table-responsive-*` classes for configuring the screen -// size of where your table will overflow. - -@each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - @include media-breakpoint-down($breakpoint) { - .table-responsive#{$infix} { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } - } -} diff --git a/assets/stylesheets/bootstrap/_theme.scss b/assets/stylesheets/bootstrap/_theme.scss new file mode 100644 index 00000000..0327ccfe --- /dev/null +++ b/assets/stylesheets/bootstrap/_theme.scss @@ -0,0 +1,217 @@ +@use "sass:map"; + +@function theme-color-values($key) { + $result: (); + + @each $color-name, $color-map in $theme-colors { + @if map.has-key($color-map, $key) { + $result: map.merge($result, ($color-name: map.get($color-map, $key))); + } + } + + @return $result; +} + +// Themes map sub-keys +// +// Return var() references to root tokens instead of raw values. +// Ex: theme-color-refs("bg") => (primary: var(--primary-bg), accent: var(--accent-bg), ...) +@function theme-color-refs($key) { + $result: (); + + @each $color-name, $color-map in $theme-colors { + @if map.has-key($color-map, $key) { + $result: map.merge($result, ($color-name: var(--#{$color-name}-#{$key}))); + } + } + + @return $result; +} + +// Theme token to root tokens +// +// Returns the global :root token reference for a given a given token map, prefix, and key. +// Ex: theme-token-refs($theme-bgs, "bg") => (body: var(--bg-body), 1: var(--bg-1), ...) +// Skips `inherit` since it's a CSS-wide keyword that can't be stored in a custom property. +@function theme-token-refs($map, $prefix) { + $result: (); + + @each $key, $value in $map { + @if $value != inherit { + $result: map.merge($result, ($key: var(--#{$prefix}-#{$key}))); + } + } + + @return $result; +} + +// Generate opacity values using color-mix() +@function theme-opacity-values($color-var, $opacities: $util-opacity) { + $result: (); + + @each $key, $value in $opacities { + @if $key == 100 { + // For 100%, use direct variable reference (more efficient) + $result: map.merge($result, ($key: var($color-var))); + } @else { + // For other values, use color-mix() + $percentage: $key * 1%; + $result: map.merge($result, ($key: color-mix(in oklch, var($color-var) $percentage, transparent))); + } + } + + @return $result; +} + +// Generate theme classes dynamically based on the keys in each theme color map +@mixin generate-theme-classes() { + @each $color-name, $color-map in $theme-colors { + .theme-#{$color-name} { + @each $key, $value in $color-map { + --theme-#{$key}: var(--#{$color-name}-#{$key}); + } + } + } +} + +// scss-docs-start theme-colors +$theme-colors: ( + "primary": ( + "base": var(--blue-500), + "fg": light-dark(var(--blue-600), var(--blue-400)), + "fg-emphasis": light-dark(var(--blue-800), var(--blue-200)), + "bg": var(--blue-500), + "bg-subtle": light-dark(var(--blue-100), var(--blue-900)), + "bg-muted": light-dark(var(--blue-200), var(--blue-800)), + "border": light-dark(var(--blue-300), var(--blue-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--blue-500) 50%, var(--bg-body)), color-mix(in oklch, var(--blue-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "accent": ( + "base": var(--indigo-500), + "fg": light-dark(var(--indigo-600), color-mix(in oklch, var(--indigo-400), var(--indigo-300))), + "fg-emphasis": light-dark(var(--indigo-800), var(--indigo-300)), + "bg": var(--indigo-500), + "bg-subtle": light-dark(var(--indigo-100), var(--indigo-900)), + "bg-muted": light-dark(var(--indigo-200), var(--indigo-800)), + "border": light-dark(var(--indigo-300), var(--indigo-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--indigo-500) 50%, var(--bg-body)), color-mix(in oklch, var(--indigo-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "success": ( + "base": var(--green-500), + "fg": light-dark(var(--green-600), var(--green-400)), + "fg-emphasis": light-dark(var(--green-800), var(--green-300)), + "bg": var(--green-500), + "bg-subtle": light-dark(var(--green-100), var(--green-900)), + "bg-muted": light-dark(var(--green-200), var(--green-800)), + "border": light-dark(var(--green-300), var(--green-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--green-500) 50%, var(--bg-body)), color-mix(in oklch, var(--green-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "danger": ( + "base": var(--red-500), + "fg": light-dark(var(--red-600), var(--red-400)), + "fg-emphasis": light-dark(var(--red-800), var(--red-300)), + "bg": var(--red-500), + "bg-subtle": light-dark(var(--red-100), var(--red-900)), + "bg-muted": light-dark(var(--red-200), var(--red-800)), + "border": light-dark(var(--red-300), var(--red-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--red-500) 50%, var(--bg-body)), color-mix(in oklch, var(--red-500) 75%, var(--bg-body))), + "contrast": var(--white) + ), + "warning": ( + "base": var(--yellow-500), + "fg": light-dark(var(--yellow-700), var(--yellow-400)), + "fg-emphasis": light-dark(var(--yellow-800), var(--yellow-300)), + "bg": var(--yellow-500), + "bg-subtle": light-dark(var(--yellow-100), var(--yellow-900)), + "bg-muted": light-dark(var(--yellow-200), var(--yellow-800)), + "border": light-dark(var(--yellow-300), var(--yellow-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--yellow-500) 50%, var(--bg-body)), color-mix(in oklch, var(--yellow-400) 85%, var(--bg-body))), + "contrast": var(--gray-900) + ), + "info": ( + "base": var(--cyan-500), + "fg": light-dark(var(--cyan-600), var(--cyan-400)), + "fg-emphasis": light-dark(var(--cyan-800), var(--cyan-300)), + "bg": var(--cyan-500), + "bg-subtle": light-dark(var(--cyan-100), var(--cyan-900)), + "bg-muted": light-dark(var(--cyan-200), var(--cyan-800)), + "border": light-dark(var(--cyan-300), var(--cyan-600)), + "focus-ring": light-dark(color-mix(in oklch, var(--cyan-500) 50%, var(--bg-body)), color-mix(in oklch, var(--cyan-500) 75%, var(--bg-body))), + "contrast": var(--gray-900) + ), + "inverse": ( + "base": var(--gray-900), + "fg": light-dark(var(--gray-900), var(--gray-200)), + "fg-emphasis": light-dark(var(--gray-975), var(--white)), + "bg": light-dark(var(--gray-900), var(--gray-025)), + "bg-subtle": light-dark(var(--gray-100), var(--gray-900)), + "bg-muted": light-dark(var(--gray-200), var(--gray-300)), + "border": light-dark(var(--gray-400), var(--gray-100)), + "focus-ring": color-mix(in oklch, light-dark(var(--gray-900), var(--gray-100)) 50%, var(--bg-body)), + "contrast": light-dark(var(--white), var(--gray-900)) + ), + "secondary": ( + "base": var(--gray-200), + "fg": light-dark(var(--gray-600), var(--gray-400)), + "fg-emphasis": light-dark(var(--gray-800), var(--gray-200)), + "bg": light-dark(var(--gray-100), var(--gray-600)), + "bg-subtle": light-dark(var(--gray-050), var(--gray-800)), + "bg-muted": light-dark(var(--gray-100), var(--gray-700)), + "border": light-dark(var(--gray-300), var(--gray-600)), + "focus-ring": color-mix(in oklch, light-dark(var(--gray-500), var(--gray-300)) 50%, var(--bg-body)), + "contrast": light-dark(var(--gray-900), var(--white)) + ) +) !default; +// scss-docs-end theme-colors + +// mdo-do: consider using muted, subtle, ghost or something instead of linear scale? +$theme-bgs: ( + "body": light-dark(var(--white), var(--gray-975)), + "1": light-dark(var(--gray-025), var(--gray-950)), + "2": light-dark(var(--gray-050), var(--gray-900)), + "3": light-dark(var(--gray-100), var(--gray-800)), + "4": light-dark(var(--gray-200), var(--gray-700)), + "fg": var(--fg-body), + "white": var(--white), + "black": var(--black), + "transparent": transparent, + "inherit": inherit, +) !default; + +$theme-fgs: ( + "body": light-dark(var(--gray-900), var(--gray-050)), + "1": light-dark(var(--gray-800), var(--gray-200)), + "2": light-dark(var(--gray-700), var(--gray-300)), + "3": light-dark(var(--gray-600), var(--gray-500)), + "4": light-dark(var(--gray-500), var(--gray-600)), + "bg": var(--bg-body), + "white": var(--white), + "black": var(--black), + "inherit": inherit, +) !default; + +$theme-borders: ( + "bg": var(--bg-body), + "body": light-dark(var(--gray-300), var(--gray-800)), + "muted": light-dark(var(--gray-200), var(--gray-800)), + "subtle": light-dark(color-mix(in oklch, var(--gray-100), var(--gray-200)), var(--gray-900)), + "emphasized": light-dark(var(--gray-400), var(--gray-600)), + "white": var(--white), + "black": var(--black), +) !default; + +$util-opacity: ( + 10: .1, + 20: .2, + 30: .3, + 40: .4, + 50: .5, + 60: .6, + 70: .7, + 80: .8, + 90: .9, + 100: 1 +) !default; diff --git a/assets/stylesheets/bootstrap/_toasts.scss b/assets/stylesheets/bootstrap/_toasts.scss index 2ce378d5..6b6359ea 100644 --- a/assets/stylesheets/bootstrap/_toasts.scss +++ b/assets/stylesheets/bootstrap/_toasts.scss @@ -1,73 +1,99 @@ -.toast { - // scss-docs-start toast-css-vars - --#{$prefix}toast-zindex: #{$zindex-toast}; - --#{$prefix}toast-padding-x: #{$toast-padding-x}; - --#{$prefix}toast-padding-y: #{$toast-padding-y}; - --#{$prefix}toast-spacing: #{$toast-spacing}; - --#{$prefix}toast-max-width: #{$toast-max-width}; - @include rfs($toast-font-size, --#{$prefix}toast-font-size); - --#{$prefix}toast-color: #{$toast-color}; - --#{$prefix}toast-bg: #{$toast-background-color}; - --#{$prefix}toast-border-width: #{$toast-border-width}; - --#{$prefix}toast-border-color: #{$toast-border-color}; - --#{$prefix}toast-border-radius: #{$toast-border-radius}; - --#{$prefix}toast-box-shadow: #{$toast-box-shadow}; - --#{$prefix}toast-header-color: #{$toast-header-color}; - --#{$prefix}toast-header-bg: #{$toast-header-background-color}; - --#{$prefix}toast-header-border-color: #{$toast-header-border-color}; - // scss-docs-end toast-css-vars +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/tokens" as *; - width: var(--#{$prefix}toast-max-width); - max-width: 100%; - @include font-size(var(--#{$prefix}toast-font-size)); - color: var(--#{$prefix}toast-color); - pointer-events: auto; - background-color: var(--#{$prefix}toast-bg); - background-clip: padding-box; - border: var(--#{$prefix}toast-border-width) solid var(--#{$prefix}toast-border-color); - box-shadow: var(--#{$prefix}toast-box-shadow); - @include border-radius(var(--#{$prefix}toast-border-radius)); +$toast-tokens: () !default; - &.showing { - opacity: 0; - } +// scss-docs-start toast-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$toast-tokens: defaults( + ( + --toast-zindex: #{$zindex-toast}, + --toast-padding-x: 1rem, + --toast-padding-y: .75rem, + --toast-spacing: #{$container-padding-x}, + --toast-max-width: 350px, + --toast-font-size: var(--font-size-sm), + --toast-color: null, + --toast-bg: var(--bg-body), + --toast-border-width: var(--border-width), + --toast-border-color: var(--border-color-translucent), + --toast-border-radius: null, + --toast-box-shadow: var(--box-shadow), + --toast-header-color: var(--fg-3), + --toast-header-bg: var(--bg-1), + --toast-header-border-color: var(--border-color-translucent), + ), + $toast-tokens +); +// stylelint-enable custom-property-no-missing-var-function +// scss-docs-end toast-tokens + +@layer components { + .toast { + @include tokens($toast-tokens); + + display: flex; + flex-direction: column; + width: var(--toast-max-width); + max-width: 100%; + overflow: hidden; + font-size: var(--toast-font-size); + color: var(--toast-color, var(--fg-body)); + pointer-events: auto; + background-color: var(--toast-bg); + background-clip: padding-box; + border: var(--toast-border-width) solid var(--theme-border, var(--toast-border-color)); + box-shadow: var(--toast-box-shadow); + @include border-radius(var(--toast-border-radius, var(--radius-7))); - &:not(.show) { - display: none; + &.showing { + opacity: 0; + } + + &:not(.show) { + display: none; + } } -} -.toast-container { - --#{$prefix}toast-zindex: #{$zindex-toast}; + .toast-container { + --toast-zindex: #{$zindex-toast}; - position: absolute; - z-index: var(--#{$prefix}toast-zindex); - width: max-content; - max-width: 100%; - pointer-events: none; + position: absolute; + z-index: var(--toast-zindex); + width: max-content; + max-width: 100%; + pointer-events: none; - > :not(:last-child) { - margin-bottom: var(--#{$prefix}toast-spacing); + > :not(:last-child) { + margin-bottom: var(--toast-spacing); + } } -} -.toast-header { - display: flex; - align-items: center; - padding: var(--#{$prefix}toast-padding-y) var(--#{$prefix}toast-padding-x); - color: var(--#{$prefix}toast-header-color); - background-color: var(--#{$prefix}toast-header-bg); - background-clip: padding-box; - border-bottom: var(--#{$prefix}toast-border-width) solid var(--#{$prefix}toast-header-border-color); - @include border-top-radius(calc(var(--#{$prefix}toast-border-radius) - var(--#{$prefix}toast-border-width))); + .toast-header { + display: flex; + align-items: center; + padding: var(--toast-padding-y) var(--toast-padding-x); + color: var(--theme-fg-emphasis, var(--toast-header-color)); + background-color: var(--theme-bg-subtle, var(--toast-header-bg)); + // background-clip: padding-box; + border-block-end: var(--toast-border-width, var(--border-width)) solid var(--theme-border, var(--toast-header-border-color, var(--border-color-translucent))); - .btn-close { - margin-right: calc(-.5 * var(--#{$prefix}toast-padding-x)); // stylelint-disable-line function-disallowed-list - margin-left: var(--#{$prefix}toast-padding-x); + .btn-close { + margin-inline-start: calc(.5 * var(--toast-padding-x)); + margin-inline-end: calc(-.25 * var(--toast-padding-x)); + color: inherit; + } } -} -.toast-body { - padding: var(--#{$prefix}toast-padding-x); - word-wrap: break-word; + .toast-translucent { + backdrop-filter: blur(5px) saturate(180%); + } + + .toast-body { + padding: var(--toast-padding-x); + word-wrap: break-word; + } } diff --git a/assets/stylesheets/bootstrap/_tooltip.scss b/assets/stylesheets/bootstrap/_tooltip.scss index 85de90f5..ccbb6bb0 100644 --- a/assets/stylesheets/bootstrap/_tooltip.scss +++ b/assets/stylesheets/bootstrap/_tooltip.scss @@ -1,119 +1,127 @@ -// Base class -.tooltip { - // scss-docs-start tooltip-css-vars - --#{$prefix}tooltip-zindex: #{$zindex-tooltip}; - --#{$prefix}tooltip-max-width: #{$tooltip-max-width}; - --#{$prefix}tooltip-padding-x: #{$tooltip-padding-x}; - --#{$prefix}tooltip-padding-y: #{$tooltip-padding-y}; - --#{$prefix}tooltip-margin: #{$tooltip-margin}; - @include rfs($tooltip-font-size, --#{$prefix}tooltip-font-size); - --#{$prefix}tooltip-color: #{$tooltip-color}; - --#{$prefix}tooltip-bg: #{$tooltip-bg}; - --#{$prefix}tooltip-border-radius: #{$tooltip-border-radius}; - --#{$prefix}tooltip-opacity: #{$tooltip-opacity}; - --#{$prefix}tooltip-arrow-width: #{$tooltip-arrow-width}; - --#{$prefix}tooltip-arrow-height: #{$tooltip-arrow-height}; - // scss-docs-end tooltip-css-vars - - z-index: var(--#{$prefix}tooltip-zindex); - display: block; - margin: var(--#{$prefix}tooltip-margin); - @include deprecate("`$tooltip-margin`", "v5", "v5.x", true); - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - @include reset-text(); - @include font-size(var(--#{$prefix}tooltip-font-size)); - // Allow breaking very long words so they don't overflow the tooltip's bounds - word-wrap: break-word; - opacity: 0; - - &.show { opacity: var(--#{$prefix}tooltip-opacity); } - - .tooltip-arrow { +@use "config" as *; +@use "functions" as *; +@use "mixins/border-radius" as *; +@use "mixins/reset-text" as *; +@use "mixins/tokens" as *; + +$tooltip-tokens: () !default; + +// scss-docs-start tooltip-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$tooltip-tokens: defaults( + ( + --tooltip-zindex: #{$zindex-tooltip}, + --tooltip-max-width: 200px, + --tooltip-padding-x: var(--spacer-3), + --tooltip-padding-y: calc(var(--spacer) * .375), + --tooltip-font-size: var(--font-size-sm), + --tooltip-color: var(--bg-body), + --tooltip-bg: var(--fg-body), + --tooltip-border-radius: var(--radius-5), + --tooltip-opacity: .95, + --tooltip-arrow-width: .8rem, + --tooltip-arrow-height: .4rem, + ), + $tooltip-tokens +); +// scss-docs-end tooltip-tokens + +@layer components { + .tooltip { + @include tokens($tooltip-tokens); + + z-index: var(--tooltip-zindex); display: block; - width: var(--#{$prefix}tooltip-arrow-width); - height: var(--#{$prefix}tooltip-arrow-height); - - &::before { - position: absolute; - content: ""; - border-color: transparent; - border-style: solid; + // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. + // So reset our font and text properties to avoid inheriting weird values. + @include reset-text(); + font-size: var(--tooltip-font-size); + // Allow breaking very long words so they don't overflow the tooltip's bounds + word-wrap: break-word; + opacity: 0; + + &.show { opacity: var(--tooltip-opacity); } + + .tooltip-arrow { + display: block; + width: var(--tooltip-arrow-width); + height: var(--tooltip-arrow-height); + + &::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; + } } } -} -.bs-tooltip-top .tooltip-arrow { - bottom: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list + .bs-tooltip-top .tooltip-arrow { + bottom: calc(-1 * var(--tooltip-arrow-height)); - &::before { - top: -1px; - border-width: var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - border-top-color: var(--#{$prefix}tooltip-bg); + &::before { + top: -1px; + border-width: var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) * .5) 0; + border-block-start-color: var(--tooltip-bg); + } } -} -/* rtl:begin:ignore */ -.bs-tooltip-end .tooltip-arrow { - left: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}tooltip-arrow-height); - height: var(--#{$prefix}tooltip-arrow-width); + .bs-tooltip-end .tooltip-arrow { + left: calc(-1 * var(--tooltip-arrow-height)); + width: var(--tooltip-arrow-height); + height: var(--tooltip-arrow-width); - &::before { - right: -1px; - border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0; // stylelint-disable-line function-disallowed-list - border-right-color: var(--#{$prefix}tooltip-bg); + &::before { + right: -1px; + border-width: calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height) calc(var(--tooltip-arrow-width) * .5) 0; + border-inline-end-color: var(--tooltip-bg); + } } -} - -/* rtl:end:ignore */ -.bs-tooltip-bottom .tooltip-arrow { - top: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list + .bs-tooltip-bottom .tooltip-arrow { + top: calc(-1 * var(--tooltip-arrow-height)); - &::before { - bottom: -1px; - border-width: 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list - border-bottom-color: var(--#{$prefix}tooltip-bg); + &::before { + bottom: -1px; + border-width: 0 calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height); + border-block-end-color: var(--tooltip-bg); + } } -} -/* rtl:begin:ignore */ -.bs-tooltip-start .tooltip-arrow { - right: calc(-1 * var(--#{$prefix}tooltip-arrow-height)); // stylelint-disable-line function-disallowed-list - width: var(--#{$prefix}tooltip-arrow-height); - height: var(--#{$prefix}tooltip-arrow-width); + .bs-tooltip-start .tooltip-arrow { + right: calc(-1 * var(--tooltip-arrow-height)); + width: var(--tooltip-arrow-height); + height: var(--tooltip-arrow-width); - &::before { - left: -1px; - border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height); // stylelint-disable-line function-disallowed-list - border-left-color: var(--#{$prefix}tooltip-bg); + &::before { + left: -1px; + border-width: calc(var(--tooltip-arrow-width) * .5) 0 calc(var(--tooltip-arrow-width) * .5) var(--tooltip-arrow-height); + border-inline-start-color: var(--tooltip-bg); + } } -} - -/* rtl:end:ignore */ -.bs-tooltip-auto { - &[data-popper-placement^="top"] { - @extend .bs-tooltip-top; - } - &[data-popper-placement^="right"] { - @extend .bs-tooltip-end; - } - &[data-popper-placement^="bottom"] { - @extend .bs-tooltip-bottom; - } - &[data-popper-placement^="left"] { - @extend .bs-tooltip-start; + .bs-tooltip-auto { + &[data-bs-placement^="top"] { + @extend .bs-tooltip-top; + } + &[data-bs-placement^="right"] { + @extend .bs-tooltip-end; + } + &[data-bs-placement^="bottom"] { + @extend .bs-tooltip-bottom; + } + &[data-bs-placement^="left"] { + @extend .bs-tooltip-start; + } } -} -// Wrapper for the tooltip content -.tooltip-inner { - max-width: var(--#{$prefix}tooltip-max-width); - padding: var(--#{$prefix}tooltip-padding-y) var(--#{$prefix}tooltip-padding-x); - color: var(--#{$prefix}tooltip-color); - text-align: center; - background-color: var(--#{$prefix}tooltip-bg); - @include border-radius(var(--#{$prefix}tooltip-border-radius)); + // Wrapper for the tooltip content + .tooltip-inner { + max-width: var(--tooltip-max-width); + padding: var(--tooltip-padding-y) var(--tooltip-padding-x); + color: var(--tooltip-color); + text-align: center; + background-color: var(--tooltip-bg); + @include border-radius(var(--tooltip-border-radius)); + } } diff --git a/assets/stylesheets/bootstrap/_transitions.scss b/assets/stylesheets/bootstrap/_transitions.scss index bfb26aa8..3a7f936b 100644 --- a/assets/stylesheets/bootstrap/_transitions.scss +++ b/assets/stylesheets/bootstrap/_transitions.scss @@ -1,3 +1,6 @@ +@use "config" as *; +@use "mixins/transition" as *; + .fade { @include transition($transition-fade); diff --git a/assets/stylesheets/bootstrap/_type.scss b/assets/stylesheets/bootstrap/_type.scss deleted file mode 100644 index 6961390f..00000000 --- a/assets/stylesheets/bootstrap/_type.scss +++ /dev/null @@ -1,106 +0,0 @@ -// -// Headings -// -.h1 { - @extend h1; -} - -.h2 { - @extend h2; -} - -.h3 { - @extend h3; -} - -.h4 { - @extend h4; -} - -.h5 { - @extend h5; -} - -.h6 { - @extend h6; -} - - -.lead { - @include font-size($lead-font-size); - font-weight: $lead-font-weight; -} - -// Type display classes -@each $display, $font-size in $display-font-sizes { - .display-#{$display} { - font-family: $display-font-family; - font-style: $display-font-style; - font-weight: $display-font-weight; - line-height: $display-line-height; - @include font-size($font-size); - } -} - -// -// Emphasis -// -.small { - @extend small; -} - -.mark { - @extend mark; -} - -// -// Lists -// - -.list-unstyled { - @include list-unstyled(); -} - -// Inline turns list items into inline-block -.list-inline { - @include list-unstyled(); -} -.list-inline-item { - display: inline-block; - - &:not(:last-child) { - margin-right: $list-inline-padding; - } -} - - -// -// Misc -// - -// Builds on `abbr` -.initialism { - @include font-size($initialism-font-size); - text-transform: uppercase; -} - -// Blockquotes -.blockquote { - margin-bottom: $blockquote-margin-y; - @include font-size($blockquote-font-size); - - > :last-child { - margin-bottom: 0; - } -} - -.blockquote-footer { - margin-top: -$blockquote-margin-y; - margin-bottom: $blockquote-margin-y; - @include font-size($blockquote-footer-font-size); - color: $blockquote-footer-color; - - &::before { - content: "\2014\00A0"; // em dash, nbsp - } -} diff --git a/assets/stylesheets/bootstrap/_utilities.scss b/assets/stylesheets/bootstrap/_utilities.scss index 696f906e..3bd09620 100644 --- a/assets/stylesheets/bootstrap/_utilities.scss +++ b/assets/stylesheets/bootstrap/_utilities.scss @@ -1,8 +1,17 @@ -// Utilities +@use "sass:map"; +@use "config" as *; +@use "functions" as *; +@use "theme" as *; + +// add: +// - double check css grid helpers +// +// update: +// - focus-ring if needed $utilities: () !default; // stylelint-disable-next-line scss/dollar-variable-default -$utilities: map-merge( +$utilities: map.merge( ( // scss-docs-start utils-vertical-align "align": ( @@ -11,13 +20,27 @@ $utilities: map-merge( values: baseline top middle bottom text-bottom text-top ), // scss-docs-end utils-vertical-align + // scss-docs-start utils-aspect-ratio + "aspect-ratio-attr": ( + selector: "attr-includes", + class: "ratio-", + property: aspect-ratio, + values: var(--ratio), + ), + "aspect-ratio": ( + // property: aspect-ratio, + property: --ratio, + class: ratio, + values: $aspect-ratios + ), + // scss-docs-end utils-aspect-ratio // scss-docs-start utils-float "float": ( - responsive: true, property: float, + responsive: true, values: ( - start: left, - end: right, + start: inline-start, + end: inline-end, none: none, ) ), @@ -63,13 +86,20 @@ $utilities: map-merge( values: auto hidden visible scroll, ), // scss-docs-end utils-overflow + "container": ( + property: container-type, + class: contains, + values: ( + "inline": inline-size, + "size": size, + ) + ), // scss-docs-start utils-display "display": ( responsive: true, - print: true, property: display, class: d, - values: inline inline-block block grid inline-grid table table-row table-cell flex inline-flex none + values: inline inline-block block grid inline-grid table table-row table-cell flex inline-flex contents flow-root none ), // scss-docs-end utils-display // scss-docs-start utils-shadow @@ -77,21 +107,14 @@ $utilities: map-merge( property: box-shadow, class: shadow, values: ( - null: var(--#{$prefix}box-shadow), - sm: var(--#{$prefix}box-shadow-sm), - lg: var(--#{$prefix}box-shadow-lg), + null: var(--box-shadow), + xs: var(--box-shadow-xs), + sm: var(--box-shadow-sm), + lg: var(--box-shadow-lg), none: none, ) ), // scss-docs-end utils-shadow - // scss-docs-start utils-focus-ring - "focus-ring": ( - css-var: true, - css-variable-name: focus-ring-color, - class: focus-ring, - values: map-loop($theme-colors-rgb, rgba-css-var, "$key", "focus-ring") - ), - // scss-docs-end utils-focus-ring // scss-docs-start utils-position "position": ( property: position, @@ -106,12 +129,12 @@ $utilities: map-merge( values: $position-values ), "start": ( - property: left, + property: inset-inline-start, class: start, values: $position-values ), "end": ( - property: right, + property: inset-inline-end, class: end, values: $position-values ), @@ -129,52 +152,75 @@ $utilities: map-merge( "border": ( property: border, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-top": ( - property: border-top, + class: border-top, + property: border-block-start, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-end": ( - property: border-right, + property: border-inline-end, class: border-end, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-bottom": ( - property: border-bottom, + property: border-block-end, + class: border-bottom, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), "border-start": ( - property: border-left, + property: border-inline-start, class: border-start, values: ( - null: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color), + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "border-y": ( + class: border-y, + property: border-block, + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "border-x": ( + class: border-x, + property: border-inline, + values: ( + null: var(--border-width) var(--border-style) var(--border-color), 0: 0, ) ), + // scss-docs-end utils-borders + // scss-docs-start utils-border-color "border-color": ( - property: border-color, - class: border, - local-vars: ( - "border-opacity": 1 + property: ( + "--border-color": null, + "border-color": var(--border-color) ), - values: $utilities-border-colors - ), - "subtle-border-color": ( - property: border-color, class: border, - values: $utilities-border-subtle + values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-borders, "border")), + ), + "border-color-subtle": ( + property: ( + "--border-color": null, + "border-color": var(--border-color) + ), + class: border-subtle, + values: theme-color-refs("border"), ), "border-width": ( property: border-width, @@ -182,35 +228,43 @@ $utilities: map-merge( values: $border-widths ), "border-opacity": ( - css-var: true, - class: border-opacity, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + class: border, + property: border-color, + values: theme-opacity-values(--border-color) ), - // scss-docs-end utils-borders + // scss-docs-end utils-border-color // Sizing utilities - // scss-docs-start utils-sizing + // scss-docs-start utils-width "width": ( property: width, class: w, - values: ( - 25: 25%, - 50: 50%, - 75: 75%, - 100: 100%, - auto: auto + values: map.merge( + $sizes, + ( + 25: 25%, + 50: 50%, + 75: 75%, + 100: 100%, + auto: auto, + min: min-content, + max: max-content, + fit: fit-content, + ) ) ), "max-width": ( property: max-width, - class: mw, + class: max-w, values: (100: 100%) ), + "min-width": ( + property: min-width, + class: min-w, + values: ( + 0: 0, + 100: 100% + ) + ), "viewport-width": ( property: width, class: vw, @@ -221,6 +275,8 @@ $utilities: map-merge( class: min-vw, values: (100: 100vw) ), + // scss-docs-end utils-width + // scss-docs-start utils-height "height": ( property: height, class: h, @@ -229,14 +285,25 @@ $utilities: map-merge( 50: 50%, 75: 75%, 100: 100%, - auto: auto + auto: auto, + min: min-content, + max: max-content, + fit: fit-content, ) ), "max-height": ( property: max-height, - class: mh, + class: max-h, values: (100: 100%) ), + "min-height": ( + property: min-height, + class: min-h, + values: ( + 0: 0, + 100: 100%, + ), + ), "viewport-height": ( property: height, class: vh, @@ -247,7 +314,7 @@ $utilities: map-merge( class: min-vh, values: (100: 100vh) ), - // scss-docs-end utils-sizing + // scss-docs-end utils-height // Flex utilities // scss-docs-start utils-flex "flex": ( @@ -297,6 +364,25 @@ $utilities: map-merge( evenly: space-evenly, ) ), + "justify-items": ( + responsive: true, + property: justify-items, + values: ( + start: start, + end: end, + center: center, + stretch: stretch, + ) + ), + "justify-self": ( + responsive: true, + property: justify-self, + values: ( + start: start, + end: end, + center: center, + ) + ), "align-items": ( responsive: true, property: align-items, @@ -332,6 +418,43 @@ $utilities: map-merge( stretch: stretch, ) ), + "place-items": ( + responsive: true, + property: place-items, + values: ( + start: start, + end: end, + center: center, + stretch: stretch, + ) + ), + "grid-column-counts": ( + responsive: true, + // property: --columns, + property: grid-template-columns, + class: grid-cols, + values: ( + "1": 1fr, + "2": repeat(2, 1fr), + "3": repeat(3, 1fr), + "4": repeat(4, 1fr), + "6": repeat(6, 1fr), + ) + ), + "grid-columns": ( + responsive: true, + property: grid-column, + class: grid-cols, + values: ( + fill: #{"1 / -1"}, + ) + ), + "grid-auto-flow": ( + responsive: true, + property: grid-auto-flow, + class: grid-auto-flow, + values: row column dense + ), "order": ( responsive: true, property: order, @@ -349,92 +472,52 @@ $utilities: map-merge( // scss-docs-end utils-flex // Margin utilities // scss-docs-start utils-spacing + // scss-docs-start utils-margin "margin": ( responsive: true, property: margin, class: m, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-x": ( responsive: true, - property: margin-right margin-left, + property: margin-inline, class: mx, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-y": ( responsive: true, - property: margin-top margin-bottom, + property: margin-block, class: my, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-top": ( responsive: true, - property: margin-top, + property: margin-block-start, class: mt, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-end": ( responsive: true, - property: margin-right, + property: margin-inline-end, class: me, - values: map-merge($spacers, (auto: auto)) + values: map-merge-multiple($spacers, $negative-spacers, (auto: auto)) ), "margin-bottom": ( responsive: true, - property: margin-bottom, + property: margin-block-end, class: mb, - values: map-merge($spacers, (auto: auto)) + values: map.merge($spacers, (auto: auto)) ), "margin-start": ( responsive: true, - property: margin-left, + property: margin-inline-start, class: ms, - values: map-merge($spacers, (auto: auto)) - ), - // Negative margin utilities - "negative-margin": ( - responsive: true, - property: margin, - class: m, - values: $negative-spacers - ), - "negative-margin-x": ( - responsive: true, - property: margin-right margin-left, - class: mx, - values: $negative-spacers - ), - "negative-margin-y": ( - responsive: true, - property: margin-top margin-bottom, - class: my, - values: $negative-spacers - ), - "negative-margin-top": ( - responsive: true, - property: margin-top, - class: mt, - values: $negative-spacers - ), - "negative-margin-end": ( - responsive: true, - property: margin-right, - class: me, - values: $negative-spacers - ), - "negative-margin-bottom": ( - responsive: true, - property: margin-bottom, - class: mb, - values: $negative-spacers - ), - "negative-margin-start": ( - responsive: true, - property: margin-left, - class: ms, - values: $negative-spacers + values: map-merge-multiple($spacers, $negative-spacers, (auto: auto)) ), + // scss-docs-end utils-margin // Padding utilities + // scss-docs-start utils-padding "padding": ( responsive: true, property: padding, @@ -443,41 +526,43 @@ $utilities: map-merge( ), "padding-x": ( responsive: true, - property: padding-right padding-left, + property: padding-inline, class: px, values: $spacers ), "padding-y": ( responsive: true, - property: padding-top padding-bottom, + property: padding-block, class: py, values: $spacers ), "padding-top": ( responsive: true, - property: padding-top, + property: padding-block-start, class: pt, values: $spacers ), "padding-end": ( responsive: true, - property: padding-right, + property: padding-inline-end, class: pe, values: $spacers ), "padding-bottom": ( responsive: true, - property: padding-bottom, + property: padding-block-end, class: pb, values: $spacers ), "padding-start": ( responsive: true, - property: padding-left, + property: padding-inline-start, class: ps, values: $spacers ), + // scss-docs-end utils-padding // Gap utility + // scss-docs-start utils-gap "gap": ( responsive: true, property: gap, @@ -496,20 +581,72 @@ $utilities: map-merge( class: column-gap, values: $spacers ), + // scss-docs-end utils-gap // scss-docs-end utils-spacing + // scss-docs-start utils-space + "space-x": ( + responsive: true, + property: margin-inline-end, + class: space-x, + child-selector: "> :not(:last-child)", + values: $spacers + ), + "space-y": ( + responsive: true, + property: margin-block-end, + class: space-y, + child-selector: "> :not(:last-child)", + values: $spacers + ), + // scss-docs-end utils-space + // scss-docs-start utils-divide + "divide-x": ( + responsive: true, + property: border-inline-start, + class: divide-x, + child-selector: "> :not(:first-child)", + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + "divide-y": ( + responsive: true, + property: border-block-start, + class: divide-y, + child-selector: "> :not(:first-child)", + values: ( + null: var(--border-width) var(--border-style) var(--border-color), + 0: 0, + ) + ), + // scss-docs-end utils-divide // Text - // scss-docs-start utils-text + // scss-docs-start utils-font-family "font-family": ( property: font-family, class: font, - values: (monospace: var(--#{$prefix}font-monospace)) + values: ( + "monospace": var(--font-mono), + "body": var(--body-font-family), + ) ), + // scss-docs-end utils-font-family + // scss-docs-start utils-font-size "font-size": ( - rfs: true, property: font-size, class: fs, + values: map-get-nested($font-sizes, "font-size") + ), + "text-size": ( + property: ( + "font-size": 1rem, + "line-height": 1.5 + ), + class: text, values: $font-sizes ), + // scss-docs-end utils-font-size "font-style": ( property: font-style, class: fst, @@ -543,8 +680,8 @@ $utilities: map-merge( property: text-align, class: text, values: ( - start: left, - end: right, + start: start, + end: end, center: center, ) ), @@ -552,78 +689,75 @@ $utilities: map-merge( property: text-decoration, values: none underline line-through ), + // scss-docs-start utils-text-transform "text-transform": ( property: text-transform, class: text, values: lowercase uppercase capitalize ), - "white-space": ( - property: white-space, + // scss-docs-end utils-text-transform + // scss-docs-start utils-text-wrap + "text-wrap": ( + property: text-wrap, class: text, - values: ( - wrap: normal, - nowrap: nowrap, - ) + values: wrap nowrap balance pretty, ), + // scss-docs-end utils-text-wrap + // scss-docs-start utils-text-break "word-wrap": ( property: word-wrap word-break, class: text, values: (break: break-word), - rtl: false ), + // scss-docs-end utils-text-break // scss-docs-end utils-text // scss-docs-start utils-color - "color": ( - property: color, - class: text, - local-vars: ( - "text-opacity": 1 + "fg": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg, + values: map.merge( + map.merge(theme-color-refs("fg"), theme-token-refs($theme-fgs, "fg")), + (reset: inherit) ), - values: map-merge( - $utilities-text-colors, - ( - "muted": var(--#{$prefix}secondary-color), // deprecated - "black-50": rgba($black, .5), // deprecated - "white-50": rgba($white, .5), // deprecated - "body-secondary": var(--#{$prefix}secondary-color), - "body-tertiary": var(--#{$prefix}tertiary-color), - "body-emphasis": var(--#{$prefix}emphasis-color), - "reset": inherit, - ) - ) ), - "text-opacity": ( - css-var: true, - class: text-opacity, - values: ( - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + "fg-emphasis": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg-emphasis, + values: theme-color-refs("fg-emphasis"), ), - "text-color": ( + "fg-contrast": ( + property: ( + "--fg": null, + "color": var(--fg) + ), + class: fg-contrast, + values: theme-color-refs("contrast"), + ), + "fg-opacity": ( + class: fg, property: color, - class: text, - values: $utilities-text-emphasis-colors + values: theme-opacity-values(--fg) ), // scss-docs-end utils-color // scss-docs-start utils-links "link-opacity": ( - css-var: true, - class: link-opacity, + property: color, + // css-var: true, + class: link, state: hover, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + values: theme-opacity-values(--link-color) ), - "link-offset": ( + // scss-docs-end utils-links + // scss-docs-start utils-underline + "underline-offset": ( property: text-underline-offset, - class: link-offset, + class: underline-offset, state: hover, values: ( 1: .125em, @@ -631,75 +765,102 @@ $utilities: map-merge( 3: .375em, ) ), - "link-underline": ( + "underline-color": ( property: text-decoration-color, - class: link-underline, - local-vars: ( - "link-underline-opacity": 1 - ), - values: map-merge( - $utilities-links-underline, - ( - null: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-underline-opacity, 1)), - ) - ) + class: underline, + values: theme-color-values("fg"), + ), + "underline-opacity": ( + property: text-decoration-color, + class: underline, + state: hover, + values: theme-opacity-values(--link-color) ), - "link-underline-opacity": ( - css-var: true, - class: link-underline-opacity, + "underline-thickness": ( + property: text-decoration-thickness, + class: underline-thickness, state: hover, values: ( - 0: 0, - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ), + 1: 1px, + 2: 2px, + 3: 3px, + 4: 4px, + 5: 5px, + ) ), - // scss-docs-end utils-links + // scss-docs-end utils-underline // scss-docs-start utils-bg-color - "background-color": ( - property: background-color, + "bg-color": ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), class: bg, - local-vars: ( - "bg-opacity": 1 + values: map.merge(theme-color-refs("bg"), theme-token-refs($theme-bgs, "bg")), + ), + "bg-color-subtle": ( + property: ( + "--bg": null, + "background-color": var(--bg) ), - values: map-merge( - $utilities-bg-colors, - ( - "transparent": transparent, - "body-secondary": rgba(var(--#{$prefix}secondary-bg-rgb), var(--#{$prefix}bg-opacity)), - "body-tertiary": rgba(var(--#{$prefix}tertiary-bg-rgb), var(--#{$prefix}bg-opacity)), - ) - ) + class: bg-subtle, + values: theme-color-refs("bg-subtle"), ), - "bg-opacity": ( - css-var: true, - class: bg-opacity, - values: ( - 10: .1, - 25: .25, - 50: .5, - 75: .75, - 100: 1 - ) + "bg-color-muted": ( + property: ( + "--bg": null, + "background-color": var(--bg) + ), + class: bg-muted, + values: theme-color-refs("bg-muted"), ), - "subtle-background-color": ( - property: background-color, + "bg-opacity": ( class: bg, - values: $utilities-bg-subtle + property: background-color, + values: theme-opacity-values(--bg) ), // scss-docs-end utils-bg-color + // scss-docs-start utils-theme + // Theme style utilities - pair with .theme-{color} to apply coordinated bg + text colors + "theme-contrast": ( + property: ( + "background-color": var(--theme-bg), + "color": var(--theme-contrast) + ), + class: theme-contrast, + values: (null: null) + ), + "theme-subtle": ( + property: ( + "background-color": var(--theme-bg-subtle), + "color": var(--theme-fg) + ), + class: theme-subtle, + values: (null: null) + ), + "theme-muted": ( + property: ( + "background-color": var(--theme-bg-muted), + "color": var(--theme-fg-emphasis) + ), + class: theme-muted, + values: (null: null) + ), + "theme-border": ( + property: border, + class: theme-border, + values: (null: var(--border-width) solid var(--theme-border)) + ), + // scss-docs-end utils-theme "gradient": ( property: background-image, class: bg, - values: (gradient: var(--#{$prefix}gradient)) + values: (gradient: var(--gradient)) ), // scss-docs-start utils-interaction "user-select": ( property: user-select, - values: all auto none + values: all auto text none ), "pointer-events": ( property: pointer-events, @@ -708,79 +869,79 @@ $utilities: map-merge( ), // scss-docs-end utils-interaction // scss-docs-start utils-border-radius - "rounded": ( - property: border-radius, + "border-radius": ( + property: ( + "--rounded-size": null, + "border-radius": var(--rounded-size) + ), class: rounded, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: map.get($radii, 5), + circle: 50%, + pill: var(--radius-pill) + ) + ) + ), + "rounded-size": ( + property: --rounded-size, + class: rounded-size, + values: map.merge( + $radii, + ( + null: map.get($radii, 5), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-top": ( - property: border-top-left-radius border-top-right-radius, + property: border-start-start-radius border-start-end-radius, class: rounded-top, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-end": ( - property: border-top-right-radius border-bottom-right-radius, + property: border-start-end-radius border-end-end-radius, class: rounded-end, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-bottom": ( - property: border-bottom-right-radius border-bottom-left-radius, + property: border-end-start-radius border-end-end-radius, class: rounded-bottom, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), "rounded-start": ( - property: border-bottom-left-radius border-top-left-radius, + property: border-start-start-radius border-end-start-radius, class: rounded-start, - values: ( - null: var(--#{$prefix}border-radius), - 0: 0, - 1: var(--#{$prefix}border-radius-sm), - 2: var(--#{$prefix}border-radius), - 3: var(--#{$prefix}border-radius-lg), - 4: var(--#{$prefix}border-radius-xl), - 5: var(--#{$prefix}border-radius-xxl), - circle: 50%, - pill: var(--#{$prefix}border-radius-pill) + values: map.merge( + $radii, + ( + null: var(--rounded-size, var(--radius-5)), + circle: 50%, + pill: var(--radius-pill) + ) ) ), // scss-docs-end utils-border-radius @@ -799,7 +960,7 @@ $utilities: map-merge( property: z-index, class: z, values: $zindex-levels, - ) + ), // scss-docs-end utils-zindex ), $utilities diff --git a/assets/stylesheets/bootstrap/_variables-dark.scss b/assets/stylesheets/bootstrap/_variables-dark.scss deleted file mode 100644 index 260f6dcc..00000000 --- a/assets/stylesheets/bootstrap/_variables-dark.scss +++ /dev/null @@ -1,102 +0,0 @@ -// Dark color mode variables -// -// Custom variables for the `[data-bs-theme="dark"]` theme. Use this as a starting point for your own custom color modes by creating a new theme-specific file like `_variables-dark.scss` and adding the variables you need. - -// -// Global colors -// - -// scss-docs-start sass-dark-mode-vars -// scss-docs-start theme-text-dark-variables -$primary-text-emphasis-dark: tint-color($primary, 40%) !default; -$secondary-text-emphasis-dark: tint-color($secondary, 40%) !default; -$success-text-emphasis-dark: tint-color($success, 40%) !default; -$info-text-emphasis-dark: tint-color($info, 40%) !default; -$warning-text-emphasis-dark: tint-color($warning, 40%) !default; -$danger-text-emphasis-dark: tint-color($danger, 40%) !default; -$light-text-emphasis-dark: $gray-100 !default; -$dark-text-emphasis-dark: $gray-300 !default; -// scss-docs-end theme-text-dark-variables - -// scss-docs-start theme-bg-subtle-dark-variables -$primary-bg-subtle-dark: shade-color($primary, 80%) !default; -$secondary-bg-subtle-dark: shade-color($secondary, 80%) !default; -$success-bg-subtle-dark: shade-color($success, 80%) !default; -$info-bg-subtle-dark: shade-color($info, 80%) !default; -$warning-bg-subtle-dark: shade-color($warning, 80%) !default; -$danger-bg-subtle-dark: shade-color($danger, 80%) !default; -$light-bg-subtle-dark: $gray-800 !default; -$dark-bg-subtle-dark: mix($gray-800, $black) !default; -// scss-docs-end theme-bg-subtle-dark-variables - -// scss-docs-start theme-border-subtle-dark-variables -$primary-border-subtle-dark: shade-color($primary, 40%) !default; -$secondary-border-subtle-dark: shade-color($secondary, 40%) !default; -$success-border-subtle-dark: shade-color($success, 40%) !default; -$info-border-subtle-dark: shade-color($info, 40%) !default; -$warning-border-subtle-dark: shade-color($warning, 40%) !default; -$danger-border-subtle-dark: shade-color($danger, 40%) !default; -$light-border-subtle-dark: $gray-700 !default; -$dark-border-subtle-dark: $gray-800 !default; -// scss-docs-end theme-border-subtle-dark-variables - -$body-color-dark: $gray-300 !default; -$body-bg-dark: $gray-900 !default; -$body-secondary-color-dark: rgba($body-color-dark, .75) !default; -$body-secondary-bg-dark: $gray-800 !default; -$body-tertiary-color-dark: rgba($body-color-dark, .5) !default; -$body-tertiary-bg-dark: mix($gray-800, $gray-900, 50%) !default; -$body-emphasis-color-dark: $white !default; -$border-color-dark: $gray-700 !default; -$border-color-translucent-dark: rgba($white, .15) !default; -$headings-color-dark: inherit !default; -$link-color-dark: tint-color($primary, 40%) !default; -$link-hover-color-dark: shift-color($link-color-dark, -$link-shade-percentage) !default; -$code-color-dark: tint-color($code-color, 40%) !default; -$mark-color-dark: $body-color-dark !default; -$mark-bg-dark: $yellow-800 !default; - - -// -// Forms -// - -$form-select-indicator-color-dark: $body-color-dark !default; -$form-select-indicator-dark: url("data:image/svg+xml,") !default; - -$form-switch-color-dark: rgba($white, .25) !default; -$form-switch-bg-image-dark: url("data:image/svg+xml,") !default; - -// scss-docs-start form-validation-colors-dark -$form-valid-color-dark: $green-300 !default; -$form-valid-border-color-dark: $green-300 !default; -$form-invalid-color-dark: $red-300 !default; -$form-invalid-border-color-dark: $red-300 !default; -// scss-docs-end form-validation-colors-dark - - -// -// Accordion -// - -$accordion-icon-color-dark: $primary-text-emphasis-dark !default; -$accordion-icon-active-color-dark: $primary-text-emphasis-dark !default; - -$accordion-button-icon-dark: url("data:image/svg+xml,") !default; -$accordion-button-active-icon-dark: url("data:image/svg+xml,") !default; -// scss-docs-end sass-dark-mode-vars - - -// -// Carousel -// - -$carousel-indicator-active-bg-dark: $carousel-dark-indicator-active-bg !default; -$carousel-caption-color-dark: $carousel-dark-caption-color !default; -$carousel-control-icon-filter-dark: $carousel-dark-control-icon-filter !default; - -// -// Close button -// - -$btn-close-filter-dark: $btn-close-white-filter !default; diff --git a/assets/stylesheets/bootstrap/_variables.scss b/assets/stylesheets/bootstrap/_variables.scss deleted file mode 100644 index 1ffa7e74..00000000 --- a/assets/stylesheets/bootstrap/_variables.scss +++ /dev/null @@ -1,1753 +0,0 @@ -// Variables -// -// Variables should follow the `$component-state-property-size` formula for -// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs. - -// Color system - -// scss-docs-start gray-color-variables -$white: #fff !default; -$gray-100: #f8f9fa !default; -$gray-200: #e9ecef !default; -$gray-300: #dee2e6 !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #6c757d !default; -$gray-700: #495057 !default; -$gray-800: #343a40 !default; -$gray-900: #212529 !default; -$black: #000 !default; -// scss-docs-end gray-color-variables - -// fusv-disable -// scss-docs-start gray-colors-map -$grays: ( - "100": $gray-100, - "200": $gray-200, - "300": $gray-300, - "400": $gray-400, - "500": $gray-500, - "600": $gray-600, - "700": $gray-700, - "800": $gray-800, - "900": $gray-900 -) !default; -// scss-docs-end gray-colors-map -// fusv-enable - -// scss-docs-start color-variables -$blue: #0d6efd !default; -$indigo: #6610f2 !default; -$purple: #6f42c1 !default; -$pink: #d63384 !default; -$red: #dc3545 !default; -$orange: #fd7e14 !default; -$yellow: #ffc107 !default; -$green: #198754 !default; -$teal: #20c997 !default; -$cyan: #0dcaf0 !default; -// scss-docs-end color-variables - -// scss-docs-start colors-map -$colors: ( - "blue": $blue, - "indigo": $indigo, - "purple": $purple, - "pink": $pink, - "red": $red, - "orange": $orange, - "yellow": $yellow, - "green": $green, - "teal": $teal, - "cyan": $cyan, - "black": $black, - "white": $white, - "gray": $gray-600, - "gray-dark": $gray-800 -) !default; -// scss-docs-end colors-map - -// The contrast ratio to reach against white, to determine if color changes from "light" to "dark". Acceptable values for WCAG 2.2 are 3, 4.5 and 7. -// See https://www.w3.org/TR/WCAG/#contrast-minimum -$min-contrast-ratio: 4.5 !default; - -// Customize the light and dark text colors for use in our color contrast function. -$color-contrast-dark: $black !default; -$color-contrast-light: $white !default; - -// fusv-disable -$blue-100: tint-color($blue, 80%) !default; -$blue-200: tint-color($blue, 60%) !default; -$blue-300: tint-color($blue, 40%) !default; -$blue-400: tint-color($blue, 20%) !default; -$blue-500: $blue !default; -$blue-600: shade-color($blue, 20%) !default; -$blue-700: shade-color($blue, 40%) !default; -$blue-800: shade-color($blue, 60%) !default; -$blue-900: shade-color($blue, 80%) !default; - -$indigo-100: tint-color($indigo, 80%) !default; -$indigo-200: tint-color($indigo, 60%) !default; -$indigo-300: tint-color($indigo, 40%) !default; -$indigo-400: tint-color($indigo, 20%) !default; -$indigo-500: $indigo !default; -$indigo-600: shade-color($indigo, 20%) !default; -$indigo-700: shade-color($indigo, 40%) !default; -$indigo-800: shade-color($indigo, 60%) !default; -$indigo-900: shade-color($indigo, 80%) !default; - -$purple-100: tint-color($purple, 80%) !default; -$purple-200: tint-color($purple, 60%) !default; -$purple-300: tint-color($purple, 40%) !default; -$purple-400: tint-color($purple, 20%) !default; -$purple-500: $purple !default; -$purple-600: shade-color($purple, 20%) !default; -$purple-700: shade-color($purple, 40%) !default; -$purple-800: shade-color($purple, 60%) !default; -$purple-900: shade-color($purple, 80%) !default; - -$pink-100: tint-color($pink, 80%) !default; -$pink-200: tint-color($pink, 60%) !default; -$pink-300: tint-color($pink, 40%) !default; -$pink-400: tint-color($pink, 20%) !default; -$pink-500: $pink !default; -$pink-600: shade-color($pink, 20%) !default; -$pink-700: shade-color($pink, 40%) !default; -$pink-800: shade-color($pink, 60%) !default; -$pink-900: shade-color($pink, 80%) !default; - -$red-100: tint-color($red, 80%) !default; -$red-200: tint-color($red, 60%) !default; -$red-300: tint-color($red, 40%) !default; -$red-400: tint-color($red, 20%) !default; -$red-500: $red !default; -$red-600: shade-color($red, 20%) !default; -$red-700: shade-color($red, 40%) !default; -$red-800: shade-color($red, 60%) !default; -$red-900: shade-color($red, 80%) !default; - -$orange-100: tint-color($orange, 80%) !default; -$orange-200: tint-color($orange, 60%) !default; -$orange-300: tint-color($orange, 40%) !default; -$orange-400: tint-color($orange, 20%) !default; -$orange-500: $orange !default; -$orange-600: shade-color($orange, 20%) !default; -$orange-700: shade-color($orange, 40%) !default; -$orange-800: shade-color($orange, 60%) !default; -$orange-900: shade-color($orange, 80%) !default; - -$yellow-100: tint-color($yellow, 80%) !default; -$yellow-200: tint-color($yellow, 60%) !default; -$yellow-300: tint-color($yellow, 40%) !default; -$yellow-400: tint-color($yellow, 20%) !default; -$yellow-500: $yellow !default; -$yellow-600: shade-color($yellow, 20%) !default; -$yellow-700: shade-color($yellow, 40%) !default; -$yellow-800: shade-color($yellow, 60%) !default; -$yellow-900: shade-color($yellow, 80%) !default; - -$green-100: tint-color($green, 80%) !default; -$green-200: tint-color($green, 60%) !default; -$green-300: tint-color($green, 40%) !default; -$green-400: tint-color($green, 20%) !default; -$green-500: $green !default; -$green-600: shade-color($green, 20%) !default; -$green-700: shade-color($green, 40%) !default; -$green-800: shade-color($green, 60%) !default; -$green-900: shade-color($green, 80%) !default; - -$teal-100: tint-color($teal, 80%) !default; -$teal-200: tint-color($teal, 60%) !default; -$teal-300: tint-color($teal, 40%) !default; -$teal-400: tint-color($teal, 20%) !default; -$teal-500: $teal !default; -$teal-600: shade-color($teal, 20%) !default; -$teal-700: shade-color($teal, 40%) !default; -$teal-800: shade-color($teal, 60%) !default; -$teal-900: shade-color($teal, 80%) !default; - -$cyan-100: tint-color($cyan, 80%) !default; -$cyan-200: tint-color($cyan, 60%) !default; -$cyan-300: tint-color($cyan, 40%) !default; -$cyan-400: tint-color($cyan, 20%) !default; -$cyan-500: $cyan !default; -$cyan-600: shade-color($cyan, 20%) !default; -$cyan-700: shade-color($cyan, 40%) !default; -$cyan-800: shade-color($cyan, 60%) !default; -$cyan-900: shade-color($cyan, 80%) !default; - -$blues: ( - "blue-100": $blue-100, - "blue-200": $blue-200, - "blue-300": $blue-300, - "blue-400": $blue-400, - "blue-500": $blue-500, - "blue-600": $blue-600, - "blue-700": $blue-700, - "blue-800": $blue-800, - "blue-900": $blue-900 -) !default; - -$indigos: ( - "indigo-100": $indigo-100, - "indigo-200": $indigo-200, - "indigo-300": $indigo-300, - "indigo-400": $indigo-400, - "indigo-500": $indigo-500, - "indigo-600": $indigo-600, - "indigo-700": $indigo-700, - "indigo-800": $indigo-800, - "indigo-900": $indigo-900 -) !default; - -$purples: ( - "purple-100": $purple-100, - "purple-200": $purple-200, - "purple-300": $purple-300, - "purple-400": $purple-400, - "purple-500": $purple-500, - "purple-600": $purple-600, - "purple-700": $purple-700, - "purple-800": $purple-800, - "purple-900": $purple-900 -) !default; - -$pinks: ( - "pink-100": $pink-100, - "pink-200": $pink-200, - "pink-300": $pink-300, - "pink-400": $pink-400, - "pink-500": $pink-500, - "pink-600": $pink-600, - "pink-700": $pink-700, - "pink-800": $pink-800, - "pink-900": $pink-900 -) !default; - -$reds: ( - "red-100": $red-100, - "red-200": $red-200, - "red-300": $red-300, - "red-400": $red-400, - "red-500": $red-500, - "red-600": $red-600, - "red-700": $red-700, - "red-800": $red-800, - "red-900": $red-900 -) !default; - -$oranges: ( - "orange-100": $orange-100, - "orange-200": $orange-200, - "orange-300": $orange-300, - "orange-400": $orange-400, - "orange-500": $orange-500, - "orange-600": $orange-600, - "orange-700": $orange-700, - "orange-800": $orange-800, - "orange-900": $orange-900 -) !default; - -$yellows: ( - "yellow-100": $yellow-100, - "yellow-200": $yellow-200, - "yellow-300": $yellow-300, - "yellow-400": $yellow-400, - "yellow-500": $yellow-500, - "yellow-600": $yellow-600, - "yellow-700": $yellow-700, - "yellow-800": $yellow-800, - "yellow-900": $yellow-900 -) !default; - -$greens: ( - "green-100": $green-100, - "green-200": $green-200, - "green-300": $green-300, - "green-400": $green-400, - "green-500": $green-500, - "green-600": $green-600, - "green-700": $green-700, - "green-800": $green-800, - "green-900": $green-900 -) !default; - -$teals: ( - "teal-100": $teal-100, - "teal-200": $teal-200, - "teal-300": $teal-300, - "teal-400": $teal-400, - "teal-500": $teal-500, - "teal-600": $teal-600, - "teal-700": $teal-700, - "teal-800": $teal-800, - "teal-900": $teal-900 -) !default; - -$cyans: ( - "cyan-100": $cyan-100, - "cyan-200": $cyan-200, - "cyan-300": $cyan-300, - "cyan-400": $cyan-400, - "cyan-500": $cyan-500, - "cyan-600": $cyan-600, - "cyan-700": $cyan-700, - "cyan-800": $cyan-800, - "cyan-900": $cyan-900 -) !default; -// fusv-enable - -// scss-docs-start theme-color-variables -$primary: $blue !default; -$secondary: $gray-600 !default; -$success: $green !default; -$info: $cyan !default; -$warning: $yellow !default; -$danger: $red !default; -$light: $gray-100 !default; -$dark: $gray-900 !default; -// scss-docs-end theme-color-variables - -// scss-docs-start theme-colors-map -$theme-colors: ( - "primary": $primary, - "secondary": $secondary, - "success": $success, - "info": $info, - "warning": $warning, - "danger": $danger, - "light": $light, - "dark": $dark -) !default; -// scss-docs-end theme-colors-map - -// scss-docs-start theme-text-variables -$primary-text-emphasis: shade-color($primary, 60%) !default; -$secondary-text-emphasis: shade-color($secondary, 60%) !default; -$success-text-emphasis: shade-color($success, 60%) !default; -$info-text-emphasis: shade-color($info, 60%) !default; -$warning-text-emphasis: shade-color($warning, 60%) !default; -$danger-text-emphasis: shade-color($danger, 60%) !default; -$light-text-emphasis: $gray-700 !default; -$dark-text-emphasis: $gray-700 !default; -// scss-docs-end theme-text-variables - -// scss-docs-start theme-bg-subtle-variables -$primary-bg-subtle: tint-color($primary, 80%) !default; -$secondary-bg-subtle: tint-color($secondary, 80%) !default; -$success-bg-subtle: tint-color($success, 80%) !default; -$info-bg-subtle: tint-color($info, 80%) !default; -$warning-bg-subtle: tint-color($warning, 80%) !default; -$danger-bg-subtle: tint-color($danger, 80%) !default; -$light-bg-subtle: mix($gray-100, $white) !default; -$dark-bg-subtle: $gray-400 !default; -// scss-docs-end theme-bg-subtle-variables - -// scss-docs-start theme-border-subtle-variables -$primary-border-subtle: tint-color($primary, 60%) !default; -$secondary-border-subtle: tint-color($secondary, 60%) !default; -$success-border-subtle: tint-color($success, 60%) !default; -$info-border-subtle: tint-color($info, 60%) !default; -$warning-border-subtle: tint-color($warning, 60%) !default; -$danger-border-subtle: tint-color($danger, 60%) !default; -$light-border-subtle: $gray-200 !default; -$dark-border-subtle: $gray-500 !default; -// scss-docs-end theme-border-subtle-variables - -// Characters which are escaped by the escape-svg function -$escaped-characters: ( - ("<", "%3c"), - (">", "%3e"), - ("#", "%23"), - ("(", "%28"), - (")", "%29"), -) !default; - -// Options -// -// Quickly modify global styling by enabling or disabling optional features. - -$enable-caret: true !default; -$enable-rounded: true !default; -$enable-shadows: false !default; -$enable-gradients: false !default; -$enable-transitions: true !default; -$enable-reduced-motion: true !default; -$enable-smooth-scroll: true !default; -$enable-grid-classes: true !default; -$enable-container-classes: true !default; -$enable-cssgrid: false !default; -$enable-button-pointers: true !default; -$enable-rfs: true !default; -$enable-validation-icons: true !default; -$enable-negative-margins: false !default; -$enable-deprecation-messages: true !default; -$enable-important-utilities: true !default; - -$enable-dark-mode: true !default; -$color-mode-type: data !default; // `data` or `media-query` - -// Prefix for :root CSS variables - -$variable-prefix: bs- !default; // Deprecated in v5.2.0 for the shorter `$prefix` -$prefix: $variable-prefix !default; - -// Gradient -// -// The gradient which is added to components if `$enable-gradients` is `true` -// This gradient is also added to elements with `.bg-gradient` -// scss-docs-start variable-gradient -$gradient: linear-gradient(180deg, rgba($white, .15), rgba($white, 0)) !default; -// scss-docs-end variable-gradient - -// Spacing -// -// Control the default styling of most Bootstrap elements by modifying these -// variables. Mostly focused on spacing. -// You can add more entries to the $spacers map, should you need more variation. - -// scss-docs-start spacer-variables-maps -$spacer: 1rem !default; -$spacers: ( - 0: 0, - 1: $spacer * .25, - 2: $spacer * .5, - 3: $spacer, - 4: $spacer * 1.5, - 5: $spacer * 3, -) !default; -// scss-docs-end spacer-variables-maps - -// Position -// -// Define the edge positioning anchors of the position utilities. - -// scss-docs-start position-map -$position-values: ( - 0: 0, - 50: 50%, - 100: 100% -) !default; -// scss-docs-end position-map - -// Body -// -// Settings for the `` element. - -$body-text-align: null !default; -$body-color: $gray-900 !default; -$body-bg: $white !default; - -$body-secondary-color: rgba($body-color, .75) !default; -$body-secondary-bg: $gray-200 !default; - -$body-tertiary-color: rgba($body-color, .5) !default; -$body-tertiary-bg: $gray-100 !default; - -$body-emphasis-color: $black !default; - -// Links -// -// Style anchor elements. - -$link-color: $primary !default; -$link-decoration: underline !default; -$link-shade-percentage: 20% !default; -$link-hover-color: shift-color($link-color, $link-shade-percentage) !default; -$link-hover-decoration: null !default; - -$stretched-link-pseudo-element: after !default; -$stretched-link-z-index: 1 !default; - -// Icon links -// scss-docs-start icon-link-variables -$icon-link-gap: .375rem !default; -$icon-link-underline-offset: .25em !default; -$icon-link-icon-size: 1em !default; -$icon-link-icon-transition: .2s ease-in-out transform !default; -$icon-link-icon-transform: translate3d(.25em, 0, 0) !default; -// scss-docs-end icon-link-variables - -// Paragraphs -// -// Style p element. - -$paragraph-margin-bottom: 1rem !default; - - -// Grid breakpoints -// -// Define the minimum dimensions at which your layout will change, -// adapting to different screen sizes, for use in media queries. - -// scss-docs-start grid-breakpoints -$grid-breakpoints: ( - xs: 0, - sm: 576px, - md: 768px, - lg: 992px, - xl: 1200px, - xxl: 1400px -) !default; -// scss-docs-end grid-breakpoints - -@include _assert-ascending($grid-breakpoints, "$grid-breakpoints"); -@include _assert-starts-at-zero($grid-breakpoints, "$grid-breakpoints"); - - -// Grid containers -// -// Define the maximum width of `.container` for different screen sizes. - -// scss-docs-start container-max-widths -$container-max-widths: ( - sm: 540px, - md: 720px, - lg: 960px, - xl: 1140px, - xxl: 1320px -) !default; -// scss-docs-end container-max-widths - -@include _assert-ascending($container-max-widths, "$container-max-widths"); - - -// Grid columns -// -// Set the number of columns and specify the width of the gutters. - -$grid-columns: 12 !default; -$grid-gutter-width: 1.5rem !default; -$grid-row-columns: 6 !default; - -// Container padding - -$container-padding-x: $grid-gutter-width !default; - - -// Components -// -// Define common padding and border radius sizes and more. - -// scss-docs-start border-variables -$border-width: 1px !default; -$border-widths: ( - 1: 1px, - 2: 2px, - 3: 3px, - 4: 4px, - 5: 5px -) !default; -$border-style: solid !default; -$border-color: $gray-300 !default; -$border-color-translucent: rgba($black, .175) !default; -// scss-docs-end border-variables - -// scss-docs-start border-radius-variables -$border-radius: .375rem !default; -$border-radius-sm: .25rem !default; -$border-radius-lg: .5rem !default; -$border-radius-xl: 1rem !default; -$border-radius-xxl: 2rem !default; -$border-radius-pill: 50rem !default; -// scss-docs-end border-radius-variables -// fusv-disable -$border-radius-2xl: $border-radius-xxl !default; // Deprecated in v5.3.0 -// fusv-enable - -// scss-docs-start box-shadow-variables -$box-shadow: 0 .5rem 1rem rgba($black, .15) !default; -$box-shadow-sm: 0 .125rem .25rem rgba($black, .075) !default; -$box-shadow-lg: 0 1rem 3rem rgba($black, .175) !default; -$box-shadow-inset: inset 0 1px 2px rgba($black, .075) !default; -// scss-docs-end box-shadow-variables - -$component-active-color: $white !default; -$component-active-bg: $primary !default; - -// scss-docs-start focus-ring-variables -$focus-ring-width: .25rem !default; -$focus-ring-opacity: .25 !default; -$focus-ring-color: rgba($primary, $focus-ring-opacity) !default; -$focus-ring-blur: 0 !default; -$focus-ring-box-shadow: 0 0 $focus-ring-blur $focus-ring-width $focus-ring-color !default; -// scss-docs-end focus-ring-variables - -// scss-docs-start caret-variables -$caret-width: .3em !default; -$caret-vertical-align: $caret-width * .85 !default; -$caret-spacing: $caret-width * .85 !default; -// scss-docs-end caret-variables - -$transition-base: all .2s ease-in-out !default; -$transition-fade: opacity .15s linear !default; -// scss-docs-start collapse-transition -$transition-collapse: height .35s ease !default; -$transition-collapse-width: width .35s ease !default; -// scss-docs-end collapse-transition - -// stylelint-disable function-disallowed-list -// scss-docs-start aspect-ratios -$aspect-ratios: ( - "1x1": 100%, - "4x3": calc(3 / 4 * 100%), - "16x9": calc(9 / 16 * 100%), - "21x9": calc(9 / 21 * 100%) -) !default; -// scss-docs-end aspect-ratios -// stylelint-enable function-disallowed-list - -// Typography -// -// Font, line-height, and color for body text, headings, and more. - -// scss-docs-start font-variables -// stylelint-disable value-keyword-case -$font-family-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; -$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default; -// stylelint-enable value-keyword-case -$font-family-base: var(--#{$prefix}font-sans-serif) !default; -$font-family-code: var(--#{$prefix}font-monospace) !default; - -// $font-size-root affects the value of `rem`, which is used for as well font sizes, paddings, and margins -// $font-size-base affects the font size of the body text -$font-size-root: null !default; -$font-size-base: 1rem !default; // Assumes the browser default, typically `16px` -$font-size-sm: $font-size-base * .875 !default; -$font-size-lg: $font-size-base * 1.25 !default; - -$font-weight-lighter: lighter !default; -$font-weight-light: 300 !default; -$font-weight-normal: 400 !default; -$font-weight-medium: 500 !default; -$font-weight-semibold: 600 !default; -$font-weight-bold: 700 !default; -$font-weight-bolder: bolder !default; - -$font-weight-base: $font-weight-normal !default; - -$line-height-base: 1.5 !default; -$line-height-sm: 1.25 !default; -$line-height-lg: 2 !default; - -$h1-font-size: $font-size-base * 2.5 !default; -$h2-font-size: $font-size-base * 2 !default; -$h3-font-size: $font-size-base * 1.75 !default; -$h4-font-size: $font-size-base * 1.5 !default; -$h5-font-size: $font-size-base * 1.25 !default; -$h6-font-size: $font-size-base !default; -// scss-docs-end font-variables - -// scss-docs-start font-sizes -$font-sizes: ( - 1: $h1-font-size, - 2: $h2-font-size, - 3: $h3-font-size, - 4: $h4-font-size, - 5: $h5-font-size, - 6: $h6-font-size -) !default; -// scss-docs-end font-sizes - -// scss-docs-start headings-variables -$headings-margin-bottom: $spacer * .5 !default; -$headings-font-family: null !default; -$headings-font-style: null !default; -$headings-font-weight: 500 !default; -$headings-line-height: 1.2 !default; -$headings-color: inherit !default; -// scss-docs-end headings-variables - -// scss-docs-start display-headings -$display-font-sizes: ( - 1: 5rem, - 2: 4.5rem, - 3: 4rem, - 4: 3.5rem, - 5: 3rem, - 6: 2.5rem -) !default; - -$display-font-family: null !default; -$display-font-style: null !default; -$display-font-weight: 300 !default; -$display-line-height: $headings-line-height !default; -// scss-docs-end display-headings - -// scss-docs-start type-variables -$lead-font-size: $font-size-base * 1.25 !default; -$lead-font-weight: 300 !default; - -$small-font-size: .875em !default; - -$sub-sup-font-size: .75em !default; - -// fusv-disable -$text-muted: var(--#{$prefix}secondary-color) !default; // Deprecated in 5.3.0 -// fusv-enable - -$initialism-font-size: $small-font-size !default; - -$blockquote-margin-y: $spacer !default; -$blockquote-font-size: $font-size-base * 1.25 !default; -$blockquote-footer-color: $gray-600 !default; -$blockquote-footer-font-size: $small-font-size !default; - -$hr-margin-y: $spacer !default; -$hr-color: inherit !default; - -// fusv-disable -$hr-bg-color: null !default; // Deprecated in v5.2.0 -$hr-height: null !default; // Deprecated in v5.2.0 -// fusv-enable - -$hr-border-color: null !default; // Allows for inherited colors -$hr-border-width: var(--#{$prefix}border-width) !default; -$hr-opacity: .25 !default; - -// scss-docs-start vr-variables -$vr-border-width: var(--#{$prefix}border-width) !default; -// scss-docs-end vr-variables - -$legend-margin-bottom: .5rem !default; -$legend-font-size: 1.5rem !default; -$legend-font-weight: null !default; - -$dt-font-weight: $font-weight-bold !default; - -$list-inline-padding: .5rem !default; - -$mark-padding: .1875em !default; -$mark-color: $body-color !default; -$mark-bg: $yellow-100 !default; -// scss-docs-end type-variables - - -// Tables -// -// Customizes the `.table` component with basic values, each used across all table variations. - -// scss-docs-start table-variables -$table-cell-padding-y: .5rem !default; -$table-cell-padding-x: .5rem !default; -$table-cell-padding-y-sm: .25rem !default; -$table-cell-padding-x-sm: .25rem !default; - -$table-cell-vertical-align: top !default; - -$table-color: var(--#{$prefix}emphasis-color) !default; -$table-bg: var(--#{$prefix}body-bg) !default; -$table-accent-bg: transparent !default; - -$table-th-font-weight: null !default; - -$table-striped-color: $table-color !default; -$table-striped-bg-factor: .05 !default; -$table-striped-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-striped-bg-factor) !default; - -$table-active-color: $table-color !default; -$table-active-bg-factor: .1 !default; -$table-active-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-active-bg-factor) !default; - -$table-hover-color: $table-color !default; -$table-hover-bg-factor: .075 !default; -$table-hover-bg: rgba(var(--#{$prefix}emphasis-color-rgb), $table-hover-bg-factor) !default; - -$table-border-factor: .2 !default; -$table-border-width: var(--#{$prefix}border-width) !default; -$table-border-color: var(--#{$prefix}border-color) !default; - -$table-striped-order: odd !default; -$table-striped-columns-order: even !default; - -$table-group-separator-color: currentcolor !default; - -$table-caption-color: var(--#{$prefix}secondary-color) !default; - -$table-bg-scale: -80% !default; -// scss-docs-end table-variables - -// scss-docs-start table-loop -$table-variants: ( - "primary": shift-color($primary, $table-bg-scale), - "secondary": shift-color($secondary, $table-bg-scale), - "success": shift-color($success, $table-bg-scale), - "info": shift-color($info, $table-bg-scale), - "warning": shift-color($warning, $table-bg-scale), - "danger": shift-color($danger, $table-bg-scale), - "light": $light, - "dark": $dark, -) !default; -// scss-docs-end table-loop - - -// Buttons + Forms -// -// Shared variables that are reassigned to `$input-` and `$btn-` specific variables. - -// scss-docs-start input-btn-variables -$input-btn-padding-y: .375rem !default; -$input-btn-padding-x: .75rem !default; -$input-btn-font-family: null !default; -$input-btn-font-size: $font-size-base !default; -$input-btn-line-height: $line-height-base !default; - -$input-btn-focus-width: $focus-ring-width !default; -$input-btn-focus-color-opacity: $focus-ring-opacity !default; -$input-btn-focus-color: $focus-ring-color !default; -$input-btn-focus-blur: $focus-ring-blur !default; -$input-btn-focus-box-shadow: $focus-ring-box-shadow !default; - -$input-btn-padding-y-sm: .25rem !default; -$input-btn-padding-x-sm: .5rem !default; -$input-btn-font-size-sm: $font-size-sm !default; - -$input-btn-padding-y-lg: .5rem !default; -$input-btn-padding-x-lg: 1rem !default; -$input-btn-font-size-lg: $font-size-lg !default; - -$input-btn-border-width: var(--#{$prefix}border-width) !default; -// scss-docs-end input-btn-variables - - -// Buttons -// -// For each of Bootstrap's buttons, define text, background, and border color. - -// scss-docs-start btn-variables -$btn-color: var(--#{$prefix}body-color) !default; -$btn-padding-y: $input-btn-padding-y !default; -$btn-padding-x: $input-btn-padding-x !default; -$btn-font-family: $input-btn-font-family !default; -$btn-font-size: $input-btn-font-size !default; -$btn-line-height: $input-btn-line-height !default; -$btn-white-space: null !default; // Set to `nowrap` to prevent text wrapping - -$btn-padding-y-sm: $input-btn-padding-y-sm !default; -$btn-padding-x-sm: $input-btn-padding-x-sm !default; -$btn-font-size-sm: $input-btn-font-size-sm !default; - -$btn-padding-y-lg: $input-btn-padding-y-lg !default; -$btn-padding-x-lg: $input-btn-padding-x-lg !default; -$btn-font-size-lg: $input-btn-font-size-lg !default; - -$btn-border-width: $input-btn-border-width !default; - -$btn-font-weight: $font-weight-normal !default; -$btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; -$btn-focus-width: $input-btn-focus-width !default; -$btn-focus-box-shadow: $input-btn-focus-box-shadow !default; -$btn-disabled-opacity: .65 !default; -$btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default; - -$btn-link-color: var(--#{$prefix}link-color) !default; -$btn-link-hover-color: var(--#{$prefix}link-hover-color) !default; -$btn-link-disabled-color: $gray-600 !default; -$btn-link-focus-shadow-rgb: to-rgb(mix(color-contrast($link-color), $link-color, 15%)) !default; - -// Allows for customizing button radius independently from global border radius -$btn-border-radius: var(--#{$prefix}border-radius) !default; -$btn-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$btn-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; - -$btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$btn-hover-bg-shade-amount: 15% !default; -$btn-hover-bg-tint-amount: 15% !default; -$btn-hover-border-shade-amount: 20% !default; -$btn-hover-border-tint-amount: 10% !default; -$btn-active-bg-shade-amount: 20% !default; -$btn-active-bg-tint-amount: 20% !default; -$btn-active-border-shade-amount: 25% !default; -$btn-active-border-tint-amount: 10% !default; -// scss-docs-end btn-variables - - -// Forms - -// scss-docs-start form-text-variables -$form-text-margin-top: .25rem !default; -$form-text-font-size: $small-font-size !default; -$form-text-font-style: null !default; -$form-text-font-weight: null !default; -$form-text-color: var(--#{$prefix}secondary-color) !default; -// scss-docs-end form-text-variables - -// scss-docs-start form-label-variables -$form-label-margin-bottom: .5rem !default; -$form-label-font-size: null !default; -$form-label-font-style: null !default; -$form-label-font-weight: null !default; -$form-label-color: null !default; -// scss-docs-end form-label-variables - -// scss-docs-start form-input-variables -$input-padding-y: $input-btn-padding-y !default; -$input-padding-x: $input-btn-padding-x !default; -$input-font-family: $input-btn-font-family !default; -$input-font-size: $input-btn-font-size !default; -$input-font-weight: $font-weight-base !default; -$input-line-height: $input-btn-line-height !default; - -$input-padding-y-sm: $input-btn-padding-y-sm !default; -$input-padding-x-sm: $input-btn-padding-x-sm !default; -$input-font-size-sm: $input-btn-font-size-sm !default; - -$input-padding-y-lg: $input-btn-padding-y-lg !default; -$input-padding-x-lg: $input-btn-padding-x-lg !default; -$input-font-size-lg: $input-btn-font-size-lg !default; - -$input-bg: var(--#{$prefix}body-bg) !default; -$input-disabled-color: null !default; -$input-disabled-bg: var(--#{$prefix}secondary-bg) !default; -$input-disabled-border-color: null !default; - -$input-color: var(--#{$prefix}body-color) !default; -$input-border-color: var(--#{$prefix}border-color) !default; -$input-border-width: $input-btn-border-width !default; -$input-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$input-border-radius: var(--#{$prefix}border-radius) !default; -$input-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$input-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; - -$input-focus-bg: $input-bg !default; -$input-focus-border-color: tint-color($component-active-bg, 50%) !default; -$input-focus-color: $input-color !default; -$input-focus-width: $input-btn-focus-width !default; -$input-focus-box-shadow: $input-btn-focus-box-shadow !default; - -$input-placeholder-color: var(--#{$prefix}secondary-color) !default; -$input-plaintext-color: var(--#{$prefix}body-color) !default; - -$input-height-border: calc(#{$input-border-width} * 2) !default; // stylelint-disable-line function-disallowed-list - -$input-height-inner: add($input-line-height * 1em, $input-padding-y * 2) !default; -$input-height-inner-half: add($input-line-height * .5em, $input-padding-y) !default; -$input-height-inner-quarter: add($input-line-height * .25em, $input-padding-y * .5) !default; - -$input-height: add($input-line-height * 1em, add($input-padding-y * 2, $input-height-border, false)) !default; -$input-height-sm: add($input-line-height * 1em, add($input-padding-y-sm * 2, $input-height-border, false)) !default; -$input-height-lg: add($input-line-height * 1em, add($input-padding-y-lg * 2, $input-height-border, false)) !default; - -$input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$form-color-width: 3rem !default; -// scss-docs-end form-input-variables - -// scss-docs-start form-check-variables -$form-check-input-width: 1em !default; -$form-check-min-height: $font-size-base * $line-height-base !default; -$form-check-padding-start: $form-check-input-width + .5em !default; -$form-check-margin-bottom: .125rem !default; -$form-check-label-color: null !default; -$form-check-label-cursor: null !default; -$form-check-transition: null !default; - -$form-check-input-active-filter: brightness(90%) !default; - -$form-check-input-bg: $input-bg !default; -$form-check-input-border: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color) !default; -$form-check-input-border-radius: .25em !default; -$form-check-radio-border-radius: 50% !default; -$form-check-input-focus-border: $input-focus-border-color !default; -$form-check-input-focus-box-shadow: $focus-ring-box-shadow !default; - -$form-check-input-checked-color: $component-active-color !default; -$form-check-input-checked-bg-color: $component-active-bg !default; -$form-check-input-checked-border-color: $form-check-input-checked-bg-color !default; -$form-check-input-checked-bg-image: url("data:image/svg+xml,") !default; -$form-check-radio-checked-bg-image: url("data:image/svg+xml,") !default; - -$form-check-input-indeterminate-color: $component-active-color !default; -$form-check-input-indeterminate-bg-color: $component-active-bg !default; -$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color !default; -$form-check-input-indeterminate-bg-image: url("data:image/svg+xml,") !default; - -$form-check-input-disabled-opacity: .5 !default; -$form-check-label-disabled-opacity: $form-check-input-disabled-opacity !default; -$form-check-btn-check-disabled-opacity: $btn-disabled-opacity !default; - -$form-check-inline-margin-end: 1rem !default; -// scss-docs-end form-check-variables - -// scss-docs-start form-switch-variables -$form-switch-color: rgba($black, .25) !default; -$form-switch-width: 2em !default; -$form-switch-padding-start: $form-switch-width + .5em !default; -$form-switch-bg-image: url("data:image/svg+xml,") !default; -$form-switch-border-radius: $form-switch-width !default; -$form-switch-transition: background-position .15s ease-in-out !default; - -$form-switch-focus-color: $input-focus-border-color !default; -$form-switch-focus-bg-image: url("data:image/svg+xml,") !default; - -$form-switch-checked-color: $component-active-color !default; -$form-switch-checked-bg-image: url("data:image/svg+xml,") !default; -$form-switch-checked-bg-position: right center !default; -// scss-docs-end form-switch-variables - -// scss-docs-start input-group-variables -$input-group-addon-padding-y: $input-padding-y !default; -$input-group-addon-padding-x: $input-padding-x !default; -$input-group-addon-font-weight: $input-font-weight !default; -$input-group-addon-color: $input-color !default; -$input-group-addon-bg: var(--#{$prefix}tertiary-bg) !default; -$input-group-addon-border-color: $input-border-color !default; -// scss-docs-end input-group-variables - -// scss-docs-start form-select-variables -$form-select-padding-y: $input-padding-y !default; -$form-select-padding-x: $input-padding-x !default; -$form-select-font-family: $input-font-family !default; -$form-select-font-size: $input-font-size !default; -$form-select-indicator-padding: $form-select-padding-x * 3 !default; // Extra padding for background-image -$form-select-font-weight: $input-font-weight !default; -$form-select-line-height: $input-line-height !default; -$form-select-color: $input-color !default; -$form-select-bg: $input-bg !default; -$form-select-disabled-color: null !default; -$form-select-disabled-bg: $input-disabled-bg !default; -$form-select-disabled-border-color: $input-disabled-border-color !default; -$form-select-bg-position: right $form-select-padding-x center !default; -$form-select-bg-size: 16px 12px !default; // In pixels because image dimensions -$form-select-indicator-color: $gray-800 !default; -$form-select-indicator: url("data:image/svg+xml,") !default; - -$form-select-feedback-icon-padding-end: $form-select-padding-x * 2.5 + $form-select-indicator-padding !default; -$form-select-feedback-icon-position: center right $form-select-indicator-padding !default; -$form-select-feedback-icon-size: $input-height-inner-half $input-height-inner-half !default; - -$form-select-border-width: $input-border-width !default; -$form-select-border-color: $input-border-color !default; -$form-select-border-radius: $input-border-radius !default; -$form-select-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$form-select-focus-border-color: $input-focus-border-color !default; -$form-select-focus-width: $input-focus-width !default; -$form-select-focus-box-shadow: 0 0 0 $form-select-focus-width $input-btn-focus-color !default; - -$form-select-padding-y-sm: $input-padding-y-sm !default; -$form-select-padding-x-sm: $input-padding-x-sm !default; -$form-select-font-size-sm: $input-font-size-sm !default; -$form-select-border-radius-sm: $input-border-radius-sm !default; - -$form-select-padding-y-lg: $input-padding-y-lg !default; -$form-select-padding-x-lg: $input-padding-x-lg !default; -$form-select-font-size-lg: $input-font-size-lg !default; -$form-select-border-radius-lg: $input-border-radius-lg !default; - -$form-select-transition: $input-transition !default; -// scss-docs-end form-select-variables - -// scss-docs-start form-range-variables -$form-range-track-width: 100% !default; -$form-range-track-height: .5rem !default; -$form-range-track-cursor: pointer !default; -$form-range-track-bg: var(--#{$prefix}secondary-bg) !default; -$form-range-track-border-radius: 1rem !default; -$form-range-track-box-shadow: var(--#{$prefix}box-shadow-inset) !default; - -$form-range-thumb-width: 1rem !default; -$form-range-thumb-height: $form-range-thumb-width !default; -$form-range-thumb-bg: $component-active-bg !default; -$form-range-thumb-border: 0 !default; -$form-range-thumb-border-radius: 1rem !default; -$form-range-thumb-box-shadow: 0 .1rem .25rem rgba($black, .1) !default; -$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow !default; -$form-range-thumb-focus-box-shadow-width: $input-focus-width !default; // For focus box shadow issue in Edge -$form-range-thumb-active-bg: tint-color($component-active-bg, 70%) !default; -$form-range-thumb-disabled-bg: var(--#{$prefix}secondary-color) !default; -$form-range-thumb-transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; -// scss-docs-end form-range-variables - -// scss-docs-start form-file-variables -$form-file-button-color: $input-color !default; -$form-file-button-bg: var(--#{$prefix}tertiary-bg) !default; -$form-file-button-hover-bg: var(--#{$prefix}secondary-bg) !default; -// scss-docs-end form-file-variables - -// scss-docs-start form-floating-variables -$form-floating-height: add(3.5rem, $input-height-border) !default; -$form-floating-line-height: 1.25 !default; -$form-floating-padding-x: $input-padding-x !default; -$form-floating-padding-y: 1rem !default; -$form-floating-input-padding-t: 1.625rem !default; -$form-floating-input-padding-b: .625rem !default; -$form-floating-label-height: 1.5em !default; -$form-floating-label-opacity: .65 !default; -$form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem) !default; -$form-floating-label-disabled-color: $gray-600 !default; -$form-floating-transition: opacity .1s ease-in-out, transform .1s ease-in-out !default; -// scss-docs-end form-floating-variables - -// Form validation - -// scss-docs-start form-feedback-variables -$form-feedback-margin-top: $form-text-margin-top !default; -$form-feedback-font-size: $form-text-font-size !default; -$form-feedback-font-style: $form-text-font-style !default; -$form-feedback-valid-color: $success !default; -$form-feedback-invalid-color: $danger !default; - -$form-feedback-icon-valid-color: $form-feedback-valid-color !default; -$form-feedback-icon-valid: url("data:image/svg+xml,") !default; -$form-feedback-icon-invalid-color: $form-feedback-invalid-color !default; -$form-feedback-icon-invalid: url("data:image/svg+xml,") !default; -// scss-docs-end form-feedback-variables - -// scss-docs-start form-validation-colors -$form-valid-color: $form-feedback-valid-color !default; -$form-valid-border-color: $form-feedback-valid-color !default; -$form-invalid-color: $form-feedback-invalid-color !default; -$form-invalid-border-color: $form-feedback-invalid-color !default; -// scss-docs-end form-validation-colors - -// scss-docs-start form-validation-states -$form-validation-states: ( - "valid": ( - "color": var(--#{$prefix}form-valid-color), - "icon": $form-feedback-icon-valid, - "tooltip-color": #fff, - "tooltip-bg-color": var(--#{$prefix}success), - "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}success-rgb), $input-btn-focus-color-opacity), - "border-color": var(--#{$prefix}form-valid-border-color), - ), - "invalid": ( - "color": var(--#{$prefix}form-invalid-color), - "icon": $form-feedback-icon-invalid, - "tooltip-color": #fff, - "tooltip-bg-color": var(--#{$prefix}danger), - "focus-box-shadow": 0 0 $input-btn-focus-blur $input-focus-width rgba(var(--#{$prefix}danger-rgb), $input-btn-focus-color-opacity), - "border-color": var(--#{$prefix}form-invalid-border-color), - ) -) !default; -// scss-docs-end form-validation-states - -// Z-index master list -// -// Warning: Avoid customizing these values. They're used for a bird's eye view -// of components dependent on the z-axis and are designed to all work together. - -// scss-docs-start zindex-stack -$zindex-dropdown: 1000 !default; -$zindex-sticky: 1020 !default; -$zindex-fixed: 1030 !default; -$zindex-offcanvas-backdrop: 1040 !default; -$zindex-offcanvas: 1045 !default; -$zindex-modal-backdrop: 1050 !default; -$zindex-modal: 1055 !default; -$zindex-popover: 1070 !default; -$zindex-tooltip: 1080 !default; -$zindex-toast: 1090 !default; -// scss-docs-end zindex-stack - -// scss-docs-start zindex-levels-map -$zindex-levels: ( - n1: -1, - 0: 0, - 1: 1, - 2: 2, - 3: 3 -) !default; -// scss-docs-end zindex-levels-map - - -// Navs - -// scss-docs-start nav-variables -$nav-link-padding-y: .5rem !default; -$nav-link-padding-x: 1rem !default; -$nav-link-font-size: null !default; -$nav-link-font-weight: null !default; -$nav-link-color: var(--#{$prefix}link-color) !default; -$nav-link-hover-color: var(--#{$prefix}link-hover-color) !default; -$nav-link-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out !default; -$nav-link-disabled-color: var(--#{$prefix}secondary-color) !default; -$nav-link-focus-box-shadow: $focus-ring-box-shadow !default; - -$nav-tabs-border-color: var(--#{$prefix}border-color) !default; -$nav-tabs-border-width: var(--#{$prefix}border-width) !default; -$nav-tabs-border-radius: var(--#{$prefix}border-radius) !default; -$nav-tabs-link-hover-border-color: var(--#{$prefix}secondary-bg) var(--#{$prefix}secondary-bg) $nav-tabs-border-color !default; -$nav-tabs-link-active-color: var(--#{$prefix}emphasis-color) !default; -$nav-tabs-link-active-bg: var(--#{$prefix}body-bg) !default; -$nav-tabs-link-active-border-color: var(--#{$prefix}border-color) var(--#{$prefix}border-color) $nav-tabs-link-active-bg !default; - -$nav-pills-border-radius: var(--#{$prefix}border-radius) !default; -$nav-pills-link-active-color: $component-active-color !default; -$nav-pills-link-active-bg: $component-active-bg !default; - -$nav-underline-gap: 1rem !default; -$nav-underline-border-width: .125rem !default; -$nav-underline-link-active-color: var(--#{$prefix}emphasis-color) !default; -// scss-docs-end nav-variables - - -// Navbar - -// scss-docs-start navbar-variables -$navbar-padding-y: $spacer * .5 !default; -$navbar-padding-x: null !default; - -$navbar-nav-link-padding-x: .5rem !default; - -$navbar-brand-font-size: $font-size-lg !default; -// Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link -$nav-link-height: $font-size-base * $line-height-base + $nav-link-padding-y * 2 !default; -$navbar-brand-height: $navbar-brand-font-size * $line-height-base !default; -$navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) * .5 !default; -$navbar-brand-margin-end: 1rem !default; - -$navbar-toggler-padding-y: .25rem !default; -$navbar-toggler-padding-x: .75rem !default; -$navbar-toggler-font-size: $font-size-lg !default; -$navbar-toggler-border-radius: $btn-border-radius !default; -$navbar-toggler-focus-width: $btn-focus-width !default; -$navbar-toggler-transition: box-shadow .15s ease-in-out !default; - -$navbar-light-color: rgba(var(--#{$prefix}emphasis-color-rgb), .65) !default; -$navbar-light-hover-color: rgba(var(--#{$prefix}emphasis-color-rgb), .8) !default; -$navbar-light-active-color: rgba(var(--#{$prefix}emphasis-color-rgb), 1) !default; -$navbar-light-disabled-color: rgba(var(--#{$prefix}emphasis-color-rgb), .3) !default; -$navbar-light-icon-color: rgba($body-color, .75) !default; -$navbar-light-toggler-icon-bg: url("data:image/svg+xml,") !default; -$navbar-light-toggler-border-color: rgba(var(--#{$prefix}emphasis-color-rgb), .15) !default; -$navbar-light-brand-color: $navbar-light-active-color !default; -$navbar-light-brand-hover-color: $navbar-light-active-color !default; -// scss-docs-end navbar-variables - -// scss-docs-start navbar-dark-variables -$navbar-dark-color: rgba($white, .55) !default; -$navbar-dark-hover-color: rgba($white, .75) !default; -$navbar-dark-active-color: $white !default; -$navbar-dark-disabled-color: rgba($white, .25) !default; -$navbar-dark-icon-color: $navbar-dark-color !default; -$navbar-dark-toggler-icon-bg: url("data:image/svg+xml,") !default; -$navbar-dark-toggler-border-color: rgba($white, .1) !default; -$navbar-dark-brand-color: $navbar-dark-active-color !default; -$navbar-dark-brand-hover-color: $navbar-dark-active-color !default; -// scss-docs-end navbar-dark-variables - - -// Dropdowns -// -// Dropdown menu container and contents. - -// scss-docs-start dropdown-variables -$dropdown-min-width: 10rem !default; -$dropdown-padding-x: 0 !default; -$dropdown-padding-y: .5rem !default; -$dropdown-spacer: .125rem !default; -$dropdown-font-size: $font-size-base !default; -$dropdown-color: var(--#{$prefix}body-color) !default; -$dropdown-bg: var(--#{$prefix}body-bg) !default; -$dropdown-border-color: var(--#{$prefix}border-color-translucent) !default; -$dropdown-border-radius: var(--#{$prefix}border-radius) !default; -$dropdown-border-width: var(--#{$prefix}border-width) !default; -$dropdown-inner-border-radius: calc(#{$dropdown-border-radius} - #{$dropdown-border-width}) !default; // stylelint-disable-line function-disallowed-list -$dropdown-divider-bg: $dropdown-border-color !default; -$dropdown-divider-margin-y: $spacer * .5 !default; -$dropdown-box-shadow: var(--#{$prefix}box-shadow) !default; - -$dropdown-link-color: var(--#{$prefix}body-color) !default; -$dropdown-link-hover-color: $dropdown-link-color !default; -$dropdown-link-hover-bg: var(--#{$prefix}tertiary-bg) !default; - -$dropdown-link-active-color: $component-active-color !default; -$dropdown-link-active-bg: $component-active-bg !default; - -$dropdown-link-disabled-color: var(--#{$prefix}tertiary-color) !default; - -$dropdown-item-padding-y: $spacer * .25 !default; -$dropdown-item-padding-x: $spacer !default; - -$dropdown-header-color: $gray-600 !default; -$dropdown-header-padding-x: $dropdown-item-padding-x !default; -$dropdown-header-padding-y: $dropdown-padding-y !default; -// fusv-disable -$dropdown-header-padding: $dropdown-header-padding-y $dropdown-header-padding-x !default; // Deprecated in v5.2.0 -// fusv-enable -// scss-docs-end dropdown-variables - -// scss-docs-start dropdown-dark-variables -$dropdown-dark-color: $gray-300 !default; -$dropdown-dark-bg: $gray-800 !default; -$dropdown-dark-border-color: $dropdown-border-color !default; -$dropdown-dark-divider-bg: $dropdown-divider-bg !default; -$dropdown-dark-box-shadow: null !default; -$dropdown-dark-link-color: $dropdown-dark-color !default; -$dropdown-dark-link-hover-color: $white !default; -$dropdown-dark-link-hover-bg: rgba($white, .15) !default; -$dropdown-dark-link-active-color: $dropdown-link-active-color !default; -$dropdown-dark-link-active-bg: $dropdown-link-active-bg !default; -$dropdown-dark-link-disabled-color: $gray-500 !default; -$dropdown-dark-header-color: $gray-500 !default; -// scss-docs-end dropdown-dark-variables - - -// Pagination - -// scss-docs-start pagination-variables -$pagination-padding-y: .375rem !default; -$pagination-padding-x: .75rem !default; -$pagination-padding-y-sm: .25rem !default; -$pagination-padding-x-sm: .5rem !default; -$pagination-padding-y-lg: .75rem !default; -$pagination-padding-x-lg: 1.5rem !default; - -$pagination-font-size: $font-size-base !default; - -$pagination-color: var(--#{$prefix}link-color) !default; -$pagination-bg: var(--#{$prefix}body-bg) !default; -$pagination-border-radius: var(--#{$prefix}border-radius) !default; -$pagination-border-width: var(--#{$prefix}border-width) !default; -$pagination-margin-start: calc(-1 * #{$pagination-border-width}) !default; // stylelint-disable-line function-disallowed-list -$pagination-border-color: var(--#{$prefix}border-color) !default; - -$pagination-focus-color: var(--#{$prefix}link-hover-color) !default; -$pagination-focus-bg: var(--#{$prefix}secondary-bg) !default; -$pagination-focus-box-shadow: $focus-ring-box-shadow !default; -$pagination-focus-outline: 0 !default; - -$pagination-hover-color: var(--#{$prefix}link-hover-color) !default; -$pagination-hover-bg: var(--#{$prefix}tertiary-bg) !default; -$pagination-hover-border-color: var(--#{$prefix}border-color) !default; // Todo in v6: remove this? - -$pagination-active-color: $component-active-color !default; -$pagination-active-bg: $component-active-bg !default; -$pagination-active-border-color: $component-active-bg !default; - -$pagination-disabled-color: var(--#{$prefix}secondary-color) !default; -$pagination-disabled-bg: var(--#{$prefix}secondary-bg) !default; -$pagination-disabled-border-color: var(--#{$prefix}border-color) !default; - -$pagination-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; - -$pagination-border-radius-sm: var(--#{$prefix}border-radius-sm) !default; -$pagination-border-radius-lg: var(--#{$prefix}border-radius-lg) !default; -// scss-docs-end pagination-variables - - -// Placeholders - -// scss-docs-start placeholders -$placeholder-opacity-max: .5 !default; -$placeholder-opacity-min: .2 !default; -// scss-docs-end placeholders - -// Cards - -// scss-docs-start card-variables -$card-spacer-y: $spacer !default; -$card-spacer-x: $spacer !default; -$card-title-spacer-y: $spacer * .5 !default; -$card-title-color: null !default; -$card-subtitle-color: null !default; -$card-border-width: var(--#{$prefix}border-width) !default; -$card-border-color: var(--#{$prefix}border-color-translucent) !default; -$card-border-radius: var(--#{$prefix}border-radius) !default; -$card-box-shadow: null !default; -$card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default; -$card-cap-padding-y: $card-spacer-y * .5 !default; -$card-cap-padding-x: $card-spacer-x !default; -$card-cap-bg: rgba(var(--#{$prefix}body-color-rgb), .03) !default; -$card-cap-color: null !default; -$card-height: null !default; -$card-color: null !default; -$card-bg: var(--#{$prefix}body-bg) !default; -$card-img-overlay-padding: $spacer !default; -$card-group-margin: $grid-gutter-width * .5 !default; -// scss-docs-end card-variables - -// Accordion - -// scss-docs-start accordion-variables -$accordion-padding-y: 1rem !default; -$accordion-padding-x: 1.25rem !default; -$accordion-color: var(--#{$prefix}body-color) !default; -$accordion-bg: var(--#{$prefix}body-bg) !default; -$accordion-border-width: var(--#{$prefix}border-width) !default; -$accordion-border-color: var(--#{$prefix}border-color) !default; -$accordion-border-radius: var(--#{$prefix}border-radius) !default; -$accordion-inner-border-radius: subtract($accordion-border-radius, $accordion-border-width) !default; - -$accordion-body-padding-y: $accordion-padding-y !default; -$accordion-body-padding-x: $accordion-padding-x !default; - -$accordion-button-padding-y: $accordion-padding-y !default; -$accordion-button-padding-x: $accordion-padding-x !default; -$accordion-button-color: var(--#{$prefix}body-color) !default; -$accordion-button-bg: var(--#{$prefix}accordion-bg) !default; -$accordion-transition: $btn-transition, border-radius .15s ease !default; -$accordion-button-active-bg: var(--#{$prefix}primary-bg-subtle) !default; -$accordion-button-active-color: var(--#{$prefix}primary-text-emphasis) !default; - -// fusv-disable -$accordion-button-focus-border-color: $input-focus-border-color !default; // Deprecated in v5.3.3 -// fusv-enable -$accordion-button-focus-box-shadow: $btn-focus-box-shadow !default; - -$accordion-icon-width: 1.25rem !default; -$accordion-icon-color: $body-color !default; -$accordion-icon-active-color: $primary-text-emphasis !default; -$accordion-icon-transition: transform .2s ease-in-out !default; -$accordion-icon-transform: rotate(-180deg) !default; - -$accordion-button-icon: url("data:image/svg+xml,") !default; -$accordion-button-active-icon: url("data:image/svg+xml,") !default; -// scss-docs-end accordion-variables - -// Tooltips - -// scss-docs-start tooltip-variables -$tooltip-font-size: $font-size-sm !default; -$tooltip-max-width: 200px !default; -$tooltip-color: var(--#{$prefix}body-bg) !default; -$tooltip-bg: var(--#{$prefix}emphasis-color) !default; -$tooltip-border-radius: var(--#{$prefix}border-radius) !default; -$tooltip-opacity: .9 !default; -$tooltip-padding-y: $spacer * .25 !default; -$tooltip-padding-x: $spacer * .5 !default; -$tooltip-margin: null !default; // TODO: remove this in v6 - -$tooltip-arrow-width: .8rem !default; -$tooltip-arrow-height: .4rem !default; -// fusv-disable -$tooltip-arrow-color: null !default; // Deprecated in Bootstrap 5.2.0 for CSS variables -// fusv-enable -// scss-docs-end tooltip-variables - -// Form tooltips must come after regular tooltips -// scss-docs-start tooltip-feedback-variables -$form-feedback-tooltip-padding-y: $tooltip-padding-y !default; -$form-feedback-tooltip-padding-x: $tooltip-padding-x !default; -$form-feedback-tooltip-font-size: $tooltip-font-size !default; -$form-feedback-tooltip-line-height: null !default; -$form-feedback-tooltip-opacity: $tooltip-opacity !default; -$form-feedback-tooltip-border-radius: $tooltip-border-radius !default; -// scss-docs-end tooltip-feedback-variables - - -// Popovers - -// scss-docs-start popover-variables -$popover-font-size: $font-size-sm !default; -$popover-bg: var(--#{$prefix}body-bg) !default; -$popover-max-width: 276px !default; -$popover-border-width: var(--#{$prefix}border-width) !default; -$popover-border-color: var(--#{$prefix}border-color-translucent) !default; -$popover-border-radius: var(--#{$prefix}border-radius-lg) !default; -$popover-inner-border-radius: calc(#{$popover-border-radius} - #{$popover-border-width}) !default; // stylelint-disable-line function-disallowed-list -$popover-box-shadow: var(--#{$prefix}box-shadow) !default; - -$popover-header-font-size: $font-size-base !default; -$popover-header-bg: var(--#{$prefix}secondary-bg) !default; -$popover-header-color: $headings-color !default; -$popover-header-padding-y: .5rem !default; -$popover-header-padding-x: $spacer !default; - -$popover-body-color: var(--#{$prefix}body-color) !default; -$popover-body-padding-y: $spacer !default; -$popover-body-padding-x: $spacer !default; - -$popover-arrow-width: 1rem !default; -$popover-arrow-height: .5rem !default; -// scss-docs-end popover-variables - -// fusv-disable -// Deprecated in Bootstrap 5.2.0 for CSS variables -$popover-arrow-color: $popover-bg !default; -$popover-arrow-outer-color: var(--#{$prefix}border-color-translucent) !default; -// fusv-enable - - -// Toasts - -// scss-docs-start toast-variables -$toast-max-width: 350px !default; -$toast-padding-x: .75rem !default; -$toast-padding-y: .5rem !default; -$toast-font-size: .875rem !default; -$toast-color: null !default; -$toast-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; -$toast-border-width: var(--#{$prefix}border-width) !default; -$toast-border-color: var(--#{$prefix}border-color-translucent) !default; -$toast-border-radius: var(--#{$prefix}border-radius) !default; -$toast-box-shadow: var(--#{$prefix}box-shadow) !default; -$toast-spacing: $container-padding-x !default; - -$toast-header-color: var(--#{$prefix}secondary-color) !default; -$toast-header-background-color: rgba(var(--#{$prefix}body-bg-rgb), .85) !default; -$toast-header-border-color: $toast-border-color !default; -// scss-docs-end toast-variables - - -// Badges - -// scss-docs-start badge-variables -$badge-font-size: .75em !default; -$badge-font-weight: $font-weight-bold !default; -$badge-color: $white !default; -$badge-padding-y: .35em !default; -$badge-padding-x: .65em !default; -$badge-border-radius: var(--#{$prefix}border-radius) !default; -// scss-docs-end badge-variables - - -// Modals - -// scss-docs-start modal-variables -$modal-inner-padding: $spacer !default; - -$modal-footer-margin-between: .5rem !default; - -$modal-dialog-margin: .5rem !default; -$modal-dialog-margin-y-sm-up: 1.75rem !default; - -$modal-title-line-height: $line-height-base !default; - -$modal-content-color: var(--#{$prefix}body-color) !default; -$modal-content-bg: var(--#{$prefix}body-bg) !default; -$modal-content-border-color: var(--#{$prefix}border-color-translucent) !default; -$modal-content-border-width: var(--#{$prefix}border-width) !default; -$modal-content-border-radius: var(--#{$prefix}border-radius-lg) !default; -$modal-content-inner-border-radius: subtract($modal-content-border-radius, $modal-content-border-width) !default; -$modal-content-box-shadow-xs: var(--#{$prefix}box-shadow-sm) !default; -$modal-content-box-shadow-sm-up: var(--#{$prefix}box-shadow) !default; - -$modal-backdrop-bg: $black !default; -$modal-backdrop-opacity: .5 !default; - -$modal-header-border-color: var(--#{$prefix}border-color) !default; -$modal-header-border-width: $modal-content-border-width !default; -$modal-header-padding-y: $modal-inner-padding !default; -$modal-header-padding-x: $modal-inner-padding !default; -$modal-header-padding: $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility - -$modal-footer-bg: null !default; -$modal-footer-border-color: $modal-header-border-color !default; -$modal-footer-border-width: $modal-header-border-width !default; - -$modal-sm: 300px !default; -$modal-md: 500px !default; -$modal-lg: 800px !default; -$modal-xl: 1140px !default; - -$modal-fade-transform: translate(0, -50px) !default; -$modal-show-transform: none !default; -$modal-transition: transform .3s ease-out !default; -$modal-scale-transform: scale(1.02) !default; -// scss-docs-end modal-variables - - -// Alerts -// -// Define alert colors, border radius, and padding. - -// scss-docs-start alert-variables -$alert-padding-y: $spacer !default; -$alert-padding-x: $spacer !default; -$alert-margin-bottom: 1rem !default; -$alert-border-radius: var(--#{$prefix}border-radius) !default; -$alert-link-font-weight: $font-weight-bold !default; -$alert-border-width: var(--#{$prefix}border-width) !default; -$alert-dismissible-padding-r: $alert-padding-x * 3 !default; // 3x covers width of x plus default padding on either side -// scss-docs-end alert-variables - -// fusv-disable -$alert-bg-scale: -80% !default; // Deprecated in v5.2.0, to be removed in v6 -$alert-border-scale: -70% !default; // Deprecated in v5.2.0, to be removed in v6 -$alert-color-scale: 40% !default; // Deprecated in v5.2.0, to be removed in v6 -// fusv-enable - -// Progress bars - -// scss-docs-start progress-variables -$progress-height: 1rem !default; -$progress-font-size: $font-size-base * .75 !default; -$progress-bg: var(--#{$prefix}secondary-bg) !default; -$progress-border-radius: var(--#{$prefix}border-radius) !default; -$progress-box-shadow: var(--#{$prefix}box-shadow-inset) !default; -$progress-bar-color: $white !default; -$progress-bar-bg: $primary !default; -$progress-bar-animation-timing: 1s linear infinite !default; -$progress-bar-transition: width .6s ease !default; -// scss-docs-end progress-variables - - -// List group - -// scss-docs-start list-group-variables -$list-group-color: var(--#{$prefix}body-color) !default; -$list-group-bg: var(--#{$prefix}body-bg) !default; -$list-group-border-color: var(--#{$prefix}border-color) !default; -$list-group-border-width: var(--#{$prefix}border-width) !default; -$list-group-border-radius: var(--#{$prefix}border-radius) !default; - -$list-group-item-padding-y: $spacer * .5 !default; -$list-group-item-padding-x: $spacer !default; -// fusv-disable -$list-group-item-bg-scale: -80% !default; // Deprecated in v5.3.0 -$list-group-item-color-scale: 40% !default; // Deprecated in v5.3.0 -// fusv-enable - -$list-group-hover-bg: var(--#{$prefix}tertiary-bg) !default; -$list-group-active-color: $component-active-color !default; -$list-group-active-bg: $component-active-bg !default; -$list-group-active-border-color: $list-group-active-bg !default; - -$list-group-disabled-color: var(--#{$prefix}secondary-color) !default; -$list-group-disabled-bg: $list-group-bg !default; - -$list-group-action-color: var(--#{$prefix}secondary-color) !default; -$list-group-action-hover-color: var(--#{$prefix}emphasis-color) !default; - -$list-group-action-active-color: var(--#{$prefix}body-color) !default; -$list-group-action-active-bg: var(--#{$prefix}secondary-bg) !default; -// scss-docs-end list-group-variables - - -// Image thumbnails - -// scss-docs-start thumbnail-variables -$thumbnail-padding: .25rem !default; -$thumbnail-bg: var(--#{$prefix}body-bg) !default; -$thumbnail-border-width: var(--#{$prefix}border-width) !default; -$thumbnail-border-color: var(--#{$prefix}border-color) !default; -$thumbnail-border-radius: var(--#{$prefix}border-radius) !default; -$thumbnail-box-shadow: var(--#{$prefix}box-shadow-sm) !default; -// scss-docs-end thumbnail-variables - - -// Figures - -// scss-docs-start figure-variables -$figure-caption-font-size: $small-font-size !default; -$figure-caption-color: var(--#{$prefix}secondary-color) !default; -// scss-docs-end figure-variables - - -// Breadcrumbs - -// scss-docs-start breadcrumb-variables -$breadcrumb-font-size: null !default; -$breadcrumb-padding-y: 0 !default; -$breadcrumb-padding-x: 0 !default; -$breadcrumb-item-padding-x: .5rem !default; -$breadcrumb-margin-bottom: 1rem !default; -$breadcrumb-bg: null !default; -$breadcrumb-divider-color: var(--#{$prefix}secondary-color) !default; -$breadcrumb-active-color: var(--#{$prefix}secondary-color) !default; -$breadcrumb-divider: quote("/") !default; -$breadcrumb-divider-flipped: $breadcrumb-divider !default; -$breadcrumb-border-radius: null !default; -// scss-docs-end breadcrumb-variables - -// Carousel - -// scss-docs-start carousel-variables -$carousel-control-color: $white !default; -$carousel-control-width: 15% !default; -$carousel-control-opacity: .5 !default; -$carousel-control-hover-opacity: .9 !default; -$carousel-control-transition: opacity .15s ease !default; -$carousel-control-icon-filter: null !default; - -$carousel-indicator-width: 30px !default; -$carousel-indicator-height: 3px !default; -$carousel-indicator-hit-area-height: 10px !default; -$carousel-indicator-spacer: 3px !default; -$carousel-indicator-opacity: .5 !default; -$carousel-indicator-active-bg: $white !default; -$carousel-indicator-active-opacity: 1 !default; -$carousel-indicator-transition: opacity .6s ease !default; - -$carousel-caption-width: 70% !default; -$carousel-caption-color: $white !default; -$carousel-caption-padding-y: 1.25rem !default; -$carousel-caption-spacer: 1.25rem !default; - -$carousel-control-icon-width: 2rem !default; - -$carousel-control-prev-icon-bg: url("data:image/svg+xml,") !default; -$carousel-control-next-icon-bg: url("data:image/svg+xml,") !default; - -$carousel-transition-duration: .6s !default; -$carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`) -// scss-docs-end carousel-variables - -// scss-docs-start carousel-dark-variables -$carousel-dark-indicator-active-bg: $black !default; // Deprecated in v5.3.4 -$carousel-dark-caption-color: $black !default; // Deprecated in v5.3.4 -$carousel-dark-control-icon-filter: invert(1) grayscale(100) !default; // Deprecated in v5.3.4 -// scss-docs-end carousel-dark-variables - - -// Spinners - -// scss-docs-start spinner-variables -$spinner-width: 2rem !default; -$spinner-height: $spinner-width !default; -$spinner-vertical-align: -.125em !default; -$spinner-border-width: .25em !default; -$spinner-animation-speed: .75s !default; - -$spinner-width-sm: 1rem !default; -$spinner-height-sm: $spinner-width-sm !default; -$spinner-border-width-sm: .2em !default; -// scss-docs-end spinner-variables - - -// Close - -// scss-docs-start close-variables -$btn-close-width: 1em !default; -$btn-close-height: $btn-close-width !default; -$btn-close-padding-x: .25em !default; -$btn-close-padding-y: $btn-close-padding-x !default; -$btn-close-color: $black !default; -$btn-close-bg: url("data:image/svg+xml,") !default; -$btn-close-focus-shadow: $focus-ring-box-shadow !default; -$btn-close-opacity: .5 !default; -$btn-close-hover-opacity: .75 !default; -$btn-close-focus-opacity: 1 !default; -$btn-close-disabled-opacity: .25 !default; -$btn-close-filter: null !default; -$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default; // Deprecated in v5.3.4 -// scss-docs-end close-variables - - -// Offcanvas - -// scss-docs-start offcanvas-variables -$offcanvas-padding-y: $modal-inner-padding !default; -$offcanvas-padding-x: $modal-inner-padding !default; -$offcanvas-horizontal-width: 400px !default; -$offcanvas-vertical-height: 30vh !default; -$offcanvas-transition-duration: .3s !default; -$offcanvas-border-color: $modal-content-border-color !default; -$offcanvas-border-width: $modal-content-border-width !default; -$offcanvas-title-line-height: $modal-title-line-height !default; -$offcanvas-bg-color: var(--#{$prefix}body-bg) !default; -$offcanvas-color: var(--#{$prefix}body-color) !default; -$offcanvas-box-shadow: $modal-content-box-shadow-xs !default; -$offcanvas-backdrop-bg: $modal-backdrop-bg !default; -$offcanvas-backdrop-opacity: $modal-backdrop-opacity !default; -// scss-docs-end offcanvas-variables - -// Code - -$code-font-size: $small-font-size !default; -$code-color: $pink !default; - -$kbd-padding-y: .1875rem !default; -$kbd-padding-x: .375rem !default; -$kbd-font-size: $code-font-size !default; -$kbd-color: var(--#{$prefix}body-bg) !default; -$kbd-bg: var(--#{$prefix}body-color) !default; -$nested-kbd-font-weight: null !default; // Deprecated in v5.2.0, removing in v6 - -$pre-color: null !default; - -@import "variables-dark"; // TODO: can be removed safely in v6, only here to avoid breaking changes in v5.3 diff --git a/assets/stylesheets/bootstrap/bootstrap-grid.scss b/assets/stylesheets/bootstrap/bootstrap-grid.scss new file mode 100644 index 00000000..7dff9bf7 --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-grid.scss @@ -0,0 +1,68 @@ +@use "banner" with ( + $file: "Grid" +); + +@use "config" as *; +@use "functions" as *; + +@forward "utilities"; // Make utilities available downstream +@use "utilities" as *; // Bring utilities into the current namespace + +@forward "layout/containers"; +@forward "layout/grid"; + +// stylelint-disable-next-line scss/dollar-variable-default +$utilities: map-get-multiple( + $utilities, + ( + "display", + "order", + "grid-column-counts", + "grid-columns", + "grid-auto-flow", + "gap", + "row-gap", + "column-gap", + "flex", + "flex-direction", + "flex-grow", + "flex-shrink", + "flex-wrap", + "justify-content", + "justify-items", + "align-items", + "align-content", + "align-self", + "place-items", + "margin", + "margin-x", + "margin-y", + "margin-top", + "margin-end", + "margin-bottom", + "margin-start", + "negative-margin", + "negative-margin-x", + "negative-margin-y", + "negative-margin-top", + "negative-margin-end", + "negative-margin-bottom", + "negative-margin-start", + "padding", + "padding-x", + "padding-y", + "padding-top", + "padding-end", + "padding-bottom", + "padding-start", + ) +); + +// check-unused-imports-disable-next-line — side-effect import: generates utility CSS. +@use "utilities/api"; + +:root { + @each $name, $value in $breakpoints { + --breakpoint-#{$name}: #{$value}; + } +} diff --git a/assets/stylesheets/bootstrap/bootstrap-reboot.scss b/assets/stylesheets/bootstrap/bootstrap-reboot.scss new file mode 100644 index 00000000..d8b59518 --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-reboot.scss @@ -0,0 +1,6 @@ +@use "banner" with ( + $file: "Reboot" +); + +@forward "root"; +@forward "content/reboot"; diff --git a/assets/stylesheets/bootstrap/bootstrap-utilities.scss b/assets/stylesheets/bootstrap/bootstrap-utilities.scss new file mode 100644 index 00000000..44ffa78f --- /dev/null +++ b/assets/stylesheets/bootstrap/bootstrap-utilities.scss @@ -0,0 +1,13 @@ +@use "banner" with ( + $file: "Utilities" +); + +// Layout & components +@forward "root"; + +// Helpers +@forward "helpers"; + +// Utilities +@forward "utilities"; +@forward "utilities/api"; diff --git a/assets/stylesheets/bootstrap/buttons/_button-group.scss b/assets/stylesheets/bootstrap/buttons/_button-group.scss new file mode 100644 index 00000000..0a86f95e --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_button-group.scss @@ -0,0 +1,135 @@ +@use "../mixins/border-radius" as *; + +@layer components { + // Make the div behave like a button + .btn-group, + .btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > [class*="btn-"] { + position: relative; + flex: 1 1 auto; + + &:hover { + z-index: 1; + } + } + + > .btn-check:has(input:checked), + > [class*="btn-"]:active, + > [class*="btn-"].active { + z-index: 2; + } + + > .btn-check:has(input:focus), + > [class*="btn-"]:focus { + z-index: 3; + } + } + + .btn-group-divider { + > [class*="btn-"] + [class*="btn-"] { + &::before { + position: absolute; + // top: 25%; + // bottom: 25%; + // left: calc(var(--btn-border-width) * -1); + z-index: 3; + // width: var(--btn-border-width); + content: ""; + background-color: var(--btn-color); + opacity: .25; + } + } + } + + .btn-group:where(.btn-group-divider) { + > [class*="btn-"] + [class*="btn-"] { + &::before { + top: 25%; + bottom: 25%; + left: calc(var(--btn-border-width) * -1); + width: var(--btn-border-width); + } + } + } + + .btn-group-vertical:where(.btn-group-divider) { + > [class*="btn-"] + [class*="btn-"] { + &::before { + top: calc(var(--btn-border-width) * -1); + right: var(--btn-padding-x); + left: var(--btn-padding-x); + height: var(--btn-border-width); + } + } + } + + // Optional: Group multiple button groups together for a toolbar + .btn-toolbar { + display: flex; + flex-wrap: wrap; + gap: .5rem; + justify-content: flex-start; + + .input-group { + width: auto; + } + } + + .btn-group { + @include border-radius(var(--btn-border-radius)); + + // Prevent double borders when buttons are next to each other + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) { + margin-inline-start: calc(-1 * var(--btn-border-width)); + } + + // Reset rounded corners + > [class*="btn-"]:not(:last-child, :has(+ .menu)), + > .btn-group:not(:last-child) > [class*="btn-"] { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is not the first child + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) > [class*="btn-"] { + @include border-start-radius(0); + } + } + + // + // Vertical button groups + // + + .btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > [class*="btn-"], + > .btn-group { + width: 100%; + } + + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: calc(-1 * var(--btn-border-width)); + } + + // Reset rounded corners + > [class*="btn-"]:not(:last-child, :has(+ .menu)), + > .btn-group:not(:last-child) > [class*="btn-"] { + @include border-bottom-radius(0); + } + + // The top radius should be 0 if the button is not the first child + > [class*="btn-"]:not(:first-child), + > .btn-group:not(:first-child) > [class*="btn-"] { + @include border-top-radius(0); + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/_button.scss b/assets/stylesheets/bootstrap/buttons/_button.scss new file mode 100644 index 00000000..a778c09a --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_button.scss @@ -0,0 +1,451 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:meta"; +@use "sass:string"; +@use "../config" as *; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function, scss/dollar-variable-default + +$button-tokens: () !default; + +// scss-docs-start btn-tokens +$button-tokens: defaults( + ( + --btn-min-height: var(--btn-input-min-height), + --btn-padding-x: var(--btn-input-padding-x), + --btn-padding-y: var(--btn-input-padding-y), + --btn-font-size: var(--btn-input-font-size), + --btn-font-weight: var(--btn-input-font-weight), + --btn-line-height: var(--btn-input-line-height), + --btn-color: var(--fg-body), + --btn-white-space: nowrap, + --btn-border-width: var(--border-width), + --btn-border-color: transparent, + --btn-border-radius: var(--radius-5), + --btn-hover-border-color: transparent, + --btn-disabled-opacity: .65, + --btn-transition-timing: .15s ease-in-out, + --btn-transition-property: "color, background-color, border-color, box-shadow", + --btn-transition: var(--btn-transition-property) var(--btn-transition-timing), + ), + $button-tokens +); +// scss-docs-end btn-tokens + +$button-link-tokens: () !default; + +// scss-docs-start button-link-tokens +$button-link-tokens: defaults( + ( + --btn-font-weight: var(--font-weight-normal), + --btn-color: var(--link-color), + --btn-bg: transparent, + --btn-border-color: transparent, + --btn-hover-color: var(--link-hover-color), + --btn-hover-bg: transparent, + --btn-hover-border-color: transparent, + --btn-active-color: var(--link-hover-color), + --btn-active-bg: transparent, + --btn-active-border-color: transparent, + --btn-disabled-color: var(--fg-3), + --btn-disabled-border-color: transparent, + ), + $button-link-tokens +); +// scss-docs-end button-link-tokens + +$button-styled-tokens: () !default; + +// scss-docs-start button-styled-tokens +$button-styled-tokens: defaults( + ( + --btn-gradient-start: rgb(255 255 255 / 12.5%), + --btn-gradient-end: rgb(0 0 0 / 7.5%) , + --btn-border-mix-color: #000, + --btn-border-mix-amount: 10%, + --btn-border-hover-mix-amount: 12.5%, + --btn-border-active-mix-amount: 20%, + --btn-shadow: "0 1px 2px rgb(0 0 0 / 15%), inset 0 1px 0 rgb(255 255 255 / 10%)", + --btn-active-shadow: inset 0 2px 4px rgb(0 0 0 / .15) , + ), + $button-styled-tokens +); +// scss-docs-end button-styled-tokens + +// scss-docs-start button-sizes +$button-sizes: () !default; +$button-sizes: defaults( + ("xs", "sm", "lg"), + $button-sizes +); +// scss-docs-end button-sizes + +$button-variants: () !default; + +// scss-docs-start btn-variants +$button-variants: defaults( + ( + "solid": ( + "base": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ), + "hover": ( + "bg": "bg", + "border-color": "bg", + "color": "contrast" + ), + "active": ( + "bg": "bg", + "border-color": "bg", + "color": "contrast" + ) + ), + "outline": ( + "base": ( + "bg": "transparent", + "color": "fg", + "border-color": "border" + ), + "hover": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ), + "active": ( + "bg": "bg", + "color": "contrast", + "border-color": "bg" + ) + ), + "subtle": ( + "base": ( + "bg": "bg-subtle", + "color": "fg", + "border-color": "transparent" + ), + "hover": ( + "bg": ("bg-muted", "bg-subtle"), + "color": "fg-emphasis" + ), + "active": ( + "bg": "bg-subtle", + "color": "fg-emphasis" + ) + ), + "text": ( + "base": ( + "color": "fg", + "bg": "transparent", + "border-color": "transparent" + ), + "hover": ( + "color": "fg", + "bg": "bg-subtle" + ), + "active": ( + "color": "fg", + "bg": "bg-subtle" + ) + ) + ), + $button-variants +); +// scss-docs-end btn-variants +// stylelint-enable custom-property-no-missing-var-function, scss/dollar-variable-default + +// +// Base styles +// + +// scss-docs-start btn-variant-selectors +$btn-variant-selectors: (string.unquote(".btn"), string.unquote(".btn-link"), string.unquote(".btn-icon")) !default; +@each $variant, $config in $button-variants { + $btn-variant-selectors: list.append($btn-variant-selectors, string.unquote(".btn-#{$variant}"), comma); +} +// scss-docs-end btn-variant-selectors + +@layer components { + #{$btn-variant-selectors} { + @include tokens($button-tokens); + + display: inline-flex; + gap: var(--btn-gap, .25rem); + align-items: center; + justify-content: center; + min-height: var(--btn-min-height); + padding: var(--btn-padding-y) var(--btn-padding-x); + // font-family: var(--btn-font-family); + font-size: var(--btn-font-size); + font-weight: var(--btn-font-weight); + line-height: var(--btn-line-height); + color: var(--btn-color); + text-decoration: none; + white-space: var(--btn-white-space); + vertical-align: middle; + // stylelint-disable-next-line scss/at-function-named-arguments + cursor: if(sass($enable-button-pointers): pointer; else: null); + user-select: none; + background-color: var(--btn-bg, var(--bg-2)); + border: var(--btn-border-width) solid var(--btn-border-color); + @include border-radius(var(--btn-border-radius)); + @include transition(var(--btn-transition)); + + &:hover { + color: var(--btn-hover-color); + background-color: var(--btn-hover-bg, var(--bg-3)); + border-color: var(--btn-hover-border-color); + } + + &:focus-visible { + @include focus-ring(true); + --focus-ring-offset: 1px; + } + + &.active, + &.show { + color: var(--btn-active-color); + background-color: var(--btn-active-bg, var(--bg-3)); + border-color: var(--btn-active-border-color); + + &:focus-visible { + @include focus-ring(true); + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + color: var(--btn-disabled-color); + pointer-events: none; + background-color: var(--btn-disabled-bg, var(--bg-1)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-disabled-border-color); + opacity: var(--btn-disabled-opacity); + } + } + + // Main button style generator mixin + // Generate button variant classes (e.g., .btn-solid, .btn-outline, etc.) + // scss-docs-start btn-variant-mixin + @each $variant, $config in $button-variants { + .btn-#{$variant} { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-#{$property}: transparent; + } @else { + --btn-#{$property}: var(--theme-#{$value}); + } + } + + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --btn-active-#{$property}: var(--theme-#{$value}); + } @else { + --btn-active-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-disabled-#{$property}: transparent; + } @else { + --btn-disabled-#{$property}: var(--theme-#{$value}); + } + } + + &:hover { + @each $property, $value in map.get($button-variants, $variant, "hover") { + @if $value == "transparent" { + --btn-hover-#{$property}: transparent; + } @else if meta.type-of($value) == "list" { + $first-value: list.nth($value, 1); + $second-value: list.nth($value, 2); + --btn-hover-#{$property}: color-mix(in oklch, var(--theme-#{$first-value}) 50%, var(--theme-#{$second-value})); + } @else if $value == "bg-subtle" { + --btn-hover-#{$property}: var(--theme-#{$value}); + } @else { + --btn-hover-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .95) calc(c * 1.1) h); + } + } + } + + &:focus-visible { + outline-color: var(--theme-focus-ring); + } + + &:active, + &.active, + &.btn-check:has(input:checked) { + @each $property, $value in map.get($button-variants, $variant, "active") { + @if $value == "transparent" { + --btn-active-#{$property}: transparent; + } @else if $value == "bg-subtle" { + --btn-active-#{$property}: var(--theme-#{$value}); + } @else { + --btn-active-#{$property}: oklch(from var(--theme-#{$value}) calc(l * .9) calc(c * 1.15) h); + } + } + } + + // Disabled state for toggle buttons + &:disabled, + &.disabled, + &.btn-check:has(input:disabled) { + @each $property, $value in map.get($button-variants, $variant, "base") { + @if $value == "transparent" { + --btn-disabled-#{$property}: transparent; + } @else { + --btn-disabled-#{$property}: var(--theme-#{$value}); + } + } + } + } + } + // scss-docs-end btn-variant-mixin + + // + // Link buttons + // + + // Make a button look and behave like a link + .btn-link { + @include tokens($button-link-tokens); + + color: var(--theme-fg, var(--btn-color)); + text-decoration: var(--link-decoration); + + @if $enable-gradients { + background-image: none; + } + + &:focus-visible { + color: var(--theme-fg, var(--btn-color)); + } + + &:hover { + color: var(--theme-fg-emphasis, var(--btn-hover-color)); + } + + // No need for an active state here + } + + // + // Button Sizes + // + + // Generate button size classes from the $button-sizes map + // Skip "md" as it's the default size for .btn + + // scss-docs-start btn-sizes-loop + @each $size, $_ in $button-sizes { + .btn-#{$size}, + .btn-group-#{$size} > [class*="btn-"] { + --btn-min-height: var(--btn-input-#{$size}-min-height); + --btn-padding-y: var(--btn-input-#{$size}-padding-y); + --btn-padding-x: var(--btn-input-#{$size}-padding-x); + --btn-font-size: var(--btn-input-#{$size}-font-size); + --btn-line-height: var(--btn-input-#{$size}-line-height); + --btn-border-radius: var(--btn-input-#{$size}-border-radius); + } + } + // scss-docs-end btn-sizes-loop + + .btn-icon { + align-items: center; + justify-content: center; + aspect-ratio: 1; + padding: 0; + } + + // + // Toggle buttons (.btn-check) + // + // Checkbox and radio inputs that look like buttons. Add .btn-check to a + // label with button classes, with the input nested inside. + // + // Example: Toggle + + .btn-check { + > input { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; + } + + &:has(input:checked) { + color: var(--btn-active-color); + background-color: var(--btn-active-bg, var(--bg-3)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-active-border-color); + @include box-shadow(var(--btn-active-shadow)); + } + + &:has(input:focus-visible) { + @include focus-ring(true); + --focus-ring-offset: 1px; + } + + &:has(input:disabled) { + color: var(--btn-disabled-color); + pointer-events: none; + background-color: var(--btn-disabled-bg, var(--bg-1)); + // stylelint-disable-next-line scss/at-function-named-arguments + background-image: if(sass($enable-gradients): none; else: null); + border-color: var(--btn-disabled-border-color); + opacity: var(--btn-disabled-opacity); + @include box-shadow(none); + } + } + + // + // Styled buttons + // + // Add visual depth with gradients and shadows. Customize via CSS variables. + + .btn-styled { + @include tokens($button-styled-tokens); + + background-image: + linear-gradient( + to bottom, + var(--btn-gradient-start), + var(--btn-gradient-end) + ); + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-mix-amount)); + box-shadow: var(--btn-shadow); + + &:hover { + background-image: + linear-gradient( + to bottom, + var(--btn-gradient-start), + var(--btn-gradient-end) + ); + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-hover-mix-amount)); + } + + &:active, + &.active { + background-image: none; + border-color: color-mix(in lab, var(--theme-bg), var(--btn-border-mix-color) var(--btn-border-active-mix-amount)); + box-shadow: var(--btn-active-shadow); + } + + &:disabled, + &.disabled { + background-image: none; + box-shadow: none; + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/_close.scss b/assets/stylesheets/bootstrap/buttons/_close.scss new file mode 100644 index 00000000..7cb76120 --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/_close.scss @@ -0,0 +1,63 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; +@use "../mixins/tokens" as *; + +$btn-close-tokens: () !default; + +// scss-docs-start btn-close-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$btn-close-tokens: defaults( + ( + --btn-close-size: 1.5rem, + --btn-close-color: inherit, + --btn-close-icon: #{escape-svg(url("data:image/svg+xml,"))}, + --btn-close-opacity: .5, + --btn-close-hover-opacity: .75, + --btn-close-focus-opacity: .85, + --btn-close-disabled-opacity: .25, + ), + $btn-close-tokens +); +// scss-docs-end btn-close-tokens + +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +@layer components { + .btn-close { + @include tokens($btn-close-tokens); + + box-sizing: content-box; + min-width: var(--btn-close-size); + min-height: var(--btn-close-size); + padding: 0; + color: var(--btn-close-color); + background-color: currentcolor; + border: 0; // for button elements + @include border-radius(var(--radius-5)); + opacity: var(--btn-close-opacity); + @include mask-icon(var(--btn-close-icon)); + + // Override 's hover style + &:hover { + color: var(--btn-close-color); + text-decoration: none; + opacity: var(--btn-close-hover-opacity); + } + + &:focus-visible { + opacity: var(--btn-close-focus-opacity); + @include focus-ring(); + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: var(--btn-close-disabled-opacity); + } + } +} diff --git a/assets/stylesheets/bootstrap/buttons/index.scss b/assets/stylesheets/bootstrap/buttons/index.scss new file mode 100644 index 00000000..0122a4ef --- /dev/null +++ b/assets/stylesheets/bootstrap/buttons/index.scss @@ -0,0 +1,3 @@ +@forward "button"; +@forward "button-group"; +@forward "close"; diff --git a/assets/stylesheets/bootstrap/content/_images.scss b/assets/stylesheets/bootstrap/content/_images.scss new file mode 100644 index 00000000..c1f17212 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_images.scss @@ -0,0 +1,74 @@ +@use "../functions" as *; +@use "../mixins/image" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/tokens" as *; + +$thumbnail-tokens: () !default; + +// scss-docs-start thumbnail-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$thumbnail-tokens: defaults( + ( + --thumbnail-padding: .25rem, + --thumbnail-bg: var(--bg-body), + --thumbnail-border-width: var(--border-width), + --thumbnail-border-color: var(--border-color), + --thumbnail-border-radius: var(--radius-5), + --thumbnail-box-shadow: var(--box-shadow-sm), + ), + $thumbnail-tokens +); +// scss-docs-end thumbnail-tokens + +$figure-tokens: () !default; + +// scss-docs-start figure-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$figure-tokens: defaults( + ( + --figure-gap: calc(var(--spacer) * .5), + --figure-caption-font-size: var(--font-size-sm), + --figure-caption-color: var(--fg-3), + ), + $figure-tokens +); +// scss-docs-end figure-tokens + +@layer content { + // Responsive images (ensure images don't scale beyond their parents) + // + // This is purposefully opt-in via an explicit class rather than being the default for all ``s. + // We previously tried the "images are responsive by default" approach in Bootstrap v2, + // and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps) + // which weren't expecting the images within themselves to be involuntarily resized. + // See also https://github.com/twbs/bootstrap/issues/18178 + .img-fluid { + @include img-fluid(); + } + + .img-thumbnail { + @include tokens($thumbnail-tokens); + padding: var(--thumbnail-padding); + background-color: var(--thumbnail-bg); + border: var(--thumbnail-border-width) solid var(--thumbnail-border-color); + @include border-radius(var(--thumbnail-border-radius)); + @include box-shadow(var(--thumbnail-box-shadow)); + + // Keep them at most 100% wide + @include img-fluid(); + } + + .figure { + @include tokens($figure-tokens); + // Ensures the caption's text aligns with the image. + display: flex; + flex-direction: column; + gap: var(--figure-gap); + } + + .figure-caption { + font-size: var(--figure-caption-font-size); + color: var(--figure-caption-color); + } +} diff --git a/assets/stylesheets/bootstrap/content/_prose.scss b/assets/stylesheets/bootstrap/content/_prose.scss new file mode 100644 index 00000000..0408e9ba --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_prose.scss @@ -0,0 +1,143 @@ +@use "../functions" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function +$prose-tokens: () !default; + +// scss-docs-start prose-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$prose-tokens: defaults( + ( + --content-font-size: 1rem, + --content-line-height: 1.5, + --content-gap: calc(var(--content-font-size) * var(--content-line-height)), + --heading-color: light-dark(var(--gray-900), var(--white)), + ), + $prose-tokens +); +// scss-docs-end prose-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer content { + .prose { + @include tokens($prose-tokens); + position: relative; + display: flex; + flex-direction: column; + gap: var(--content-gap); + max-width: 1000px; + margin-inline: auto; + font-size: var(--content-font-size); + line-height: var(--content-line-height); + + @media (width >= 1024px) { + --content-font-size: var(--font-size-md); + --content-line-height: 1.625; + // --content-gap: calc(var(--content-font-size) * var(--content-line-height)); + } + + :where(p, ul, ol, dl, pre, table, blockquote):not(:where(.not-prose, .not-prose *)) { + margin-block: 0; + } + + :where(ul, ol):not([class], :where(.not-prose, .not-prose *)) li:not(:last-child) { + margin-bottom: calc(var(--content-gap) / 4); + } + + :where(li ul, li ol):not(:where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) / 4); + } + + :where(hr):not(:where(.not-prose, .not-prose *)) { + margin: calc(var(--content-gap) * 1.5) 0; + border: 0; + border-block-start: var(--border-width) solid var(--hr-border-color); + } + + :where(h1, h2, h3, h4, h5, h6):not([class], :where(.not-prose, .not-prose *)) { + margin-top: 0; + margin-bottom: calc(var(--content-gap) / -2); + font-weight: 500; + line-height: 1.25; + + code { + font-weight: 600; + color: inherit; + } + } + + :where(h1, h2):not(:first-child, :where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) * .75); + } + + :where(h3, h4, h5, h6):not(:first-child, :where(.not-prose, .not-prose *)) { + margin-top: calc(var(--content-gap) * .5); + } + + :where(h1):not(:where(.not-prose, .not-prose *)) { + font-size: 2.25em; + line-height: 1.1; + } + :where(h2):not(:where(.not-prose, .not-prose *)) { + font-size: 1.75em; + } + :where(h3):not(:where(.not-prose, .not-prose *)) { + font-size: 1.5em; + } + :where(h4):not(:where(.not-prose, .not-prose *)) { + font-size: 1.25em; + } + :where(h5):not(:where(.not-prose, .not-prose *)) { + font-size: 1.125em; + } + :where(h6):not(:where(.not-prose, .not-prose *)) { + font-size: 1em; + } + + :where(a:not([class])):not(:where(.not-prose, .not-prose *)) { + color: var(--link-color); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--link-color) 25%, transparent); + text-underline-offset: 4px; + @include transition(.1s text-decoration-color ease-in-out); + + &:hover { + text-decoration-color: var(--link-hover-color); + } + } + + :where(img):not(:where(.not-prose, .not-prose *)) { + max-width: 100%; + } + + :where(blockquote):not(:where(.not-prose, .not-prose *)) { + padding-inline-start: calc(var(--content-gap) / 2); + margin: 0; + border-inline-start: 4px solid var(--border-color); + } + + :where(table):not(:where(.not-prose, .not-prose *)) { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + } + + :where(table:not([class])):not(:where(.not-prose, .not-prose *)) { + td, + th { + padding: 6px 12px; + text-align: inherit; + border: 1px solid var(--border-color); + } + } + + :where(dt):not(:where(.not-prose, .not-prose *)) { + font-weight: 500; + } + + :where(video, img):not(:where(.not-prose, .not-prose *)) { + max-width: 100%; + } + } +} diff --git a/assets/stylesheets/bootstrap/content/_reboot.scss b/assets/stylesheets/bootstrap/content/_reboot.scss new file mode 100644 index 00000000..578a8881 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_reboot.scss @@ -0,0 +1,635 @@ +@use "../config" as *; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix + +$reboot-kbd-tokens: () !default; +$reboot-mark-tokens: () !default; + +// scss-docs-start reboot-kbd-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$reboot-kbd-tokens: defaults( + ( + --kbd-padding-y: .125rem, + --kbd-padding-x: .25rem, + --kbd-font-size: var(--font-size-xs), + --kbd-color: var(--bg-body), + --kbd-bg: var(--fg-2), + --kbd-border-radius: var(--radius-5), + ), + $reboot-kbd-tokens +); +// scss-docs-end reboot-kbd-tokens + +// scss-docs-start reboot-mark-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$reboot-mark-tokens: defaults( + ( + --mark-padding: .1875em, + --mark-color: var(--fg-body), + --mark-bg: light-dark(var(--yellow-100), var(--yellow-900)), + ), + $reboot-mark-tokens +); +// scss-docs-end reboot-mark-tokens + +@layer reboot { + // Reboot + // + // Normalization of HTML elements, manually forked from Normalize.css to remove + // styles targeting irrelevant browsers while applying new styles. + // + // Normalize is licensed MIT. https://github.com/necolas/normalize.css + + // Document + // + // Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`. + + *, + *::before, + *::after { + box-sizing: border-box; + } + + // Root + // + // Ability to the value of the root font sizes, affecting the value of `rem`. + // null by default, thus nothing is generated. + + :root { + // Assume browser default font-size of 16px, or a user's preference + accent-color: var(--primary-base); + + @if $enable-smooth-scroll { + @media (prefers-reduced-motion: no-preference) { + scroll-behavior: smooth; + } + } + } + + // Reset iframe color-scheme + // + // If the color scheme of an iframe differs from parent document, iframe gets + // an opaque canvas background appropriate (resulting in a white background + // on the iframe when in a page with a dark color scheme). + iframe { + color-scheme: light dark; + border: 0; + } + + // Body + // + // 1. Remove the margin in all browsers. + // 2. As a best practice, apply a default `background-color`. + // 3. Prevent adjustments of font size after orientation changes in iOS. + // 4. Change the default tap highlight to be completely transparent in iOS. + + // scss-docs-start reboot-body-rules + body { + margin: 0; // 1 + font-family: var(--body-font-family); + font-size: var(--body-font-size); + font-weight: var(--body-font-weight); + line-height: var(--body-line-height); + color: var(--fg-body); + text-align: var(--body-text-align); + background-color: var(--bg-body); // 2 + -webkit-text-size-adjust: 100%; // 3 + -webkit-tap-highlight-color: transparent; // 4 + } + // scss-docs-end reboot-body-rules + + hr { + margin: var(--hr-margin-y, var(--spacer)) 0; + border: 0; + border-block-start: var(--border-width) solid var(--hr-border-color); + } + + // Typography + // + // 1. Remove top margins from headings + // By default, ``-`` all receive top and bottom margins. We nuke the top + // margin for easier control within type scales as it avoids margin collapsing. + + %heading { + margin-top: 0; // 1 + margin-bottom: $headings-margin-bottom; + font-family: $headings-font-family; + font-style: $headings-font-style; + font-weight: $headings-font-weight; + line-height: $headings-line-height; + color: var(--heading-color); + } + + h1, + .h1 { + @extend %heading; + font-size: var(--font-size-3xl); + } + + h2, + .h2 { + @extend %heading; + font-size: var(--font-size-2xl); + } + + h3, + .h3 { + @extend %heading; + font-size: var(--font-size-xl); + } + + h4, + .h4 { + @extend %heading; + font-size: var(--font-size-lg); + } + + h5, + .h5 { + @extend %heading; + font-size: var(--font-size-md); + } + + h6, + .h6 { + @extend %heading; + font-size: var(--font-size-sm); + } + + // Reset margins on paragraphs + // + // Similarly, the top margin on ``s get reset. However, we also reset the + // bottom margin to use `rem` units instead of `em`. + + p { + margin-top: 0; + margin-bottom: $paragraph-margin-bottom; + } + + // Abbreviations + // + // 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. + // 2. Add explicit cursor to indicate changed behavior. + // 3. Prevent the text-decoration to be skipped. + + abbr[title] { + text-decoration: underline dotted; // 1 + cursor: help; // 2 + text-decoration-skip-ink: none; // 3 + } + + // Address + + address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; + } + + // Lists + + ol, + ul { + padding-inline-start: 2rem; + } + + ol, + ul, + dl { + margin-top: 0; + margin-bottom: 1rem; + } + + ol ol, + ul ul, + ol ul, + ul ol { + margin-bottom: 0; + } + + dt { + font-weight: $dt-font-weight; + } + + // 1. Undo browser default + + dd { + margin-inline-start: 0; // 1 + margin-bottom: .5rem; + } + + // Blockquote + + blockquote { + margin: 0 0 1rem; + > * { + margin-block: 0; + } + } + + // Strong + // + // Add the correct font weight in Chrome, Edge, and Safari + + b, + strong { + font-weight: $font-weight-bolder; + } + + // Small + // + // Add the correct font size in all browsers + + small, + .small { + font-size: var(--small-font-size, 87.5%); + } + + // Mark + + mark, + .mark { + @include tokens($reboot-mark-tokens); + padding: var(--mark-padding); + color: var(--mark-color); + background-color: var(--mark-bg); + } + + // Sub and Sup + // + // Prevent `sub` and `sup` elements from affecting the line height in + // all browsers. + + sub, + sup { + position: relative; + font-size: var(--sub-sup-font-size, .75em); + line-height: 0; + vertical-align: baseline; + } + + sub { bottom: -.25em; } + sup { top: -.5em; } + + // Links + + a { + color: var(--theme-fg, var(--link-color)); + text-decoration: var(--link-decoration); + text-underline-offset: $link-underline-offset; + + &:hover { + // --link-color: var(--link-hover-color); + // --link-decoration: var(--link-hover-decoration, var(--link-decoration)); + color: var(--theme-fg-emphasis, var(--link-hover-color)); + text-decoration: var(--link-hover-decoration, var(--link-decoration)); + } + } + + // And undo these styles for placeholder links/named anchors (without href). + // It would be more straightforward to just use a[href] in previous block, but that + // causes specificity issues in many other styles that are too complex to fix. + // See https://github.com/twbs/bootstrap/issues/19402 + + a:not([href], [class]) { + &, + &:hover { + color: inherit; + text-decoration: none; + } + } + + // Code + + pre, + code, + kbd, + samp { + font-family: var(--font-mono); + font-size: 1em; // Correct the odd `em` font sizing in all browsers. + } + + // 1. Remove browser default top margin + // 2. Reset browser default of `1em` to use `rem`s + // 3. Don't allow content to break outside + + pre { + display: block; + margin-top: 0; // 1 + margin-bottom: 1rem; // 2 + overflow: auto; // 3 + font-size: var(--code-font-size); + color: var(--code-color, inherit); + + // Account for some code outputs that place code tags in pre tags + code { + font-size: inherit; + color: inherit; + word-break: normal; + } + } + + code { + font-size: var(--code-font-size); + color: var(--code-color); + word-wrap: break-word; + + // Streamline the style when inside anchors to avoid broken underline and more + a > & { + color: inherit; + } + } + + kbd { + @include tokens($reboot-kbd-tokens); + padding: var(--kbd-padding-y) var(--kbd-padding-x); + font-size: var(--kbd-font-size); + color: var(--kbd-color); + background-color: var(--kbd-bg); + @include border-radius(var(--kbd-border-radius)); + + kbd { + padding: 0; + font-size: 1em; + font-weight: inherit; // mdo-do: check if this is needed + } + } + + // Figures + // + // Apply a consistent margin strategy (matches our type styles). + + figure { + margin: 0 0 1rem; + } + + // Images and content + + img, + svg { + vertical-align: middle; + } + + // Tables + // + // Prevent double borders + + table { + caption-side: bottom; + border-collapse: collapse; + } + + caption { + // padding-top: $table-cell-padding-y; + // padding-bottom: $table-cell-padding-y; + // color: $table-caption-color; + padding-block: .5rem; + color: var(--fg-3); + text-align: start; + } + + // 1. Removes font-weight bold by inheriting + // 2. Matches default `` alignment by inheriting `text-align`. + // 3. Fix alignment for Safari + + th { + // font-weight: $table-th-font-weight; // 1 // mdo-do: it's null by default. maybe we remove? + text-align: inherit; // 2 + text-align: -webkit-match-parent; // 3 + } + + thead, + tbody, + tfoot, + tr, + td, + th { + border-color: inherit; + border-style: solid; + border-width: 0; + } + + // Forms + // + // 1. Allow labels to use `margin` for spacing. + + label { + display: inline-block; // 1 + } + + // Remove the default `border-radius` that macOS Chrome adds. + // See https://github.com/twbs/bootstrap/issues/24093 + + button { + // stylelint-disable-next-line property-disallowed-list + border-radius: 0; + } + + // Explicitly remove focus outline in Chromium when it shouldn't be + // visible (e.g. as result of mouse click or touch tap). It already + // should be doing this automatically, but seems to currently be + // confused and applies its very visible two-tone outline anyway. + + button:focus:not(:focus-visible) { + outline: 0; + } + + // 1. Remove the margin in Firefox and Safari + + input, + button, + select, + optgroup, + textarea { + margin: 0; // 1 + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + // Set the cursor for non-`` buttons + // + // Details at https://github.com/twbs/bootstrap/pull/30562 + [role="button"] { + cursor: pointer; + } + + select { + // Remove the inheritance of word-wrap in Safari. + // See https://github.com/twbs/bootstrap/issues/24990 + word-wrap: normal; + + // Undo the opacity change from Chrome + &:disabled { + opacity: 1; + } + } + + // Remove the dropdown arrow only from text type inputs built with datalists in Chrome. + // See https://stackoverflow.com/a/54997118 + + [list]:not([type="date"], [type="datetime-local"], [type="month"], [type="week"], [type="time"])::-webkit-calendar-picker-indicator { + display: none !important; + } + + // 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` + // controls in Android 4. + // 2. Correct the inability to style clickable types in iOS and Safari. + // 3. Opinionated: add "hand" cursor to non-disabled button elements. + + button, + [type="button"], // 1 + [type="reset"], + [type="submit"] { + -webkit-appearance: button; // 2 + + @if $enable-button-pointers { + &:not(:disabled) { + cursor: pointer; // 3 + } + } + } + + // 1. Textareas should really only resize vertically so they don't break their (horizontal) containers. + + textarea { + resize: vertical; // 1 + } + + // 1. Browsers set a default `min-width: min-content;` on fieldsets, + // unlike e.g. ``s, which have `min-width: 0;` by default. + // So we reset that to ensure fieldsets behave more like a standard block element. + // See https://github.com/twbs/bootstrap/issues/12359 + // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements + // 2. Reset the default outline behavior of fieldsets so they don't affect page layout. + + fieldset { + min-width: 0; // 1 + padding: 0; // 2 + margin: 0; // 2 + border: 0; // 2 + } + + // 1. By using `float: inline-start`, the legend will behave like a block element. + // This way the border of a fieldset wraps around the legend if present. + // 2. Fix wrapping bug. + // See https://github.com/twbs/bootstrap/issues/29712 + + legend { + float: inline-start; // 1 + width: 100%; + padding: 0; + margin-bottom: $legend-margin-bottom; + font-size: $legend-font-size; + font-weight: $legend-font-weight; + line-height: inherit; + + + * { + clear: inline-start; // 2 + } + } + + // Fix height of inputs with a type of datetime-local, date, month, week, or time + // See https://github.com/twbs/bootstrap/issues/18842 + + ::-webkit-datetime-edit-fields-wrapper, + ::-webkit-datetime-edit-text, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-meridiem-field, // WebKit + ::-webkit-datetime-edit-ampm-field, // Chromium + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-week-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-year-field { + padding: 0; + } + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + // 1. This overrides the extra rounded corners on search inputs in iOS so that our + // `.form-control` class can properly style them. Note that this cannot simply + // be added to `.form-control` as it's not specific enough. For details, see + // https://github.com/twbs/bootstrap/issues/11586. + // 2. Correct the outline style in Safari. + + [type="search"] { + -webkit-appearance: textfield; // 1 + outline-offset: -2px; // 2 + + // 3. Better affordance and consistent appearance for search cancel button + &::-webkit-search-cancel-button { + cursor: pointer; + filter: grayscale(1); + } + } + + // A few input types should stay LTR regardless of document direction + // See https://rtlstyling.com/posts/rtl-styling#form-inputs + + [type="tel"], + [type="url"], + [type="email"], + [type="number"] { + direction: ltr; + } + + // Remove the inner padding in Chrome and Safari on macOS. + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + // Remove padding around color pickers in webkit browsers + + ::-webkit-color-swatch-wrapper { + padding: 0; + } + + // 1. Inherit font family and line height for file input buttons + // 2. Correct the inability to style clickable types in iOS and Safari. + + ::file-selector-button { + font: inherit; // 1 + -webkit-appearance: button; // 2 + } + + // Correct element displays + + output { + display: inline-block; + } + + // Summary + // + // 1. Add the correct display in all browsers + + summary { + display: list-item; // 1 + cursor: pointer; + } + + // Progress + // + // Add the correct vertical alignment in Chrome, Firefox, and Opera. + + progress { + vertical-align: baseline; + } + + // Hidden attribute + // + // Always hide an element with the `hidden` HTML attribute. + + [hidden] { + display: none !important; + } +} diff --git a/assets/stylesheets/bootstrap/content/_tables.scss b/assets/stylesheets/bootstrap/content/_tables.scss new file mode 100644 index 00000000..781d1f2c --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_tables.scss @@ -0,0 +1,255 @@ +@use "sass:map"; +@use "../config" as *; +@use "../functions" as *; +@use "../layout/breakpoints" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$table-tokens: () !default; + +// scss-docs-start table-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$table-tokens: defaults( + ( + --table-cell-padding-y: .5rem, + --table-cell-padding-x: .5rem, + --table-cell-vertical-align: top, + --table-color: var(--fg-body), + --table-bg: var(--bg-body), + --table-accent-bg: transparent, + --table-border-width: var(--border-width), + --table-border-color: var(--border-color), + --table-group-separator-color: currentcolor, + --table-striped-color: var(--table-color), + --table-striped-bg-factor: 5%, + --table-striped-bg: color-mix(in srgb, var(--table-color) var(--table-striped-bg-factor), transparent), + --table-active-color: var(--table-color), + --table-active-bg-factor: 10%, + --table-active-bg: color-mix(in srgb, var(--table-color) var(--table-active-bg-factor), transparent), + --table-hover-color: var(--table-color), + --table-hover-bg-factor: 7.5%, + --table-hover-bg: color-mix(in srgb, var(--table-color) var(--table-hover-bg-factor), transparent), + ), + $table-tokens +); +// scss-docs-end table-tokens +// stylelint-enable custom-property-no-missing-var-function + +$table-striped-order: odd !default; +$table-striped-columns-order: even !default; + +// +// Basic Bootstrap table +// + +@layer content { + .table { + @include tokens($table-tokens); + + // Reset needed for nesting tables + --table-color-type: initial; + --table-bg-type: initial; + --table-color-state: initial; + --table-bg-state: initial; + // End of reset + + width: 100%; + margin-bottom: var(--spacer); + vertical-align: var(--table-cell-vertical-align); + border-color: var(--theme-border, var(--table-border-color)); + + // Target th & td + // We need the child combinator to prevent styles leaking to nested tables which doesn't have a `.table` class. + // We use the universal selectors here to simplify the selector (else we would need 6 different selectors). + // Another advantage is that this generates less code and makes the selector less specific making it easier to override. + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + padding: var(--table-cell-padding-y) var(--table-cell-padding-x); + // Following the precept of cascades: https://codepen.io/miriamsuzanne/full/vYNgodb + color: var(--table-color-state, var(--table-color-type, var(--theme-fg, var(--table-color)))); + background-color: var(--theme-bg-subtle, var(--table-bg)); + border-block-end-width: var(--table-border-width); + box-shadow: inset 0 0 0 9999px var(--table-bg-state, var(--table-bg-type, var(--theme-bg-subtle, var(--table-accent-bg)))); + } + + > tbody { + vertical-align: inherit; + } + + > thead { + vertical-align: bottom; + } + } + + .table-group-divider { + border-block-start: calc(var(--table-border-width) * 2) solid var(--table-group-separator-color); + } + + // + // Change placement of captions with a class + // + + .caption-top { + caption-side: top; + } + + // + // Condensed table w/ half padding + // + + .table-sm { + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + --table-cell-padding-y: .25rem; + --table-cell-padding-x: .25rem; + } + } + + // Border versions + // + // Add or remove borders all around the table and between all the columns. + // + // When borders are added on all sides of the cells, the corners can render odd when + // these borders do not have the same color or if they are semi-transparent. + // Therefore we add top and border bottoms to the `tr`s and left and right borders + // to the `td`s or `th`s + + .table-bordered { + > :not(caption) > * { + border-width: var(--table-border-width) 0; + + // stylelint-disable-next-line selector-max-universal + > * { + border-width: 0 var(--table-border-width); + } + } + } + + .table-borderless { + // stylelint-disable-next-line selector-max-universal + > :not(caption) > * > * { + border-block-end-width: 0; + } + + > :not(:first-child) { + border-block-start-width: 0; + } + } + + // Zebra-striping + // + // Default zebra-stripe styles (alternating gray and transparent backgrounds) + + // For rows + .table-striped { + > tbody > tr:nth-of-type(#{$table-striped-order}) > * { + --table-color-type: var(--theme-fg, var(--table-striped-color)); + --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent); + } + } + + // For columns + .table-striped-columns { + > :not(caption) > tr > :nth-child(#{$table-striped-columns-order}) { + --table-color-type: var(--theme-fg, var(--table-striped-color)); + --table-bg-type: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-striped-bg-factor), transparent); + } + } + + // Active table + // + // The `.table-active` class can be added to highlight rows or cells + + .table-active { + --table-color-state: var(--theme-fg, var(--table-active-color)); + --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-active-bg-factor), transparent); + } + + // Hover effect + // + // Placed here since it has to come after the potential zebra striping + + .table-hover { + > tbody > tr:hover > * { + --table-color-state: var(--theme-fg, var(--table-hover-color)); + --table-bg-state: color-mix(in srgb, var(--theme-fg, var(--table-color)) var(--table-hover-bg-factor), transparent); + } + } + + // Responsive tables + // + // Generate `.table-responsive` classes that act as container query contexts + // and enable horizontal scrolling when table content overflows. + + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + .#{$prefix}table-responsive { + container-type: inline-size; + + @include media-breakpoint-down($breakpoint) { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + } + } + + // Stacked tables + // + // Generate `.table-stacked` classes that convert table rows into stacked + // blocks using container queries. Requires a `.table-responsive` ancestor + // and `data-cell` attributes on `` elements for column labels. + + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + + @include container-breakpoint-down($breakpoint) { + .#{$prefix}table-stacked { + > thead { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + > tbody > tr { + display: block; + padding-block: var(--table-cell-padding-y); + + + tr { + border-block-start: var(--table-border-width) solid var(--table-border-color); + } + + > td { + display: block; + padding: calc(var(--table-cell-padding-y) * .25) calc(var(--table-cell-padding-x) * 2); + border: 0; + + &:first-child { + font-weight: var(--font-weight-bold); + } + + // + td::before { + // margin-block-start: .25rem; + // } + + &[data-cell]:not(:first-child)::before { + display: block; + font-weight: var(--font-weight-semibold); + content: attr(data-cell); + } + } + + > td:not(:first-child) + td::before { + margin-block-start: .25rem; + } + } + } + } + } +} diff --git a/assets/stylesheets/bootstrap/content/_type.scss b/assets/stylesheets/bootstrap/content/_type.scss new file mode 100644 index 00000000..fec15e14 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/_type.scss @@ -0,0 +1,86 @@ +@use "../functions" as *; +@use "../mixins/lists" as *; +@use "../mixins/tokens" as *; + +$blockquote-tokens: () !default; + +// scss-docs-start blockquote-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$blockquote-tokens: defaults( + ( + --blockquote-gap: calc(var(--spacer) / 2), + --blockquote-padding-x: var(--spacer), + --blockquote-margin-y: 1rem, + --blockquote-font-size: var(--font-size-md), + --blockquote-border-width: .25rem, + --blockquote-border-color: var(--border-color), + --blockquote-footer-font-size: var(--font-size-sm), + --blockquote-footer-color: var(--fg-3), + ), + $blockquote-tokens +); +// scss-docs-end blockquote-tokens + +@layer content { + // + // Lists + // + + .list-unstyled { + @include list-unstyled(); + } + + // Inline turns list items into inline-block + .list-inline { + @include list-unstyled(); + } + .list-inline-item { + display: inline-block; + + &:not(:last-child) { + margin-inline-end: var(--list-inline-padding, var(--spacer) / 2); + } + } + + // + // Misc + // + + // Builds on `abbr` + .initialism { + font-size: var(--initialism-font-size, var(--font-size-xs)); + text-transform: uppercase; + } + + // Blockquotes + .blockquote { + @include tokens($blockquote-tokens); + display: flex; + flex-direction: column; + gap: var(--blockquote-gap); + padding-inline-start: var(--blockquote-padding-x); + margin-bottom: var(--blockquote-margin-y); + font-size: var(--blockquote-font-size); + border-inline-start: var(--blockquote-border-width) solid var(--blockquote-border-color); + + > * { + margin-bottom: 0; + } + } + + // stylelint-disable-next-line selector-no-qualifying-type + figure.blockquote { + blockquote { + margin-bottom: 0; + } + } + + .blockquote-footer { + font-size: var(--blockquote-footer-font-size); + color: var(--blockquote-footer-color); + + &::before { + content: "\2014\00A0"; // em dash, nbsp + } + } +} diff --git a/assets/stylesheets/bootstrap/content/index.scss b/assets/stylesheets/bootstrap/content/index.scss new file mode 100644 index 00000000..8b141c94 --- /dev/null +++ b/assets/stylesheets/bootstrap/content/index.scss @@ -0,0 +1,5 @@ +@forward "reboot"; +@forward "type"; +@forward "tables"; +@forward "images"; +@forward "prose"; diff --git a/assets/stylesheets/bootstrap/forms/_check.scss b/assets/stylesheets/bootstrap/forms/_check.scss new file mode 100644 index 00000000..86c354a0 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_check.scss @@ -0,0 +1,105 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/mask-icon" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$check-tokens: () !default; + +// scss-docs-start check-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$check-tokens: defaults( + ( + --check-size: 1.25rem, + --check-margin-block: .125rem, + --check-bg: var(--bg-body), + --check-border-color: var(--border-color), + --check-border-radius: var(--radius-5), + --check-icon-checked: #{escape-svg(url("data:image/svg+xml,"))}, + --check-icon-indeterminate: #{escape-svg(url("data:image/svg+xml,"))}, + --check-checked-bg: var(--control-checked-bg), + --check-checked-border-color: var(--control-checked-border-color), + --check-indeterminate-bg: var(--control-checked-bg), + --check-indeterminate-border-color: var(--control-checked-border-color), + --check-active-bg: var(--control-active-bg), + --check-active-border-color: var(--control-active-border-color), + --check-disabled-bg: var(--control-disabled-bg), + --check-disabled-opacity: var(--control-disabled-opacity), + ), + $check-tokens +); +// scss-docs-end check-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + // The class lives on the `` itself; `appearance: none` controls render + // pseudo-elements, so the mark is drawn directly on the input — no wrapper. + .check { + @include tokens($check-tokens); + + position: relative; + flex-shrink: 0; + width: var(--check-size); + height: var(--check-size); + margin-block: var(--check-margin-block); + appearance: none; + // later: maybe set a tertiary bg color? + background-color: var(--theme-bg, var(--check-bg)); + border: 1px solid var(--theme-bg, var(--check-border-color)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 33%; + + &:checked, + &:indeterminate { + background-color: var(--theme-bg, var(--check-checked-bg)); + border-color: var(--theme-bg, var(--check-checked-border-color)); + + // Check/indeterminate mark, overlaid on the input and rendered via a CSS + // mask so it inherits the contrast color without an inline SVG. + &::before { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + background-color: var(--theme-contrast, var(--primary-contrast)); + @include mask-icon(); + } + } + + &:checked::before { mask-image: var(--check-icon-checked); } + &:indeterminate::before { mask-image: var(--check-icon-indeterminate); } + + &:focus-visible { + @include focus-ring(true); + --focus-ring-offset: -1px; + } + + &:disabled { + --check-bg: var(--check-disabled-bg); + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + &:disabled:checked { + opacity: var(--check-disabled-opacity); + } + } + + .check-sm { + --check-size: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .check-lg { + --check-size: 1.5rem; + --check-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_chip-input.scss b/assets/stylesheets/bootstrap/forms/_chip-input.scss new file mode 100644 index 00000000..1fb6db31 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_chip-input.scss @@ -0,0 +1,74 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +$chip-input-tokens: () !default; + +// scss-docs-start chip-input-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$chip-input-tokens: defaults( + ( + --chip-input-padding-y: .75rem, + --chip-input-padding-x: .75rem, + --chip-input-gap: .375rem, + --chip-input-ghost-min-width: 5rem, + --control-fg: var(--btn-input-fg), + --control-bg: var(--btn-input-bg), + --control-border-width: var(--border-width), + --control-border-color: var(--border-color), + --control-border-radius: var(--radius-5), + ), + $chip-input-tokens +); +// scss-docs-end chip-input-tokens + +@layer forms { + .chip-input { + @include tokens($chip-input-tokens); + + // Flexbox wrapping layout + display: flex; + flex-wrap: wrap; + gap: var(--chip-input-gap); + align-items: center; + padding: var(--chip-input-padding-y) var(--chip-input-padding-x); + + color: var(--control-fg); + background-color: var(--control-bg); + border: var(--control-border-width) solid var(--control-border-color); + @include border-radius(var(--control-border-radius), 0); + + // Focus state when ghost input is focused + &:focus-within { + --focus-ring-offset: -1px; + border-color: var(--focus-ring-color); + @include focus-ring(true); + } + + // Ghost input fills remaining space + > .form-ghost { + flex: 1 1 0; + min-width: var(--chip-input-ghost-min-width); + min-height: 1.75rem; + } + + // Disabled state + &.disabled, + &:has(.form-ghost:disabled) { + cursor: not-allowed; + background-color: var(--bg-2); + opacity: 1; + + &:focus-within { + border-color: var(--control-border-color); + outline: 0; + } + + > .chip { + pointer-events: none; + opacity: var(--control-disabled-opacity); + } + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_combobox.scss b/assets/stylesheets/bootstrap/forms/_combobox.scss new file mode 100644 index 00000000..9b99994f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_combobox.scss @@ -0,0 +1,71 @@ +@use "../mixins/transition" as *; + +@layer components { + .combobox-toggle { + display: inline-flex; + gap: .5rem; + align-items: center; + justify-content: space-between; + width: 100%; + padding-inline-end: var(--control-padding-x); + text-align: start; + cursor: pointer; + + &.show { + background-color: var(--bg-1); + } + + &:disabled, + &.disabled { + cursor: not-allowed; + opacity: .65; + } + } + + .combobox-value { + display: flex; + flex: 1; + gap: .5rem; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .combobox-placeholder { + color: color-mix(in oklch, currentcolor 65%, transparent); + } + + .combobox-caret { + flex-shrink: 0; + @include transition(transform .2s ease-in-out); + + .show > & { + transform: rotate(180deg); + } + } + + .combobox-toggle + .menu { + --menu-max-height: 300px; + --menu-overflow-y: auto; + } + + .combobox-search { + position: sticky; + top: 0; + z-index: 1; + padding: var(--menu-padding-x, .25rem); + background-color: var(--menu-bg, var(--bg-body)); + } + + .combobox-search-input { + width: 100%; + } + + .combobox-no-results { + padding: 1rem; + font-size: var(--font-size-sm); + color: var(--fg-3); + text-align: center; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_floating-labels.scss b/assets/stylesheets/bootstrap/forms/_floating-labels.scss index 38df1155..8f9b79aa 100644 --- a/assets/stylesheets/bootstrap/forms/_floating-labels.scss +++ b/assets/stylesheets/bootstrap/forms/_floating-labels.scss @@ -1,97 +1,129 @@ -.form-floating { - position: relative; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; - > .form-control, - > .form-control-plaintext, - > .form-select { - height: $form-floating-height; - min-height: $form-floating-height; - line-height: $form-floating-line-height; - } +$form-floating-tokens: () !default; - > label { - position: absolute; - top: 0; - left: 0; - z-index: 2; - max-width: 100%; - height: 100%; // allow textareas - padding: $form-floating-padding-y $form-floating-padding-x; - overflow: hidden; - color: rgba(var(--#{$prefix}body-color-rgb), #{$form-floating-label-opacity}); - text-align: start; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - border: $input-border-width solid transparent; // Required for aligning label's text with the input as it affects inner box model - transform-origin: 0 0; - @include transition($form-floating-transition); - } +// scss-docs-start form-floating-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-floating-tokens: defaults( + ( + --form-floating-height: calc(3.75rem + (var(--border-width) * 2)), + --form-floating-line-height: 1.25, + --form-floating-padding-x: calc(var(--btn-input-padding-x) * 1.25), + --form-floating-padding-y: 1rem, + --form-floating-input-padding-t: 1.625rem, + --form-floating-input-padding-b: .625rem, + --form-floating-label-height: 1.5em, + // Backgrounds for the textarea label's masking pseudo-element. Mirrors + // `.form-control` here because the label is a sibling of the control, so it + // can't inherit the control's own `--control-bg`/`--control-disabled-bg`. + --form-floating-label-bg: var(--btn-input-bg), + --form-floating-label-disabled-bg: var(--bg-2), + --form-floating-label-opacity: .65, + --form-floating-label-transform: scale(.85) translateY(-.5rem) translateX(.15rem), + --form-floating-label-disabled-color: var(--fg-3), + --form-floating-transition-property: "opacity, transform", + --form-floating-transition-timing: .1s ease-in-out, + --form-floating-transition: var(--form-floating-transition-property) var(--form-floating-transition-timing), + ), + $form-floating-tokens +); +// scss-docs-end form-floating-tokens + +@layer forms { + .form-floating { + @include tokens($form-floating-tokens); - > .form-control, - > .form-control-plaintext { - padding: $form-floating-padding-y $form-floating-padding-x; + position: relative; - &::placeholder { - color: transparent; + > label { + position: absolute; + inset-block-start: 0; + inset-inline-start: 0; + z-index: 2; + display: flex; + align-items: center; + max-width: 100%; + height: 100%; // allow textareas + padding: var(--form-floating-padding-y) var(--form-floating-padding-x); + overflow: hidden; + color: color-mix(in oklch, var(--fg-body) var(--form-floating-label-opacity), transparent); + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--border-width) solid transparent; // Required for aligning label's text with the input as it affects inner box model + transform-origin: 0 0; + @include transition(var(--form-floating-transition)); } - &:focus, - &:not(:placeholder-shown) { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; + // Anchor the label to the top for textareas so it floats correctly at any height + > label:has(~ textarea) { + align-items: flex-start; } - // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped - &:-webkit-autofill { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; + + > .form-control, + > .form-control-plaintext { + height: var(--form-floating-height); + min-height: var(--form-floating-height); + padding: var(--form-floating-padding-y) var(--form-floating-padding-x); + line-height: var(--form-floating-line-height); + + &::placeholder { + color: transparent; + } + + &:focus, + &:not(:placeholder-shown) { + padding-top: var(--form-floating-input-padding-t); + padding-bottom: var(--form-floating-input-padding-b); + } + // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped + &:-webkit-autofill { + padding-top: var(--form-floating-input-padding-t); + padding-bottom: var(--form-floating-input-padding-b); + } } - } - > .form-select { - padding-top: $form-floating-input-padding-t; - padding-bottom: $form-floating-input-padding-b; - padding-left: $form-floating-padding-x; - } + // The label precedes the control in the DOM so screen readers announce it + // before the field's value, so we look forward with `:has()` to react to the + // control's state (focus, value, disabled, etc.). + > label:has(~ .form-control:focus), + > label:has(~ .form-control:not(:placeholder-shown)), + > label:has(~ .form-control-plaintext) { + transform: var(--form-floating-label-transform); + } - > .form-control:focus, - > .form-control:not(:placeholder-shown), - > .form-control-plaintext, - > .form-select { - ~ label { - transform: $form-floating-label-transform; + // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped + > label:has(~ .form-control:-webkit-autofill) { + transform: var(--form-floating-label-transform); } - } - // Duplicated because `:-webkit-autofill` invalidates other selectors when grouped - > .form-control:-webkit-autofill { - ~ label { - transform: $form-floating-label-transform; + + > label:has(~ textarea:focus), + > label:has(~ textarea:not(:placeholder-shown)) { + &::after { + position: absolute; + inset: var(--form-floating-padding-y) calc(var(--form-floating-padding-x) * .5); + z-index: -1; + height: var(--form-floating-label-height); + content: ""; + background-color: var(--form-floating-label-bg); + @include border-radius(var(--btn-input-border-radius)); + } } - } - > textarea:focus, - > textarea:not(:placeholder-shown) { - ~ label::after { - position: absolute; - inset: $form-floating-padding-y ($form-floating-padding-x * .5); - z-index: -1; - height: $form-floating-label-height; - content: ""; - background-color: $input-bg; - @include border-radius($input-border-radius); + > label:has(~ textarea:disabled)::after { + background-color: var(--form-floating-label-disabled-bg); } - } - > textarea:disabled ~ label::after { - background-color: $input-disabled-bg; - } - > .form-control-plaintext { - ~ label { - border-width: $input-border-width 0; // Required to properly position label text - as explained above + > label:has(~ .form-control-plaintext) { + border-width: var(--control-border-width) 0; // Required to properly position label text - as explained above } - } - > :disabled ~ label, - > .form-control:disabled ~ label { // Required for `.form-control`s because of specificity - color: $form-floating-label-disabled-color; + > label:has(~ :disabled), + > label:has(~ .form-control:disabled) { // Required for `.form-control`s because of specificity + color: var(--form-floating-label-disabled-color); + } } } diff --git a/assets/stylesheets/bootstrap/forms/_form-adorn.scss b/assets/stylesheets/bootstrap/forms/_form-adorn.scss new file mode 100644 index 00000000..bd33d923 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_form-adorn.scss @@ -0,0 +1,68 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +$form-adorn-tokens: () !default; + +// scss-docs-start form-adorn-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-adorn-tokens: defaults( + ( + --form-adorn-gap: .375rem, + --form-adorn-icon-size: 1rem, + --form-adorn-icon-color: var(--fg-2), + ), + $form-adorn-tokens +); +// scss-docs-end form-adorn-tokens + +@layer forms { + .form-adorn { + @include tokens($form-adorn-tokens); + + gap: var(--form-adorn-gap); + align-items: center; + + // Prevent default `.form-control` focus + &:focus-visible { + outline: 0; + } + + &:focus-within { + --focus-ring-offset: -1px; + border-color: var(--focus-ring-color); + @include focus-ring(true); + } + + // Ghost input fills remaining space + > .form-ghost { + flex: 1; + min-width: 0; // Prevent text overflow + } + + &.form-adorn-end > .form-ghost { + order: -1; + } + } + + .form-adorn-icon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + color: var(--form-adorn-icon-color); + pointer-events: none; + + > svg { + width: var(--form-adorn-icon-size); + height: var(--form-adorn-icon-size); + } + } + + .form-adorn-text { + flex-shrink: 0; + color: var(--form-adorn-icon-color); + pointer-events: none; + user-select: none; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_form-check.scss b/assets/stylesheets/bootstrap/forms/_form-check.scss deleted file mode 100644 index 8a1b639d..00000000 --- a/assets/stylesheets/bootstrap/forms/_form-check.scss +++ /dev/null @@ -1,189 +0,0 @@ -// -// Check/radio -// - -.form-check { - display: block; - min-height: $form-check-min-height; - padding-left: $form-check-padding-start; - margin-bottom: $form-check-margin-bottom; - - .form-check-input { - float: left; - margin-left: $form-check-padding-start * -1; - } -} - -.form-check-reverse { - padding-right: $form-check-padding-start; - padding-left: 0; - text-align: right; - - .form-check-input { - float: right; - margin-right: $form-check-padding-start * -1; - margin-left: 0; - } -} - -.form-check-input { - --#{$prefix}form-check-bg: #{$form-check-input-bg}; - - flex-shrink: 0; - width: $form-check-input-width; - height: $form-check-input-width; - margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height - vertical-align: top; - appearance: none; - background-color: var(--#{$prefix}form-check-bg); - background-image: var(--#{$prefix}form-check-bg-image); - background-repeat: no-repeat; - background-position: center; - background-size: contain; - border: $form-check-input-border; - print-color-adjust: exact; // Keep themed appearance for print - @include transition($form-check-transition); - - &[type="checkbox"] { - @include border-radius($form-check-input-border-radius); - } - - &[type="radio"] { - // stylelint-disable-next-line property-disallowed-list - border-radius: $form-check-radio-border-radius; - } - - &:active { - filter: $form-check-input-active-filter; - } - - &:focus { - border-color: $form-check-input-focus-border; - outline: 0; - box-shadow: $form-check-input-focus-box-shadow; - } - - &:checked { - background-color: $form-check-input-checked-bg-color; - border-color: $form-check-input-checked-border-color; - - &[type="checkbox"] { - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-checked-bg-image)}; - } - } - - &[type="radio"] { - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-radio-checked-bg-image)}; - } - } - } - - &[type="checkbox"]:indeterminate { - background-color: $form-check-input-indeterminate-bg-color; - border-color: $form-check-input-indeterminate-border-color; - - @if $enable-gradients { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-check-bg-image: #{escape-svg($form-check-input-indeterminate-bg-image)}; - } - } - - &:disabled { - pointer-events: none; - filter: none; - opacity: $form-check-input-disabled-opacity; - } - - // Use disabled attribute in addition of :disabled pseudo-class - // See: https://github.com/twbs/bootstrap/issues/28247 - &[disabled], - &:disabled { - ~ .form-check-label { - cursor: default; - opacity: $form-check-label-disabled-opacity; - } - } -} - -.form-check-label { - color: $form-check-label-color; - cursor: $form-check-label-cursor; -} - -// -// Switch -// - -.form-switch { - padding-left: $form-switch-padding-start; - - .form-check-input { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image)}; - - width: $form-switch-width; - margin-left: $form-switch-padding-start * -1; - background-image: var(--#{$prefix}form-switch-bg); - background-position: left center; - @include border-radius($form-switch-border-radius, 0); - @include transition($form-switch-transition); - - &:focus { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-focus-bg-image)}; - } - - &:checked { - background-position: $form-switch-checked-bg-position; - - @if $enable-gradients { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)}, var(--#{$prefix}gradient); - } @else { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-checked-bg-image)}; - } - } - } - - &.form-check-reverse { - padding-right: $form-switch-padding-start; - padding-left: 0; - - .form-check-input { - margin-right: $form-switch-padding-start * -1; - margin-left: 0; - } - } -} - -.form-check-inline { - display: inline-block; - margin-right: $form-check-inline-margin-end; -} - -.btn-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; - - &[disabled], - &:disabled { - + .btn { - pointer-events: none; - filter: none; - opacity: $form-check-btn-check-disabled-opacity; - } - } -} - -@if $enable-dark-mode { - @include color-mode(dark) { - .form-switch .form-check-input:not(:checked):not(:focus) { - --#{$prefix}form-switch-bg: #{escape-svg($form-switch-bg-image-dark)}; - } - } -} diff --git a/assets/stylesheets/bootstrap/forms/_form-control.scss b/assets/stylesheets/bootstrap/forms/_form-control.scss index 67ae5f4f..9eedb198 100644 --- a/assets/stylesheets/bootstrap/forms/_form-control.scss +++ b/assets/stylesheets/bootstrap/forms/_form-control.scss @@ -1,214 +1,284 @@ -// -// General form controls (plus a few specific high-level interventions) -// - -.form-control { - display: block; - width: 100%; - padding: $input-padding-y $input-padding-x; - font-family: $input-font-family; - @include font-size($input-font-size); - font-weight: $input-font-weight; - line-height: $input-line-height; - color: $input-color; - appearance: none; // Fix appearance for date inputs in Safari - background-color: $input-bg; - background-clip: padding-box; - border: $input-border-width solid $input-border-color; - - // Note: This has no effect on s in some browsers, due to the limited stylability of ``s in CSS. - @include border-radius($input-border-radius, 0); - - @include box-shadow($input-box-shadow); - @include transition($input-transition); - - &[type="file"] { - overflow: hidden; // prevent pseudo element button overlap - - &:not(:disabled):not([readonly]) { - cursor: pointer; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +$form-control-tokens: () !default; + +// scss-docs-start form-control-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$form-control-tokens: defaults( + ( + --control-min-height: var(--btn-input-min-height), + --control-padding-y: var(--btn-input-padding-y), + --control-padding-x: var(--btn-input-padding-x), + --control-font-size: var(--btn-input-font-size), + --control-line-height: var(--btn-input-line-height), + --control-fg: var(--btn-input-fg), + --control-bg: var(--btn-input-bg), + --control-border-width: var(--border-width), + --control-border-color: var(--border-color), + --control-border-radius: var(--radius-5), + --control-box-shadow: var(--box-shadow-inset), + --control-action-bg: var(--bg-1), + --control-action-hover-bg: var(--bg-2), + --control-transition-property: "border-color, box-shadow", + --control-transition-timing: .15s ease-in-out, + --control-transition: var(--control-transition-property) var(--control-transition-timing), + --control-placeholder-color: var(--fg-3), + --control-disabled-color: var(--control-fg), + --control-disabled-bg: var(--bg-2), + --control-disabled-border-color: var(--control-border-color), + --control-select-bg: #{escape-svg(url("data:image/svg+xml,"))}, + --control-select-bg-position: right .75rem center, + --control-select-bg-size: 16px 12px, + --control-select-bg-dark: #{escape-svg(url("data:image/svg+xml,"))}, + ), + $form-control-tokens +); +// scss-docs-end form-control-tokens + +// scss-docs-start form-control-sizes +$form-control-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$form-control-sizes: defaults( + ("sm", "lg"), + $form-control-sizes +); +// scss-docs-end form-control-sizes +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .form-control { + @include tokens($form-control-tokens); + + display: flex; + width: 100%; + min-height: var(--control-min-height); + padding: var(--control-padding-y) var(--control-padding-x); + font-size: var(--control-font-size); + line-height: var(--control-line-height); + color: var(--control-fg); + appearance: none; + background-color: var(--control-bg); + background-clip: padding-box; + border: var(--control-border-width) solid var(--control-border-color); + @include border-radius(var(--control-border-radius), 0); + @include box-shadow(var(--control-box-shadow)); + @include transition(var(--control-transition)); + + // Customize the `:focus` state to imitate native WebKit styles. + &:focus-visible { + --focus-ring-offset: -1px; + @include focus-ring(true); } - } - // Customize the `:focus` state to imitate native WebKit styles. - &:focus { - color: $input-focus-color; - background-color: $input-focus-bg; - border-color: $input-focus-border-color; - outline: 0; - @if $enable-shadows { - @include box-shadow($input-box-shadow, $input-focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $input-focus-box-shadow; + // Placeholder + &::placeholder { + color: var(--control-placeholder-color); + // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526. + opacity: 1; } - } - &::-webkit-date-and-time-value { - // On Android Chrome, form-control's "width: 100%" makes the input width too small - // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // Disabled inputs // - // On iOS Safari, form-control's "appearance: none" + "width: 100%" makes the input width too small - // Tested under iOS 16.2 / Safari 16.2 - min-width: 85px; // Seems to be a good minimum safe width - - // Add some height to date inputs on iOS - // https://github.com/twbs/bootstrap/issues/23307 - // TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved - // Multiply line-height by 1em if it has no unit - height: if(unit($input-line-height) == "", $input-line-height * 1em, $input-line-height); - - // Android Chrome type="date" is taller than the other inputs - // because of "margin: 1px 24px 1px 4px" inside the shadow DOM - // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 - margin: 0; - } + // HTML5 says that controls under a fieldset > legend:first-child won't be + // disabled if the fieldset is disabled. Due to implementation difficulty, we + // don't honor that edge case; we style them as disabled anyway. + &:disabled { + color: var(--control-disabled-color); + background-color: var(--control-disabled-bg); + border-color: var(--control-disabled-border-color); + // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655. + opacity: 1; + } - // Prevent excessive date input height in Webkit - // https://github.com/twbs/bootstrap/issues/34433 - &::-webkit-datetime-edit { - display: block; - padding: 0; - } + // Date and time inputs + // &::-webkit-date-and-time-value { + // // On Android Chrome, form-control's "width: 100%" makes the input width too small + // // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // // + // // On iOS Safari, form-control's "appearance: none" + "width: 100%" makes the input width too small + // // Tested under iOS 16.2 / Safari 16.2 + // min-width: 85px; // Seems to be a good minimum safe width - // Placeholder - &::placeholder { - color: $input-placeholder-color; - // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526. - opacity: 1; - } + // // Add some height to date inputs on iOS + // // https://github.com/twbs/bootstrap/issues/23307 + // // TODO: we can remove this workaround once https://bugs.webkit.org/show_bug.cgi?id=198959 is resolved + // // Multiply line-height by 1em if it has no unit + // height: 1.5em; - // Disabled inputs - // - // HTML5 says that controls under a fieldset > legend:first-child won't be - // disabled if the fieldset is disabled. Due to implementation difficulty, we - // don't honor that edge case; we style them as disabled anyway. - &:disabled { - color: $input-disabled-color; - background-color: $input-disabled-bg; - border-color: $input-disabled-border-color; - // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655. - opacity: 1; - } + // // Android Chrome type="date" is taller than the other inputs + // // because of "margin: 1px 24px 1px 4px" inside the shadow DOM + // // Tested under Android 11 / Chrome 89, Android 12 / Chrome 100, Android 13 / Chrome 109 + // margin: 0; + // background-color: var(--red-500); + // } - // File input buttons theming - &::file-selector-button { - padding: $input-padding-y $input-padding-x; - margin: (-$input-padding-y) (-$input-padding-x); - margin-inline-end: $input-padding-x; - color: $form-file-button-color; - @include gradient-bg($form-file-button-bg); - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: $input-border-width; - border-radius: 0; // stylelint-disable-line property-disallowed-list - @include transition($btn-transition); - } + // Prevent excessive date input height in Webkit + // https://github.com/twbs/bootstrap/issues/34433 - &:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: $form-file-button-hover-bg; - } -} + // mdo-do: need to check this stuff out across browsers + &::-webkit-datetime-edit { + display: block; + height: 1.5rem; + padding: 0; + margin-bottom: -.125rem; + } + &::-webkit-datetime-edit-fields-wrapper { + height: 1.5rem; + } -// Readonly controls as plain text -// -// Apply class to a readonly input to make it appear like regular plain -// text (without any border, background color, focus indicator) - -.form-control-plaintext { - display: block; - width: 100%; - padding: $input-padding-y 0; - margin-bottom: 0; // match inputs if this class comes on inputs with default margins - line-height: $input-line-height; - color: $input-plaintext-color; - background-color: transparent; - border: solid transparent; - border-width: $input-border-width 0; - - &:focus { - outline: 0; - } + // File inputs + &[type="file"] { + overflow: hidden; // prevent pseudo element button overlap - &.form-control-sm, - &.form-control-lg { - padding-right: 0; - padding-left: 0; - } -} + &:not(:disabled, [readonly]) { + cursor: pointer; + } + } + &::file-selector-button { + min-height: var(--control-min-height); + padding: var(--control-padding-y) var(--control-padding-x); + margin: calc(var(--control-padding-y) * -1) calc(var(--control-padding-x) * -1); + margin-inline-end: var(--control-padding-x); + color: var(--control-fg); + // @include gradient-bg(var(--control-action-bg)); + pointer-events: none; + background-color: var(--control-action-bg); + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--control-border-width); + border-radius: 0; // stylelint-disable-line property-disallowed-list + @include transition(var(--control-transition)); + } -// Form control sizing -// -// Build on `.form-control` with modifier classes to decrease or increase the -// height and font-size of form controls. -// -// Repeated in `_input_group.scss` to avoid Sass extend issues. - -.form-control-sm { - min-height: $input-height-sm; - padding: $input-padding-y-sm $input-padding-x-sm; - @include font-size($input-font-size-sm); - @include border-radius($input-border-radius-sm); - - &::file-selector-button { - padding: $input-padding-y-sm $input-padding-x-sm; - margin: (-$input-padding-y-sm) (-$input-padding-x-sm); - margin-inline-end: $input-padding-x-sm; + &:hover:not(:disabled, [readonly])::file-selector-button { + background-color: var(--control-action-hover-bg); + } } -} -.form-control-lg { - min-height: $input-height-lg; - padding: $input-padding-y-lg $input-padding-x-lg; - @include font-size($input-font-size-lg); - @include border-radius($input-border-radius-lg); + // Readonly controls as plain text + // + // Apply class to a readonly input to make it appear like regular plain + // text (without any border, background color, focus indicator) - &::file-selector-button { - padding: $input-padding-y-lg $input-padding-x-lg; - margin: (-$input-padding-y-lg) (-$input-padding-x-lg); - margin-inline-end: $input-padding-x-lg; + .form-control-plaintext { + // Plaintext is a standalone class (not combined with `.form-control`), so it + // needs its own copy of the control tokens. Without them the `var(--control-*)` + // references below are invalid and fall back to their initial values (e.g. + // `border-width: medium`), which adds phantom inline borders and misaligns the + // text from a floating label. + @include tokens($form-control-tokens); + + display: block; + width: 100%; + padding: var(--control-padding-y) 0; + margin-bottom: 0; // match inputs if this class comes on inputs with default margins + line-height: var(--control-line-height); + color: var(--control-fg); + background-color: transparent; + border: solid transparent; + border-width: var(--control-border-width) 0; + + &:focus { + outline: 0; + } + + &.form-control-sm, + &.form-control-lg { + padding-inline: 0; + } } -} -// Make sure textareas don't shrink too much when resized -// https://github.com/twbs/bootstrap/pull/29124 -// stylelint-disable selector-no-qualifying-type -textarea { - &.form-control { - min-height: $input-height; + // stylelint-disable selector-no-qualifying-type + select.form-control, + .form-control-caret { + padding-inline-end: calc(var(--control-padding-x) * 3); + background-image: var(--control-select-bg); + background-repeat: no-repeat; + background-position: var(--control-select-bg-position); + background-size: var(--control-select-bg-size); + + &[multiple], + &[size]:not([size="1"]) { + padding-inline-end: var(--control-padding-x); + background-image: none; + } } - &.form-control-sm { - min-height: $input-height-sm; + [data-bs-theme="dark"] { + select.form-control, + .form-control-caret { + background-image: var(--control-select-bg-dark); + } } + // stylelint-enable selector-no-qualifying-type - &.form-control-lg { - min-height: $input-height-lg; + // Form control sizing + // + // Build on `.form-control` with modifier classes to decrease or increase the + // height and font-size of form controls. + // + // Repeated in `_input_group.scss` to avoid Sass extend issues. + @each $size, $_ in $form-control-sizes { + .form-control-#{$size} { + --control-min-height: var(--btn-input-#{$size}-min-height); + --control-padding-y: var(--btn-input-#{$size}-padding-y); + --control-padding-x: var(--btn-input-#{$size}-padding-x); + --control-font-size: var(--btn-input-#{$size}-font-size); + --control-line-height: var(--btn-input-#{$size}-line-height); + --control-border-radius: var(--btn-input-#{$size}-border-radius); + } } -} -// stylelint-enable selector-no-qualifying-type -.form-control-color { - width: $form-color-width; - height: $input-height; - padding: $input-padding-y; + .form-control-color { + width: var(--control-min-height); + padding: var(--control-padding-y); - &:not(:disabled):not([readonly]) { - cursor: pointer; - } + &:not(:disabled, [readonly]) { + cursor: pointer; + } - &::-moz-color-swatch { - border: 0 !important; // stylelint-disable-line declaration-no-important - @include border-radius($input-border-radius); - } + &::-moz-color-swatch { + border: 0 !important; // stylelint-disable-line declaration-no-important + @include border-radius(var(--radius-5)); + } - &::-webkit-color-swatch { - border: 0 !important; // stylelint-disable-line declaration-no-important - @include border-radius($input-border-radius); + &::-webkit-color-swatch { + border: 0 !important; // stylelint-disable-line declaration-no-important + @include border-radius(var(--radius-5)); + } } - &.form-control-sm { height: $input-height-sm; } - &.form-control-lg { height: $input-height-lg; } + // Ghost input - removes all visual styling + // Used inside custom wrappers that handle their own styling + .form-ghost { + display: block; + width: 100%; + padding: 0; + font: inherit; + color: inherit; + appearance: none; + background: transparent; + border: 0; + + &:focus { + outline: 0; + } + + &::placeholder { + color: var(--fg-3); + opacity: 1; + } + + &:disabled { + color: var(--fg-4); + cursor: not-allowed; + } + } } diff --git a/assets/stylesheets/bootstrap/forms/_form-field.scss b/assets/stylesheets/bootstrap/forms/_form-field.scss new file mode 100644 index 00000000..3f58835f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_form-field.scss @@ -0,0 +1,79 @@ +@use "../mixins/border-radius" as *; + +// scss-docs-start form-field +@layer forms { + .form-field { + position: relative; + display: grid; + gap: .5rem; + // width: 100%; + + > label, + > .form-label { + justify-self: start; + margin-bottom: 0; + } + + &:has(> .check, > .radio, > .switch) { + grid-template-columns: auto 1fr; + column-gap: .5rem; + align-items: start; + + > .check, + > .radio, + > .switch { + grid-column: 1; + } + + > :not(.check, .radio, .switch) { + grid-column: 2; + } + + > .form-label { + grid-column: 1 / -1; + } + } + } + + .form-field-content { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .form-field-card { + position: relative; + padding: calc(var(--spacer) * .75); + cursor: pointer; + border: var(--border-width) solid transparent; + @include border-radius(var(--radius-7)); + + &:hover { + background-color: var(--bg-1); + } + + &:has(:checked) { + background-color: var(--bg-1); + border-color: var(--border-color); + } + + label::before { + position: absolute; + inset: 0; + content: ""; + } + } + + .form-group { + display: grid; + gap: .5rem; + + > label, + > .form-label, + > legend { + justify-self: start; + margin-bottom: 0; + } + } +} +// scss-docs-end form-field diff --git a/assets/stylesheets/bootstrap/forms/_form-range.scss b/assets/stylesheets/bootstrap/forms/_form-range.scss index 4732213e..fa051549 100644 --- a/assets/stylesheets/bootstrap/forms/_form-range.scss +++ b/assets/stylesheets/bootstrap/forms/_form-range.scss @@ -1,91 +1,216 @@ -// Range -// -// Style range inputs the same across browsers. Vendor-specific rules for pseudo -// elements cannot be mixed. As such, there are no shared styles for focus or -// active states on prefixed selectors. - -.form-range { - width: 100%; - height: add($form-range-thumb-height, $form-range-thumb-focus-box-shadow-width * 2); - padding: 0; // Need to reset padding - appearance: none; - background-color: transparent; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/transition" as *; +@use "../mixins/gradients" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$range-tokens: () !default; - &:focus { - outline: 0; +// scss-docs-start range-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$range-tokens: defaults( + ( + --range-track-width: 100%, + --range-track-height: .5rem, + --range-track-cursor: pointer, + --range-track-bg: var(--bg-3), + --range-track-border-radius: 1rem, + --range-track-fill-bg: var(--primary-base), + --range-track-disabled-bg: color-mix(in oklch, var(--bg-4), var(--fg-3)), + --range-thumb-width: 1rem, + --range-thumb-height: var(--range-thumb-width), + --range-thumb-bg: var(--primary-base), + --range-thumb-border: var(--range-thumb-bg) solid var(--border-color), + --range-thumb-border-radius: 1rem, + --range-thumb-box-shadow: "0 1px 2px rgb(0 0 0 / 7.5%), 0 2px 4px rgb(0 0 0 / 7.5%)", + --range-thumb-active-bg: color-mix(in oklch, var(--primary-base) 70%, var(--bg-body)), + --range-thumb-disabled-bg: var(--fg-3), + --range-thumb-transition-property: "background-color, border-color, box-shadow", + --range-thumb-transition-timing: .15s ease-in-out, + --range-thumb-transition: var(--range-thumb-transition-property) var(--range-thumb-transition-timing), + --range-tick-width: var(--border-width), + --range-tick-height: .5rem, + --range-tick-bg: var(--border-color), + ), + $range-tokens +); +// scss-docs-end range-tokens +// stylelint-enable custom-property-no-missing-var-function - // Pseudo-elements must be split across multiple rulesets to have an effect. - // No box-shadow() mixin for focus accessibility. - &::-webkit-slider-thumb { box-shadow: $form-range-thumb-focus-box-shadow; } - &::-moz-range-thumb { box-shadow: $form-range-thumb-focus-box-shadow; } +// scss-docs-start range-mixins +@mixin range-thumb() { + width: var(--range-thumb-width); + height: var(--range-thumb-height); + appearance: none; + @include gradient-bg(var(--range-thumb-bg)); + border: var(--range-thumb-border); + @include border-radius(var(--range-thumb-border-radius)); + @include box-shadow(var(--range-thumb-box-shadow)); + @include transition(var(--range-thumb-transition)); + + &:active { + @include gradient-bg(var(--range-thumb-active-bg)); } +} - &::-moz-focus-outer { - border: 0; +@mixin range-track() { + width: var(--range-track-width); + height: var(--range-track-height); + color: transparent; + cursor: var(--range-track-cursor); + // Fill (progress) up to the thumb. The Range plugin keeps `--range-fill` (0–1) in sync. + background-color: var(--range-track-bg); + background-image: + linear-gradient( + to right, + var(--range-track-fill-bg) calc(var(--range-fill, 0) * 100%), + transparent calc(var(--range-fill, 0) * 100%) + ); + border-color: transparent; + @include border-radius(var(--range-track-border-radius)); + @include box-shadow(var(--range-track-box-shadow)); +} +// scss-docs-end range-mixins + +@layer forms { + .form-range { + @include tokens($range-tokens); + + position: relative; + display: block; + width: 100%; } - &::-webkit-slider-thumb { - width: $form-range-thumb-width; - height: $form-range-thumb-height; - margin-top: ($form-range-track-height - $form-range-thumb-height) * .5; // Webkit specific + .form-range-input { + display: block; + width: 100%; + height: calc(var(--range-thumb-height) + (var(--focus-ring-width) * 2)); + padding: 0; appearance: none; - @include gradient-bg($form-range-thumb-bg); - border: $form-range-thumb-border; - @include border-radius($form-range-thumb-border-radius); - @include box-shadow($form-range-thumb-box-shadow); - @include transition($form-range-thumb-transition); - - &:active { - @include gradient-bg($form-range-thumb-active-bg); + background-color: transparent; + + &:hover { + &::-webkit-slider-thumb { + @include focus-ring(false, color-mix(in oklch, var(--primary-focus-ring), transparent)); + } + &::-moz-range-thumb { + @include focus-ring(false, color-mix(in oklch, var(--primary-focus-ring), transparent)); + } + } + + &:focus-visible { + outline: 0; + + &::-webkit-slider-thumb { + @include focus-ring(true); + --focus-ring-offset: 0; + } + &::-moz-range-thumb { + @include focus-ring(true); + --focus-ring-offset: 0; + } + } + + &::-moz-focus-outer { + border: 0; + } + + &::-webkit-slider-thumb { + @include range-thumb(); + margin-top: calc((var(--range-track-height) - var(--range-thumb-height)) * .5); + } + + &::-moz-range-thumb { + @include range-thumb(); + } + + &::-webkit-slider-runnable-track { + @include range-track(); + } + + &::-moz-range-track { + @include range-track(); } - } - &::-webkit-slider-runnable-track { - width: $form-range-track-width; - height: $form-range-track-height; - color: transparent; // Why? - cursor: $form-range-track-cursor; - background-color: $form-range-track-bg; - border-color: transparent; - @include border-radius($form-range-track-border-radius); - @include box-shadow($form-range-track-box-shadow); + &:disabled { + pointer-events: none; + + &::-webkit-slider-thumb { + background-color: var(--range-thumb-disabled-bg); + } + + &::-moz-range-thumb { + background-color: var(--range-thumb-disabled-bg); + } + + &::-webkit-slider-runnable-track { + --range-track-fill-bg: var(--range-track-disabled-bg); + } + + &::-moz-range-track { + --range-track-fill-bg: var(--range-track-disabled-bg); + } + } } - &::-moz-range-thumb { - width: $form-range-thumb-width; - height: $form-range-thumb-height; - appearance: none; - @include gradient-bg($form-range-thumb-bg); - border: $form-range-thumb-border; - @include border-radius($form-range-thumb-border-radius); - @include box-shadow($form-range-thumb-box-shadow); - @include transition($form-range-thumb-transition); - - &:active { - @include gradient-bg($form-range-thumb-active-bg); + // Value bubble: reuses the tooltip styles (`.tooltip` markup) so we don't duplicate the + // pill and arrow. We only add the static positioning the Tooltip plugin would normally do. + .form-range-bubble { + position: absolute; + bottom: 100%; + left: calc((var(--range-thumb-width) * .5) + var(--range-fill, 0) * (100% - var(--range-thumb-width))); + margin-bottom: var(--tooltip-arrow-height); + pointer-events: none; + transform: translateX(-50%); + + .tooltip-arrow { + position: absolute; + bottom: calc(-1 * var(--tooltip-arrow-height)); + left: 50%; + transform: translateX(-50%); } } - &::-moz-range-track { - width: $form-range-track-width; - height: $form-range-track-height; - color: transparent; - cursor: $form-range-track-cursor; - background-color: $form-range-track-bg; - border-color: transparent; // Firefox specific? - @include border-radius($form-range-track-border-radius); - @include box-shadow($form-range-track-box-shadow); + // Tick marks generated from the linked . Plugin builds `grid-template-columns` + // from the gaps between values so each tick lands on a grid line (handles uneven values). + // Track is inset by 1/4th of the thumb width to keep alignment. + .form-range-ticks { + display: grid; + padding-inline: calc(var(--range-thumb-width) * .25); } - &:disabled { - pointer-events: none; + .form-range-tick { + display: flex; + flex-direction: column; + align-items: center; + justify-self: start; + // Zero-width items so labels never widen their `fr` column; the tick line and label + // overflow centered on the grid line via `align-items`. + width: 0; - &::-webkit-slider-thumb { - background-color: $form-range-thumb-disabled-bg; + &::before { + width: var(--range-tick-width); + height: var(--range-tick-height); + content: ""; + background-color: var(--range-tick-bg); } - &::-moz-range-thumb { - background-color: $form-range-thumb-disabled-bg; + &:first-child { + align-items: flex-start; + } + + &:last-child { + align-items: flex-end; } } + + .form-range-tick-label { + margin-top: .125rem; + font-size: var(--font-size-sm); + color: var(--fg-2); + white-space: nowrap; + } } diff --git a/assets/stylesheets/bootstrap/forms/_form-select.scss b/assets/stylesheets/bootstrap/forms/_form-select.scss deleted file mode 100644 index 69ace529..00000000 --- a/assets/stylesheets/bootstrap/forms/_form-select.scss +++ /dev/null @@ -1,80 +0,0 @@ -// Select -// -// Replaces the browser default select with a custom one, mostly pulled from -// https://primer.github.io/. - -.form-select { - --#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator)}; - - display: block; - width: 100%; - padding: $form-select-padding-y $form-select-indicator-padding $form-select-padding-y $form-select-padding-x; - font-family: $form-select-font-family; - @include font-size($form-select-font-size); - font-weight: $form-select-font-weight; - line-height: $form-select-line-height; - color: $form-select-color; - appearance: none; - background-color: $form-select-bg; - background-image: var(--#{$prefix}form-select-bg-img), var(--#{$prefix}form-select-bg-icon, none); - background-repeat: no-repeat; - background-position: $form-select-bg-position; - background-size: $form-select-bg-size; - border: $form-select-border-width solid $form-select-border-color; - @include border-radius($form-select-border-radius, 0); - @include box-shadow($form-select-box-shadow); - @include transition($form-select-transition); - - &:focus { - border-color: $form-select-focus-border-color; - outline: 0; - @if $enable-shadows { - @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $form-select-focus-box-shadow; - } - } - - &[multiple], - &[size]:not([size="1"]) { - padding-right: $form-select-padding-x; - background-image: none; - } - - &:disabled { - color: $form-select-disabled-color; - background-color: $form-select-disabled-bg; - border-color: $form-select-disabled-border-color; - } - - // Remove outline from select box in FF - &:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 $form-select-color; - } -} - -.form-select-sm { - padding-top: $form-select-padding-y-sm; - padding-bottom: $form-select-padding-y-sm; - padding-left: $form-select-padding-x-sm; - @include font-size($form-select-font-size-sm); - @include border-radius($form-select-border-radius-sm); -} - -.form-select-lg { - padding-top: $form-select-padding-y-lg; - padding-bottom: $form-select-padding-y-lg; - padding-left: $form-select-padding-x-lg; - @include font-size($form-select-font-size-lg); - @include border-radius($form-select-border-radius-lg); -} - -@if $enable-dark-mode { - @include color-mode(dark) { - .form-select { - --#{$prefix}form-select-bg-img: #{escape-svg($form-select-indicator-dark)}; - } - } -} diff --git a/assets/stylesheets/bootstrap/forms/_form-text.scss b/assets/stylesheets/bootstrap/forms/_form-text.scss index f080d1a2..81259b73 100644 --- a/assets/stylesheets/bootstrap/forms/_form-text.scss +++ b/assets/stylesheets/bootstrap/forms/_form-text.scss @@ -1,11 +1,30 @@ -// -// Form text -// - -.form-text { - margin-top: $form-text-margin-top; - @include font-size($form-text-font-size); - font-style: $form-text-font-style; - font-weight: $form-text-font-weight; - color: $form-text-color; +@use "../functions" as *; +@use "../mixins/tokens" as *; + +$form-text-tokens: () !default; + +// scss-docs-start form-text-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-text-tokens: defaults( + ( + --form-text-margin-top: .25rem, + --form-text-font-size: var(--font-size-sm), + --form-text-font-style: null, + --form-text-font-weight: null, + --form-text-color: var(--fg-2), + ), + $form-text-tokens +); +// scss-docs-end form-text-tokens + +@layer forms { + .form-text { + @include tokens($form-text-tokens); + + // margin-top: var(--form-text-margin-top); + font-size: var(--form-text-font-size); + font-style: var(--form-text-font-style); + font-weight: var(--form-text-font-weight); + color: var(--form-text-color); + } } diff --git a/assets/stylesheets/bootstrap/forms/_input-group.scss b/assets/stylesheets/bootstrap/forms/_input-group.scss index 8078ebb1..c73598e5 100644 --- a/assets/stylesheets/bootstrap/forms/_input-group.scss +++ b/assets/stylesheets/bootstrap/forms/_input-group.scss @@ -1,132 +1,135 @@ -// -// Base styles -// - -.input-group { - position: relative; - display: flex; - flex-wrap: wrap; // For form validation feedback - align-items: stretch; - width: 100%; - - > .form-control, - > .form-select, - > .form-floating { - position: relative; // For focus state's z-index - flex: 1 1 auto; - width: 1%; - min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size - } - - // Bring the "active" form control to the top of surrounding elements - > .form-control:focus, - > .form-select:focus, - > .form-floating:focus-within { - z-index: 5; - } +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; + +$input-group-addon-tokens: () !default; + +// scss-docs-start input-group-addon-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$input-group-addon-tokens: defaults( + ( + --input-group-addon-padding-y: var(--btn-input-padding-y), + --input-group-addon-padding-x: var(--btn-input-padding-x), + --input-group-addon-font-size: var(--btn-input-font-size), + --input-group-addon-line-height: var(--btn-input-line-height), + --input-group-addon-color: var(--fg-body), + --input-group-addon-bg: var(--bg-2), + --input-group-addon-border-color: var(--border-color), + ), + $input-group-addon-tokens +); +// scss-docs-end input-group-addon-tokens + +// scss-docs-start input-group-sizes +$input-group-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$input-group-sizes: defaults( + ("sm", "lg"), + $input-group-sizes +); +// scss-docs-end input-group-sizes + +@layer components { + .input-group { + @include tokens($input-group-addon-tokens); - // Ensure buttons are always above inputs for more visually pleasing borders. - // This isn't needed for `.input-group-text` since it shares the same border-color - // as our inputs. - .btn { position: relative; - z-index: 2; + display: flex; + align-items: stretch; + width: 100%; + + > .form-control, + > .form-floating { + position: relative; // For focus state's z-index + flex: 1 1 auto; + width: 1%; + min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size + } - &:focus { + // Bring the "active" form control to the top of surrounding elements + > .form-control:focus, + > .form-floating:focus-within { z-index: 5; } - } -} - - -// Textual addons -// -// Serves as a catch-all element for any text or radio/checkbox input you wish -// to prepend or append to an input. - -.input-group-text { - display: flex; - align-items: center; - padding: $input-group-addon-padding-y $input-group-addon-padding-x; - @include font-size($input-font-size); // Match inputs - font-weight: $input-group-addon-font-weight; - line-height: $input-line-height; - color: $input-group-addon-color; - text-align: center; - white-space: nowrap; - background-color: $input-group-addon-bg; - border: $input-border-width solid $input-group-addon-border-color; - @include border-radius($input-border-radius); -} + // Ensure buttons are always above inputs for more visually pleasing borders. + // This isn't needed for `.input-group-text` since it shares the same border-color + // as our inputs. + > .input-group-btn { + position: relative; + z-index: 2; -// Sizing -// -// Remix the default form control sizing classes into new ones for easier -// manipulation. - -.input-group-lg > .form-control, -.input-group-lg > .form-select, -.input-group-lg > .input-group-text, -.input-group-lg > .btn { - padding: $input-padding-y-lg $input-padding-x-lg; - @include font-size($input-font-size-lg); - @include border-radius($input-border-radius-lg); -} - -.input-group-sm > .form-control, -.input-group-sm > .form-select, -.input-group-sm > .input-group-text, -.input-group-sm > .btn { - padding: $input-padding-y-sm $input-padding-x-sm; - @include font-size($input-font-size-sm); - @include border-radius($input-border-radius-sm); -} + &:focus { + z-index: 5; + } + } + } -.input-group-lg > .form-select, -.input-group-sm > .form-select { - padding-right: $form-select-padding-x + $form-select-indicator-padding; -} + // Textual addons + // + // Serves as a catch-all element for any text or radio/checkbox input you wish + // to prepend or append to an input. + + .input-group-text { + display: flex; + align-items: center; + padding: var(--input-group-addon-padding-y) var(--input-group-addon-padding-x); + font-size: var(--input-group-addon-font-size); // Match inputs + // font-weight: $input-group-addon-font-weight; + line-height: var(--input-group-addon-line-height); + color: var(--input-group-addon-color); + text-align: center; + white-space: nowrap; + background-color: var(--input-group-addon-bg); + border: var(--border-width) solid var(--input-group-addon-border-color); + @include border-radius(var(--btn-input-border-radius)); + } + // Sizing + // + // Remix the default form control sizing classes into new ones for easier + // manipulation. + + @each $size, $_ in $input-group-sizes { + .input-group-#{$size} { + > .form-control, + > .input-group-text, + > .btn { + min-height: var(--btn-input-#{$size}-min-height); + padding: var(--btn-input-#{$size}-padding-y) var(--btn-input-#{$size}-padding-x); + font-size: var(--btn-input-#{$size}-font-size); + @include border-radius(var(--btn-input-#{$size}-border-radius)); + } + } + } -// Rounded corners -// -// These rulesets must come after the sizing ones to properly override sm and lg -// border-radius values when extending. They're more specific than we'd like -// with the `.input-group >` part, but without it, we cannot override the sizing. + // Rounded corners + // + // These rulesets must come after the sizing ones to properly override sm and lg + // border-radius values when extending. They're more specific than we'd like + // with the `.input-group >` part, but without it, we cannot override the sizing. -// stylelint-disable-next-line no-duplicate-selectors -.input-group { - &:not(.has-validation) { - > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), - > .dropdown-toggle:nth-last-child(n + 3), + // stylelint-disable-next-line no-duplicate-selectors + .input-group { + > :not(:last-child, .menu-toggle-split, .menu, .input-group-ignore, .form-floating, :has(+ :is(.menu, .input-group-ignore):last-child)), + > .menu-toggle-split:nth-last-child(n + 3), > .form-floating:not(:last-child) > .form-control, > .form-floating:not(:last-child) > .form-select { @include border-end-radius(0); } - } - &.has-validation { - > :nth-last-child(n + 3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), - > .dropdown-toggle:nth-last-child(n + 4), - > .form-floating:nth-last-child(n + 3) > .form-control, - > .form-floating:nth-last-child(n + 3) > .form-select { - @include border-end-radius(0); + > :not(:first-child, .menu, .input-group-ignore) { + margin-inline-start: calc(-1 * var(--border-width)); + @include border-start-radius(0); } - } - - $validation-messages: ""; - @each $state in map-keys($form-validation-states) { - $validation-messages: $validation-messages + ":not(." + unquote($state) + "-tooltip)" + ":not(." + unquote($state) + "-feedback)"; - } - > :not(:first-child):not(.dropdown-menu)#{$validation-messages} { - margin-left: calc(-1 * #{$input-border-width}); // stylelint-disable-line function-disallowed-list - @include border-start-radius(0); - } + > :first-child:is(.input-group-ignore) + :not(.menu, .input-group-ignore) { + @include border-start-radius(var(--btn-input-border-radius)); + } - > .form-floating:not(:first-child) > .form-control, - > .form-floating:not(:first-child) > .form-select { - @include border-start-radius(0); + > .form-floating:not(:first-child) > .form-control, + > .form-floating:not(:first-child) > .form-select { + @include border-start-radius(0); + } } } diff --git a/assets/stylesheets/bootstrap/forms/_labels.scss b/assets/stylesheets/bootstrap/forms/_labels.scss index 39ecafcd..462a1bcc 100644 --- a/assets/stylesheets/bootstrap/forms/_labels.scss +++ b/assets/stylesheets/bootstrap/forms/_labels.scss @@ -1,36 +1,49 @@ -// -// Labels -// +@use "../functions" as *; -.form-label { - margin-bottom: $form-label-margin-bottom; - @include font-size($form-label-font-size); - font-style: $form-label-font-style; - font-weight: $form-label-font-weight; - color: $form-label-color; -} +$form-label-tokens: () !default; -// For use with horizontal and inline forms, when you need the label (or legend) -// text to align with the form controls. -.col-form-label { - padding-top: add($input-padding-y, $input-border-width); - padding-bottom: add($input-padding-y, $input-border-width); - margin-bottom: 0; // Override the `` default - @include font-size(inherit); // Override the `` default - font-style: $form-label-font-style; - font-weight: $form-label-font-weight; - line-height: $input-line-height; - color: $form-label-color; -} +// scss-docs-start form-label-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$form-label-tokens: defaults( + ( + --label-margin-bottom: calc(var(--spacer) / 2), + --label-font-size: null, + --label-font-style: null, + --label-font-weight: null, + --label-color: null, + ), + $form-label-tokens +); +// scss-docs-end form-label-tokens -.col-form-label-lg { - padding-top: add($input-padding-y-lg, $input-border-width); - padding-bottom: add($input-padding-y-lg, $input-border-width); - @include font-size($input-font-size-lg); -} +@layer forms { + .form-label, + .col-form-label { + font-size: var(--label-font-size, inherit); + font-style: var(--label-font-style, inherit); + font-weight: var(--label-font-weight, 500); + color: var(--label-color, var(--fg-body)); + } + + .form-label { + margin-bottom: var(--label-margin-bottom, calc(var(--spacer) / 2)); + } + + // For use with horizontal and inline forms, when you need the label (or legend) + // text to align with the form controls. + .col-form-label { + --label-padding-y: calc(var(--btn-input-padding-y) + var(--border-width)); + padding-block: var(--label-padding-y); + margin-bottom: 0; // Override the `` default + } + + .col-form-label-lg { + --label-padding-y: calc(var(--btn-input-lg-padding-y) + var(--border-width)); + font-size: var(--btn-input-lg-font-size); + } -.col-form-label-sm { - padding-top: add($input-padding-y-sm, $input-border-width); - padding-bottom: add($input-padding-y-sm, $input-border-width); - @include font-size($input-font-size-sm); + .col-form-label-sm { + --label-padding-y: calc(var(--btn-input-sm-padding-y) + var(--border-width)); + font-size: var(--btn-input-sm-font-size); + } } diff --git a/assets/stylesheets/bootstrap/forms/_otp-input.scss b/assets/stylesheets/bootstrap/forms/_otp-input.scss new file mode 100644 index 00000000..06e34a57 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_otp-input.scss @@ -0,0 +1,159 @@ +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/box-shadow" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +$otp-tokens: () !default; + +// scss-docs-start otp-tokens +// stylelint-disable custom-property-no-missing-var-function +// stylelint-disable-next-line scss/dollar-variable-default +$otp-tokens: defaults( + ( + --otp-size: var(--btn-input-lg-min-height), + --otp-font-size: var(--btn-input-font-size), + --otp-gap: .5rem, + --otp-slot-fg: var(--btn-input-fg), + --otp-slot-bg: var(--btn-input-bg), + --otp-slot-border-width: var(--border-width), + --otp-slot-border-color: var(--border-color), + --otp-slot-border-radius: var(--radius-5), + ), + $otp-tokens +); +// scss-docs-end otp-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start otp-sizes +$otp-sizes: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$otp-sizes: defaults( + ("sm", "lg"), + $otp-sizes +); +// scss-docs-end otp-sizes + +@layer components { + .otp { + @include tokens($otp-tokens); + + position: relative; + display: flex; + } + + // A single real input backs the whole control. Once the JS renders the + // visual slots (`.otp-rendered`), the input becomes a transparent overlay + // that captures all interaction while the slots display the value. + .otp-rendered .otp-input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 0; + color: transparent; + text-align: center; + cursor: default; + caret-color: transparent; + background-color: transparent; + border: 0; + outline: 0; + box-shadow: none; + + // Never reveal the underlying characters, even on selection + &::selection { + color: transparent; + background-color: transparent; + } + } + + .otp-slots { + display: inline-flex; + gap: var(--otp-gap); + pointer-events: none; // let clicks fall through to the input overlay + } + + .otp-slot { + display: flex; + align-items: center; + justify-content: center; + width: var(--otp-size); + min-height: var(--otp-size); + font-size: var(--otp-font-size); + font-weight: 500; + line-height: 1; + color: var(--otp-slot-fg); + background-color: var(--otp-slot-bg); + border: var(--otp-slot-border-width) solid var(--otp-slot-border-color); + @include border-radius(var(--otp-slot-border-radius)); + @include box-shadow(var(--box-shadow-inset)); + @include transition(border-color .15s ease-in-out, box-shadow .15s ease-in-out); + } + + // The slot at the caret gets the focus ring; empty active slots show a + // blinking caret so the entry point is obvious. + .otp-slot-active { + --focus-ring-offset: -1px; + z-index: 1; + @include focus-ring(true); + + &:not(.otp-slot-filled)::after { + width: 1px; + height: 50%; + content: ""; + background-color: var(--otp-slot-fg); + animation: otp-caret-blink 1s step-end infinite; + } + } + + // Disabled state mirrors disabled form controls + .otp-input:disabled ~ .otp-slots .otp-slot { + background-color: var(--bg-2); + } + + // Connected slots share borders for a single cohesive field + .otp-connected .otp-slots { + gap: 0; + } + .otp-connected .otp-slot { + border-radius: 0; // stylelint-disable-line property-disallowed-list + + &:not(:first-child) { + margin-inline-start: calc(var(--otp-slot-border-width) * -1); + } + &:first-child { + @include border-start-radius(var(--otp-slot-border-radius)); + } + &:last-child { + @include border-end-radius(var(--otp-slot-border-radius)); + } + } + + .otp-separator { + display: flex; + align-items: center; + padding-inline: var(--otp-gap); + font-size: var(--otp-font-size); + color: var(--fg-4); + user-select: none; + } + + // OTP input sizing — keep in sync with `$form-control-sizes`. + @each $size, $_ in $otp-sizes { + .otp-#{$size} { + --otp-size: var(--btn-input-#{$size}-min-height); + --otp-font-size: var(--btn-input-#{$size}-font-size); + } + } +} + +@keyframes otp-caret-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} diff --git a/assets/stylesheets/bootstrap/forms/_radio.scss b/assets/stylesheets/bootstrap/forms/_radio.scss new file mode 100644 index 00000000..71dd811c --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_radio.scss @@ -0,0 +1,88 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$radio-tokens: () !default; + +// scss-docs-start radio-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$radio-tokens: defaults( + ( + --radio-size: 1.25rem, + --radio-margin-block: .125rem, + --radio-bg: var(--bg-body), + --radio-border-color: var(--border-color), + --radio-checked-bg: var(--control-checked-bg), + --radio-checked-border-color: var(--control-checked-border-color), + --radio-disabled-bg: var(--control-disabled-bg), + --radio-disabled-opacity: var(--control-disabled-opacity), + ), + $radio-tokens +); +// scss-docs-end radio-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .radio { + @include tokens($radio-tokens); + + position: relative; + flex-shrink: 0; + width: var(--radio-size); + height: var(--radio-size); + margin-block: var(--radio-margin-block); + appearance: none; + background-color: var(--theme-bg, var(--radio-bg)); + border: 1px solid var(--theme-bg, var(--radio-border-color)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + + &:checked { + color: var(--theme-contrast, var(--primary-contrast)); + background-color: var(--theme-bg, var(--radio-checked-bg)); + border-color: var(--theme-bg, var(--radio-checked-border-color)); + + &::before { + position: absolute; + inset: calc(var(--radio-size) * .25); + content: ""; + background-color: currentcolor; + // stylelint-disable-next-line property-disallowed-list + border-radius: 50%; + } + } + + &:disabled { + --radio-bg: var(--radio-disabled-bg); + + ~ label { + color: var(--secondary-fg); + cursor: default; + } + } + &:disabled:checked { + opacity: var(--radio-disabled-opacity); + } + + &:focus-visible { + @include focus-ring(true); + } + } + + .radio-sm { + --radio-size: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .radio-lg { + --radio-size: 1.5rem; + --radio-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_strength.scss b/assets/stylesheets/bootstrap/forms/_strength.scss new file mode 100644 index 00000000..a4140234 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_strength.scss @@ -0,0 +1,111 @@ +@use "sass:list"; +@use "../functions" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/tokens" as *; +@use "../mixins/transition" as *; + +// stylelint-disable custom-property-no-missing-var-function +$strength-tokens: () !default; + +// scss-docs-start strength-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$strength-tokens: defaults( + ( + --strength-height: .375rem, + --strength-gap: .25rem, + --strength-margin-top: .25rem, + --strength-border-radius: var(--radius-pill), + --strength-bg: var(--bg-2), + --strength-color: var(--bg-2), + --strength-weak-color: var(--danger-bg), + --strength-fair-color: var(--warning-bg), + --strength-good-color: var(--info-bg), + --strength-strong-color: var(--success-bg), + ), + $strength-tokens +); +// scss-docs-end strength-tokens +// stylelint-enable custom-property-no-missing-var-function + +// scss-docs-start strength-levels +$strength-levels: weak, fair, good, strong !default; +// scss-docs-end strength-levels + +$strength-transition: background-color .2s ease-in-out, width .3s ease-in-out !default; + +@layer forms { + // Strength meter container + .strength { + @include tokens($strength-tokens); + + display: flex; + gap: var(--strength-gap); + width: 100%; + margin-top: var(--strength-margin-top); + } + + // Individual strength segments + .strength-segment { + flex: 1; + height: var(--strength-height); + background-color: var(--strength-bg); + @include border-radius(var(--strength-border-radius)); + @include transition($strength-transition); + + // Filled state + &.active { + background-color: var(--strength-color); + } + } + + @each $level in $strength-levels { + .strength[data-bs-strength="#{$level}"] { + --strength-color: var(--strength-#{$level}-color); + } + } + // Optional text feedback + .strength-text { + display: block; + margin-top: var(--strength-margin-top); + font-size: var(--font-size-xs); + color: var(--strength-color, var(--fg-3)); + @include transition(color .2s ease-in-out); + + // Hide when empty + &:empty { + display: none; + } + } + + // Alternative: Single bar variant (like a progress bar) + .strength-bar { + @include tokens($strength-tokens); + + --strength-color: transparent; + --strength-width: 0%; + + width: 100%; + height: var(--strength-height); + margin-top: var(--strength-margin-top); + overflow: hidden; + background-color: var(--strength-bg); + @include border-radius(var(--strength-border-radius)); + + &::after { + display: block; + width: var(--strength-width); + height: 100%; + content: ""; + background-color: var(--strength-color); + @include border-radius(var(--strength-border-radius)); + @include transition($strength-transition); + } + + @each $level in $strength-levels { + &[data-bs-strength="#{$level}"] { + --strength-color: var(--strength-#{$level}-color); + --strength-width: #{list.index($strength-levels, $level) * 25%}; + } + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_switch.scss b/assets/stylesheets/bootstrap/forms/_switch.scss new file mode 100644 index 00000000..2357b744 --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/_switch.scss @@ -0,0 +1,123 @@ +@use "../functions" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/tokens" as *; + +// stylelint-disable custom-property-no-missing-var-function +$switch-tokens: () !default; + +// scss-docs-start switch-tokens +// stylelint-disable-next-line scss/dollar-variable-default +$switch-tokens: defaults( + ( + --switch-height: 1.25rem, + --switch-width: calc(var(--switch-height) * 1.75), + --switch-padding: .0625rem, + --switch-margin-block: .125rem, + --switch-bg: var(--bg-3), + --switch-border-width: var(--border-width), + --switch-border-color: var(--border-color), + --switch-indicator-bg: var(--white), + --switch-indicator-width: calc(var(--switch-height) - calc(var(--switch-padding) * 2) - var(--switch-border-width) * 2), + --switch-indicator-height: calc(var(--switch-height) - calc(var(--switch-padding) * 2) - var(--switch-border-width) * 2), + --switch-checked-bg: var(--control-checked-bg), + --switch-checked-border-color: var(--switch-checked-bg), + --switch-checked-indicator-bg: var(--white), + --switch-disabled-bg: var(--control-disabled-bg), + --switch-disabled-indicator-bg: var(--fg-3), + ), + $switch-tokens +); +// scss-docs-end switch-tokens +// stylelint-enable custom-property-no-missing-var-function + +@layer forms { + .switch { + @include tokens($switch-tokens); + + position: relative; + flex-shrink: 0; + width: var(--switch-width); + height: var(--switch-height); + padding: var(--switch-padding); + margin-block: var(--switch-margin-block); + background-color: var(--switch-bg); + border: var(--switch-border-width) solid var(--switch-border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: 10rem; + box-shadow: inset 0 1px 2px rgb(0 0 0 / .05); + // stylelint-disable-next-line property-disallowed-list + transition: background-color .15s ease-in-out; + + &::before { + position: absolute; + inset-block: var(--switch-padding); + inset-inline-start: var(--switch-padding); + width: var(--switch-indicator-width); + height: var(--switch-indicator-height); + content: ""; + background-color: var(--theme-contrast, var(--switch-indicator-bg)); + // stylelint-disable-next-line property-disallowed-list + border-radius: 10rem; + box-shadow: 0 1px 2px rgb(0 0 0 / .1); + // stylelint-disable-next-line property-disallowed-list + transition: inset-inline-start .15s ease-in-out; + } + + input { + position: absolute; + inset: 0; + appearance: none; + background-color: transparent; + outline: 0; + } + + &:focus-within { + @include focus-ring(true); + } + + &:has(input:disabled:not(:checked)) { + --switch-bg: var(--switch-disabled-bg); + --switch-indicator-bg: var(--switch-disabled-indicator-bg); + + &::before { opacity: .4; } + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + + &:has(input:checked) { + background-color: var(--theme-bg, var(--switch-checked-bg)); + border-color: var(--theme-bg, var(--switch-checked-border-color)); + + &::before { + inset-inline-start: calc(100% - var(--switch-indicator-width) - var(--switch-padding)); + } + } + + &:has(input:checked:disabled) { + opacity: .65; + + ~ label { + color: var(--fg-3); + cursor: default; + } + } + } + .switch-sm { + --switch-height: 1rem; + + + label { + font-size: var(--font-size-sm); + } + } + .switch-lg { + --switch-height: 1.5rem; + --switch-margin-block: .375rem; + + + label { + font-size: var(--font-size-lg); + } + } +} diff --git a/assets/stylesheets/bootstrap/forms/_validation.scss b/assets/stylesheets/bootstrap/forms/_validation.scss index c48123a7..acf51fd2 100644 --- a/assets/stylesheets/bootstrap/forms/_validation.scss +++ b/assets/stylesheets/bootstrap/forms/_validation.scss @@ -1,12 +1,360 @@ +@use "../config" as *; +@use "../mixins/border-radius" as *; +@use "../mixins/focus-ring" as *; +@use "../mixins/form-validation" as *; + // Form validation // -// Provide feedback to users when form field values are valid or invalid. Works -// primarily for client-side validation via scoped `:invalid` and `:valid` -// pseudo-classes but also includes `.is-invalid` and `.is-valid` classes for -// server-side validation. - -// scss-docs-start form-validation-states-loop -@each $state, $data in $form-validation-states { - @include form-validation-state($state, $data...); +// Provide feedback to users when form field values are valid or invalid. +// Server-side: `.is-invalid` / `.is-valid` classes work globally. +// Client-side: `:user-invalid` pseudo-class is scoped behind `[data-bs-validate]`. +// `:user-valid` is scoped behind `[data-bs-validate~="valid"]` so success styling is opt-in. +// Custom states (e.g., "warning") use only `.is-*` classes. + +// scss-docs-start form-validation-states +$validation-states: () !default; +// stylelint-disable-next-line scss/dollar-variable-default +$validation-states: defaults( + ( + "valid": "success", + "invalid": "danger", + ), + $validation-states +); +// scss-docs-end form-validation-states + +// scss-docs-start form-validation-state-mixin +@mixin form-validation-state($state, $theme) { + .#{$state}-feedback { + display: none; + width: 100%; + font-size: var(--font-size-sm); + color: var(--#{$theme}-fg); + } + + // More specific to override base tooltip styles + .tooltip.#{$state}-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: var(--tooltip-padding-y) var(--tooltip-padding-x); + margin-top: .1rem; + color: var(--#{$theme}-contrast); + text-align: center; + background-color: var(--#{$theme}-bg); + opacity: 1; + @include border-radius(var(--tooltip-border-radius)); + } + + // Generic sibling feedback display — works for .form-control, .form-range, + // and any element where feedback is a direct sibling. + @include form-validation-state-selector($state) { + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { + display: block; + } + } + + // Form control + .form-control { + @include form-validation-state-selector($state) { + --control-border-color: var(--#{$theme}-border); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + --control-border-color: var(--#{$theme}-border); + } + } + } + + // Checkbox — control-level styling (border, checked bg, focus ring). + .check { + @include form-validation-state-selector($state) { + --check-border-color: var(--#{$theme}-border); + --check-checked-bg: var(--#{$theme}-bg); + --check-checked-border-color: var(--#{$theme}-bg); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Checkbox — label color and feedback display via .form-field:has(). + .form-field:has(.check.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.check:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.check:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Radio — control-level styling. + .radio { + @include form-validation-state-selector($state) { + --radio-border-color: var(--#{$theme}-border); + --radio-checked-bg: var(--#{$theme}-bg); + --radio-checked-border-color: var(--#{$theme}-bg); + + &:focus-visible { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Radio — label color and feedback display via .form-field:has(). + .form-field:has(.radio.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.radio:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.radio:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Switch — control-level styling. The input is an invisible overlay; + // all visuals are on the .switch wrapper. + .switch:has(input.is-#{$state}) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + + @if $state == "invalid" { + [data-bs-validate] .switch:has(input:user-invalid) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .switch:has(input:user-valid) { + --switch-border-color: var(--#{$theme}-border); + --switch-checked-bg: var(--#{$theme}-bg); + --switch-checked-border-color: var(--#{$theme}-bg); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + + // Switch — label color and feedback display via .form-field:has(). + .form-field:has(.switch input.is-#{$state}) { + label { color: var(--#{$theme}-fg); } + + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.switch input:user-invalid) { + label { color: var(--#{$theme}-fg); } + + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.switch input:user-valid) { + label { color: var(--#{$theme}-fg); } + + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + // Chip input — wrapper has the visible border; the .form-ghost inside + // receives the native pseudo-class. + .chip-input:has(.form-ghost.is-#{$state}) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .chip-input:has(.form-ghost:user-invalid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .invalid-feedback, + ~ .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .chip-input:has(.form-ghost:user-valid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .valid-feedback, + ~ .valid-tooltip { display: block; } + } + } + + // Form adorn — :user-invalid fires on the inner .form-ghost, so we + // propagate it to the visible wrapper with :has(). + .form-adorn:has(.form-ghost.is-#{$state}) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-adorn:has(.form-ghost:user-invalid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .invalid-feedback, + ~ .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-adorn:has(.form-ghost:user-valid) { + border-color: var(--#{$theme}-border); + + &:focus-within { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + border-color: var(--#{$theme}-border); + } + + ~ .valid-feedback, + ~ .valid-tooltip { display: block; } + } + } + + // Range — the validation class lives on .form-range-input, while feedback sits outside + // the .form-range wrapper, so we use :has() to toggle it. + .form-range-input { + @include form-validation-state-selector($state) { + &::-webkit-slider-thumb { background: var(--#{$theme}-bg); } + &::-moz-range-thumb { background: var(--#{$theme}-bg); } + + &:focus-visible { + &::-webkit-slider-thumb { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + &::-moz-range-thumb { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } + } + + .form-range:has(.form-range-input.is-#{$state}) { + ~ .#{$state}-feedback, + ~ .#{$state}-tooltip { display: block; } + } + + // Input group — feedback lives outside the input-group in the parent + // .form-field, so we use :has() to toggle display. + .form-field:has(.input-group .form-control.is-#{$state}) { + .#{$state}-feedback, + .#{$state}-tooltip { display: block; } + } + + @if $state == "invalid" { + [data-bs-validate] .form-field:has(.input-group .form-control:user-invalid) { + .invalid-feedback, + .invalid-tooltip { display: block; } + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] .form-field:has(.input-group .form-control:user-valid) { + .valid-feedback, + .valid-tooltip { display: block; } + } + } + + .input-group { + > .form-control:not(:focus), + > .form-floating:not(:focus-within) { + @include form-validation-state-selector($state) { + @if $state == "valid" { + z-index: 3; + } @else if $state == "invalid" { + z-index: 4; + } + } + } + } + + // OTP — validation applies to the wrapper; the visual slots inherit the state. + .otp { + @include form-validation-state-selector($state) { + .otp-slot { + --otp-slot-border-color: var(--#{$theme}-border); + } + + .otp-slot-active { + @include focus-ring(true, $color: var(--#{$theme}-focus-ring)); + } + } + } +} +// scss-docs-end form-validation-state-mixin + +@layer components { + // scss-docs-start form-validation-states-loop + @each $state, $theme in $validation-states { + @include form-validation-state($state, $theme); + } + // scss-docs-end form-validation-states-loop } -// scss-docs-end form-validation-states-loop diff --git a/assets/stylesheets/bootstrap/forms/index.scss b/assets/stylesheets/bootstrap/forms/index.scss new file mode 100644 index 00000000..58cd150f --- /dev/null +++ b/assets/stylesheets/bootstrap/forms/index.scss @@ -0,0 +1,16 @@ +@forward "labels"; +@forward "form-text"; +@forward "form-control"; +@forward "check"; +@forward "radio"; +@forward "switch"; +@forward "form-range"; +@forward "floating-labels"; +@forward "input-group"; +@forward "strength"; +@forward "otp-input"; +@forward "form-adorn"; +@forward "chip-input"; +@forward "combobox"; +@forward "form-field"; +@forward "validation"; diff --git a/assets/stylesheets/bootstrap/helpers/_clearfix.scss b/assets/stylesheets/bootstrap/helpers/_clearfix.scss deleted file mode 100644 index e92522a9..00000000 --- a/assets/stylesheets/bootstrap/helpers/_clearfix.scss +++ /dev/null @@ -1,3 +0,0 @@ -.clearfix { - @include clearfix(); -} diff --git a/assets/stylesheets/bootstrap/helpers/_color-bg.scss b/assets/stylesheets/bootstrap/helpers/_color-bg.scss deleted file mode 100644 index 1a3a4cff..00000000 --- a/assets/stylesheets/bootstrap/helpers/_color-bg.scss +++ /dev/null @@ -1,7 +0,0 @@ -// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251 -@each $color, $value in $theme-colors { - .text-bg-#{$color} { - color: color-contrast($value) if($enable-important-utilities, !important, null); - background-color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}bg-opacity, 1)) if($enable-important-utilities, !important, null); - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_colored-links.scss b/assets/stylesheets/bootstrap/helpers/_colored-links.scss deleted file mode 100644 index 5f868578..00000000 --- a/assets/stylesheets/bootstrap/helpers/_colored-links.scss +++ /dev/null @@ -1,30 +0,0 @@ -// All-caps `RGBA()` function used because of this Sass bug: https://github.com/sass/node-sass/issues/2251 -@each $color, $value in $theme-colors { - .link-#{$color} { - color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}#{$color}-rgb), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - - @if $link-shade-percentage != 0 { - &:hover, - &:focus { - $hover-color: if(color-contrast($value) == $color-contrast-light, shade-color($value, $link-shade-percentage), tint-color($value, $link-shade-percentage)); - color: RGBA(#{to-rgb($hover-color)}, var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(to-rgb($hover-color), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - } - } - } -} - -// One-off special link helper as a bridge until v6 -.link-body-emphasis { - color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-opacity, 1)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-underline-opacity, 1)) if($enable-important-utilities, !important, null); - - @if $link-shade-percentage != 0 { - &:hover, - &:focus { - color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-opacity, .75)) if($enable-important-utilities, !important, null); - text-decoration-color: RGBA(var(--#{$prefix}emphasis-color-rgb), var(--#{$prefix}link-underline-opacity, .75)) if($enable-important-utilities, !important, null); - } - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_focus-ring.scss b/assets/stylesheets/bootstrap/helpers/_focus-ring.scss index 26508a8d..c210d8d3 100644 --- a/assets/stylesheets/bootstrap/helpers/_focus-ring.scss +++ b/assets/stylesheets/bootstrap/helpers/_focus-ring.scss @@ -1,5 +1,6 @@ -.focus-ring:focus { - outline: 0; - // By default, there is no `--bs-focus-ring-x`, `--bs-focus-ring-y`, or `--bs-focus-ring-blur`, but we provide CSS variables with fallbacks to initial `0` values - box-shadow: var(--#{$prefix}focus-ring-x, 0) var(--#{$prefix}focus-ring-y, 0) var(--#{$prefix}focus-ring-blur, 0) var(--#{$prefix}focus-ring-width) var(--#{$prefix}focus-ring-color); +@layer helpers { + .focus-ring:focus-visible { + // outline: var(--focus-ring); + outline: var(--focus-ring-width) solid var(--theme-focus-ring, var(--focus-ring-color)); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_icon-link.scss b/assets/stylesheets/bootstrap/helpers/_icon-link.scss index 3f8bcb33..23f0ad71 100644 --- a/assets/stylesheets/bootstrap/helpers/_icon-link.scss +++ b/assets/stylesheets/bootstrap/helpers/_icon-link.scss @@ -1,25 +1,30 @@ -.icon-link { - display: inline-flex; - gap: $icon-link-gap; - align-items: center; - text-decoration-color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, .5)); - text-underline-offset: $icon-link-underline-offset; - backface-visibility: hidden; +@use "../config" as *; +@use "../mixins/transition" as *; - > .bi { - flex-shrink: 0; - width: $icon-link-icon-size; - height: $icon-link-icon-size; - fill: currentcolor; - @include transition($icon-link-icon-transition); - } -} +@layer helpers { + .icon-link { + display: inline-flex; + gap: $icon-link-gap; + align-items: center; + text-decoration-color: rgba(var(--link-color-rgb), var(--link-opacity, .5)); + text-underline-offset: $icon-link-underline-offset; + backface-visibility: hidden; -.icon-link-hover { - &:hover, - &:focus-visible { > .bi { - transform: var(--#{$prefix}icon-link-transform, $icon-link-icon-transform); + flex-shrink: 0; + width: $icon-link-icon-size; + height: $icon-link-icon-size; + fill: currentcolor; + @include transition($icon-link-icon-transition); + } + } + + .icon-link-hover { + &:hover, + &:focus-visible { + > .bi { + transform: var(--icon-link-transform, $icon-link-icon-transform); + } } } } diff --git a/assets/stylesheets/bootstrap/helpers/_position.scss b/assets/stylesheets/bootstrap/helpers/_position.scss index 59103d94..3c94967d 100644 --- a/assets/stylesheets/bootstrap/helpers/_position.scss +++ b/assets/stylesheets/bootstrap/helpers/_position.scss @@ -1,36 +1,36 @@ -// Shorthand +@use "sass:map"; +@use "../config" as *; +@use "../layout/breakpoints" as *; -.fixed-top { - position: fixed; - top: 0; - right: 0; - left: 0; - z-index: $zindex-fixed; -} +@layer helpers { + .fixed-top { + position: fixed; + inset: 0 0 auto; + z-index: $zindex-fixed; + } -.fixed-bottom { - position: fixed; - right: 0; - bottom: 0; - left: 0; - z-index: $zindex-fixed; -} + .fixed-bottom { + position: fixed; + inset: auto 0 0; + z-index: $zindex-fixed; + } -// Responsive sticky top and bottom -@each $breakpoint in map-keys($grid-breakpoints) { - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + // Responsive sticky top and bottom + @each $breakpoint in map.keys($breakpoints) { + @include media-breakpoint-up($breakpoint) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); - .sticky#{$infix}-top { - position: sticky; - top: 0; - z-index: $zindex-sticky; - } + .#{$prefix}sticky-top { + position: sticky; + top: 0; + z-index: $zindex-sticky; + } - .sticky#{$infix}-bottom { - position: sticky; - bottom: 0; - z-index: $zindex-sticky; + .#{$prefix}sticky-bottom { + position: sticky; + bottom: 0; + z-index: $zindex-sticky; + } } } } diff --git a/assets/stylesheets/bootstrap/helpers/_ratio.scss b/assets/stylesheets/bootstrap/helpers/_ratio.scss deleted file mode 100644 index b6a7654c..00000000 --- a/assets/stylesheets/bootstrap/helpers/_ratio.scss +++ /dev/null @@ -1,26 +0,0 @@ -// Credit: Nicolas Gallagher and SUIT CSS. - -.ratio { - position: relative; - width: 100%; - - &::before { - display: block; - padding-top: var(--#{$prefix}aspect-ratio); - content: ""; - } - - > * { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } -} - -@each $key, $ratio in $aspect-ratios { - .ratio-#{$key} { - --#{$prefix}aspect-ratio: #{$ratio}; - } -} diff --git a/assets/stylesheets/bootstrap/helpers/_stacks.scss b/assets/stylesheets/bootstrap/helpers/_stacks.scss index 6cd237ae..1ed716e5 100644 --- a/assets/stylesheets/bootstrap/helpers/_stacks.scss +++ b/assets/stylesheets/bootstrap/helpers/_stacks.scss @@ -1,15 +1,33 @@ -// scss-docs-start stacks -.hstack { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; -} +@use "../layout/breakpoints" as *; + +@layer helpers { + // scss-docs-start stacks + .stack-container { + @include set-container(); + } + + [class*="hstack"], + [class*="vstack"] { + display: flex; + flex: var(--stack-flex, 1 1 auto); + flex-direction: var(--stack-direction, row); + align-items: var(--stack-align-items, center); + align-self: var(--stack-align-self, stretch); + } -.vstack { - display: flex; - flex: 1 1 auto; - flex-direction: column; - align-self: stretch; + @include loop-breakpoints-up() using ($breakpoint, $prefix) { + .#{$prefix}vstack { + @include container-breakpoint-up($breakpoint) { + --stack-direction: column; + --stack-align-items: stretch; + } + } + .#{$prefix}hstack { + @include container-breakpoint-up($breakpoint) { + --stack-direction: row; + --stack-align-items: flex-start; + } + } + } + // scss-docs-end stacks } -// scss-docs-end stacks diff --git a/assets/stylesheets/bootstrap/helpers/_stretched-link.scss b/assets/stylesheets/bootstrap/helpers/_stretched-link.scss index 71a1c755..c3a319b6 100644 --- a/assets/stylesheets/bootstrap/helpers/_stretched-link.scss +++ b/assets/stylesheets/bootstrap/helpers/_stretched-link.scss @@ -1,15 +1,12 @@ -// -// Stretched link -// +@use "../config" as *; -.stretched-link { - &::#{$stretched-link-pseudo-element} { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: $stretched-link-z-index; - content: ""; +@layer helpers { + .stretched-link { + &::#{$stretched-link-pseudo-element} { + position: absolute; + inset: 0; + z-index: $stretched-link-z-index; + content: ""; + } } } diff --git a/assets/stylesheets/bootstrap/helpers/_text-truncation.scss b/assets/stylesheets/bootstrap/helpers/_text-truncation.scss index 6421dac9..b2f423cf 100644 --- a/assets/stylesheets/bootstrap/helpers/_text-truncation.scss +++ b/assets/stylesheets/bootstrap/helpers/_text-truncation.scss @@ -1,7 +1,7 @@ -// -// Text truncation -// +@use "../mixins/text-truncate" as *; -.text-truncate { - @include text-truncate(); +@layer helpers { + .text-truncate { + @include text-truncate(); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_theme-colors.scss b/assets/stylesheets/bootstrap/helpers/_theme-colors.scss new file mode 100644 index 00000000..b40fa196 --- /dev/null +++ b/assets/stylesheets/bootstrap/helpers/_theme-colors.scss @@ -0,0 +1,6 @@ +@use "../theme" as *; + +// Generate theme modifier classes (e.g., .theme-primary, .theme-accent, etc.) +@layer helpers { + @include generate-theme-classes(); +} diff --git a/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss b/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss index 4760ff03..327dc0cb 100644 --- a/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss +++ b/assets/stylesheets/bootstrap/helpers/_visually-hidden.scss @@ -1,8 +1,8 @@ -// -// Visually hidden -// +@use "../mixins/visually-hidden" as *; -.visually-hidden, -.visually-hidden-focusable:not(:focus):not(:focus-within) { - @include visually-hidden(); +@layer helpers { + .visually-hidden, + .visually-hidden-focusable:not(:focus, :focus-within) { + @include visually-hidden(); + } } diff --git a/assets/stylesheets/bootstrap/helpers/_vr.scss b/assets/stylesheets/bootstrap/helpers/_vr.scss index b6f9d42c..56b57c97 100644 --- a/assets/stylesheets/bootstrap/helpers/_vr.scss +++ b/assets/stylesheets/bootstrap/helpers/_vr.scss @@ -1,8 +1,9 @@ -.vr { - display: inline-block; - align-self: stretch; - width: $vr-border-width; - min-height: 1em; - background-color: currentcolor; - opacity: $hr-opacity; +@layer helpers { + .vr { + display: inline-block; + align-self: stretch; + width: var(--vr-border-width, var(--border-width)); + min-height: 1em; + background-color: var(--border-color); + } } diff --git a/assets/stylesheets/bootstrap/helpers/index.scss b/assets/stylesheets/bootstrap/helpers/index.scss new file mode 100644 index 00000000..07f3c267 --- /dev/null +++ b/assets/stylesheets/bootstrap/helpers/index.scss @@ -0,0 +1,9 @@ +@forward "focus-ring"; +@forward "icon-link"; +@forward "position"; +@forward "stacks"; +@forward "theme-colors"; +@forward "visually-hidden"; +@forward "stretched-link"; +@forward "text-truncation"; +@forward "vr"; diff --git a/assets/stylesheets/bootstrap/layout/_breakpoints.scss b/assets/stylesheets/bootstrap/layout/_breakpoints.scss new file mode 100644 index 00000000..0d9163b7 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_breakpoints.scss @@ -0,0 +1,324 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:string"; +@use "../config" as *; + +// Breakpoint viewport sizes and media queries. +// +// Breakpoints are defined as a map of (name: minimum width), order from small to large: +// +// (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px) +// +// The map defined in the `$breakpoints` global variable is used as the `$breakpoints` argument by default. + +// Name of the next breakpoint, or null for the last breakpoint. +// +// >> breakpoint-next(sm) +// md +// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// md +// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl 2xl)) +// md +@function breakpoint-next($name, $breakpoints: $breakpoints, $breakpoint-names: map.keys($breakpoints)) { + $n: list.index($breakpoint-names, $name); + @if not $n { + @error "breakpoint `#{$name}` not found in `#{$breakpoint-names}`"; + } + // Use @if/@else because list.nth would error if evaluated when $n equals list length + @if $n < list.length($breakpoint-names) { + @return list.nth($breakpoint-names, $n + 1); + } @else { + @return null; + } +} + +// Minimum breakpoint width. Null for the smallest (first) breakpoint. +// +// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// 576px +@function breakpoint-min($name, $breakpoints: $breakpoints) { + $min: map.get($breakpoints, $name); + @return if(sass($min != 0): $min; else: null); +} + +// Maximum breakpoint width for range media queries. +// Returns the breakpoint value to use as an upper bound in range queries. +// +// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// 576px +// >> breakpoint-max(xxl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// null +@function breakpoint-max($name, $breakpoints: $breakpoints) { + @if $name == null { + @return null; + } + $max: map.get($breakpoints, $name); + @return if(sass($max and $max > 0): $max; else: null); +} + +// Escape a name for use at the start of a CSS identifier. +// Leading digits are hex-escaped (e.g., 2xl becomes \32 xl). +@function css-escape-ident($name) { + $name-str: "#{$name}"; + $digits: "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"; + $first: string.slice($name-str, 1, 1); + + @if list.index($digits, $first) { + @return "\\3#{$first} #{string.slice($name-str, 2)}"; + } + + @return $name-str; +} + +// Returns a blank string if smallest breakpoint, otherwise returns the name +// with an escaped colon as a Tailwind-style prefix for responsive class names. +// Leading digits are CSS-escaped (e.g., 2xl becomes \32 xl) for valid identifiers. +// +// >> breakpoint-prefix(xs, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "" (Returns a blank string) +// >> breakpoint-prefix(sm, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "sm\:" +// >> breakpoint-prefix(2xl, (xs: 0, sm: 576px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px)) +// "\32 xl\:" +@function breakpoint-prefix($name, $breakpoints: $breakpoints) { + @if breakpoint-min($name, $breakpoints) == null { + @return ""; + } + + @return "#{css-escape-ident($name)}\\:"; +} + +// Iterate all breakpoints and provide the current name and prefix. +// +// @include loop-breakpoints-up() using ($breakpoint, $prefix) { +// // ... +// } +@mixin loop-breakpoints-up($breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); + @content($breakpoint, $prefix); + } +} + +// Iterate all breakpoints and provide the current name, next name, and next prefix. +// +// @include loop-breakpoints-down() using ($breakpoint, $next, $prefix) { +// // ... +// } +@mixin loop-breakpoints-down($breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $next: breakpoint-next($breakpoint, $breakpoints); + $prefix: breakpoint-prefix($next, $breakpoints); + @content($breakpoint, $next, $prefix); + } +} + +// Backwards-compatible alias for next/down breakpoint loops. +@mixin loop-breakpoints($breakpoints: $breakpoints) { + @include loop-breakpoints-down($breakpoints) using ($breakpoint, $next, $prefix) { + @content($breakpoint, $next, $prefix); + } +} + +// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider. +@mixin media-breakpoint-up($name, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + @if $min { + @media (width >= $min) { + @content; + } + } @else { + @content; + } +} + +// Media of at most the maximum breakpoint width. No query for the largest breakpoint. +// Makes the @content apply to the given breakpoint and narrower. +@mixin media-breakpoint-down($name, $breakpoints: $breakpoints) { + $max: breakpoint-max($name, $breakpoints); + @if $max { + @media (width < $max) { + @content; + } + } @else { + @content; + } +} + +// Media that spans multiple breakpoint widths. +// Makes the @content apply between the min and max breakpoints +@mixin media-breakpoint-between($lower, $upper, $breakpoints: $breakpoints) { + $min: breakpoint-min($lower, $breakpoints); + $max: breakpoint-max($upper, $breakpoints); + + @if $min != null and $max != null { + @media (width >= $min) and (width < $max) { + @content; + } + } @else if $max == null { + @include media-breakpoint-up($lower, $breakpoints) { + @content; + } + } @else if $min == null { + @include media-breakpoint-down($upper, $breakpoints) { + @content; + } + } +} + +// Media between the breakpoint's minimum and maximum widths. +// No minimum for the smallest breakpoint, and no maximum for the largest one. +// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower. +@mixin media-breakpoint-only($name, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + $next: breakpoint-next($name, $breakpoints); + $max: breakpoint-max($next, $breakpoints); + + @if $min != null and $max != null { + @media (width >= $min) and (width < $max) { + @content; + } + } @else if $max == null { + @include media-breakpoint-up($name, $breakpoints) { + @content; + } + } @else if $min == null { + @include media-breakpoint-down($next, $breakpoints) { + @content; + } + } +} + + +// Container queries +// +// Container queries allow elements to respond to the size of a containing element +// rather than the viewport. These mixins mirror the media-breakpoint-* mixins above. +// +// scss-docs-start container-query-mixins + +// Set an element as a query container. +// +// @include set-container(); // container-type: inline-size +// @include set-container(size); // container-type: size +// @include set-container(inline-size, sidebar); // container: sidebar / inline-size +// +@mixin set-container($type: inline-size, $name: null) { + @if $name { + container: #{$name} / #{$type}; + } @else { + container-type: #{$type}; + } +} + +// Container query of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider within the container. +// +// @include container-breakpoint-up(md) { ... } +// @include container-breakpoint-up(lg, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-up($name, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + @if $min { + @if $container-name { + @container #{$container-name} (width >= #{$min}) { + @content; + } + } @else { + @container (width >= #{$min}) { + @content; + } + } + } @else { + @content; + } +} + +// Container query of at most the maximum breakpoint width. No query for the largest breakpoint. +// Makes the @content apply to the given breakpoint and narrower within the container. +// +// @include container-breakpoint-down(lg) { ... } +// @include container-breakpoint-down(lg, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-down($name, $container-name: null, $breakpoints: $breakpoints) { + $max: breakpoint-max($name, $breakpoints); + @if $max { + @if $container-name { + @container #{$container-name} (width < #{$max}) { + @content; + } + } @else { + @container (width < #{$max}) { + @content; + } + } + } @else { + @content; + } +} + +// Container query that spans multiple breakpoint widths. +// Makes the @content apply between the min and max breakpoints within the container. +// +// @include container-breakpoint-between(md, xl) { ... } +// @include container-breakpoint-between(md, xl, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-between($lower, $upper, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($lower, $breakpoints); + $max: breakpoint-max($upper, $breakpoints); + + @if $min != null and $max != null { + @if $container-name { + @container #{$container-name} (width >= #{$min}) and (width < #{$max}) { + @content; + } + } @else { + @container (width >= #{$min}) and (width < #{$max}) { + @content; + } + } + } @else if $max == null { + @include container-breakpoint-up($lower, $container-name, $breakpoints) { + @content; + } + } @else if $min == null { + @include container-breakpoint-down($upper, $container-name, $breakpoints) { + @content; + } + } +} + +// Container query between the breakpoint's minimum and maximum widths. +// No minimum for the smallest breakpoint, and no maximum for the largest one. +// Makes the @content apply only to the given breakpoint within the container. +// +// @include container-breakpoint-only(md) { ... } +// @include container-breakpoint-only(md, sidebar) { ... } // Query named container +// +@mixin container-breakpoint-only($name, $container-name: null, $breakpoints: $breakpoints) { + $min: breakpoint-min($name, $breakpoints); + $next: breakpoint-next($name, $breakpoints); + $max: breakpoint-max($next, $breakpoints); + + @if $min != null and $max != null { + @if $container-name { + @container #{$container-name} (width >= #{$min}) and (width < #{$max}) { + @content; + } + } @else { + @container (width >= #{$min}) and (width < #{$max}) { + @content; + } + } + } @else if $max == null { + @include container-breakpoint-up($name, $container-name, $breakpoints) { + @content; + } + } @else if $min == null { + @include container-breakpoint-down($next, $container-name, $breakpoints) { + @content; + } + } +} +// scss-docs-end container-query-mixins diff --git a/assets/stylesheets/bootstrap/layout/_containers.scss b/assets/stylesheets/bootstrap/layout/_containers.scss new file mode 100644 index 00000000..a190197f --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_containers.scss @@ -0,0 +1,55 @@ +@use "../config" as *; +@use "breakpoints" as *; + +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. +// Container mixins + +@mixin make-container($gutter: $container-padding-x) { + --gutter-x: #{$gutter}; + --gutter-y: 0; + width: 100%; + padding-inline: calc(var(--gutter-x) * .5); + margin-inline: auto; +} + +@layer layout { + @if $enable-container-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .#{breakpoint-prefix($breakpoint, $breakpoints)}container { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $breakpoints) { + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + @each $name, $width in $breakpoints { + @if ($extend-breakpoint) { + .#{breakpoint-prefix($name, $breakpoints)}container { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } + } +} diff --git a/assets/stylesheets/bootstrap/layout/_grid.scss b/assets/stylesheets/bootstrap/layout/_grid.scss new file mode 100644 index 00000000..dab87eb7 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/_grid.scss @@ -0,0 +1,68 @@ +@use "../config" as *; +@use "../mixins/grid" as *; + +// mdo-do +// - check gap utilities as replacement for gutter classes from v5 + +@layer layout { + @if $enable-grid-classes { + .row { + @include make-row(); + + > * { + @include make-col-ready(); + } + } + + @include make-grid-columns(); + } + + @if $enable-cssgrid { + .grid { + --columns: #{$grid-columns}; + --rows: 1; + --gap: #{$grid-gutter-x}; + + display: grid; + grid-template-rows: repeat(var(--rows), 1fr); + grid-template-columns: repeat(var(--columns), 1fr); + gap: var(--gap); + + } + + @include make-cssgrid(); + } + + // mdo-do: add to utilities? + .grid-cols-subgrid { + grid-template-columns: subgrid; + } + + .grid-fill { + --gap: #{$grid-gutter-x}; + + display: grid; + grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + grid-auto-flow: row; + gap: var(--gap); + } + + // .g-col-auto { + // grid-column: auto; + // } + + // mdo-do: add to utilities? + // .grid-cols-3 { + // --columns: 3; + // } + // .grid-cols-4 { + // --columns: 4; + // } + // .grid-cols-6 { + // --columns: 6; + // } + + // .grid-full { + // grid-column: 1 / -1; + // } +} diff --git a/assets/stylesheets/bootstrap/layout/index.scss b/assets/stylesheets/bootstrap/layout/index.scss new file mode 100644 index 00000000..df0a0f29 --- /dev/null +++ b/assets/stylesheets/bootstrap/layout/index.scss @@ -0,0 +1,3 @@ +@forward "breakpoints"; +@forward "containers"; +@forward "grid"; diff --git a/assets/stylesheets/bootstrap/mixins/_alert.scss b/assets/stylesheets/bootstrap/mixins/_alert.scss deleted file mode 100644 index fb524af1..00000000 --- a/assets/stylesheets/bootstrap/mixins/_alert.scss +++ /dev/null @@ -1,18 +0,0 @@ -@include deprecate("`alert-variant()`", "v5.3.0", "v6.0.0"); - -// scss-docs-start alert-variant-mixin -@mixin alert-variant($background, $border, $color) { - --#{$prefix}alert-color: #{$color}; - --#{$prefix}alert-bg: #{$background}; - --#{$prefix}alert-border-color: #{$border}; - --#{$prefix}alert-link-color: #{shade-color($color, 20%)}; - - @if $enable-gradients { - background-image: var(--#{$prefix}gradient); - } - - .alert-link { - color: var(--#{$prefix}alert-link-color); - } -} -// scss-docs-end alert-variant-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_backdrop.scss b/assets/stylesheets/bootstrap/mixins/_backdrop.scss index 9705ae9e..9b6c1fbe 100644 --- a/assets/stylesheets/bootstrap/mixins/_backdrop.scss +++ b/assets/stylesheets/bootstrap/mixins/_backdrop.scss @@ -1,14 +1,14 @@ -// Shared between modals and offcanvases -@mixin overlay-backdrop($zindex, $backdrop-bg, $backdrop-opacity) { +// Shared between modals and drawers +@mixin overlay-backdrop($zindex, $backdrop-bg, $backdrop-opacity, $backdrop-blur) { position: fixed; - top: 0; - left: 0; + inset: 0; z-index: $zindex; - width: 100vw; - height: 100vh; - background-color: $backdrop-bg; + background-color: color-mix(in oklch, var(--drawer-backdrop-bg) var(--drawer-backdrop-opacity), transparent); + @if $backdrop-blur { + backdrop-filter: blur($backdrop-blur); + } // Fade for backdrop &.fade { opacity: 0; } - &.show { opacity: $backdrop-opacity; } + &.show { opacity: 1; } } diff --git a/assets/stylesheets/bootstrap/mixins/_banner.scss b/assets/stylesheets/bootstrap/mixins/_banner.scss index dd8a5103..1fd399c1 100644 --- a/assets/stylesheets/bootstrap/mixins/_banner.scss +++ b/assets/stylesheets/bootstrap/mixins/_banner.scss @@ -1,7 +1,7 @@ @mixin bsBanner($file) { /*! - * Bootstrap #{$file} v5.3.8 (https://getbootstrap.com/) - * Copyright 2011-2025 The Bootstrap Authors + * Bootstrap #{$file} v6.0.0-alpha1 (https://getbootstrap.com/) + * Copyright 2011-2026 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ } diff --git a/assets/stylesheets/bootstrap/mixins/_border-radius.scss b/assets/stylesheets/bootstrap/mixins/_border-radius.scss index 616decbc..6dd7e617 100644 --- a/assets/stylesheets/bootstrap/mixins/_border-radius.scss +++ b/assets/stylesheets/bootstrap/mixins/_border-radius.scss @@ -1,3 +1,8 @@ +@use "sass:list"; +@use "sass:math"; +@use "sass:meta"; +@use "../config" as *; + // stylelint-disable property-disallowed-list // Single side border-radius @@ -5,17 +10,17 @@ @function valid-radius($radius) { $return: (); @each $value in $radius { - @if type-of($value) == number { - $return: append($return, max($value, 0)); + @if meta.type-of($value) == number { + $return: list.append($return, math.max($value, 0)); } @else { - $return: append($return, $value); + $return: list.append($return, $value); } } @return $return; } // scss-docs-start border-radius-mixins -@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) { +@mixin border-radius($radius: var(--radius-5), $fallback-border-radius: false) { @if $enable-rounded { border-radius: valid-radius($radius); } @@ -24,55 +29,55 @@ } } -@mixin border-top-radius($radius: $border-radius) { +@mixin border-top-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); - border-top-right-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); } } -@mixin border-end-radius($radius: $border-radius) { +@mixin border-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-right-radius: valid-radius($radius); - border-bottom-right-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-bottom-radius($radius: $border-radius) { +@mixin border-bottom-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-right-radius: valid-radius($radius); - border-bottom-left-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-start-radius($radius: $border-radius) { +@mixin border-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); - border-bottom-left-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); } } -@mixin border-top-start-radius($radius: $border-radius) { +@mixin border-top-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-left-radius: valid-radius($radius); + border-start-start-radius: valid-radius($radius); } } -@mixin border-top-end-radius($radius: $border-radius) { +@mixin border-top-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-top-right-radius: valid-radius($radius); + border-start-end-radius: valid-radius($radius); } } -@mixin border-bottom-end-radius($radius: $border-radius) { +@mixin border-bottom-end-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-right-radius: valid-radius($radius); + border-end-end-radius: valid-radius($radius); } } -@mixin border-bottom-start-radius($radius: $border-radius) { +@mixin border-bottom-start-radius($radius: var(--radius-5)) { @if $enable-rounded { - border-bottom-left-radius: valid-radius($radius); + border-end-start-radius: valid-radius($radius); } } // scss-docs-end border-radius-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_box-shadow.scss b/assets/stylesheets/bootstrap/mixins/_box-shadow.scss index 0bb6bf7e..fa6c2227 100644 --- a/assets/stylesheets/bootstrap/mixins/_box-shadow.scss +++ b/assets/stylesheets/bootstrap/mixins/_box-shadow.scss @@ -1,3 +1,6 @@ +@use "sass:list"; +@use "../config" as *; + @mixin box-shadow($shadow...) { @if $enable-shadows { $result: (); @@ -10,14 +13,14 @@ $has-single-value: true; $single-value: $value; } @else { - $result: append($result, $value, "comma"); + $result: list.append($result, $value, "comma"); } } } @if $has-single-value { box-shadow: $single-value; - } @else if (length($result) > 0) { + } @else if (list.length($result) > 0) { box-shadow: $result; } } diff --git a/assets/stylesheets/bootstrap/mixins/_breakpoints.scss b/assets/stylesheets/bootstrap/mixins/_breakpoints.scss deleted file mode 100644 index 286be893..00000000 --- a/assets/stylesheets/bootstrap/mixins/_breakpoints.scss +++ /dev/null @@ -1,127 +0,0 @@ -// Breakpoint viewport sizes and media queries. -// -// Breakpoints are defined as a map of (name: minimum width), order from small to large: -// -// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px) -// -// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default. - -// Name of the next breakpoint, or null for the last breakpoint. -// -// >> breakpoint-next(sm) -// md -// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// md -// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl xxl)) -// md -@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { - $n: index($breakpoint-names, $name); - @if not $n { - @error "breakpoint `#{$name}` not found in `#{$breakpoints}`"; - } - @return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null); -} - -// Minimum breakpoint width. Null for the smallest (first) breakpoint. -// -// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// 576px -@function breakpoint-min($name, $breakpoints: $grid-breakpoints) { - $min: map-get($breakpoints, $name); - @return if($min != 0, $min, null); -} - -// Maximum breakpoint width. -// The maximum value is reduced by 0.02px to work around the limitations of -// `min-` and `max-` prefixes and viewports with fractional widths. -// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max -// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari. -// See https://bugs.webkit.org/show_bug.cgi?id=178261 -// -// >> breakpoint-max(md, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// 767.98px -@function breakpoint-max($name, $breakpoints: $grid-breakpoints) { - $max: map-get($breakpoints, $name); - @return if($max and $max > 0, $max - .02, null); -} - -// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front. -// Useful for making responsive utilities. -// -// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// "" (Returns a blank string) -// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px)) -// "-sm" -@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) { - @return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}"); -} - -// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. -// Makes the @content apply to the given breakpoint and wider. -@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($name, $breakpoints); - @if $min { - @media (min-width: $min) { - @content; - } - } @else { - @content; - } -} - -// Media of at most the maximum breakpoint width. No query for the largest breakpoint. -// Makes the @content apply to the given breakpoint and narrower. -@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) { - $max: breakpoint-max($name, $breakpoints); - @if $max { - @media (max-width: $max) { - @content; - } - } @else { - @content; - } -} - -// Media that spans multiple breakpoint widths. -// Makes the @content apply between the min and max breakpoints -@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($lower, $breakpoints); - $max: breakpoint-max($upper, $breakpoints); - - @if $min != null and $max != null { - @media (min-width: $min) and (max-width: $max) { - @content; - } - } @else if $max == null { - @include media-breakpoint-up($lower, $breakpoints) { - @content; - } - } @else if $min == null { - @include media-breakpoint-down($upper, $breakpoints) { - @content; - } - } -} - -// Media between the breakpoint's minimum and maximum widths. -// No minimum for the smallest breakpoint, and no maximum for the largest one. -// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower. -@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) { - $min: breakpoint-min($name, $breakpoints); - $next: breakpoint-next($name, $breakpoints); - $max: breakpoint-max($next, $breakpoints); - - @if $min != null and $max != null { - @media (min-width: $min) and (max-width: $max) { - @content; - } - } @else if $max == null { - @include media-breakpoint-up($name, $breakpoints) { - @content; - } - } @else if $min == null { - @include media-breakpoint-down($next, $breakpoints) { - @content; - } - } -} diff --git a/assets/stylesheets/bootstrap/mixins/_buttons.scss b/assets/stylesheets/bootstrap/mixins/_buttons.scss deleted file mode 100644 index cf087fda..00000000 --- a/assets/stylesheets/bootstrap/mixins/_buttons.scss +++ /dev/null @@ -1,70 +0,0 @@ -// Button variants -// -// Easily pump out default styles, as well as :hover, :focus, :active, -// and disabled options for all buttons - -// scss-docs-start btn-variant-mixin -@mixin button-variant( - $background, - $border, - $color: color-contrast($background), - $hover-background: if($color == $color-contrast-light, shade-color($background, $btn-hover-bg-shade-amount), tint-color($background, $btn-hover-bg-tint-amount)), - $hover-border: if($color == $color-contrast-light, shade-color($border, $btn-hover-border-shade-amount), tint-color($border, $btn-hover-border-tint-amount)), - $hover-color: color-contrast($hover-background), - $active-background: if($color == $color-contrast-light, shade-color($background, $btn-active-bg-shade-amount), tint-color($background, $btn-active-bg-tint-amount)), - $active-border: if($color == $color-contrast-light, shade-color($border, $btn-active-border-shade-amount), tint-color($border, $btn-active-border-tint-amount)), - $active-color: color-contrast($active-background), - $disabled-background: $background, - $disabled-border: $border, - $disabled-color: color-contrast($disabled-background) -) { - --#{$prefix}btn-color: #{$color}; - --#{$prefix}btn-bg: #{$background}; - --#{$prefix}btn-border-color: #{$border}; - --#{$prefix}btn-hover-color: #{$hover-color}; - --#{$prefix}btn-hover-bg: #{$hover-background}; - --#{$prefix}btn-hover-border-color: #{$hover-border}; - --#{$prefix}btn-focus-shadow-rgb: #{to-rgb(mix($color, $border, 15%))}; - --#{$prefix}btn-active-color: #{$active-color}; - --#{$prefix}btn-active-bg: #{$active-background}; - --#{$prefix}btn-active-border-color: #{$active-border}; - --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow}; - --#{$prefix}btn-disabled-color: #{$disabled-color}; - --#{$prefix}btn-disabled-bg: #{$disabled-background}; - --#{$prefix}btn-disabled-border-color: #{$disabled-border}; -} -// scss-docs-end btn-variant-mixin - -// scss-docs-start btn-outline-variant-mixin -@mixin button-outline-variant( - $color, - $color-hover: color-contrast($color), - $active-background: $color, - $active-border: $color, - $active-color: color-contrast($active-background) -) { - --#{$prefix}btn-color: #{$color}; - --#{$prefix}btn-border-color: #{$color}; - --#{$prefix}btn-hover-color: #{$color-hover}; - --#{$prefix}btn-hover-bg: #{$active-background}; - --#{$prefix}btn-hover-border-color: #{$active-border}; - --#{$prefix}btn-focus-shadow-rgb: #{to-rgb($color)}; - --#{$prefix}btn-active-color: #{$active-color}; - --#{$prefix}btn-active-bg: #{$active-background}; - --#{$prefix}btn-active-border-color: #{$active-border}; - --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow}; - --#{$prefix}btn-disabled-color: #{$color}; - --#{$prefix}btn-disabled-bg: transparent; - --#{$prefix}btn-disabled-border-color: #{$color}; - --#{$prefix}gradient: none; -} -// scss-docs-end btn-outline-variant-mixin - -// scss-docs-start btn-size-mixin -@mixin button-size($padding-y, $padding-x, $font-size, $border-radius) { - --#{$prefix}btn-padding-y: #{$padding-y}; - --#{$prefix}btn-padding-x: #{$padding-x}; - @include rfs($font-size, --#{$prefix}btn-font-size); - --#{$prefix}btn-border-radius: #{$border-radius}; -} -// scss-docs-end btn-size-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_caret.scss b/assets/stylesheets/bootstrap/mixins/_caret.scss index be731165..f227dcec 100644 --- a/assets/stylesheets/bootstrap/mixins/_caret.scss +++ b/assets/stylesheets/bootstrap/mixins/_caret.scss @@ -1,29 +1,37 @@ +@use "../config" as *; + +// scss-docs-start caret-variables +$caret-width: .3em !default; +$caret-vertical-align: $caret-width * .85 !default; +$caret-spacing: $caret-width * .85 !default; +// scss-docs-end caret-variables + // scss-docs-start caret-mixins @mixin caret-down($width: $caret-width) { - border-top: $width solid; - border-right: $width solid transparent; - border-bottom: 0; - border-left: $width solid transparent; + border-block-start: $width solid; + border-block-end: 0; + border-inline-start: $width solid transparent; + border-inline-end: $width solid transparent; } @mixin caret-up($width: $caret-width) { - border-top: 0; - border-right: $width solid transparent; - border-bottom: $width solid; - border-left: $width solid transparent; + border-block-start: 0; + border-block-end: $width solid; + border-inline-start: $width solid transparent; + border-inline-end: $width solid transparent; } @mixin caret-end($width: $caret-width) { - border-top: $width solid transparent; - border-right: 0; - border-bottom: $width solid transparent; - border-left: $width solid; + border-block-start: $width solid transparent; + border-block-end: $width solid transparent; + border-inline-start: $width solid; + border-inline-end: 0; } @mixin caret-start($width: $caret-width) { - border-top: $width solid transparent; - border-right: $width solid; - border-bottom: $width solid transparent; + border-block-start: $width solid transparent; + border-block-end: $width solid transparent; + border-inline-end: $width solid; } @mixin caret( @@ -35,7 +43,7 @@ @if $enable-caret { &::after { display: inline-block; - margin-left: $spacing; + margin-inline-start: $spacing; vertical-align: $vertical-align; content: ""; @if $direction == down { @@ -54,7 +62,7 @@ &::before { display: inline-block; - margin-right: $spacing; + margin-inline-end: $spacing; vertical-align: $vertical-align; content: ""; @include caret-start($width); @@ -62,7 +70,7 @@ } &:empty::after { - margin-left: 0; + margin-inline-start: 0; } } } diff --git a/assets/stylesheets/bootstrap/mixins/_clearfix.scss b/assets/stylesheets/bootstrap/mixins/_clearfix.scss deleted file mode 100644 index ffc62bb2..00000000 --- a/assets/stylesheets/bootstrap/mixins/_clearfix.scss +++ /dev/null @@ -1,9 +0,0 @@ -// scss-docs-start clearfix -@mixin clearfix() { - &::after { - display: block; - clear: both; - content: ""; - } -} -// scss-docs-end clearfix diff --git a/assets/stylesheets/bootstrap/mixins/_color-mode.scss b/assets/stylesheets/bootstrap/mixins/_color-mode.scss index 03338b02..518b0b09 100644 --- a/assets/stylesheets/bootstrap/mixins/_color-mode.scss +++ b/assets/stylesheets/bootstrap/mixins/_color-mode.scss @@ -1,3 +1,5 @@ +@use "../config" as *; + // scss-docs-start color-mode-mixin @mixin color-mode($mode: light, $root: false) { @if $color-mode-type == "media-query" { diff --git a/assets/stylesheets/bootstrap/mixins/_container.scss b/assets/stylesheets/bootstrap/mixins/_container.scss deleted file mode 100644 index b9f33519..00000000 --- a/assets/stylesheets/bootstrap/mixins/_container.scss +++ /dev/null @@ -1,11 +0,0 @@ -// Container mixins - -@mixin make-container($gutter: $container-padding-x) { - --#{$prefix}gutter-x: #{$gutter}; - --#{$prefix}gutter-y: 0; - width: 100%; - padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - margin-right: auto; - margin-left: auto; -} diff --git a/assets/stylesheets/bootstrap/mixins/_deprecate.scss b/assets/stylesheets/bootstrap/mixins/_deprecate.scss index df070bc5..862823df 100644 --- a/assets/stylesheets/bootstrap/mixins/_deprecate.scss +++ b/assets/stylesheets/bootstrap/mixins/_deprecate.scss @@ -1,3 +1,5 @@ +@use "../config" as *; + // Deprecate mixin // // This mixin can be used to deprecate mixins or functions. diff --git a/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss b/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss new file mode 100644 index 00000000..28f58fb4 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_dialog-shared.scss @@ -0,0 +1,49 @@ +// Shared mixins for Dialog and Drawer sub-components. +// Both components use identical header/footer/body/title patterns +// with different token namespaces. + +@use "transition" as *; + +// Header: flex row with close button alignment +@mixin dialog-header($padding) { + display: flex; + flex-shrink: 0; + align-items: center; + padding: $padding; +} + +// Footer: flex row with end-aligned actions +@mixin dialog-footer($padding, $gap, $border-width, $border-color) { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + gap: $gap; + align-items: center; + justify-content: flex-end; + padding: $padding; + border-block-start: $border-width solid $border-color; +} + +// Body: flexible scrollable content area +@mixin dialog-body($padding) { + flex: 1 1 auto; + padding: $padding; +} + +// Title: reset margin, set line-height +@mixin dialog-title($line-height: 1.5) { + margin-bottom: 0; + line-height: $line-height; +} + +// Backdrop transitions for ::backdrop pseudo-element. +// Both Dialog and Drawer use identical allow-discrete transitions +// on display and overlay to keep ::backdrop in the top layer. +@mixin backdrop-transitions($duration, $timing) { + @include transition( + background-color $duration $timing, + backdrop-filter $duration $timing, + display $duration allow-discrete, + overlay $duration allow-discrete + ); +} diff --git a/assets/stylesheets/bootstrap/mixins/_focus-ring.scss b/assets/stylesheets/bootstrap/mixins/_focus-ring.scss new file mode 100644 index 00000000..156a2173 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_focus-ring.scss @@ -0,0 +1,10 @@ +@mixin focus-ring($offset: false, $color: null) { + @if $color != null { + outline: var(--focus-ring-width) solid #{$color}; + } @else { + outline: var(--focus-ring); + } + @if $offset { + outline-offset: var(--focus-ring-offset); + } +} diff --git a/assets/stylesheets/bootstrap/mixins/_form-validation.scss b/assets/stylesheets/bootstrap/mixins/_form-validation.scss new file mode 100644 index 00000000..3c575e10 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_form-validation.scss @@ -0,0 +1,33 @@ +// scss-docs-start form-validation-state-selector +@mixin form-validation-state-selector($state) { + @if & { + &.is-#{$state} { + @content; + } + + @if $state == "invalid" { + @at-root [data-bs-validate] #{&}:user-invalid { + @content; + } + } @else if $state == "valid" { + @at-root [data-bs-validate~="valid"] #{&}:user-valid { + @content; + } + } + } @else { + .is-#{$state} { + @content; + } + + @if $state == "invalid" { + [data-bs-validate] :user-invalid { + @content; + } + } @else if $state == "valid" { + [data-bs-validate~="valid"] :user-valid { + @content; + } + } + } +} +// scss-docs-end form-validation-state-selector diff --git a/assets/stylesheets/bootstrap/mixins/_forms.scss b/assets/stylesheets/bootstrap/mixins/_forms.scss deleted file mode 100644 index 00b47641..00000000 --- a/assets/stylesheets/bootstrap/mixins/_forms.scss +++ /dev/null @@ -1,163 +0,0 @@ -// This mixin uses an `if()` technique to be compatible with Dart Sass -// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details - -// scss-docs-start form-validation-mixins -@mixin form-validation-state-selector($state) { - @if ($state == "valid" or $state == "invalid") { - .was-validated #{if(&, "&", "")}:#{$state}, - #{if(&, "&", "")}.is-#{$state} { - @content; - } - } @else { - #{if(&, "&", "")}.is-#{$state} { - @content; - } - } -} - -@mixin form-validation-state( - $state, - $color, - $icon, - $tooltip-color: color-contrast($color), - $tooltip-bg-color: rgba($color, $form-feedback-tooltip-opacity), - $focus-box-shadow: 0 0 $input-btn-focus-blur $input-focus-width rgba($color, $input-btn-focus-color-opacity), - $border-color: $color -) { - .#{$state}-feedback { - display: none; - width: 100%; - margin-top: $form-feedback-margin-top; - @include font-size($form-feedback-font-size); - font-style: $form-feedback-font-style; - color: $color; - } - - .#{$state}-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; // Contain to parent when possible - padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x; - margin-top: .1rem; - @include font-size($form-feedback-tooltip-font-size); - line-height: $form-feedback-tooltip-line-height; - color: $tooltip-color; - background-color: $tooltip-bg-color; - @include border-radius($form-feedback-tooltip-border-radius); - } - - @include form-validation-state-selector($state) { - ~ .#{$state}-feedback, - ~ .#{$state}-tooltip { - display: block; - } - } - - .form-control { - @include form-validation-state-selector($state) { - border-color: $border-color; - - @if $enable-validation-icons { - padding-right: $input-height-inner; - background-image: escape-svg($icon); - background-repeat: no-repeat; - background-position: right $input-height-inner-quarter center; - background-size: $input-height-inner-half $input-height-inner-half; - } - - &:focus { - border-color: $border-color; - @if $enable-shadows { - @include box-shadow($input-box-shadow, $focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $focus-box-shadow; - } - } - } - } - - // stylelint-disable-next-line selector-no-qualifying-type - textarea.form-control { - @include form-validation-state-selector($state) { - @if $enable-validation-icons { - padding-right: $input-height-inner; - background-position: top $input-height-inner-quarter right $input-height-inner-quarter; - } - } - } - - .form-select { - @include form-validation-state-selector($state) { - border-color: $border-color; - - @if $enable-validation-icons { - &:not([multiple]):not([size]), - &:not([multiple])[size="1"] { - --#{$prefix}form-select-bg-icon: #{escape-svg($icon)}; - padding-right: $form-select-feedback-icon-padding-end; - background-position: $form-select-bg-position, $form-select-feedback-icon-position; - background-size: $form-select-bg-size, $form-select-feedback-icon-size; - } - } - - &:focus { - border-color: $border-color; - @if $enable-shadows { - @include box-shadow($form-select-box-shadow, $focus-box-shadow); - } @else { - // Avoid using mixin so we can pass custom focus shadow properly - box-shadow: $focus-box-shadow; - } - } - } - } - - .form-control-color { - @include form-validation-state-selector($state) { - @if $enable-validation-icons { - width: add($form-color-width, $input-height-inner); - } - } - } - - .form-check-input { - @include form-validation-state-selector($state) { - border-color: $border-color; - - &:checked { - background-color: $color; - } - - &:focus { - box-shadow: $focus-box-shadow; - } - - ~ .form-check-label { - color: $color; - } - } - } - .form-check-inline .form-check-input { - ~ .#{$state}-feedback { - margin-left: .5em; - } - } - - .input-group { - > .form-control:not(:focus), - > .form-select:not(:focus), - > .form-floating:not(:focus-within) { - @include form-validation-state-selector($state) { - @if $state == "valid" { - z-index: 3; - } @else if $state == "invalid" { - z-index: 4; - } - } - } - } -} -// scss-docs-end form-validation-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_gradients.scss b/assets/stylesheets/bootstrap/mixins/_gradients.scss index 608e18df..1789d35d 100644 --- a/assets/stylesheets/bootstrap/mixins/_gradients.scss +++ b/assets/stylesheets/bootstrap/mixins/_gradients.scss @@ -1,3 +1,6 @@ +@use "../colors" as *; +@use "../config" as *; + // Gradients // scss-docs-start gradient-bg-mixin @@ -5,7 +8,7 @@ background-color: $color; @if $enable-gradients { - background-image: var(--#{$prefix}gradient); + background-image: var(--gradient); } } // scss-docs-end gradient-bg-mixin @@ -14,18 +17,18 @@ // Horizontal gradient, from left to right // // Creates two color stops, start and end, by specifying a color and position for each color stop. -@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) { +@mixin gradient-x($start-color: var(--gray-700), $end-color: var(--gray-800), $start-percent: 0%, $end-percent: 100%) { background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent); } // Vertical gradient, from top to bottom // // Creates two color stops, start and end, by specifying a color and position for each color stop. -@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: null, $end-percent: null) { +@mixin gradient-y($start-color: var(--gray-700), $end-color: var(--gray-800), $start-percent: null, $end-percent: null) { background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent); } -@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) { +@mixin gradient-directional($start-color: var(--gray-700), $end-color: var(--gray-800), $deg: 45deg) { background-image: linear-gradient($deg, $start-color, $end-color); } @@ -37,11 +40,11 @@ background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color); } -@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) { +@mixin gradient-radial($inner-color: var(--gray-700), $outer-color: var(--gray-800)) { background-image: radial-gradient(circle, $inner-color, $outer-color); } -@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) { +@mixin gradient-striped($color: rgb(255 255 255 / .15), $angle: 45deg) { background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent); } // scss-docs-end gradient-mixins diff --git a/assets/stylesheets/bootstrap/mixins/_grid.scss b/assets/stylesheets/bootstrap/mixins/_grid.scss index db77e07f..a25007af 100644 --- a/assets/stylesheets/bootstrap/mixins/_grid.scss +++ b/assets/stylesheets/bootstrap/mixins/_grid.scss @@ -1,36 +1,41 @@ +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +@use "../config" as *; +@use "../layout/breakpoints" as *; + // Grid system // // Generate semantic grid columns with these mixins. -@mixin make-row($gutter: $grid-gutter-width) { - --#{$prefix}gutter-x: #{$gutter}; - --#{$prefix}gutter-y: 0; +@mixin make-row($gutter-x: $grid-gutter-x, $gutter-y: $grid-gutter-y) { + --gutter-x: #{$gutter-x}; + --gutter-y: #{$gutter-y}; display: flex; flex-wrap: wrap; // TODO: Revisit calc order after https://github.com/react-bootstrap/react-bootstrap/issues/6039 is fixed - margin-top: calc(-1 * var(--#{$prefix}gutter-y)); // stylelint-disable-line function-disallowed-list - margin-right: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list - margin-left: calc(-.5 * var(--#{$prefix}gutter-x)); // stylelint-disable-line function-disallowed-list + margin-inline: calc(-.5 * var(--gutter-x)); + margin-top: calc(-1 * var(--gutter-y)); } @mixin make-col-ready() { // Add box sizing if only the grid is loaded - box-sizing: if(variable-exists(include-column-box-sizing) and $include-column-box-sizing, border-box, null); + // stylelint-disable-next-line scss/at-function-named-arguments + box-sizing: if(sass(meta.variable-exists(include-column-box-sizing) and $include-column-box-sizing): border-box; else: null); // Prevent columns from becoming too narrow when at smaller grid tiers by // always setting `width: 100%;`. This works because we set the width // later on to override this initial width. flex-shrink: 0; width: 100%; max-width: 100%; // Prevent `.col-auto`, `.col` (& responsive variants) from breaking out the grid - padding-right: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - padding-left: calc(var(--#{$prefix}gutter-x) * .5); // stylelint-disable-line function-disallowed-list - margin-top: var(--#{$prefix}gutter-y); + padding-inline: calc(var(--gutter-x) * .5); + margin-top: var(--gutter-y); } @mixin make-col($size: false, $columns: $grid-columns) { @if $size { flex: 0 0 auto; - width: percentage(divide($size, $columns)); + width: math.percentage(math.div($size, $columns)); } @else { flex: 1 1 0; @@ -44,8 +49,9 @@ } @mixin make-col-offset($size, $columns: $grid-columns) { - $num: divide($size, $columns); - margin-left: if($num == 0, 0, percentage($num)); + $num: math.div($size, $columns); + // stylelint-disable-next-line scss/at-function-named-arguments + margin-inline-start: if(sass($num == 0): 0; else: math.percentage($num)); } // Row columns @@ -56,7 +62,7 @@ @mixin row-cols($count) { > * { flex: 0 0 auto; - width: percentage(divide(1, $count)); + width: math.percentage(math.div(1, $count)); } } @@ -65,43 +71,42 @@ // Used only by Bootstrap to generate the correct number of grid classes given // any value of `$grid-columns`. -@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) { - @each $breakpoint in map-keys($breakpoints) { - $infix: breakpoint-infix($breakpoint, $breakpoints); +@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-x, $breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); @include media-breakpoint-up($breakpoint, $breakpoints) { - // Provide basic `.col-{bp}` classes for equal-width flexbox columns - .col#{$infix} { + .#{$prefix}col { flex: 1 0 0; } - .row-cols#{$infix}-auto > * { + .#{$prefix}row-cols-auto > * { @include make-col-auto(); } @if $grid-row-columns > 0 { @for $i from 1 through $grid-row-columns { - .row-cols#{$infix}-#{$i} { + .#{$prefix}row-cols-#{$i} { @include row-cols($i); } } } - .col#{$infix}-auto { + .#{$prefix}col-auto { @include make-col-auto(); } @if $columns > 0 { @for $i from 1 through $columns { - .col#{$infix}-#{$i} { + .#{$prefix}col-#{$i} { @include make-col($i, $columns); } } // `$columns - 1` because offsetting by the width of an entire row isn't possible @for $i from 0 through ($columns - 1) { - @if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0 - .offset#{$infix}-#{$i} { + @if not ($prefix == "" and $i == 0) { // Avoid emitting useless .offset-0 + .#{$prefix}offset-#{$i} { @include make-col-offset($i, $columns); } } @@ -112,28 +117,28 @@ // // Make use of `.g-*`, `.gx-*` or `.gy-*` utilities to change spacing between the columns. @each $key, $value in $gutters { - .g#{$infix}-#{$key}, - .gx#{$infix}-#{$key} { - --#{$prefix}gutter-x: #{$value}; + .#{$prefix}g-#{$key}, + .#{$prefix}gx-#{$key} { + --gutter-x: #{$value}; } - .g#{$infix}-#{$key}, - .gy#{$infix}-#{$key} { - --#{$prefix}gutter-y: #{$value}; + .#{$prefix}g-#{$key}, + .#{$prefix}gy-#{$key} { + --gutter-y: #{$value}; } } } } } -@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $grid-breakpoints) { - @each $breakpoint in map-keys($breakpoints) { - $infix: breakpoint-infix($breakpoint, $breakpoints); +@mixin make-cssgrid($columns: $grid-columns, $breakpoints: $breakpoints) { + @each $breakpoint in map.keys($breakpoints) { + $prefix: breakpoint-prefix($breakpoint, $breakpoints); @include media-breakpoint-up($breakpoint, $breakpoints) { @if $columns > 0 { @for $i from 1 through $columns { - .g-col#{$infix}-#{$i} { + .#{$prefix}g-col-#{$i} { grid-column: auto / span $i; } } @@ -141,7 +146,7 @@ // Start with `1` because `0` is an invalid value. // Ends with `$columns - 1` because offsetting by the width of an entire row isn't possible. @for $i from 1 through ($columns - 1) { - .g-start#{$infix}-#{$i} { + .#{$prefix}g-start-#{$i} { grid-column-start: $i; } } diff --git a/assets/stylesheets/bootstrap/mixins/_list-group.scss b/assets/stylesheets/bootstrap/mixins/_list-group.scss deleted file mode 100644 index 6274f343..00000000 --- a/assets/stylesheets/bootstrap/mixins/_list-group.scss +++ /dev/null @@ -1,26 +0,0 @@ -@include deprecate("`list-group-item-variant()`", "v5.3.0", "v6.0.0"); - -// List Groups - -// scss-docs-start list-group-mixin -@mixin list-group-item-variant($state, $background, $color) { - .list-group-item-#{$state} { - color: $color; - background-color: $background; - - &.list-group-item-action { - &:hover, - &:focus { - color: $color; - background-color: shade-color($background, 10%); - } - - &.active { - color: $white; - background-color: $color; - border-color: $color; - } - } - } -} -// scss-docs-end list-group-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_lists.scss b/assets/stylesheets/bootstrap/mixins/_lists.scss index 25185626..acc3b53d 100644 --- a/assets/stylesheets/bootstrap/mixins/_lists.scss +++ b/assets/stylesheets/bootstrap/mixins/_lists.scss @@ -2,6 +2,6 @@ // Unstyled keeps list items block level, just removes default browser padding and list-style @mixin list-unstyled { - padding-left: 0; - list-style: none; + padding-inline-start: 0; + list-style-type: ""; } diff --git a/assets/stylesheets/bootstrap/mixins/_mask-icon.scss b/assets/stylesheets/bootstrap/mixins/_mask-icon.scss new file mode 100644 index 00000000..f4d1fd7f --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_mask-icon.scss @@ -0,0 +1,21 @@ +// Mask icon +// +// Renders an SVG icon via a CSS mask so the shape is painted with the +// element's `background-color` and therefore inherits theme/dark-mode color. +// Set `background-color` on the element itself; pass `null` for `$icon` when +// the mask image is applied conditionally (e.g. per state or direction). + +// scss-docs-start mask-icon-mixin +@mixin mask-icon($icon: null, $size: contain, $position: center) { + @if $icon != null { + mask-image: $icon; + } + mask-repeat: no-repeat; + @if $position != null { + mask-position: $position; + } + @if $size != null { + mask-size: $size; + } +} +// scss-docs-end mask-icon-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_pagination.scss b/assets/stylesheets/bootstrap/mixins/_pagination.scss deleted file mode 100644 index 0d657964..00000000 --- a/assets/stylesheets/bootstrap/mixins/_pagination.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Pagination - -// scss-docs-start pagination-mixin -@mixin pagination-size($padding-y, $padding-x, $font-size, $border-radius) { - --#{$prefix}pagination-padding-x: #{$padding-x}; - --#{$prefix}pagination-padding-y: #{$padding-y}; - @include rfs($font-size, --#{$prefix}pagination-font-size); - --#{$prefix}pagination-border-radius: #{$border-radius}; -} -// scss-docs-end pagination-mixin diff --git a/assets/stylesheets/bootstrap/mixins/_reset-text.scss b/assets/stylesheets/bootstrap/mixins/_reset-text.scss index f5bd1afe..4dac3c7e 100644 --- a/assets/stylesheets/bootstrap/mixins/_reset-text.scss +++ b/assets/stylesheets/bootstrap/mixins/_reset-text.scss @@ -1,10 +1,9 @@ @mixin reset-text { - font-family: $font-family-base; + font-family: var(--body-font-family); // We deliberately do NOT reset font-size or overflow-wrap / word-wrap. font-style: normal; - font-weight: $font-weight-normal; - line-height: $line-height-base; - text-align: left; // Fallback for where `start` is not supported + font-weight: var(--body-font-weight); + line-height: var(--body-line-height); text-align: start; text-decoration: none; text-shadow: none; diff --git a/assets/stylesheets/bootstrap/mixins/_table-variants.scss b/assets/stylesheets/bootstrap/mixins/_table-variants.scss deleted file mode 100644 index 5fe1b9b2..00000000 --- a/assets/stylesheets/bootstrap/mixins/_table-variants.scss +++ /dev/null @@ -1,24 +0,0 @@ -// scss-docs-start table-variant -@mixin table-variant($state, $background) { - .table-#{$state} { - $color: color-contrast(opaque($body-bg, $background)); - $hover-bg: mix($color, $background, percentage($table-hover-bg-factor)); - $striped-bg: mix($color, $background, percentage($table-striped-bg-factor)); - $active-bg: mix($color, $background, percentage($table-active-bg-factor)); - $table-border-color: mix($color, $background, percentage($table-border-factor)); - - --#{$prefix}table-color: #{$color}; - --#{$prefix}table-bg: #{$background}; - --#{$prefix}table-border-color: #{$table-border-color}; - --#{$prefix}table-striped-bg: #{$striped-bg}; - --#{$prefix}table-striped-color: #{color-contrast($striped-bg)}; - --#{$prefix}table-active-bg: #{$active-bg}; - --#{$prefix}table-active-color: #{color-contrast($active-bg)}; - --#{$prefix}table-hover-bg: #{$hover-bg}; - --#{$prefix}table-hover-color: #{color-contrast($hover-bg)}; - - color: var(--#{$prefix}table-color); - border-color: var(--#{$prefix}table-border-color); - } -} -// scss-docs-end table-variant diff --git a/assets/stylesheets/bootstrap/mixins/_tokens.scss b/assets/stylesheets/bootstrap/mixins/_tokens.scss new file mode 100644 index 00000000..d4132add --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/_tokens.scss @@ -0,0 +1,9 @@ +// Mixin to output tokens as CSS custom properties + +// scss-docs-start mixin-tokens +@mixin tokens($map) { + @each $prop, $value in $map { + #{$prop}: #{$value}; + } +} +// scss-docs-end mixin-tokens diff --git a/assets/stylesheets/bootstrap/mixins/_transition.scss b/assets/stylesheets/bootstrap/mixins/_transition.scss index d437f6d8..c9f2ca41 100644 --- a/assets/stylesheets/bootstrap/mixins/_transition.scss +++ b/assets/stylesheets/bootstrap/mixins/_transition.scss @@ -1,10 +1,13 @@ +@use "sass:list"; +@use "../config" as *; + // stylelint-disable property-disallowed-list @mixin transition($transition...) { - @if length($transition) == 0 { + @if list.length($transition) == 0 { $transition: $transition-base; } - @if length($transition) > 1 { + @if list.length($transition) > 1 { @each $value in $transition { @if $value == null or $value == none { @warn "The keyword 'none' or 'null' must be used as a single argument."; @@ -13,11 +16,11 @@ } @if $enable-transitions { - @if nth($transition, 1) != null { + @if list.nth($transition, 1) != null { transition: $transition; } - @if $enable-reduced-motion and nth($transition, 1) != null and nth($transition, 1) != none { + @if $enable-reduced-motion and list.nth($transition, 1) != null and list.nth($transition, 1) != none { @media (prefers-reduced-motion: reduce) { transition: none; } diff --git a/assets/stylesheets/bootstrap/mixins/_utilities.scss b/assets/stylesheets/bootstrap/mixins/_utilities.scss index 4795e894..2d78150d 100644 --- a/assets/stylesheets/bootstrap/mixins/_utilities.scss +++ b/assets/stylesheets/bootstrap/mixins/_utilities.scss @@ -1,96 +1,291 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:meta"; +@use "../layout/breakpoints" as bp; + // Utility generator -// Used to generate utilities & print utilities -@mixin generate-utility($utility, $infix: "", $is-rfs-media-query: false) { - $values: map-get($utility, values); - // If the values are a list or string, convert it into a map - @if type-of($values) == "string" or type-of(nth($values, 1)) != "list" { - $values: zip($values, $values); - } +// - Utilities can use three different types of selectors: +// - class: .class +// - attr-starts: [class^="class"] +// - attr-includes: [class*="class"] +// - Utilities can target children via `child-selector`, wrapped in :where() for zero specificity +// - Utilities can generate regular CSS properties and CSS custom properties +// - Utilities can be responsive or not +// - Utilities can have state variants (e.g., hover, focus, active) +// - Utilities can define local CSS variables +// +// CSS custom properties can be generated in two ways: +// +// 1. Property map with null values (CSS var receives the utility value): +// "bg-color": ( +// property: ( +// "--bg": null, +// "background-color": var(--bg) +// ), +// class: bg, +// values: ( +// primary: var(--blue-500), +// ) +// ) +// Generates: +// .bg-primary { +// --bs-bg: var(--bs-blue-500); +// background-color: var(--bs-bg); +// } +// +// 2. Variables map (static CSS custom properties on every class): +// "link-underline": ( +// property: text-decoration-color, +// class: link-underline, +// variables: ( +// "link-underline-opacity": 1 +// ), +// values: (...) +// ) +// Generates: +// .link-underline { +// --bs-link-underline-opacity: 1; +// text-decoration-color: ...; +// } - @each $key, $value in $values { - $properties: map-get($utility, property); +// Helper mixin to emit CSS custom properties from a utility's `variables` key. +// When variables is a map, the provided static values are used on each class. +// When variables is a list or single identifier, each variable receives the current utility value. +@mixin generate-variables($utility, $value) { + @if map.has-key($utility, variables) { + $variables: map.get($utility, variables); + @if meta.type-of($variables) == "map" { + @each $var-key, $var-value in $variables { + --#{$var-key}: #{$var-value}; + } + } @else { + // Treat as a list (or single identifier) — each variable gets the utility value + @each $var-name in $variables { + --#{$var-name}: #{$value}; + } + } + } +} - // Multiple properties are possible, for example with vertical or horizontal margins or paddings - @if type-of($properties) == "string" { - $properties: append((), $properties); +// Helper mixin to generate CSS properties for both legacy and property map approaches +@mixin generate-properties($utility, $property-map, $properties, $value) { + @if $property-map != null { + // Property-Value Mapping approach + @each $property, $default-value in $property-map { + // If value is a map, check if it has a key for this property. + // Otherwise, use default-value (or $value if default-value is null). + $actual-value: $default-value; + @if meta.type-of($value) == "map" and map.has-key($value, $property) { + $actual-value: map.get($value, $property); + } @else if $default-value == null { + $actual-value: $value; + } + @if map.get($utility, important) { + #{$property}: $actual-value !important; // stylelint-disable-line declaration-no-important + } @else { + #{$property}: $actual-value; + } + } + } @else { + // Legacy approach + @each $property in $properties { + @if map.get($utility, important) { + #{$property}: $value !important; // stylelint-disable-line declaration-no-important + } @else { + #{$property}: $value; + } } + } +} - // Use custom class if present - $property-class: if(map-has-key($utility, class), map-get($utility, class), nth($properties, 1)); - $property-class: if($property-class == null, "", $property-class); +@mixin generate-utility($utility, $prefix: "") { + // Validate required keys + @if not map.has-key($utility, property) { + @error "Utility is missing required `property` key: #{$utility}"; + } + @if not map.has-key($utility, values) { + @error "Utility is missing required `values` key: #{$utility}"; + } - // Use custom CSS variable name if present, otherwise default to `class` - $css-variable-name: if(map-has-key($utility, css-variable-name), map-get($utility, css-variable-name), map-get($utility, class)); + // Warn on unknown keys (likely typos) + $valid-keys: property, values, class, selector, responsive, print, dark, important, state, variables, child-selector, enabled; + @each $key in map.keys($utility) { + @if not list.index($valid-keys, $key) { + @warn "Unknown utility key `#{$key}` found. Valid keys are: #{$valid-keys}"; + } + } - // State params to generate pseudo-classes - $state: if(map-has-key($utility, state), map-get($utility, state), ()); + // Validate boolean keys + @each $bool-key in (responsive, print, dark, important, enabled) { + @if map.has-key($utility, $bool-key) { + $val: map.get($utility, $bool-key); + @if $val != true and $val != false { + @error "Utility key `#{$bool-key}` should be a boolean (true or false), got: #{$val}"; + } + } + } - $infix: if($property-class == "" and str-slice($infix, 1, 1) == "-", str-slice($infix, 2), $infix); + // Determine if we're generating a class, or an attribute selector + $selector-type: "class"; + @if map.has-key($utility, selector) { + $selector-type: map.get($utility, selector); + // Validate selector type + $valid-selectors: "class", "attr-starts", "attr-includes"; + @if not list.index($valid-selectors, $selector-type) { + @error "Invalid `selector` value `#{$selector-type}`. Must be one of: #{$valid-selectors}"; + } + } + // Then get the class name to use in a class (e.g., .class) or in an attribute selector (e.g., [class^="class"]) + $selector-class: map.get($utility, class); - // Don't prefix if value key is null (e.g. with shadow class) - $property-class-modifier: if($key, if($property-class == "" and $infix == "", "", "-") + $key, ""); + // Attribute selectors require a `class` key + @if $selector-type != "class" and not map.has-key($utility, class) { + @error "Utility with `selector: #{$selector-type}` requires a `class` key."; + } - @if map-get($utility, rfs) { - // Inside the media query - @if $is-rfs-media-query { - $val: rfs-value($value); + // Get the list or map of values and ensure it's a map + $values: map.get($utility, values); + @if meta.type-of($values) != "map" { + @if meta.type-of($values) == "list" { + $list: (); + @each $value in $values { + $list: map.merge($list, ($value: $value)); + } + $values: $list; + } @else { + $values: (null: $values); + } + } + + @each $key, $value in $values { + $properties: map.get($utility, property); + $property-map: null; + $custom-class: ""; - // Do not render anything if fluid and non fluid values are the same - $value: if($val == rfs-fluid-value($value), null, $val); + // Check if property is a map (Property-Value Mapping approach) + @if meta.type-of($properties) == "map" { + $property-map: $properties; + @if map.has-key($utility, class) { + $custom-class: map.get($utility, class); } - @else { - $value: rfs-fluid-value($value); + } @else { + // Legacy approach: multiple properties are possible, for example with vertical or horizontal margins or paddings + @if meta.type-of($properties) == "string" { + $properties: list.append((), $properties); + } + // Use custom class if present, otherwise use the first value from the list of properties + @if map.has-key($utility, class) { + $custom-class: map.get($utility, class); + } @else { + $custom-class: list.nth($properties, 1); + } + @if $custom-class == null { + $custom-class: ""; } } - $is-css-var: map-get($utility, css-var); - $is-local-vars: map-get($utility, local-vars); - $is-rtl: map-get($utility, rtl); + // State params to generate state variants + $state: (); + @if map.has-key($utility, state) { + $state: map.get($utility, state); + } - @if $value != null { - @if $is-rtl == false { - /* rtl:begin:remove */ + // Don't add a dash before value key if value key is null (e.g. with shadow class) + $custom-class-modifier: ""; + @if $key { + @if $custom-class == "" { + $custom-class-modifier: $key; + } @else { + $custom-class-modifier: "-" + $key; } + } - @if $is-css-var { - .#{$property-class + $infix + $property-class-modifier} { - --#{$prefix}#{$css-variable-name}: #{$value}; - } + // Build the class name fragment (without prefix or dot) for reuse in state variants + $class-name: ""; + @if $selector-type == "class" { + @if $custom-class != "" { + $class-name: $custom-class + $custom-class-modifier; + } @else if $selector-class != null and $selector-class != "" { + $class-name: $selector-class + $custom-class-modifier; + } @else { + $class-name: $custom-class-modifier; + } + } + + $selector: ""; + @if $selector-type == "class" { + $selector: ".#{$prefix + $class-name}"; + } @else if $selector-type == "attr-starts" { + $selector: "[class^=\"#{$selector-class}\"]"; + } @else if $selector-type == "attr-includes" { + $selector: "[class*=\"#{$selector-class}\"]"; + } + + // Apply child-selector wrapping if present (wraps in :where() for zero specificity) + $child-sel: null; + @if map.has-key($utility, child-selector) { + $child-sel: map.get($utility, child-selector); + } + + $final-selector: $selector; + @if $child-sel { + $final-selector: ":where(#{$selector} #{$child-sel})"; + } - @each $pseudo in $state { - .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} { - --#{$prefix}#{$css-variable-name}: #{$value}; - } + #{$final-selector} { + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); + } + + // Generate state variants (e.g., hover:link-10 instead of link-10-hover) + @if $state != () { + @each $state-variant in $state { + $state-selector: ".#{$prefix}#{$state-variant}\\:#{$class-name}:#{$state-variant}"; + @if $child-sel { + $state-selector: ":where(#{$state-selector} #{$child-sel})"; } - } @else { - .#{$property-class + $infix + $property-class-modifier} { - @each $property in $properties { - @if $is-local-vars { - @each $local-var, $variable in $is-local-vars { - --#{$prefix}#{$local-var}: #{$variable}; - } - } - #{$property}: $value if($enable-important-utilities, !important, null); - } + + #{$state-selector} { + @include generate-variables($utility, $value); + @include generate-properties($utility, $property-map, $properties, $value); } + } + } + } +} - @each $pseudo in $state { - .#{$property-class + $infix + $property-class-modifier}-#{$pseudo}:#{$pseudo} { - @each $property in $properties { - @if $is-local-vars { - @each $local-var, $variable in $is-local-vars { - --#{$prefix}#{$local-var}: #{$variable}; - } - } - #{$property}: $value if($enable-important-utilities, !important, null); - } - } +// Generates all utility classes: base, responsive, print, and dark. +// Extracted so that tests can call this mixin directly with a custom $utilities map +// rather than having to mirror the loop conditions inline. +@mixin generate-utilities-loop($utilities, $breakpoints) { + // Base + responsive (one pass per breakpoint) + @each $breakpoint in map.keys($breakpoints) { + @include bp.media-breakpoint-up($breakpoint, $breakpoints) { + $prefix: bp.breakpoint-prefix($breakpoint, $breakpoints); + + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and (map.get($utility, responsive) or $prefix == "") { + @include generate-utility($utility, $prefix); } } + } + } + + // Print utilities + @media print { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, print) == true { + @include generate-utility($utility, "print\\:"); + } + } + } - @if $is-rtl == false { - /* rtl:end:remove */ + // Dark utilities + @media (prefers-color-scheme: dark) { + @each $key, $utility in $utilities { + @if meta.type-of($utility) == "map" and map.get($utility, enabled) != false and map.get($utility, dark) == true { + @include generate-utility($utility, "dark\\:"); } } } diff --git a/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss b/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss index 9dd0ad33..4836b817 100644 --- a/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss +++ b/assets/stylesheets/bootstrap/mixins/_visually-hidden.scss @@ -32,7 +32,7 @@ // Useful for "Skip to main content" links; see https://www.w3.org/WAI/WCAG22/Techniques/general/G1.html @mixin visually-hidden-focusable() { - &:not(:focus):not(:focus-within) { + &:not(:focus, :focus-within) { @include visually-hidden(); } } diff --git a/assets/stylesheets/bootstrap/mixins/index.scss b/assets/stylesheets/bootstrap/mixins/index.scss new file mode 100644 index 00000000..a0201ed3 --- /dev/null +++ b/assets/stylesheets/bootstrap/mixins/index.scss @@ -0,0 +1,32 @@ +// Toggles +// +// Used in conjunction with global variables to enable certain theme features. + +@forward "tokens"; + +// Deprecate +@forward "deprecate"; + +// Helpers +@forward "color-mode"; +@forward "color-scheme"; +@forward "image"; +@forward "resize"; +@forward "visually-hidden"; +@forward "reset-text"; +@forward "text-truncate"; + +// Utilities +@forward "utilities"; + +// Components +@forward "backdrop"; +@forward "caret"; +@forward "form-validation"; +@forward "mask-icon"; + +// Skins +@forward "border-radius"; +@forward "box-shadow"; +@forward "gradients"; +@forward "transition"; diff --git a/assets/stylesheets/bootstrap/utilities/_api.scss b/assets/stylesheets/bootstrap/utilities/_api.scss index 62e1d398..327573a3 100644 --- a/assets/stylesheets/bootstrap/utilities/_api.scss +++ b/assets/stylesheets/bootstrap/utilities/_api.scss @@ -1,47 +1,7 @@ -// Loop over each breakpoint -@each $breakpoint in map-keys($grid-breakpoints) { +@use "../config" as *; +@use "../mixins/utilities" as *; +@use "../utilities" as *; - // Generate media query if needed - @include media-breakpoint-up($breakpoint) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - // Loop over each utility property - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Only proceed if responsive media queries are enabled or if it's the base media query - @if type-of($utility) == "map" and (map-get($utility, responsive) or $infix == "") { - @include generate-utility($utility, $infix); - } - } - } -} - -// RFS rescaling -@media (min-width: $rfs-mq-value) { - @each $breakpoint in map-keys($grid-breakpoints) { - $infix: breakpoint-infix($breakpoint, $grid-breakpoints); - - @if (map-get($grid-breakpoints, $breakpoint) < $rfs-breakpoint) { - // Loop over each utility property - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Only proceed if responsive media queries are enabled or if it's the base media query - @if type-of($utility) == "map" and map-get($utility, rfs) and (map-get($utility, responsive) or $infix == "") { - @include generate-utility($utility, $infix, true); - } - } - } - } -} - - -// Print utilities -@media print { - @each $key, $utility in $utilities { - // The utility can be disabled with `false`, thus check if the utility is a map first - // Then check if the utility needs print styles - @if type-of($utility) == "map" and map-get($utility, print) == true { - @include generate-utility($utility, "-print"); - } - } +@layer utilities { + @include generate-utilities-loop($utilities, $breakpoints); } diff --git a/assets/stylesheets/bootstrap/vendor/_rfs.scss b/assets/stylesheets/bootstrap/vendor/_rfs.scss deleted file mode 100644 index aa1f82b9..00000000 --- a/assets/stylesheets/bootstrap/vendor/_rfs.scss +++ /dev/null @@ -1,348 +0,0 @@ -// stylelint-disable scss/dimension-no-non-numeric-values - -// SCSS RFS mixin -// -// Automated responsive values for font sizes, paddings, margins and much more -// -// Licensed under MIT (https://github.com/twbs/rfs/blob/main/LICENSE) - -// Configuration - -// Base value -$rfs-base-value: 1.25rem !default; -$rfs-unit: rem !default; - -@if $rfs-unit != rem and $rfs-unit != px { - @error "`#{$rfs-unit}` is not a valid unit for $rfs-unit. Use `px` or `rem`."; -} - -// Breakpoint at where values start decreasing if screen width is smaller -$rfs-breakpoint: 1200px !default; -$rfs-breakpoint-unit: px !default; - -@if $rfs-breakpoint-unit != px and $rfs-breakpoint-unit != em and $rfs-breakpoint-unit != rem { - @error "`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`."; -} - -// Resize values based on screen height and width -$rfs-two-dimensional: false !default; - -// Factor of decrease -$rfs-factor: 10 !default; - -@if type-of($rfs-factor) != number or $rfs-factor <= 1 { - @error "`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1."; -} - -// Mode. Possibilities: "min-media-query", "max-media-query" -$rfs-mode: min-media-query !default; - -// Generate enable or disable classes. Possibilities: false, "enable" or "disable" -$rfs-class: false !default; - -// 1 rem = $rfs-rem-value px -$rfs-rem-value: 16 !default; - -// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14 -$rfs-safari-iframe-resize-bug-fix: false !default; - -// Disable RFS by setting $enable-rfs to false -$enable-rfs: true !default; - -// Cache $rfs-base-value unit -$rfs-base-value-unit: unit($rfs-base-value); - -@function divide($dividend, $divisor, $precision: 10) { - $sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1); - $dividend: abs($dividend); - $divisor: abs($divisor); - @if $dividend == 0 { - @return 0; - } - @if $divisor == 0 { - @error "Cannot divide by 0"; - } - $remainder: $dividend; - $result: 0; - $factor: 10; - @while ($remainder > 0 and $precision >= 0) { - $quotient: 0; - @while ($remainder >= $divisor) { - $remainder: $remainder - $divisor; - $quotient: $quotient + 1; - } - $result: $result * 10 + $quotient; - $factor: $factor * .1; - $remainder: $remainder * 10; - $precision: $precision - 1; - @if ($precision < 0 and $remainder >= $divisor * 5) { - $result: $result + 1; - } - } - $result: $result * $factor * $sign; - $dividend-unit: unit($dividend); - $divisor-unit: unit($divisor); - $unit-map: ( - "px": 1px, - "rem": 1rem, - "em": 1em, - "%": 1% - ); - @if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) { - $result: $result * map-get($unit-map, $dividend-unit); - } - @return $result; -} - -// Remove px-unit from $rfs-base-value for calculations -@if $rfs-base-value-unit == px { - $rfs-base-value: divide($rfs-base-value, $rfs-base-value * 0 + 1); -} -@else if $rfs-base-value-unit == rem { - $rfs-base-value: divide($rfs-base-value, divide($rfs-base-value * 0 + 1, $rfs-rem-value)); -} - -// Cache $rfs-breakpoint unit to prevent multiple calls -$rfs-breakpoint-unit-cache: unit($rfs-breakpoint); - -// Remove unit from $rfs-breakpoint for calculations -@if $rfs-breakpoint-unit-cache == px { - $rfs-breakpoint: divide($rfs-breakpoint, $rfs-breakpoint * 0 + 1); -} -@else if $rfs-breakpoint-unit-cache == rem or $rfs-breakpoint-unit-cache == "em" { - $rfs-breakpoint: divide($rfs-breakpoint, divide($rfs-breakpoint * 0 + 1, $rfs-rem-value)); -} - -// Calculate the media query value -$rfs-mq-value: if($rfs-breakpoint-unit == px, #{$rfs-breakpoint}px, #{divide($rfs-breakpoint, $rfs-rem-value)}#{$rfs-breakpoint-unit}); -$rfs-mq-property-width: if($rfs-mode == max-media-query, max-width, min-width); -$rfs-mq-property-height: if($rfs-mode == max-media-query, max-height, min-height); - -// Internal mixin used to determine which media query needs to be used -@mixin _rfs-media-query { - @if $rfs-two-dimensional { - @if $rfs-mode == max-media-query { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}), (#{$rfs-mq-property-height}: #{$rfs-mq-value}) { - @content; - } - } - @else { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) and (#{$rfs-mq-property-height}: #{$rfs-mq-value}) { - @content; - } - } - } - @else { - @media (#{$rfs-mq-property-width}: #{$rfs-mq-value}) { - @content; - } - } -} - -// Internal mixin that adds disable classes to the selector if needed. -@mixin _rfs-rule { - @if $rfs-class == disable and $rfs-mode == max-media-query { - // Adding an extra class increases specificity, which prevents the media query to override the property - &, - .disable-rfs &, - &.disable-rfs { - @content; - } - } - @else if $rfs-class == enable and $rfs-mode == min-media-query { - .enable-rfs &, - &.enable-rfs { - @content; - } - } @else { - @content; - } -} - -// Internal mixin that adds enable classes to the selector if needed. -@mixin _rfs-media-query-rule { - - @if $rfs-class == enable { - @if $rfs-mode == min-media-query { - @content; - } - - @include _rfs-media-query () { - .enable-rfs &, - &.enable-rfs { - @content; - } - } - } - @else { - @if $rfs-class == disable and $rfs-mode == min-media-query { - .disable-rfs &, - &.disable-rfs { - @content; - } - } - @include _rfs-media-query () { - @content; - } - } -} - -// Helper function to get the formatted non-responsive value -@function rfs-value($values) { - // Convert to list - $values: if(type-of($values) != list, ($values,), $values); - - $val: ""; - - // Loop over each value and calculate value - @each $value in $values { - @if $value == 0 { - $val: $val + " 0"; - } - @else { - // Cache $value unit - $unit: if(type-of($value) == "number", unit($value), false); - - @if $unit == px { - // Convert to rem if needed - $val: $val + " " + if($rfs-unit == rem, #{divide($value, $value * 0 + $rfs-rem-value)}rem, $value); - } - @else if $unit == rem { - // Convert to px if needed - $val: $val + " " + if($rfs-unit == px, #{divide($value, $value * 0 + 1) * $rfs-rem-value}px, $value); - } @else { - // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value - $val: $val + " " + $value; - } - } - } - - // Remove first space - @return unquote(str-slice($val, 2)); -} - -// Helper function to get the responsive value calculated by RFS -@function rfs-fluid-value($values) { - // Convert to list - $values: if(type-of($values) != list, ($values,), $values); - - $val: ""; - - // Loop over each value and calculate value - @each $value in $values { - @if $value == 0 { - $val: $val + " 0"; - } @else { - // Cache $value unit - $unit: if(type-of($value) == "number", unit($value), false); - - // If $value isn't a number (like inherit) or $value has a unit (not px or rem, like 1.5em) or $ is 0, just print the value - @if not $unit or $unit != px and $unit != rem { - $val: $val + " " + $value; - } @else { - // Remove unit from $value for calculations - $value: divide($value, $value * 0 + if($unit == px, 1, divide(1, $rfs-rem-value))); - - // Only add the media query if the value is greater than the minimum value - @if abs($value) <= $rfs-base-value or not $enable-rfs { - $val: $val + " " + if($rfs-unit == rem, #{divide($value, $rfs-rem-value)}rem, #{$value}px); - } - @else { - // Calculate the minimum value - $value-min: $rfs-base-value + divide(abs($value) - $rfs-base-value, $rfs-factor); - - // Calculate difference between $value and the minimum value - $value-diff: abs($value) - $value-min; - - // Base value formatting - $min-width: if($rfs-unit == rem, #{divide($value-min, $rfs-rem-value)}rem, #{$value-min}px); - - // Use negative value if needed - $min-width: if($value < 0, -$min-width, $min-width); - - // Use `vmin` if two-dimensional is enabled - $variable-unit: if($rfs-two-dimensional, vmin, vw); - - // Calculate the variable width between 0 and $rfs-breakpoint - $variable-width: #{divide($value-diff * 100, $rfs-breakpoint)}#{$variable-unit}; - - // Return the calculated value - $val: $val + " calc(" + $min-width + if($value < 0, " - ", " + ") + $variable-width + ")"; - } - } - } - } - - // Remove first space - @return unquote(str-slice($val, 2)); -} - -// RFS mixin -@mixin rfs($values, $property: font-size) { - @if $values != null { - $val: rfs-value($values); - $fluid-val: rfs-fluid-value($values); - - // Do not print the media query if responsive & non-responsive values are the same - @if $val == $fluid-val { - #{$property}: $val; - } - @else { - @include _rfs-rule () { - #{$property}: if($rfs-mode == max-media-query, $val, $fluid-val); - - // Include safari iframe resize fix if needed - min-width: if($rfs-safari-iframe-resize-bug-fix, (0 * 1vw), null); - } - - @include _rfs-media-query-rule () { - #{$property}: if($rfs-mode == max-media-query, $fluid-val, $val); - } - } - } -} - -// Shorthand helper mixins -@mixin font-size($value) { - @include rfs($value); -} - -@mixin padding($value) { - @include rfs($value, padding); -} - -@mixin padding-top($value) { - @include rfs($value, padding-top); -} - -@mixin padding-right($value) { - @include rfs($value, padding-right); -} - -@mixin padding-bottom($value) { - @include rfs($value, padding-bottom); -} - -@mixin padding-left($value) { - @include rfs($value, padding-left); -} - -@mixin margin($value) { - @include rfs($value, margin); -} - -@mixin margin-top($value) { - @include rfs($value, margin-top); -} - -@mixin margin-right($value) { - @include rfs($value, margin-right); -} - -@mixin margin-bottom($value) { - @include rfs($value, margin-bottom); -} - -@mixin margin-left($value) { - @include rfs($value, margin-left); -} diff --git a/bootstrap.gemspec b/bootstrap.gemspec index b47a63bc..f1b839f6 100644 --- a/bootstrap.gemspec +++ b/bootstrap.gemspec @@ -12,9 +12,12 @@ Gem::Specification.new do |s| s.license = 'MIT' # SassC requires Ruby 2.3.3. Also specify here to make it obvious. + # (Bootstrap 6 stylesheets require a Dart Sass engine to compile, but the gem + # itself stays installable on the same Ruby range as before.) s.required_ruby_version = '>= 2.3.3' - s.add_runtime_dependency 'popper_js', '>= 2.11.8', '< 3' + # Bootstrap 6 uses @floating-ui/dom (vendored in assets/javascripts) instead + # of Popper, so there is no longer a popper_js runtime dependency. s.add_development_dependency 'rake' @@ -22,9 +25,10 @@ Gem::Specification.new do |s| s.add_development_dependency 'minitest', '>= 5.14.4', '< 7' s.add_development_dependency 'minitest-reporters', '~> 1.4.3' s.add_development_dependency 'term-ansicolor' - # Integration testing + # Integration testing (headless browser) s.add_development_dependency 'capybara', '>= 2.6.0' s.add_development_dependency 'cuprite' + s.add_development_dependency 'webrick' # Dummy Rails app dependencies s.add_development_dependency 'railties' s.add_development_dependency 'actionpack', '>= 4.1.5' diff --git a/lib/bootstrap.rb b/lib/bootstrap.rb index d44f7e54..296be8ef 100644 --- a/lib/bootstrap.rb +++ b/lib/bootstrap.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'bootstrap/version' -require 'popper_js' module Bootstrap class << self diff --git a/lib/bootstrap/version.rb b/lib/bootstrap/version.rb index d19fa088..ffcaf946 100644 --- a/lib/bootstrap/version.rb +++ b/lib/bootstrap/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Bootstrap - VERSION = '5.3.8' - BOOTSTRAP_SHA = '25aa8cc0b32f0d1a54be575347e6d84b70b1acd7' + VERSION = '6.0.0.alpha1' + BOOTSTRAP_SHA = '15f17673ebcd82416fc55dbbbefda19dc4f298f8' end diff --git a/tasks/updater.rb b/tasks/updater.rb index 08cc8769..3b385b9a 100644 --- a/tasks/updater.rb +++ b/tasks/updater.rb @@ -18,12 +18,13 @@ class Updater include Js include Scss - def initialize(repo: 'twbs/bootstrap', branch: 'main', save_to: {}, cache_path: 'tmp/bootstrap-cache') + def initialize(repo: 'twbs/bootstrap', branch: 'main', save_to: {}, cache_path: 'tmp/bootstrap-cache', skip_js: false) @logger = Logger.new @repo = repo @branch = branch || 'main' @branch_sha = get_branch_sha @cache_path = cache_path + @skip_js = skip_js @repo_url = "https://github.com/#@repo" @save_to = { js: 'assets/javascripts/bootstrap', @@ -40,11 +41,20 @@ def update_bootstrap puts " twbs cache: #{@cache_path}" puts '-' * 60 - FileUtils.rm_rf('assets') - @save_to.each { |_, v| FileUtils.mkdir_p(v) } - - update_scss_assets - update_javascript_assets + # Bootstrap 6's upstream `v6-dev` still ships stale Bootstrap 5 `dist/js`, + # so `skip_js: true` refreshes only the stylesheets (and version SHA), + # leaving the bundled JavaScript untouched until Bootstrap 6's JS is + # published upstream. + if @skip_js + FileUtils.rm_rf(@save_to[:scss]) + FileUtils.mkdir_p(@save_to[:scss]) + update_scss_assets + else + FileUtils.rm_rf('assets') + @save_to.each { |_, v| FileUtils.mkdir_p(v) } + update_scss_assets + update_javascript_assets + end store_version end diff --git a/tasks/updater/js.rb b/tasks/updater/js.rb index ae352807..f9167349 100644 --- a/tasks/updater/js.rb +++ b/tasks/updater/js.rb @@ -3,59 +3,72 @@ class Updater module Js - INLINED_SRCS = %w[].freeze - def update_javascript_assets log_status 'Updating javascripts...' save_to = @save_to[:js] + # Bootstrap 6 ships ES modules only (no UMD bundle, no `window.bootstrap` + # global). We keep the individual modules and the bundles for importmap + # pinning; Sprockets `//= require` concatenation no longer applies. read_files('js/dist', bootstrap_js_files).each do |name, content| save_file("#{save_to}/#{name}", remove_source_mapping_url(content)) end log_processed "#{bootstrap_js_files * ' '}" - log_status 'Updating javascript manifest' - manifest = "//= require ./bootstrap-global-this-define\n" - bootstrap_js_files.each do |name| - name = name.gsub(/\.js$/, '') - manifest << "//= require ./bootstrap/#{name}\n" - end - manifest << "//= require ./bootstrap-global-this-undefine\n" - dist_js = read_files('dist/js', %w(bootstrap.js bootstrap.min.js)) - { - 'assets/javascripts/bootstrap-global-this-define.js' => <<~JS, - // Set a `globalThis` so that bootstrap components are defined on window.bootstrap instead of window. - window['bootstrap'] = { - "@popperjs/core": window.Popper, - _originalGlobalThis: window['globalThis'] - }; - window['globalThis'] = window['bootstrap']; - JS - 'assets/javascripts/bootstrap-global-this-undefine.js' => <<~JS, - window['globalThis'] = window['bootstrap']._originalGlobalThis; - window['bootstrap']._originalGlobalThis = null; - JS - 'assets/javascripts/bootstrap-sprockets.js' => manifest, - 'assets/javascripts/bootstrap.js' => dist_js['bootstrap.js'], - 'assets/javascripts/bootstrap.min.js' => dist_js['bootstrap.min.js'], - }.each do |path, content| + log_status 'Updating javascript bundles' + # `bootstrap.{js,min.js}` import @floating-ui/dom and vanilla-calendar-pro + # as bare specifiers; the `bootstrap.bundle.{js,min.js}` builds inline those + # dependencies and are fully self-contained (the recommended importmap pin). + dist_files = %w(bootstrap.js bootstrap.min.js bootstrap.bundle.js bootstrap.bundle.min.js) + read_files('dist/js', dist_files).each do |name, content| + path = "assets/javascripts/#{name}" save_file path, remove_source_mapping_url(content) log_processed path end + + vendor_floating_ui + end + + # Bootstrap 6 depends on @floating-ui/dom (replacing Popper). Vendor a + # self-contained ESM bundle so apps can pin it via importmaps without a CDN. + def vendor_floating_ui + version = floating_ui_version + log_status "Vendoring @floating-ui/dom@#{version}" + stub = get_file("https://esm.sh/@floating-ui/dom@#{version}?bundle") + rel = stub[/from\s+"([^"]+)"/, 1] or + raise "Unexpected esm.sh response for @floating-ui/dom@#{version}:\n#{stub}" + bundle = get_file("https://esm.sh#{rel}") + path = 'assets/javascripts/floating-ui.js' + save_file path, bundle + log_processed path + end + + def floating_ui_version + pkg = get_json(file_url 'package.json') + spec = (pkg['dependencies'] || {})['@floating-ui/dom'] || + (pkg['devDependencies'] || {})['@floating-ui/dom'] or + raise 'Could not find @floating-ui/dom in upstream package.json' + spec.sub(/\A\D*/, '') end def bootstrap_js_files @bootstrap_js_files ||= begin - src_files = get_paths_by_type('js/src', /\.js$/) - INLINED_SRCS - puts "src_files: #{src_files.inspect}" + src_files = get_paths_by_type('js/src', /\.js$/) imports = Deps.new - # Get the imports from the ES6 files to order requires correctly. + # Get the imports from the ES modules to order requires correctly. read_files('js/src', src_files).each do |name, content| - file_imports = content.scan(%r{import *(?:[a-zA-Z]*|\{[a-zA-Z ,]*\}) *from '([\w/.-]+)}).flatten(1).map do |f| - Pathname.new(name).dirname.join(f).cleanpath.to_s - end.uniq - imports.add name, *(file_imports - INLINED_SRCS) + file_imports = content.scan(%r{import *(?:[a-zA-Z]*|\{[a-zA-Z ,]*\}) *from '([\w/.-]+)}).flatten(1) + # Only follow relative imports between Bootstrap's own source files; + # skip npm dependencies (e.g. `vanilla-calendar-pro`, `@floating-ui/dom`). + .select { |f| f.start_with?('.') } + .map { |f| Pathname.new(name).dirname.join(f).cleanpath.to_s } + .uniq + imports.add name, *file_imports end - imports.tsort + # Order by the src import graph, but only ship components that are + # actually present in the compiled dist (src/ may contain modules that + # have no standalone dist/ build). + dist_files = get_paths_by_type('js/dist', /\.js$/) + imports.tsort.select { |f| dist_files.include?(f) } end end diff --git a/tasks/updater/network.rb b/tasks/updater/network.rb index 6943bf56..eea26adf 100644 --- a/tasks/updater/network.rb +++ b/tasks/updater/network.rb @@ -84,7 +84,14 @@ def get_branch_sha log cmd result = %x[#{cmd}] raise 'Could not get branch sha!' unless $?.success? && !result.empty? - result.split(/\s+/).first + # `git ls-remote v6-dev` also matches suffixes like + # `refs/heads/mdo/v6-dev`, so pick the exact branch (or tag) ref + # rather than blindly taking the first line. + ref_of = ->(line) { line.split(/\s+/, 2)[1].to_s.strip } + line = result.lines.find { |l| ref_of.call(l) == "refs/heads/#@branch" } || + result.lines.find { |l| ref_of.call(l) == "refs/tags/#@branch" } || + result.lines.first + line.split(/\s+/).first end end end diff --git a/tasks/updater/scss.rb b/tasks/updater/scss.rb index 4af7e968..4c989eda 100644 --- a/tasks/updater/scss.rb +++ b/tasks/updater/scss.rb @@ -11,16 +11,14 @@ def update_scss_assets end log_processed "#{bootstrap_scss_files * ' '}" - log_status 'Updating scss main files' - %w(bootstrap bootstrap-grid bootstrap-reboot bootstrap-utilities).each do |name| - # Compass treats non-partials as targets to copy into the main project, so make them partials. - # Also move them up a level to clearly indicate entry points. - from = "#{save_to}/#{name}.scss" - to = "#{save_to}/../_#{name}.scss" - FileUtils.mv from, to - # As we moved the files, adjust imports accordingly. - File.write to, File.read(to).gsub(/ "/, ' "bootstrap/') - end + log_status 'Updating scss main file' + # Bootstrap 6 exposes a single `bootstrap` entry point. Make it a partial + # and move it up a level to clearly mark it as the entry point, rewriting + # its `@use`/`@forward` paths to account for the move. + from = "#{save_to}/bootstrap.scss" + to = "#{save_to}/../_bootstrap.scss" + FileUtils.mv from, to + File.write to, File.read(to).gsub(/ "/, ' "bootstrap/') end end end diff --git a/test/dummy_rails/app/assets/javascripts/application.js b/test/dummy_rails/app/assets/javascripts/application.js index 8a6aac2a..4b5efa6c 100644 --- a/test/dummy_rails/app/assets/javascripts/application.js +++ b/test/dummy_rails/app/assets/javascripts/application.js @@ -1,8 +1,5 @@ -//= require popper.js -//= require bootstrap-sprockets - -document.addEventListener('DOMContentLoaded', () => { - for (const tooltipTriggerEl of document.querySelectorAll('[data-bs-toggle="tooltip"]')) { - new bootstrap.Tooltip(tooltipTriggerEl) - } -}); +// Bootstrap 6 JavaScript is ES-module only and is loaded through importmaps +// (see the README), not Sprockets `//= require`. This Sprockets-based dummy app +// exercises the Bootstrap 6 *stylesheet* pipeline; the ES-module JavaScript and +// its vendored @floating-ui/dom dependency are smoke-tested separately in +// test/javascript_test.rb. diff --git a/test/dummy_rails/app/assets/stylesheets/application.sass b/test/dummy_rails/app/assets/stylesheets/application.sass index b0d09cef..1f47670e 100644 --- a/test/dummy_rails/app/assets/stylesheets/application.sass +++ b/test/dummy_rails/app/assets/stylesheets/application.sass @@ -1,4 +1,8 @@ -@import 'bootstrap' +// Bootstrap 6 uses the Sass module system (@use), not @import. +@use 'bootstrap' + +// Verify Bootstrap's Sass API is reachable through the gem's load path. +@use 'bootstrap/layout/containers' as containers .test-mixin - +make-container + @include containers.make-container diff --git a/test/javascript_test.rb b/test/javascript_test.rb new file mode 100644 index 00000000..bd871144 --- /dev/null +++ b/test/javascript_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# Smoke-tests the Bootstrap 6 ES-module JavaScript in a real headless browser, +# the same way an importmaps-based app consumes it. Two delivery paths are +# covered: +# +# 1. The self-contained `bootstrap.bundle.min.js` (inlines @floating-ui/dom and +# vanilla-calendar-pro) loaded with NO importmap pins -- the recommended path. +# 2. An individual component module (`bootstrap/tooltip.js`) resolving the bare +# `@floating-ui/dom` specifier to the gem's vendored `floating-ui.js`. +# +# Each path imports a tooltip (positioned via @floating-ui/dom) and asserts the +# component instantiates and that Floating UI actually placed the tooltip. +# Independent of Rails/Sprockets. + +require 'minitest/autorun' +require 'webrick' +require 'ferrum' +require 'fileutils' +require 'tmpdir' + +class JavascriptTest < Minitest::Test + # Resolve the path directly rather than `require 'bootstrap'`: loading the gem + # here would run `Bootstrap.load!` before the dummy Rails app boots and, under + # random test order, prevent the Rails engine from registering its asset paths. + JS_DIR = File.expand_path('../assets/javascripts', __dir__) + + def test_self_contained_bundle_needs_no_pins + # The bundle inlines its dependencies, so no importmap is required at all. + assert_tooltip_works(<<~HTML) + + Button + + + HTML + end + + def test_module_resolves_vendored_floating_ui + # The lean component module resolves the bare @floating-ui/dom specifier to + # the gem's vendored build via an importmap. + assert_tooltip_works(<<~HTML) + + + + Button + + + HTML + end + + private + + # JS that imports a tooltip class, shows a tooltip, and records the outcome on + # the dataset for the Ruby side to read. + def tooltip_probe(module_path, export) + <<~JS + document.body.dataset.status = "loading"; + try { + const Tooltip = (await import("#{module_path}")).#{export}; + const tip = new Tooltip(document.getElementById("btn")); + tip.show(); // positions via @floating-ui/dom computePosition() + const el = document.querySelector(".tooltip"); + document.body.dataset.status = (typeof tip.show === "function" && el) ? "ok" : "fail"; + } catch (e) { + document.body.dataset.status = "error: " + (e && e.message || e); + } + JS + end + + def assert_tooltip_works(html) + docroot = serve_assets(html) + port = start_server(docroot) + browser = new_browser + browser.go_to("http://127.0.0.1:#{port}/index.html") + + status = nil + 60.times do + status = browser.evaluate("document.body.dataset.status") + break if status && status != 'loading' + sleep 0.1 + end + refute_nil status, 'ES module never executed in the browser' + assert_equal 'ok', status, "Bootstrap 6 ESM tooltip failed: #{status}" + + # @floating-ui/dom's computePosition() is async; poll for the inline left/top + # px it writes onto the rendered tooltip element. + style = '' + positioned = false + 30.times do + style = browser.evaluate( + "(document.querySelector('.tooltip') || {getAttribute: () => ''}).getAttribute('style') || ''" + ).to_s + if style =~ /left:\s*[\d.]+px/ && style =~ /top:\s*[\d.]+px/ + positioned = true + break + end + sleep 0.1 + end + assert positioned, "@floating-ui/dom did not position the tooltip (style=#{style.inspect})" + ensure + browser&.quit + @server&.shutdown + end + + def serve_assets(index_html) + docroot = File.join(Dir.tmpdir, "bootstrap-js-test-#{Process.pid}-#{rand(1 << 20)}") + FileUtils.rm_rf(docroot) + FileUtils.mkdir_p(docroot) + FileUtils.cp_r(File.join(JS_DIR, '.'), docroot) + File.write(File.join(docroot, 'index.html'), index_html) + docroot + end + + def start_server(docroot) + @server = WEBrick::HTTPServer.new( + Port: 0, + DocumentRoot: docroot, + Logger: WEBrick::Log.new(File::NULL), + AccessLog: [] + ) + # Ensure JS is served with a module-compatible MIME type. + @server.config[:MimeTypes]['js'] = 'text/javascript' + port = @server.config[:Port] + Thread.new { @server.start } + port + end + + def new_browser + chrome = [ + ENV['CHROMIUM_BIN'], + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium-browser', + '/snap/bin/chromium' + ].compact.find { |p| File.executable?(p) } + + # process_timeout matches the cuprite driver in test_helper.rb: Chrome can + # take well over 30s to first start on loaded CI runners. + opts = { headless: true, process_timeout: 60, timeout: 30 } + opts[:browser_path] = chrome if chrome # otherwise let Ferrum auto-detect (CI) + Ferrum::Browser.new(**opts) + end +end diff --git a/test/rails_test.rb b/test/rails_test.rb index fc67b83d..bd526c29 100644 --- a/test/rails_test.rb +++ b/test/rails_test.rb @@ -2,8 +2,11 @@ class RailsTest < ActionDispatch::IntegrationTest include ::DummyRailsIntegration + include ::SassEngineSupport def test_visit_root + skip_unless_sass_can_compile_bootstrap! + visit root_path # ^ will raise on JS errors @@ -13,6 +16,8 @@ def test_visit_root end def test_precompile + skip_unless_sass_can_compile_bootstrap! + Dummy::Application.load_tasks Rake::Task['assets:precompile'].invoke end diff --git a/test/support/sass_engine_support.rb b/test/support/sass_engine_support.rb new file mode 100644 index 00000000..a31c62ce --- /dev/null +++ b/test/support/sass_engine_support.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Bootstrap 6's stylesheets use the Sass module system (@use) and the CSS +# if()/sass() syntax, which only a recent Dart Sass can compile. libsass (the +# sassc gem) and the older Dart Sass releases that still resolve on Ruby < 3.1 +# fail to parse them, so the stylesheet tests skip on those engines. The probe +# compiles a snippet of the same syntax rather than Bootstrap itself, so a +# genuine Bootstrap regression on a capable engine still fails the tests. +module SassEngineSupport + BOOTSTRAP6_SYNTAX_PROBE = <<~SCSS + @use "sass:math"; + @function probe($a, $b) { + @return if(sass($a > $b): math.div($a, $b); else: math.div($b, $a)); + } + .probe { opacity: probe(2, 1); } + SCSS + + def self.can_compile_bootstrap? + return @can_compile_bootstrap if defined?(@can_compile_bootstrap) + @can_compile_bootstrap = + begin + defined?(SassC::Engine) && + !SassC::Engine.new(BOOTSTRAP6_SYNTAX_PROBE, syntax: :scss).render.nil? + rescue StandardError + false + end + end + + def skip_unless_sass_can_compile_bootstrap! + return if SassEngineSupport.can_compile_bootstrap? + + skip 'Sass engine cannot compile Bootstrap 6 stylesheets (recent Dart Sass required)' + end +end
`s get reset. However, we also reset the -// bottom margin to use `rem` units instead of `em`. - -p { - margin-top: 0; - margin-bottom: $paragraph-margin-bottom; -} - - -// Abbreviations -// -// 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. -// 2. Add explicit cursor to indicate changed behavior. -// 3. Prevent the text-decoration to be skipped. - -abbr[title] { - text-decoration: underline dotted; // 1 - cursor: help; // 2 - text-decoration-skip-ink: none; // 3 -} - - -// Address - -address { - margin-bottom: 1rem; - font-style: normal; - line-height: inherit; -} - - -// Lists - -ol, -ul { - padding-left: 2rem; -} - -ol, -ul, -dl { - margin-top: 0; - margin-bottom: 1rem; -} - -ol ol, -ul ul, -ol ul, -ul ol { - margin-bottom: 0; -} - -dt { - font-weight: $dt-font-weight; -} - -// 1. Undo browser default - -dd { - margin-bottom: .5rem; - margin-left: 0; // 1 -} - - -// Blockquote - -blockquote { - margin: 0 0 1rem; -} - - -// Strong -// -// Add the correct font weight in Chrome, Edge, and Safari - -b, -strong { - font-weight: $font-weight-bolder; -} - - -// Small -// -// Add the correct font size in all browsers - -small { - @include font-size($small-font-size); -} - - -// Mark - -mark { - padding: $mark-padding; - color: var(--#{$prefix}highlight-color); - background-color: var(--#{$prefix}highlight-bg); -} - - -// Sub and Sup -// -// Prevent `sub` and `sup` elements from affecting the line height in -// all browsers. - -sub, -sup { - position: relative; - @include font-size($sub-sup-font-size); - line-height: 0; - vertical-align: baseline; -} - -sub { bottom: -.25em; } -sup { top: -.5em; } - - -// Links - -a { - color: rgba(var(--#{$prefix}link-color-rgb), var(--#{$prefix}link-opacity, 1)); - text-decoration: $link-decoration; - - &:hover { - --#{$prefix}link-color-rgb: var(--#{$prefix}link-hover-color-rgb); - text-decoration: $link-hover-decoration; - } -} - -// And undo these styles for placeholder links/named anchors (without href). -// It would be more straightforward to just use a[href] in previous block, but that -// causes specificity issues in many other styles that are too complex to fix. -// See https://github.com/twbs/bootstrap/issues/19402 - -a:not([href]):not([class]) { - &, - &:hover { - color: inherit; - text-decoration: none; - } -} - - -// Code - -pre, -code, -kbd, -samp { - font-family: $font-family-code; - @include font-size(1em); // Correct the odd `em` font sizing in all browsers. -} - -// 1. Remove browser default top margin -// 2. Reset browser default of `1em` to use `rem`s -// 3. Don't allow content to break outside - -pre { - display: block; - margin-top: 0; // 1 - margin-bottom: 1rem; // 2 - overflow: auto; // 3 - @include font-size($code-font-size); - color: $pre-color; - - // Account for some code outputs that place code tags in pre tags - code { - @include font-size(inherit); - color: inherit; - word-break: normal; - } -} - -code { - @include font-size($code-font-size); - color: var(--#{$prefix}code-color); - word-wrap: break-word; - - // Streamline the style when inside anchors to avoid broken underline and more - a > & { - color: inherit; - } -} - -kbd { - padding: $kbd-padding-y $kbd-padding-x; - @include font-size($kbd-font-size); - color: $kbd-color; - background-color: $kbd-bg; - @include border-radius($border-radius-sm); - - kbd { - padding: 0; - @include font-size(1em); - font-weight: $nested-kbd-font-weight; - } -} - - -// Figures -// -// Apply a consistent margin strategy (matches our type styles). - -figure { - margin: 0 0 1rem; -} - - -// Images and content - -img, -svg { - vertical-align: middle; -} - - -// Tables -// -// Prevent double borders - -table { - caption-side: bottom; - border-collapse: collapse; -} - -caption { - padding-top: $table-cell-padding-y; - padding-bottom: $table-cell-padding-y; - color: $table-caption-color; - text-align: left; -} - -// 1. Removes font-weight bold by inheriting -// 2. Matches default `
`s get reset. However, we also reset the + // bottom margin to use `rem` units instead of `em`. + + p { + margin-top: 0; + margin-bottom: $paragraph-margin-bottom; + } + + // Abbreviations + // + // 1. Add the correct text decoration in Chrome, Edge, Opera, and Safari. + // 2. Add explicit cursor to indicate changed behavior. + // 3. Prevent the text-decoration to be skipped. + + abbr[title] { + text-decoration: underline dotted; // 1 + cursor: help; // 2 + text-decoration-skip-ink: none; // 3 + } + + // Address + + address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; + } + + // Lists + + ol, + ul { + padding-inline-start: 2rem; + } + + ol, + ul, + dl { + margin-top: 0; + margin-bottom: 1rem; + } + + ol ol, + ul ul, + ol ul, + ul ol { + margin-bottom: 0; + } + + dt { + font-weight: $dt-font-weight; + } + + // 1. Undo browser default + + dd { + margin-inline-start: 0; // 1 + margin-bottom: .5rem; + } + + // Blockquote + + blockquote { + margin: 0 0 1rem; + > * { + margin-block: 0; + } + } + + // Strong + // + // Add the correct font weight in Chrome, Edge, and Safari + + b, + strong { + font-weight: $font-weight-bolder; + } + + // Small + // + // Add the correct font size in all browsers + + small, + .small { + font-size: var(--small-font-size, 87.5%); + } + + // Mark + + mark, + .mark { + @include tokens($reboot-mark-tokens); + padding: var(--mark-padding); + color: var(--mark-color); + background-color: var(--mark-bg); + } + + // Sub and Sup + // + // Prevent `sub` and `sup` elements from affecting the line height in + // all browsers. + + sub, + sup { + position: relative; + font-size: var(--sub-sup-font-size, .75em); + line-height: 0; + vertical-align: baseline; + } + + sub { bottom: -.25em; } + sup { top: -.5em; } + + // Links + + a { + color: var(--theme-fg, var(--link-color)); + text-decoration: var(--link-decoration); + text-underline-offset: $link-underline-offset; + + &:hover { + // --link-color: var(--link-hover-color); + // --link-decoration: var(--link-hover-decoration, var(--link-decoration)); + color: var(--theme-fg-emphasis, var(--link-hover-color)); + text-decoration: var(--link-hover-decoration, var(--link-decoration)); + } + } + + // And undo these styles for placeholder links/named anchors (without href). + // It would be more straightforward to just use a[href] in previous block, but that + // causes specificity issues in many other styles that are too complex to fix. + // See https://github.com/twbs/bootstrap/issues/19402 + + a:not([href], [class]) { + &, + &:hover { + color: inherit; + text-decoration: none; + } + } + + // Code + + pre, + code, + kbd, + samp { + font-family: var(--font-mono); + font-size: 1em; // Correct the odd `em` font sizing in all browsers. + } + + // 1. Remove browser default top margin + // 2. Reset browser default of `1em` to use `rem`s + // 3. Don't allow content to break outside + + pre { + display: block; + margin-top: 0; // 1 + margin-bottom: 1rem; // 2 + overflow: auto; // 3 + font-size: var(--code-font-size); + color: var(--code-color, inherit); + + // Account for some code outputs that place code tags in pre tags + code { + font-size: inherit; + color: inherit; + word-break: normal; + } + } + + code { + font-size: var(--code-font-size); + color: var(--code-color); + word-wrap: break-word; + + // Streamline the style when inside anchors to avoid broken underline and more + a > & { + color: inherit; + } + } + + kbd { + @include tokens($reboot-kbd-tokens); + padding: var(--kbd-padding-y) var(--kbd-padding-x); + font-size: var(--kbd-font-size); + color: var(--kbd-color); + background-color: var(--kbd-bg); + @include border-radius(var(--kbd-border-radius)); + + kbd { + padding: 0; + font-size: 1em; + font-weight: inherit; // mdo-do: check if this is needed + } + } + + // Figures + // + // Apply a consistent margin strategy (matches our type styles). + + figure { + margin: 0 0 1rem; + } + + // Images and content + + img, + svg { + vertical-align: middle; + } + + // Tables + // + // Prevent double borders + + table { + caption-side: bottom; + border-collapse: collapse; + } + + caption { + // padding-top: $table-cell-padding-y; + // padding-bottom: $table-cell-padding-y; + // color: $table-caption-color; + padding-block: .5rem; + color: var(--fg-3); + text-align: start; + } + + // 1. Removes font-weight bold by inheriting + // 2. Matches default `