diff --git a/.browserslistrc b/.browserslistrc index c17ce9049c9c..cddd23005246 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,12 +1,12 @@ # https://github.com/browserslist/browserslist#readme ->= 1% -last 1 major version +>= 0.5% +last 2 major versions not dead Chrome >= 60 Firefox >= 60 -Edge >= 16 -iOS >= 10 -Safari >= 10 -Android >= 6 +Firefox ESR +iOS >= 12 +Safari >= 12 not Explorer <= 11 +not kaios <= 2.5 # fix floating label issues in Firefox (see https://github.com/postcss/autoprefixer/issues/1533) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index b89e0207b783..6f680664ca67 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -2,59 +2,59 @@ "files": [ { "path": "./dist/css/bootstrap-grid.css", - "maxSize": "7 kB" + "maxSize": "6.5 kB" }, { "path": "./dist/css/bootstrap-grid.min.css", - "maxSize": "6 kB" + "maxSize": "6.0 kB" }, { "path": "./dist/css/bootstrap-reboot.css", - "maxSize": "2 kB" + "maxSize": "3.5 kB" }, { "path": "./dist/css/bootstrap-reboot.min.css", - "maxSize": "2 kB" + "maxSize": "3.25 kB" }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "7 kB" + "maxSize": "11.75 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", - "maxSize": "6 kB" + "maxSize": "10.75 kB" }, { "path": "./dist/css/bootstrap.css", - "maxSize": "24 kB" + "maxSize": "32.5 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "21.5 kB" + "maxSize": "30.25 kB" }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "51 kB" + "maxSize": "43.0 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "22.5 kB" + "maxSize": "23.5 kB" }, { "path": "./dist/js/bootstrap.esm.js", - "maxSize": "28 kB" + "maxSize": "28.0 kB" }, { "path": "./dist/js/bootstrap.esm.min.js", - "maxSize": "19 kB" + "maxSize": "18.25 kB" }, { "path": "./dist/js/bootstrap.js", - "maxSize": "29 kB" + "maxSize": "28.75 kB" }, { "path": "./dist/js/bootstrap.min.js", - "maxSize": "16 kB" + "maxSize": "16.25 kB" } ], "ci": { diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 000000000000..8bc5fad1be60 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,188 @@ +{ + "version": "0.2", + "words": [ + "affordance", + "allowfullscreen", + "alpadev", + "Analyser", + "asciiquotes", + "autohide", + "autohiding", + "Autoprefixer", + "autoplay", + "autoplays", + "autoplaying", + "blazingly", + "Blockquotes", + "Bootstrappers", + "borderless", + "BOTTOMCENTER", + "BOTTOMEND", + "Brotli", + "browserslist", + "browserslistrc", + "btncheck", + "btnradio", + "bundlewatch", + "Bytesize", + "callout", + "callouts", + "camelCase", + "cheatsheet", + "cleancss", + "clearfix", + "Codesniffer", + "combinator", + "contenteditable", + "Contentful", + "Cpath", + "Croll", + "Crossfade", + "crossfading", + "cssgrid", + "Csvg", + "Datalists", + "Deque", + "describedby", + "dgst", + "discoverability", + "docsearch", + "docsref", + "Dripicons", + "dropend", + "dropleft", + "dropright", + "dropstart", + "dropup", + "errorf", + "esproj", + "favicon", + "favicons", + "fieldsets", + "filterpattern", + "flexbox", + "focusables", + "focustrap", + "fullscreen", + "fullwidth", + "fusv", + "gesturechange", + "gestureend", + "gesturestart", + "getbootstrap", + "getj", + "Grayscale", + "Hoverable", + "hreflang", + "hstack", + "icofont", + "importmap", + "Ionicons", + "itemtype", + "ITUSEYJG", + "jsdelivr", + "Jumbotrons", + "Jumpstart", + "keyframes", + "KMZKZHQ", + "komodoproject", + "Komodo", + "komodotools", + "labelledby", + "libera", + "libman", + "Libsass", + "lightboxes", + "longdesc", + "Lowercased", + "markdownify", + "mediaqueries", + "minifiers", + "misfunction", + "mkdir", + "monospace", + "mouseleave", + "navbars", + "navs", + "nbproject", + "Neue", + "nextjs", + "noindex", + "Noto", + "offcanvas", + "offcanvases", + "opencollective", + "Packagist", + "popperjs", + "prebuild", + "prefersreducedmotion", + "prepended", + "printf", + "rects", + "relref", + "rgba", + "roboto", + "robotstxt", + "RTLCSS", + "rubygem", + "ruleset", + "sassrc", + "screenreaders", + "scrollbars", + "scrollspy", + "Segoe", + "selectend", + "semibold", + "slugified", + "socio", + "srcset", + "stackblitz", + "stickied", + "Stylelint", + "stylelintcache", + "subnav", + "tabbable", + "Tabler", + "textareas", + "tmproj", + "toggleable", + "topbar", + "TOPCENTER", + "TOPEND", + "touchend", + "twbs", + "unitless", + "Unported", + "unstylable", + "unstyled", + "Uppercased", + "uppercases", + "urlize", + "urlquery", + "vbtn", + "viewports", + "Vite", + "vstack", + "walkthroughs", + "WCAG", + "Werror", + "xlink", + "zindex" + ], + "language": "en-US", + "ignoreRegExpList": [ + "\\u001[Bb]\\[[0-9;]*m" + ], + "ignorePaths": [ + ".cspell.json", + ".vscode/", + "dist/", + "*.min.*", + "**/*rtl*", + "**/tests/**", + "package-lock.json", + "site/data/core-team.yml", + "site/data/translations.yml" + ], + "useGitignore": true +} diff --git a/.eslintignore b/.eslintignore index 6d874bc3658d..e42161487a5b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,9 @@ **/*.min.js **/dist/ **/vendor/ -/_gh_pages/ +/_site/ +/site/public/ /js/coverage/ /site/static/sw.js -/package.js +/site/static/docs/**/assets/sw.js +/site/layouts/partials/ diff --git a/.eslintrc.json b/.eslintrc.json index 59fe70f56405..da686166f5a8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,11 +4,45 @@ "plugin:import/errors", "plugin:import/warnings", "plugin:unicorn/recommended", - "xo/esnext", + "xo", "xo/browser" ], "rules": { + "arrow-body-style": "off", "capitalized-comments": "off", + "comma-dangle": [ + "error", + "never" + ], + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "always" + } + ], + "import/first": "error", + "import/newline-after-import": "error", + "import/no-absolute-path": "error", + "import/no-amd": "error", + "import/no-cycle": [ + "error", + { + "ignoreExternal": true + } + ], + "import/no-duplicates": "error", + "import/no-extraneous-dependencies": "error", + "import/no-mutable-exports": "error", + "import/no-named-as-default": "error", + "import/no-named-as-default-member": "error", + "import/no-named-default": "error", + "import/no-self-import": "error", + "import/no-unassigned-import": [ + "error" + ], + "import/no-useless-path-segments": "error", + "import/order": "error", "indent": [ "error", 2, @@ -17,6 +51,7 @@ "SwitchCase": 1 } ], + "logical-assignment-operators": "off", "max-params": [ "warn", 5 @@ -25,34 +60,182 @@ "error", "always-multiline" ], - "new-cap": "off", + "new-cap": [ + "error", + { + "properties": false + } + ], "no-console": "error", + "no-negated-condition": "off", "object-curly-spacing": [ "error", "always" ], - "prefer-named-capture-group": "off", + "operator-linebreak": [ + "error", + "after" + ], + "prefer-object-has-own": "off", + "prefer-template": "error", "semi": [ "error", "never" ], - "unicorn/consistent-function-scoping": "off", + "strict": "error", "unicorn/explicit-length-check": "off", - "unicorn/import-index": "off", - "unicorn/no-fn-reference-in-iterator": "off", - "unicorn/no-for-loop": "off", + "unicorn/filename-case": "off", + "unicorn/no-anonymous-default-export": "off", + "unicorn/no-array-callback-reference": "off", + "unicorn/no-array-method-this-argument": "off", "unicorn/no-null": "off", + "unicorn/no-typeof-undefined": "off", "unicorn/no-unused-properties": "error", - "unicorn/no-useless-undefined": "off", - "unicorn/prefer-array-find": "off", - "unicorn/prefer-dataset": "off", - "unicorn/prefer-includes": "off", - "unicorn/prefer-node-append": "off", - "unicorn/prefer-node-remove": "off", - "unicorn/prefer-number-properties": "off", - "unicorn/prefer-optional-catch-binding": "off", + "unicorn/numeric-separators-style": "off", + "unicorn/prefer-array-flat": "off", + "unicorn/prefer-at": "off", + "unicorn/prefer-dom-node-dataset": "off", + "unicorn/prefer-global-this": "off", + "unicorn/prefer-module": "off", "unicorn/prefer-query-selector": "off", - "unicorn/prefer-set-has": "off", + "unicorn/prefer-spread": "off", + "unicorn/prefer-string-raw": "off", + "unicorn/prefer-string-replace-all": "off", + "unicorn/prefer-structured-clone": "off", "unicorn/prevent-abbreviations": "off" - } + }, + "overrides": [ + { + "files": [ + "build/**" + ], + "env": { + "browser": false, + "node": true + }, + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-console": "off", + "unicorn/prefer-top-level-await": "off" + } + }, + { + "files": [ + "js/**" + ], + "parserOptions": { + "sourceType": "module" + } + }, + { + "files": [ + "js/tests/*.js", + "js/tests/integration/rollup*.js" + ], + "env": { + "node": true + }, + "parserOptions": { + "sourceType": "script" + } + }, + { + "files": [ + "js/tests/unit/**" + ], + "env": { + "jasmine": true + }, + "rules": { + "no-console": "off", + "unicorn/consistent-function-scoping": "off", + "unicorn/no-useless-undefined": "off", + "unicorn/prefer-add-event-listener": "off" + } + }, + { + "files": [ + "js/tests/visual/**" + ], + "plugins": [ + "html" + ], + "settings": { + "html/html-extensions": [ + ".html" + ] + }, + "rules": { + "no-console": "off", + "no-new": "off", + "unicorn/no-array-for-each": "off" + } + }, + { + "files": [ + "scss/tests/**" + ], + "env": { + "node": true + }, + "parserOptions": { + "sourceType": "script" + } + }, + { + "files": [ + "site/**" + ], + "env": { + "browser": true, + "node": false + }, + "parserOptions": { + "sourceType": "script", + "ecmaVersion": 2019 + }, + "rules": { + "no-new": "off", + "unicorn/no-array-for-each": "off" + } + }, + { + "files": [ + "site/src/assets/application.js", + "site/src/assets/partials/*.js", + "site/src/assets/search.js", + "site/src/assets/snippets.js", + "site/src/assets/stackblitz.js", + "site/src/plugins/*.js" + ], + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2020 + } + }, + { + "files": [ + "**/*.md" + ], + "plugins": [ + "markdown" + ], + "processor": "markdown/markdown" + }, + { + "files": [ + "**/*.md/*.js", + "**/*.md/*.mjs" + ], + "extends": "plugin:markdown/recommended-legacy", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "unicorn/prefer-node-protocol": "off" + } + } + ] } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 34da5de09133..73b21ac047f3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Bootstrap -Looking to contribute something to Bootstrap? **Here's how you can help.** +Looking to contribute something to Bootstrap? **Here’s how you can help.** Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. @@ -18,24 +18,26 @@ the preferred channel for [bug reports](#bug-reports), [features requests](#feat and [submitting pull requests](#pull-requests), but please respect the following restrictions: -* Please **do not** use the issue tracker for personal support requests. Stack - Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), - [Slack](https://bootstrap-slack.herokuapp.com/) or [IRC](README.md#community) are better places to get help. +- Please **do not** use the issue tracker for personal support requests. Stack Overflow ([`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag), [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions) or [IRC](/README.md#community) are better places to get help. -* Please **do not** derail or troll issues. Keep the discussion on topic and +- Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. -* Please **do not** post comments consisting solely of "+1" or ":thumbsup:". - Use [GitHub's "reactions" feature](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) +- Please **do not** post comments consisting solely of "+1" or ":thumbsup:". + Use [GitHub's "reactions" feature](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) instead. We reserve the right to delete comments which violate this rule. -* Please **do not** open issues regarding the official themes offered on . - Instead, please email any questions or feedback regarding those themes to `themes AT getbootstrap DOT com`. + +## Issues assignment + +The core team will be looking at the open issues, analyze them, and provide guidance on how to proceed. **Issues won’t be assigned to anyone outside the core team.** However, contributors are welcome to participate in the discussion and provide their input on how to best solve the issue, and even submit a PR if they want to. Please wait that the issue is ready to be worked on before submitting a PR, we don’t want to waste your time. + +Please keep in mind that the core team is small, has limited resources and that we are not always able to respond immediately. We will try to provide feedback as soon as possible, but please be patient. If you don’t get a response immediately, it doesn’t mean that we are ignoring you or that we don’t care about your issue or PR. We will get back to you as soon as we can. ## Issues and labels -Our bug tracker utilizes several labels to help organize and identify issues. Here's what they represent and how we use them: +Our bug tracker utilizes several labels to help organize and identify issues. Here’s what they represent and how we use them: - `browser bug` - Issues that are reported to us, but actually are the result of a browser-specific bug. These are diagnosed with reduced test cases and result in an issue opened on that browser's own bug tracker. - `confirmed` - Issues that have been confirmed with a reduced test case and identify a bug in Bootstrap. @@ -59,7 +61,7 @@ Good bug reports are extremely helpful, so thanks! Guidelines for bug reports: 0. **[Validate your HTML](https://html5.validator.nu/)** to ensure your - problem isn't caused by a simple error in your own code. + problem isn’t caused by a simple error in your own code. 1. **Use the GitHub issue search** — check if the issue has already been reported. @@ -69,10 +71,10 @@ Guidelines for bug reports: 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example. - [This JS Bin](https://jsbin.com/lolome/edit?html,output) is a helpful template. + These [v4 CodePen](https://codepen.io/team/bootstrap/pen/yLabNQL) and [v5 CodePen](https://codepen.io/team/bootstrap/pen/qBamdLj) are helpful templates. -A good bug report shouldn't leave others needing to chase you up for more +A good bug report shouldn’t leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS experience the problem? Do other browsers show the bug differently? What @@ -101,19 +103,19 @@ Example: Sometimes bugs reported to us are actually caused by bugs in the browser(s) themselves, not bugs in Bootstrap per se. -| Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes | -| ------------- | ---------------------------- | ---------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| Mozilla | Firefox | Gecko | https://bugzilla.mozilla.org/enter_bug.cgi | "Core" is normally the right product option to choose. | -| Apple | Safari | WebKit | https://bugs.webkit.org/enter_bug.cgi?product=WebKit
https://bugreport.apple.com/ | In Apple's bug reporter, choose "Safari" as the product. | -| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | https://bugs.chromium.org/p/chromium/issues/list | Click the "New issue" button. | -| Microsoft | Edge | EdgeHTML | https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/ | | +| Vendor(s) | Browser(s) | Rendering engine | Bug reporting website(s) | Notes | +| ------------- | ---------------------------- | ---------------- | ------------------------------------------------------ | -------------------------------------------------------- | +| Mozilla | Firefox | Gecko | | "Core" is normally the right product option to choose. | +| Apple | Safari | WebKit | | In Apple’s bug reporter, choose "Safari" as the product. | +| Google, Opera | Chrome, Chromium, Opera v15+ | Blink | | Click the "New issue" button. | +| Microsoft | Edge | Blink | | Go to "Help > Send Feedback" from the browser | ## Feature requests Feature requests are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong -case to convince the project's developers of the merits of this feature. Please +fits with the scope and aims of the project. It’s up to _you_ to make a strong +case to convince the project’s developers of the merits of this feature. Please provide as much detail and context as possible. @@ -123,21 +125,23 @@ Good pull requests—patches, improvements, new features—are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. -**Please ask first** before embarking on any significant pull request (e.g. +**Please ask first** before embarking on any **significant** pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the -project's developers might not want to merge into the project. +project’s developers might not want to merge into the project. For trivial +things, or things that don’t require a lot of your time, you can go ahead and +make a PR. Please adhere to the [coding guidelines](#code-guidelines) used throughout the project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). -**Do not edit `bootstrap.css`, or `bootstrap.js` -directly!** Those files are automatically generated. You should edit the -source files in [`/bootstrap/scss/`](https://github.com/twbs/bootstrap/tree/main/scss) +**Do not edit `bootstrap.css` or `bootstrap.js`, and do not commit +any dist files (`dist/` or `js/dist`).** Those files are automatically generated by our build tools. You should +edit the source files in [`/bootstrap/scss/`](https://github.com/twbs/bootstrap/tree/main/scss) and/or [`/bootstrap/js/src/`](https://github.com/twbs/bootstrap/tree/main/js/src) instead. -Similarly, when contributing to Bootstrap's documentation, you should edit the +Similarly, when contributing to Bootstrap’s documentation, you should edit the documentation source files in [the `/bootstrap/site/content/docs/` directory of the `main` branch](https://github.com/twbs/bootstrap/tree/main/site/content/docs). **Do not edit the `gh-pages` branch.** That branch is generated from the @@ -165,33 +169,47 @@ included in the project: git pull upstream main ``` -3. Create a new topic branch (off the main project development branch) to +3. Install or update project dependencies with npm: + + ```bash + npm install + ``` + +4. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix: ```bash git checkout -b ``` -4. Commit your changes in logical chunks. Please adhere to these [git commit +5. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) - or your code is unlikely be merged into the main project. Use Git's + or your code is unlikely be merged into the main project. Use Git’s [interactive rebase](https://help.github.com/articles/about-git-rebase/) feature to tidy up your commits before making them public. -5. Locally merge (or rebase) the upstream development branch into your topic branch: +6. Ensure your changes compile the dist CSS and JS files in the `dist/` directory. Verify + the build succeeds locally without errors. + + ```bash + npm run dist + ``` + +7. Locally merge (or rebase) the upstream development branch into your topic branch: ```bash git pull [--rebase] upstream main ``` -6. Push your topic branch up to your fork: +8. Commit your changes, but **do not push compiled CSS and JS files in `dist` and `js/dist`**. + Push your topic branch up to your fork: ```bash git push origin ``` -7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/) - with a clear title and description against the `main` branch. +9. [Open a pull request](https://help.github.com/articles/about-pull-requests/) + with a clear title and description against the `main` branch. **IMPORTANT**: By submitting a patch, you agree to allow the project owners to license your work under the terms of the [MIT License](../LICENSE) (if it @@ -207,15 +225,15 @@ includes code changes) and under the terms of the [Adhere to the Code Guide.](https://codeguide.co/#html) - Use tags and elements appropriate for an HTML5 doctype (e.g., self-closing tags). -- Use CDNs and HTTPS for third-party JS when possible. We don't use protocol-relative URLs in this case because they break when viewing the page locally via `file://`. +- Use CDNs and HTTPS for third-party JS when possible. We don’t use protocol-relative URLs in this case because they break when viewing the page locally via `file://`. - Use [WAI-ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes in documentation examples to promote accessibility. ### CSS [Adhere to the Code Guide.](https://codeguide.co/#css) -- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG20/#visual-audio-contrast). -- Except in rare cases, don't remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details. +- When feasible, default color palettes should comply with [WCAG color contrast guidelines](https://www.w3.org/TR/WCAG/#distinguishable). +- Except in rare cases, don’t remove default `:focus` styles (via e.g. `outline: none;`) without providing alternative styles. See [this A11Y Project post](https://www.a11yproject.com/posts/2013-01-25-never-remove-css-outlines/) for more details. ### JS @@ -234,4 +252,4 @@ Run `npm run test` before committing to ensure your changes follow our coding st By contributing your code, you agree to license your contribution under the [MIT License](../LICENSE). By contributing to the documentation, you agree to license your contribution under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). -Prior to v3.1.0, Bootstrap's code was released under the Apache License v2.0. +Prior to v3.1.0, Bootstrap’s code was released under the Apache License v2.0. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 43ea984bab4f..000000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -open_collective: bootstrap diff --git a/.github/INCIDENT_RESPONSE.md b/.github/INCIDENT_RESPONSE.md new file mode 100644 index 000000000000..e203a4e5527a --- /dev/null +++ b/.github/INCIDENT_RESPONSE.md @@ -0,0 +1,162 @@ +# Incident response plan + +This document describes how the Bootstrap maintainers respond to and manage security or operational incidents affecting the project, its website, or its distributed releases. This plan is public to promote transparency and community trust. Operational details (e.g., private contacts, credentials, or internal coordination tools) are maintained separately in the maintainers’ private documentation. + +--- + +## 1. Purpose & Scope + +This plan defines how Bootstrap maintainers will: + +- Identify, triage, and manage security or integrity incidents affecting project code, releases, or infrastructure. +- Communicate with the community and downstream consumers during and after an incident. +- Record lessons learned and update processes to reduce future risk. + +It applies to: + +- The Bootstrap source code, documentation, and build pipelines. +- Release artifacts (npm, CDN, GitHub releases). +- The main website ([https://getbootstrap.com](https://getbootstrap.com)). +- Any official Bootstrap GitHub organization infrastructure. + +It does **not** cover unrelated third-party forks or integrations. + +--- + +## 2. Definitions + +- **Incident**: Any event that could compromise the confidentiality, integrity, or availability of Bootstrap code, releases, or users. Examples include: + - A discovered security vulnerability. + - A compromised GitHub account or CI/CD token. + - A malicious dependency or injected code in a release. + - Website defacement or unauthorized modification of documentation. + - Leaked secrets related to the project infrastructure. + +- **Incident Commander (IC)**: The maintainer responsible for coordinating the overall response. + +--- + +## 3. Roles & Responsibilities + +| Role | Responsibilities | +|------|-------------------| +| **Incident Commander (IC)** | Coordinate the response, assign tasks, ensure timely communication. | +| **Security Maintainers** | Triage reported vulnerabilities, assess impact, create fixes, handle embargoes. | +| **Infrastructure Lead** | Manage CI/CD, website, and release infrastructure. | +| **Communications Lead** | Manage public announcements, blog posts, and social updates. | +| **Contributors & Community** | Promptly report suspected security issues and follow responsible disclosure guidelines. | + +In practice, Bootstrap’s core team fulfills these roles collectively, assigning an IC on a per-incident basis. + +--- + +## 4. Incident workflow + +### 4.1 Detection & Reporting + +- All security issues should be **privately reported** via the contact method in [`SECURITY.md`](../SECURITY.md) or through GitHub’s Security Advisory mechanism. +- Maintainers also monitor: + - Automated dependency scanners (e.g., Dependabot, npm audit). + - GitHub notifications and vulnerability alerts. + - Community channels for suspicious activity. + +### 4.2 Initial triage + +Upon receiving a report: + +1. A maintainer acknowledges receipt within 3 business days (or sooner, when possible). + Bootstrap is maintained by a small volunteer team; response times may vary slightly outside normal working hours. +2. The IC assesses severity and impact: + - **Critical:** immediate compromise of release infrastructure or code integrity. + - **High:** exploitable vulnerability in distributed assets. + - **Medium:** minor vulnerability or low-likelihood attack vector. + - **Low:** informational, no direct risk. +3. If confirmed as an incident, the IC opens a private coordination channel for maintainers and begins containment. + +### 4.3 Containment & Eradication + +- Revoke or rotate any affected credentials. +- Disable compromised infrastructure or build pipelines if necessary. +- Patch affected branches or dependencies. +- Verify integrity of artifacts and releases. + +### 4.4 Communication + +- Keep the reporting party informed (when applicable). +- For major incidents, the Communications Lead drafts a public advisory describing: + - What happened + - What was impacted + - How users can verify or mitigate + - What actions were taken +- Communications occur after containment to avoid amplifying risk. + +Public disclosures are posted via: + +- GitHub Security Advisory if appropriate +- [blog.getbootstrap.com/](https://blog.getbootstrap.com/) +- [Bootstrap GitHub discussions](https://github.com/orgs/twbs/discussions) +- [@getbootstrap](https://x.com/getbootstrap) on X (formerly Twitter) for critical security notices. + +### 4.5 Recovery + +- Validate all systems and releases are secure. +- Resume normal operations. +- Tag patched releases and notify affected users. + +### 4.6 Post-incident review + +Within two weeks after resolution: + +- Conduct an internal debrief. +- Record: + - Root cause + - What worked / what didn’t + - Remediation steps + - Documentation or automation updates needed +- Summarize lessons learned in the private maintainers’ wiki (with optional public summary if appropriate). + +--- + +## 5. Severity levels & Response targets + +| Severity | Example | Target response (volunteer team) | +|-----------|----------|----------------------------------| +| **Critical** | Compromised release, stolen signing keys | Acknowledge ≤ 24h (best effort), containment ≤ 48h, fix ideally ≤ 14d | +| **High** | Vulnerability enabling arbitrary code execution | Acknowledge ≤ 3 business days, fix ideally ≤ 14–21d | +| **Medium** | XSS or content injection on docs site | Acknowledge ≤ 5 business days, fix in next release cycle | +| **Low** | Minor issue with limited risk | Acknowledge ≤ 7 business days, fix as scheduled | + +**Note:** Timelines represent good-faith targets for a small volunteer core team, not hard SLAs. The maintainers will always prioritize public safety and transparency, even if timing varies. + +--- + +## 6. Public disclosure principles + +Bootstrap follows a responsible disclosure approach: + +- Work privately with reporters and affected parties before publishing details. +- Never name reporters without consent. +- Coordinate embargo periods with downstream consumers when needed. +- Publish advisories only after patches or mitigations are available. + +--- + +## 7. Communication Channels + +| Purpose | Channel | +|----------|----------| +| Private reporting | Email address in [`SECURITY.md`](../SECURITY.md) or GitHub advisory form | +| General updates | [blog.getbootstrap.com/](https://blog.getbootstrap.com/) blog | +| Security advisories | GitHub Security Advisory dashboard | +| Social alerts | [@getbootstrap](https://x.com/getbootstrap) | +| GitHub discussion alerts | [github.com/orgs/twbs/discussions](https://github.com/orgs/twbs/discussions) | + +--- + +## 8. Plan Maintenance + +This plan is reviewed at least annually or after any major incident. Changes are approved by the Core Team and recorded in Git history. + +--- + +_The Bootstrap maintainers are committed to transparency, user trust, and continuous improvement in our security and response practices._ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 70dcfd53281a..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Bug report -about: Tell us about a bug you may have identified in Bootstrap. -title: '' -labels: '' -assignees: '' - ---- - -Before opening: - -- [Search for duplicate or closed issues](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) -- [Validate](https://html5.validator.nu/) any HTML to avoid common problems -- Read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) - -Bug reports must include: - -- Operating system and version (Windows, macOS, Android, iOS) -- Browser and version (Chrome, Firefox, Safari, Microsoft Edge, Opera, Android Browser) -- A [reduced test case](https://css-tricks.com/reduced-test-cases/) or suggested fix using [CodePen](https://codepen.io/) or [JS Bin](https://jsbin.com/) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..3e3d6b9e55f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,62 @@ +name: Report a bug +description: Tell us about a bug or issue you may have identified in Bootstrap. +title: "Provide a general summary of the issue" +labels: [bug] +assignees: "-" +body: + - type: checkboxes + attributes: + label: Prerequisites + description: Take a couple minutes to help our maintainers work faster. + options: + - label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed issues + required: true + - label: I have [validated](https://html5.validator.nu/) any HTML to avoid common problems + required: true + - label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) + required: true + - type: textarea + id: what-happened + attributes: + label: Describe the issue + description: Provide a summary of the issue and what you expected to happen, including specific steps to reproduce. + validations: + required: true + - type: textarea + id: reduced-test-case + attributes: + label: Reduced test cases + description: Include links [reduced test case](https://css-tricks.com/reduced-test-cases/) links or suggested fixes using CodePen ([v4 template](https://codepen.io/team/bootstrap/pen/yLabNQL) or [v5 template](https://codepen.io/team/bootstrap/pen/qBamdLj)). + validations: + required: true + - type: dropdown + id: os + attributes: + label: What operating system(s) are you seeing the problem on? + multiple: true + options: + - Windows + - macOS + - Android + - iOS + - Linux + validations: + required: true + - type: dropdown + id: browser + attributes: + label: What browser(s) are you seeing the problem on? + multiple: true + options: + - Chrome + - Safari + - Firefox + - Microsoft Edge + - Opera + - type: input + id: version + attributes: + label: What version of Bootstrap are you using? + placeholder: "e.g., v5.1.0 or v4.5.2" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2913b45f02ed..f1520711335c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ contact_links: - - name: Ask a question + - name: Ask the community url: https://github.com/twbs/bootstrap/discussions/new - about: Ask and discuss questions with other Bootstrap community members + about: Ask and discuss questions with other Bootstrap community members. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 422fa2bb4c3c..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for a new feature in Bootstrap. -title: '' -labels: feature -assignees: '' - ---- - -Before opening: - -- [Search for duplicate or closed issues](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) -- Read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) - -Feature requests must include: - -- As much detail as possible for what we should add and why it's important to Bootstrap -- Relevant links to prior art, screenshots, or live demos whenever possible diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..4b757b1d6753 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Suggest new or updated features to include in Bootstrap. +title: "Suggest a new feature" +labels: [feature] +assignees: [] +body: + - type: checkboxes + attributes: + label: Prerequisites + description: Take a couple minutes to help our maintainers work faster. + options: + - label: I have [searched](https://github.com/twbs/bootstrap/issues?utf8=%E2%9C%93&q=is%3Aissue) for duplicate or closed feature requests + required: true + - label: I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Provide detailed information for what we should add, including relevant links to prior art, screenshots, or live demos whenever possible. + validations: + required: true + - type: textarea + id: motivation + attributes: + label: Motivation and context + description: Tell us why this change is needed or helpful, and what problems it may help solve. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..04df74f36a56 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,38 @@ +### Description + + + +### Motivation & Context + + + +### Type of changes + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Refactoring (non-breaking change) +- [ ] Breaking change (fix or feature that would change existing functionality) + +### Checklist + + + + +- [ ] I have read the [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md) +- [ ] My code follows the code style of the project _(using `npm run lint`)_ +- [ ] My change introduces changes to the documentation +- [ ] I have updated the documentation accordingly +- [ ] I have added tests to cover my changes +- [ ] All new and existing tests passed + +#### Live previews + + + +- + +### Related issues + + diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index c30bed3b4511..26b3be42c5b5 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -6,6 +6,6 @@ See the [contributing guidelines](CONTRIBUTING.md) for sharing bug reports. For general troubleshooting or help getting started: -- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). -- Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. +- Ask and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions). +- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel. - Ask and explore Stack Overflow with the [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5) tag. diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000000..957877282f68 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,3 @@ +name: "CodeQL config" +paths-ignore: + - dist diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 31976021e8af..7033bcfb2890 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,98 @@ version: 2 updates: - - package-ecosystem: npm + - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly day: tuesday time: "12:00" timezone: Europe/Athens - open-pull-requests-limit: 10 - reviewers: - - XhmikosR + cooldown: + default-days: 7 + groups: + github-actions: + patterns: + - "*" + - package-ecosystem: npm + directory: "/" labels: - dependencies - v5 + schedule: + interval: weekly + day: tuesday + time: "12:00" + timezone: Europe/Athens + cooldown: + default-days: 7 versioning-strategy: increase rebase-strategy: disabled + ignore: + - dependency-name: "@astrojs/mdx" + update-types: + - "version-update:semver-major" + - dependency-name: "@babel/cli" + update-types: + - "version-update:semver-major" + - dependency-name: "@babel/core" + update-types: + - "version-update:semver-major" + - dependency-name: "@babel/preset-env" + update-types: + - "version-update:semver-major" + - dependency-name: "@docsearch/js" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "astro" + update-types: + - "version-update:semver-major" + - dependency-name: "eslint" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "eslint-config-xo" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "eslint-plugin-unicorn" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "hammer-simulator" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "jasmine" + update-types: + - "version-update:semver-major" + - dependency-name: "jquery" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "karma-browserstack-launcher" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "karma-rollup-preprocessor" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "sass" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "stylelint" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - dependency-name: "vnu-jar" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - "version-update:semver-patch" + groups: + production-dependencies: + dependency-type: "production" + development-dependencies: + dependency-type: "development" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ae1367c89296..0289984bec06 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -4,6 +4,12 @@ prerelease: true exclude-labels: - 'skip-changelog' categories: + - title: '❗ Breaking Changes' + labels: + - 'breaking-change' + - title: '🚀 Highlights' + labels: + - 'release-highlight' - title: '🚀 Features' labels: - 'new-feature' @@ -14,6 +20,9 @@ categories: - 'fix' - 'bugfix' - 'bug' + - title: '⚡ Performance improvements' + labels: + - 'performance' - title: '🎨 CSS' labels: - 'css' @@ -23,14 +32,25 @@ categories: - title: '📖 Docs' labels: - 'docs' + - title: '🛠 Examples' + labels: + - 'examples' - title: '🌎 Accessibility' labels: - 'accessibility' + - title: '🔧 Utility API' + labels: + - 'utility API' + - 'utilities' + - title: '🏭 Tests' + labels: + - 'tests' - title: '🧰 Misc' labels: - 'build' - 'meta' - 'chore' + - 'CI' - title: '📦 Dependencies' labels: - 'dependencies' diff --git a/.github/workflows/browserstack.yml b/.github/workflows/browserstack.yml index f709b98da4f6..80a695fb90a0 100644 --- a/.github/workflows/browserstack.yml +++ b/.github/workflows/browserstack.yml @@ -1,31 +1,35 @@ name: BrowserStack -on: [push] + +on: + push: + branches: + - "**" + - "!dependabot/**" + workflow_dispatch: + env: - CI: true - NODE: 12.x + FORCE_COLOR: 2 + +permissions: + contents: read jobs: browserstack: runs-on: ubuntu-latest if: github.repository == 'twbs/bootstrap' + timeout-minutes: 30 steps: - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: "${{ env.NODE }}" + persist-credentials: false - - name: Set up npm cache - uses: actions/cache@v2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ env.NODE }}- + node-version-file: ".nvmrc" + cache: npm - name: Install npm dependencies run: npm ci @@ -38,3 +42,4 @@ jobs: env: BROWSER_STACK_ACCESS_KEY: "${{ secrets.BROWSER_STACK_ACCESS_KEY }}" BROWSER_STACK_USERNAME: "${{ secrets.BROWSER_STACK_USERNAME }}" + GITHUB_SHA: "${{ github.sha }}" diff --git a/.github/workflows/bundlewatch.yml b/.github/workflows/bundlewatch.yml index f09cc9667df0..fe367dcf48ef 100644 --- a/.github/workflows/bundlewatch.yml +++ b/.github/workflows/bundlewatch.yml @@ -1,8 +1,17 @@ name: Bundlewatch -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + env: - CI: true - NODE: 12.x + FORCE_COLOR: 2 + +permissions: + contents: read jobs: bundlewatch: @@ -10,21 +19,15 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: "${{ env.NODE }}" + persist-credentials: false - - name: Set up npm cache - uses: actions/cache@v2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ env.NODE }}- + node-version-file: ".nvmrc" + cache: npm - name: Install npm dependencies run: npm ci diff --git a/.github/workflows/calibreapp-image-actions.yml b/.github/workflows/calibreapp-image-actions.yml new file mode 100644 index 000000000000..09b984da3912 --- /dev/null +++ b/.github/workflows/calibreapp-image-actions.yml @@ -0,0 +1,32 @@ +name: Compress Images + +on: + pull_request: + paths: + - '**.jpg' + - '**.jpeg' + - '**.png' + - '**.webp' + +permissions: + contents: read + +jobs: + build: + # Only run on Pull Requests within the same repository, and not from forks. + if: github.event.pull_request.head.repo.full_name == github.repository + name: calibreapp/image-actions + runs-on: ubuntu-latest + permissions: + # allow calibreapp/image-actions to update PRs + pull-requests: write + steps: + - name: Clone repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Compress Images + uses: calibreapp/image-actions@9d037c06280028c110ff61c433ad4dc7d33c3c43 # v1.5.0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 777d812d6d2d..d073c76fe191 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,25 +1,44 @@ -name: "Code Scanning - Action" +name: "CodeQL" on: push: + branches: + - main + - v4-dev + - "!dependabot/**" + pull_request: + branches: + - main + - v4-dev + - "!dependabot/**" schedule: - - cron: "0 0 * * 0" + - cron: "0 2 * * 4" + workflow_dispatch: jobs: - CodeQL-Build: + analyze: + name: Analyze runs-on: ubuntu-latest + permissions: + security-events: write steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: - languages: javascript + config-file: ./.github/codeql/codeql-config.yml + languages: "javascript" + queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + category: "/language:javascript" diff --git a/.github/workflows/cspell.yml b/.github/workflows/cspell.yml new file mode 100644 index 000000000000..8d474503be47 --- /dev/null +++ b/.github/workflows/cspell.yml @@ -0,0 +1,38 @@ +name: cspell + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + FORCE_COLOR: 2 + +permissions: + contents: read + +jobs: + cspell: + permissions: + # allow streetsidesoftware/cspell-action to fetch files for commits and PRs + contents: read + pull-requests: read + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run cspell + uses: streetsidesoftware/cspell-action@e0668cf020899e887ee8ad4d173c31738a79eae8 # v9.0.1 + with: + config: ".cspell.json" + files: | + .gitignore + **/*.{md,mdx,yml,yaml,json,js,cjs,mjs,ts} + inline: error + incremental_files_only: false diff --git a/.github/workflows/css.yml b/.github/workflows/css.yml index 390bffc966e9..59e83019308a 100644 --- a/.github/workflows/css.yml +++ b/.github/workflows/css.yml @@ -1,8 +1,17 @@ name: CSS -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + env: - CI: true - NODE: 12.x + FORCE_COLOR: 2 + +permissions: + contents: read jobs: css: @@ -10,24 +19,21 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: "${{ env.NODE }}" + persist-credentials: false - - name: Set up npm cache - uses: actions/cache@v2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ env.NODE }}- + node-version-file: ".nvmrc" + cache: npm - name: Install npm dependencies run: npm ci - name: Build CSS run: npm run css + + - name: Run CSS tests + run: npm run css-test diff --git a/.github/workflows/dart-sass.yml b/.github/workflows/dart-sass.yml deleted file mode 100644 index 8482a152ef56..000000000000 --- a/.github/workflows/dart-sass.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CSS (Dart Sass) -on: [push, pull_request] -env: - CI: true - NODE: 12.x - -jobs: - css: - runs-on: ubuntu-latest - - steps: - - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 - with: - node-version: "${{ env.NODE }}" - - - name: Build CSS with Dart Sass - run: | - npx --package sass@latest sass --version - npx --package sass@latest sass --style expanded --source-map --embed-sources --no-error-css scss/:dist-sass/css/ - ls -Al dist-sass/css diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 652e27a7d0a2..37616f46a15b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,17 @@ name: Docs -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + env: - CI: true - NODE: 12.x + FORCE_COLOR: 2 + +permissions: + contents: read jobs: docs: @@ -10,26 +19,23 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - - name: Set Node.js version - uses: actions/setup-node@v1 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "${{ env.NODE }}" + node-version-file: ".nvmrc" + cache: npm - run: java -version - - name: Set up npm cache - uses: actions/cache@v2 - with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ env.NODE }}- - - name: Install npm dependencies run: npm ci - - name: Test docs - run: npm run docs + - name: Build docs + run: npm run docs-build + + - name: Validate HTML + run: npm run docs-vnu diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml new file mode 100644 index 000000000000..35fbed0e6944 --- /dev/null +++ b/.github/workflows/issue-close-require.yml @@ -0,0 +1,29 @@ +name: Close Issue Awaiting Reply + +on: + schedule: + - cron: "0 0 * * *" + +permissions: + contents: read + +jobs: + issue-close-require: + permissions: + issues: write + pull-requests: write + runs-on: ubuntu-latest + if: github.repository == 'twbs/bootstrap' + steps: + - name: awaiting reply + uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 + with: + only-issue-labels: 'awaiting-reply' + days-before-stale: 14 + days-before-close: 0 + days-before-pr-stale: -1 + days-before-pr-close: -1 + stale-issue-message: '' + close-issue-message: | + As the issue was labeled with `awaiting-reply`, but there has been no response in 14 days, this issue will be closed. If you have any questions, you can comment/reply. + close-issue-reason: not_planned diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml new file mode 100644 index 000000000000..fd781e75820d --- /dev/null +++ b/.github/workflows/issue-labeled.yml @@ -0,0 +1,27 @@ +name: Issue Labeled + +on: + issues: + types: [labeled] + +permissions: + contents: read + +jobs: + issue-labeled: + permissions: + issues: write + if: github.repository == 'twbs/bootstrap' + runs-on: ubuntu-latest + steps: + - name: awaiting reply + if: github.event.label.name == 'needs-example' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `Hello @${context.payload.issue.user.login}. Bug reports must include a **live demo** of the issue. Per our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md), please create a reduced test case on [CodePen](https://codepen.io/) or [StackBlitz](https://stackblitz.com/) and report back with your link, Bootstrap version, and specific browser and Operating System details.` + }) diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 543172ccb2fe..44322c34343d 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -1,35 +1,38 @@ name: JS Tests -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + env: - CI: true + FORCE_COLOR: 2 + +permissions: + contents: read jobs: run: - name: Node ${{ matrix.node }} + permissions: + # allow coverallsapp/github-action to create new checks issues and fetch code + checks: write + contents: read + name: JS Tests runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - node: [10, 12] - steps: - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: ${{ matrix.node }} + persist-credentials: false - - name: Set up npm cache - uses: actions/cache@v2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }}} - restore-keys: | - ${{ runner.OS }}-node-v${{ matrix.node }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ matrix.node }}- + node-version-file: ".nvmrc" + cache: npm - name: Install npm dependencies run: npm ci @@ -41,8 +44,8 @@ jobs: run: npm run js-test - name: Run Coveralls - uses: coverallsapp/github-action@master - if: matrix.node == 12 + uses: coverallsapp/github-action@8d6379e14d29928660c4ba802d8e85393440b329 # v2.3.8 + if: ${{ !github.event.repository.fork }} with: github-token: "${{ secrets.GITHUB_TOKEN }}" path-to-lcov: "./js/coverage/lcov.info" diff --git a/.github/workflows/linkinator.yml b/.github/workflows/linkinator.yml new file mode 100644 index 000000000000..e78fc9d5ca07 --- /dev/null +++ b/.github/workflows/linkinator.yml @@ -0,0 +1,47 @@ +name: Linkinator + +on: + schedule: + - cron: "0 4 1 * *" + workflow_dispatch: + +env: + FORCE_COLOR: 2 + +permissions: + contents: read + +jobs: + linkinator: + runs-on: ubuntu-latest + if: github.repository == 'twbs/bootstrap' + + steps: + - name: Clone repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: ".nvmrc" + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Build docs + run: npm run docs-build + + - name: Run linkinator + uses: JustinBeckwith/linkinator-action@36a0bfbfecd5e237e8f8531537a693616c505faf # v2.4.5 + with: + paths: _site + recurse: true + verbosity: error + skip: >- + ^http://localhost + ^https://github.com/floating-ui/popper-docs/blob/main/docs/v2/ + ^https://www.reddit.com + ^https://medium.com diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f3d00d69ff97..4b1d1a0475b9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,8 +1,17 @@ name: Lint -on: [push, pull_request] + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + env: - CI: true - NODE: 12.x + FORCE_COLOR: 2 + +permissions: + contents: read jobs: lint: @@ -10,21 +19,15 @@ jobs: steps: - name: Clone repository - uses: actions/checkout@v2 - - - name: Set Node.js version - uses: actions/setup-node@v1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: "${{ env.NODE }}" + persist-credentials: false - - name: Set up npm cache - uses: actions/cache@v2 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - path: ~/.npm - key: ${{ runner.os }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.OS }}-node-v${{ env.NODE }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} - ${{ runner.OS }}-node-v${{ env.NODE }}- + node-version-file: ".nvmrc" + cache: npm - name: Install npm dependencies run: npm ci diff --git a/.github/workflows/node-sass.yml b/.github/workflows/node-sass.yml new file mode 100644 index 000000000000..20317b08e3b4 --- /dev/null +++ b/.github/workflows/node-sass.yml @@ -0,0 +1,48 @@ +name: CSS (node-sass) + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + FORCE_COLOR: 2 + +permissions: + contents: read + +jobs: + css: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: ".nvmrc" + + - name: Build CSS with node-sass + run: | + npx --package node-sass@latest node-sass --version + npx --package node-sass@latest node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/ -o dist-sass/css/ + ls -Al dist-sass/css + + - name: Check built CSS files for Sass variables + shell: bash + run: | + SASS_VARS_FOUND=$(find "dist-sass/css/" -type f -name "*.css" -print0 | xargs -0 --no-run-if-empty grep -F "\$" || true) + if [[ -z "$SASS_VARS_FOUND" ]]; then + echo "All good, no Sass variables found!" + exit 0 + else + echo "Found $(echo "$SASS_VARS_FOUND" | wc -l | bc) Sass variables:" + echo "$SASS_VARS_FOUND" + exit 1 + fi diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 000000000000..2ea6e532d084 --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,34 @@ +name: Publish NuGet Packages + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + package-nuget: + runs-on: windows-latest + if: ${{ github.repository == 'twbs/bootstrap' && startsWith(github.event.release.tag_name, 'v') }} + env: + GITHUB_REF_NAME: ${{ github.ref_name }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up NuGet + uses: nuget/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0 + with: + nuget-api-key: ${{ secrets.NuGetAPIKey }} + nuget-version: '5.x' + + - name: Pack NuGet packages + shell: pwsh + run: | + $bsversion = $env:GITHUB_REF_NAME.Substring(1) + nuget pack "nuget\bootstrap.nuspec" -Verbosity detailed -NonInteractive -BasePath . -Version $bsversion + nuget pack "nuget\bootstrap.sass.nuspec" -Verbosity detailed -NonInteractive -BasePath . -Version $bsversion + nuget push "bootstrap.$bsversion.nupkg" -Verbosity detailed -NonInteractive -Source "https://api.nuget.org/v3/index.json" + nuget push "bootstrap.sass.$bsversion.nupkg" -Verbosity detailed -NonInteractive -Source "https://api.nuget.org/v3/index.json" diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 1c4f4be9b8e3..85034d34534f 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -4,11 +4,20 @@ on: push: branches: - main + workflow_dispatch: + +permissions: + contents: read jobs: update_release_draft: + permissions: + # allow release-drafter/release-drafter to create GitHub releases and add labels to PRs + contents: write + pull-requests: write runs-on: ubuntu-latest + if: github.repository == 'twbs/bootstrap' steps: - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000000..c95511ba0e3a --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,78 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '27 12 * * 2' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 6208c57d7037..235ad54948dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,5 @@ # Ignore docs files -/_gh_pages/ -# This is the old Jekyll docs dist folder; -# keeping it here so that when we switch branches it doesn't show up -/site/docs/**/dist/ -# Jekyll's cache folder; keeping it for the same reason as above -/site/.jekyll-cache/ -# Hugo resources folder -/resources/ - -# Ignore ruby/bundler files; -# keeping them here so that when we switch branches they don't show up -/.bundle/ -/vendor/ -/.ruby-version +/_site/ # Numerous always-ignore extensions *.diff @@ -39,7 +26,6 @@ *.sublime-workspace nbproject Thumbs.db -/.vscode/ # Local Netlify folder .netlify @@ -48,5 +34,12 @@ Thumbs.db *.komodoproject # Folders to ignore +/dist-sass/ /js/coverage/ /node_modules/ + +# Site +/site/dist +/site/node_modules +/site/.astro +/site/public diff --git a/.ncurc.json b/.ncurc.json new file mode 100644 index 000000000000..bbf89d9bbf9c --- /dev/null +++ b/.ncurc.json @@ -0,0 +1,21 @@ +{ + "reject": [ + "@astrojs/mdx", + "@babel/cli", + "@babel/core", + "@babel/preset-env", + "@docsearch/js", + "astro", + "eslint", + "eslint-config-xo", + "eslint-plugin-unicorn", + "hammer-simulator", + "jasmine", + "jquery", + "karma-browserstack-launcher", + "karma-rollup-preprocessor", + "sass", + "stylelint", + "vnu-jar" + ] +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..ca5c350055cc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..32442e87a0db --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Prettier is only used for the website + +site/.astro +site/dist +site/public +site/src/assets +site/src/scss +site/src/pages/**/*.md +site/src/pages/**/*.mdx +site/src/content/**/*.mdx +site/src/layouts/RedirectLayout.astro +site/static diff --git a/.stylelintignore b/.stylelintignore index e42e88938400..b7013de7ef81 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,5 +1,6 @@ **/*.min.css **/dist/ **/vendor/ -/_gh_pages/ +/_site/ +/site/public/ /js/coverage/ diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index 1c9ee181161c..000000000000 --- a/.stylelintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": [ - "stylelint-config-twbs-bootstrap/scss" - ], - "rules": { - "function-disallowed-list": [ - "calc", - "lighten", - "darken" - ], - "property-disallowed-list": [ - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "transition" - ], - "scss/dollar-variable-default": [ - true, - { - "ignore": "local" - } - ], - "scss/selector-no-union-class-name": true - } -} diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000000..589884aae7ab --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,60 @@ +{ + "extends": [ + "stylelint-config-twbs-bootstrap" + ], + "reportInvalidScopeDisables": true, + "reportNeedlessDisables": true, + "overrides": [ + { + "files": "**/*.scss", + "rules": { + "declaration-property-value-disallowed-list": { + "border": "none", + "outline": "none" + }, + "function-disallowed-list": [ + "calc", + "lighten", + "darken" + ], + "property-disallowed-list": [ + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "transition" + ], + "scss/dollar-variable-default": [ + true, + { + "ignore": "local" + } + ], + "scss/selector-no-union-class-name": true + } + }, + { + "files": "scss/**/*.{test,spec}.scss", + "rules": { + "scss/dollar-variable-default": null, + "declaration-no-important": null + } + }, + { + "files": "site/**/*.scss", + "rules": { + "scss/dollar-variable-default": null + } + }, + { + "files": "site/**/examples/**/*.css", + "rules": { + "comment-empty-line-before": null, + "property-no-vendor-prefix": null, + "selector-no-qualifying-type": null, + "value-no-vendor-prefix": null + } + } + ] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..bc5df00e1618 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "astro-build.astro-vscode", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "hossaini.bootstrap-intellisense", + "streetsidesoftware.code-spell-checker", + "stylelint.vscode-stylelint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..9c6ae2d622a3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll.stylelint": "always" + }, + "editor.renderWhitespace": "all", + "scss.validate": false, + "stylelint.enable": true, + "stylelint.validate": ["scss"] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 963db62fe3fb..756298316059 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,45 +2,131 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our +community include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +- Publishing others’ private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mdo@getbootstrap.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mdo@getbootstrap.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. -[homepage]: https://www.contributor-covenant.org/ -[version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/LICENSE b/LICENSE index 173a9ebbbd7a..d27a1619347b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ The MIT License (MIT) -Copyright (c) 2011-2020 Twitter, Inc. -Copyright (c) 2011-2020 The Bootstrap Authors +Copyright (c) 2011-2026 The Bootstrap Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4ce841c2b5c7..24ecdaf9b13e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- - Bootstrap logo + + Bootstrap logo

@@ -9,29 +9,27 @@

Sleek, intuitive, and powerful front-end framework for faster and easier web development.
- Explore Bootstrap docs » + Explore Bootstrap docs »

- Report bug + Report bug · - Request feature - · - Themes + Request feature · Blog

-## Bootstrap 4 +## Bootstrap 5 -Our default branch is for development of our upcoming Bootstrap 5 release. Head to the [`v4-dev` branch](https://github.com/twbs/bootstrap/tree/v4-dev) to view the readme, documentation, and source code for Bootstrap 4. +Our default branch is for development of our Bootstrap 5 release. Head to the [`v4-dev` branch](https://github.com/twbs/bootstrap/tree/v4-dev) to view the readme, documentation, and source code for Bootstrap 4. ## Table of contents - [Quick start](#quick-start) - [Status](#status) -- [What's included](#whats-included) +- [What’s included](#whats-included) - [Bugs and feature requests](#bugs-and-feature-requests) - [Documentation](#documentation) - [Contributing](#contributing) @@ -46,102 +44,119 @@ Our default branch is for development of our upcoming Bootstrap 5 release. Head Several quick start options are available: -- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.0.0-alpha2.zip) +- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.3.8.zip) - Clone the repo: `git clone https://github.com/twbs/bootstrap.git` -- Install with [npm](https://www.npmjs.com/): `npm install bootstrap@next` -- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@next` -- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.0.0-alpha2` +- Install with [npm](https://www.npmjs.com/): `npm install bootstrap@v5.3.8` +- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@v5.3.8` +- Install with [Bun](https://bun.sh/): `bun add bootstrap@v5.3.8` +- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.3.8` - Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass` -Read the [Getting started page](https://v5.getbootstrap.com/docs/5.0/getting-started/introduction/) for information on the framework contents, templates and examples, and more. +Read the [Getting started page](https://getbootstrap.com/docs/5.3/getting-started/introduction/) for information on the framework contents, templates, examples, and more. ## Status -[![Slack](https://bootstrap-slack.herokuapp.com/badge.svg)](https://bootstrap-slack.herokuapp.com/) -[![Build Status](https://github.com/twbs/bootstrap/workflows/JS%20Tests/badge.svg?branch=main)](https://github.com/twbs/bootstrap/actions?query=workflow%3AJS+Tests+branch%3Amain) -[![npm version](https://img.shields.io/npm/v/bootstrap)](https://www.npmjs.com/package/bootstrap) -[![Gem version](https://img.shields.io/gem/v/bootstrap)](https://rubygems.org/gems/bootstrap) -[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue)](https://atmospherejs.com/twbs/bootstrap) -[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap)](https://packagist.org/packages/twbs/bootstrap) -[![NuGet](https://img.shields.io/nuget/vpre/bootstrap)](https://www.nuget.org/packages/bootstrap/absoluteLatest) -[![peerDependencies Status](https://img.shields.io/david/peer/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=peer) -[![devDependency Status](https://img.shields.io/david/dev/twbs/bootstrap)](https://david-dm.org/twbs/bootstrap?type=dev) -[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main)](https://coveralls.io/github/twbs/bootstrap?branch=main) +[![Build Status](https://img.shields.io/github/actions/workflow/status/twbs/bootstrap/js.yml?branch=main&label=JS%20Tests&logo=github)](https://github.com/twbs/bootstrap/actions/workflows/js.yml?query=workflow%3AJS+branch%3Amain) +[![npm version](https://img.shields.io/npm/v/bootstrap?logo=npm&logoColor=fff)](https://www.npmjs.com/package/bootstrap) +[![Gem version](https://img.shields.io/gem/v/bootstrap?logo=rubygems&logoColor=fff)](https://rubygems.org/gems/bootstrap) +[![Meteor Atmosphere](https://img.shields.io/badge/meteor-twbs%3Abootstrap-blue?logo=meteor&logoColor=fff)](https://atmospherejs.com/twbs/bootstrap) +[![Packagist Prerelease](https://img.shields.io/packagist/vpre/twbs/bootstrap?logo=packagist&logoColor=fff)](https://packagist.org/packages/twbs/bootstrap) +[![NuGet](https://img.shields.io/nuget/vpre/bootstrap?logo=nuget&logoColor=fff)](https://www.nuget.org/packages/bootstrap/absoluteLatest) +[![Coverage Status](https://img.shields.io/coveralls/github/twbs/bootstrap/main?logo=coveralls&logoColor=fff)](https://coveralls.io/github/twbs/bootstrap?branch=main) [![CSS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=gzip&label=CSS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css) [![CSS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/css/bootstrap.min.css?compression=brotli&label=CSS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css) [![JS gzip size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js) [![JS Brotli size](https://img.badgesize.io/twbs/bootstrap/main/dist/js/bootstrap.min.js?compression=brotli&label=JS%20Brotli%20size)](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js) -[![BrowserStack Status](https://www.browserstack.com/automate/badge.svg?badge_key=SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229)](https://www.browserstack.com/automate/public-build/SkxZcStBeExEdVJqQ2hWYnlWckpkNmNEY213SFp6WHFETWk2bGFuY3pCbz0tLXhqbHJsVlZhQnRBdEpod3NLSDMzaHc9PQ==--3d0b75245708616eb93113221beece33e680b229) -[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap)](#backers) -[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap)](#sponsors) - - -## What's included - -Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: - -```text -bootstrap/ -└── dist/ - ├── css/ - │ ├── bootstrap-grid.css - │ ├── bootstrap-grid.css.map - │ ├── bootstrap-grid.min.css - │ ├── bootstrap-grid.min.css.map - │ ├── bootstrap-reboot.css - │ ├── bootstrap-reboot.css.map - │ ├── bootstrap-reboot.min.css - │ ├── bootstrap-reboot.min.css.map - │ ├── bootstrap-utilities.css - │ ├── bootstrap-utilities.css.map - │ ├── bootstrap-utilities.min.css - │ ├── bootstrap-utilities.min.css.map - │ ├── bootstrap.css - │ ├── bootstrap.css.map - │ ├── bootstrap.min.css - │ └── bootstrap.min.css.map - └── js/ - ├── bootstrap.bundle.js - ├── bootstrap.bundle.js.map - ├── bootstrap.bundle.min.js - ├── bootstrap.bundle.min.js.map - ├── bootstrap.esm.js - ├── bootstrap.esm.js.map - ├── bootstrap.esm.min.js - ├── bootstrap.esm.min.js.map - ├── bootstrap.js - ├── bootstrap.js.map - ├── bootstrap.min.js - └── bootstrap.min.js.map -``` - -We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps) (`bootstrap.*.map`) are available for use with certain browsers' developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/). +![Open Source Security Foundation Scorecard](https://img.shields.io/ossf-scorecard/github.com/twbs/bootstrap) +[![Backers on Open Collective](https://img.shields.io/opencollective/backers/bootstrap?logo=opencollective&logoColor=fff)](#backers) +[![Sponsors on Open Collective](https://img.shields.io/opencollective/sponsors/bootstrap?logo=opencollective&logoColor=fff)](#sponsors) + + +## What’s included + +Within the download you’ll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. + +
+ Download contents + + ```text + bootstrap/ + ├── css/ + │ ├── bootstrap-grid.css + │ ├── bootstrap-grid.css.map + │ ├── bootstrap-grid.min.css + │ ├── bootstrap-grid.min.css.map + │ ├── bootstrap-grid.rtl.css + │ ├── bootstrap-grid.rtl.css.map + │ ├── bootstrap-grid.rtl.min.css + │ ├── bootstrap-grid.rtl.min.css.map + │ ├── bootstrap-reboot.css + │ ├── bootstrap-reboot.css.map + │ ├── bootstrap-reboot.min.css + │ ├── bootstrap-reboot.min.css.map + │ ├── bootstrap-reboot.rtl.css + │ ├── bootstrap-reboot.rtl.css.map + │ ├── bootstrap-reboot.rtl.min.css + │ ├── bootstrap-reboot.rtl.min.css.map + │ ├── bootstrap-utilities.css + │ ├── bootstrap-utilities.css.map + │ ├── bootstrap-utilities.min.css + │ ├── bootstrap-utilities.min.css.map + │ ├── bootstrap-utilities.rtl.css + │ ├── bootstrap-utilities.rtl.css.map + │ ├── bootstrap-utilities.rtl.min.css + │ ├── bootstrap-utilities.rtl.min.css.map + │ ├── bootstrap.css + │ ├── bootstrap.css.map + │ ├── bootstrap.min.css + │ ├── bootstrap.min.css.map + │ ├── bootstrap.rtl.css + │ ├── bootstrap.rtl.css.map + │ ├── bootstrap.rtl.min.css + │ └── bootstrap.rtl.min.css.map + └── js/ + ├── bootstrap.bundle.js + ├── bootstrap.bundle.js.map + ├── bootstrap.bundle.min.js + ├── bootstrap.bundle.min.js.map + ├── bootstrap.esm.js + ├── bootstrap.esm.js.map + ├── bootstrap.esm.min.js + ├── bootstrap.esm.min.js.map + ├── bootstrap.js + ├── bootstrap.js.map + ├── bootstrap.min.js + └── bootstrap.min.js.map + ``` +
+ +We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [Source maps](https://web.dev/articles/source-maps) (`bootstrap.*.map`) are available for use with certain browsers’ developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/index.md). ## Bugs and feature requests -Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new). +Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new/choose). ## Documentation -Bootstrap's documentation, included in this repo in the root directory, is built with [Hugo](https://gohugo.io/) and publicly hosted on GitHub Pages at . The docs may also be run locally. +Bootstrap’s documentation, included in this repo in the root directory, is built with [Astro](https://astro.build/) and publicly hosted on GitHub Pages at . The docs may also be run locally. -Documentation search is powered by [Algolia's DocSearch](https://community.algolia.com/docsearch/). Working on our search? Be sure to set `debug: true` in `site/assets/js/src/search.js` file. +Documentation search is powered by [Algolia's DocSearch](https://docsearch.algolia.com/). ### Running documentation locally -1. Run `npm install` to install the Node.js dependencies, including Hugo (the site builder). +1. Run `npm install` to install the Node.js dependencies, including Astro (the site builder). 2. Run `npm run test` (or a specific npm script) to rebuild distributed CSS and JavaScript files, as well as our docs assets. 3. From the root `/bootstrap` directory, run `npm run docs-serve` in the command line. -4. Open `http://localhost:9001/` in your browser, and voilà. +4. Open in your browser, and voilà. -Learn more about using Hugo by reading its [documentation](https://gohugo.io/documentation/). +Learn more about using Astro by reading its [documentation](https://docs.astro.build/en/getting-started/). ### Documentation for previous releases -You can find all our previous releases docs on . +You can find all our previous releases docs on . [Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download. @@ -157,12 +172,13 @@ Editor preferences are available in the [editor config](https://github.com/twbs/ ## Community -Get updates on Bootstrap's development and chat with the project maintainers and community members. +Get updates on Bootstrap’s development and chat with the project maintainers and community members. -- Follow [@getbootstrap on Twitter](https://twitter.com/getbootstrap). +- Follow [@getbootstrap on X](https://x.com/getbootstrap). - Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/). -- Join [the official Slack room](https://bootstrap-slack.herokuapp.com/). -- Chat with fellow Bootstrappers in IRC. On the `irc.freenode.net` server, in the `##bootstrap` channel. +- Ask questions and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions). +- Discuss, ask questions, and more on [the community Discord](https://discord.gg/bZUvakRU3M) or [Bootstrap subreddit](https://www.reddit.com/r/bootstrap/). +- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel. - Implementation help may be found at Stack Overflow (tagged [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5)). - Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability. @@ -178,23 +194,29 @@ See [the Releases section of our GitHub project](https://github.com/twbs/bootstr **Mark Otto** -- +- - **Jacob Thornton** -- +- - ## Thanks - BrowserStack Logo + BrowserStack Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers! + + Netlify + + +Thanks to [Netlify](https://www.netlify.com/) for providing us with Deploy Previews! + ## Sponsors @@ -221,4 +243,4 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com ## Copyright and license -Code and documentation copyright 2011–2020 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors) and [Twitter, Inc.](https://twitter.com) Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). +Code and documentation copyright 2011-2026 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors). Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/). diff --git a/SECURITY.md b/SECURITY.md index e79dcd8d2038..63dbdb163991 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,6 @@ The Bootstrap team and community take security issues in Bootstrap seriously. We To report a security issue, email [security@getbootstrap.com](mailto:security@getbootstrap.com) and include the word "SECURITY" in the subject line. -We'll endeavor to respond quickly, and will keep you updated throughout the process. +We'll endeavor to respond quickly, and will keep you updated throughout the process. We aim to acknowledge vulnerability reports within 3 business days and will coordinate disclosure timelines with reporters before publishing any details. + +For details on how we handle incidents, see our [Incident Response Plan](.github/INCIDENT_RESPONSE.md). diff --git a/build/.eslintrc.json b/build/.eslintrc.json deleted file mode 100644 index 679bd26f7ba2..000000000000 --- a/build/.eslintrc.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "env": { - "browser": false, - "node": true - }, - "parserOptions": { - "sourceType": "script" - }, - "extends": "../.eslintrc.json", - "rules": { - "no-console": "off", - "strict": "error" - } -} diff --git a/build/banner.js b/build/banner.mjs similarity index 50% rename from build/banner.js rename to build/banner.mjs index df82ff32edf3..3fea93c8f1d2 100644 --- a/build/banner.js +++ b/build/banner.mjs @@ -1,6 +1,12 @@ -'use strict' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const pkgJson = path.join(__dirname, '../package.json') +const pkg = JSON.parse(await fs.readFile(pkgJson, 'utf8')) -const pkg = require('../package.json') const year = new Date().getFullYear() function getBanner(pluginFilename) { @@ -11,4 +17,4 @@ function getBanner(pluginFilename) { */` } -module.exports = getBanner +export default getBanner diff --git a/build/build-plugins.js b/build/build-plugins.js deleted file mode 100644 index c5c357645bc7..000000000000 --- a/build/build-plugins.js +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to build our plugins to use them separately. - * Copyright 2020 The Bootstrap Authors - * Copyright 2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const path = require('path') -const rollup = require('rollup') -const { babel } = require('@rollup/plugin-babel') -const banner = require('./banner.js') - -const plugins = [ - babel({ - // Only transpile our source code - exclude: 'node_modules/**', - // Inline the required helpers in each file - babelHelpers: 'inline' - }) -] -const bsPlugins = { - Data: path.resolve(__dirname, '../js/src/dom/data.js'), - EventHandler: path.resolve(__dirname, '../js/src/dom/event-handler.js'), - Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'), - Polyfill: path.resolve(__dirname, '../js/src/dom/polyfill.js'), - SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), - Alert: path.resolve(__dirname, '../js/src/alert.js'), - Button: path.resolve(__dirname, '../js/src/button.js'), - Carousel: path.resolve(__dirname, '../js/src/carousel.js'), - Collapse: path.resolve(__dirname, '../js/src/collapse.js'), - Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), - Modal: path.resolve(__dirname, '../js/src/modal.js'), - Popover: path.resolve(__dirname, '../js/src/popover.js'), - ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'), - Tab: path.resolve(__dirname, '../js/src/tab.js'), - Toast: path.resolve(__dirname, '../js/src/toast.js'), - Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') -} -const rootPath = path.resolve(__dirname, '../js/dist/') - -const defaultPluginConfig = { - external: [ - bsPlugins.Data, - bsPlugins.EventHandler, - bsPlugins.SelectorEngine - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.SelectorEngine]: 'SelectorEngine' - } -} - -const getConfigByPluginKey = pluginKey => { - if ( - pluginKey === 'Data' || - pluginKey === 'Manipulator' || - pluginKey === 'EventHandler' || - pluginKey === 'Polyfill' || - pluginKey === 'SelectorEngine' || - pluginKey === 'Util' || - pluginKey === 'Sanitizer' - ) { - return { - external: [bsPlugins.Polyfill], - globals: { - [bsPlugins.Polyfill]: 'Polyfill' - } - } - } - - if (pluginKey === 'Alert' || pluginKey === 'Tab') { - return defaultPluginConfig - } - - if ( - pluginKey === 'Button' || - pluginKey === 'Carousel' || - pluginKey === 'Collapse' || - pluginKey === 'Modal' || - pluginKey === 'ScrollSpy' - ) { - const config = Object.assign(defaultPluginConfig) - config.external.push(bsPlugins.Manipulator) - config.globals[bsPlugins.Manipulator] = 'Manipulator' - return config - } - - if (pluginKey === 'Dropdown' || pluginKey === 'Tooltip') { - const config = Object.assign(defaultPluginConfig) - config.external.push(bsPlugins.Manipulator, 'popper.js') - config.globals[bsPlugins.Manipulator] = 'Manipulator' - config.globals['popper.js'] = 'Popper' - return config - } - - if (pluginKey === 'Popover') { - return { - external: [ - bsPlugins.Data, - bsPlugins.SelectorEngine, - bsPlugins.Tooltip - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.SelectorEngine]: 'SelectorEngine', - [bsPlugins.Tooltip]: 'Tooltip' - } - } - } - - if (pluginKey === 'Toast') { - return { - external: [ - bsPlugins.Data, - bsPlugins.EventHandler, - bsPlugins.Manipulator - ], - globals: { - [bsPlugins.Data]: 'Data', - [bsPlugins.EventHandler]: 'EventHandler', - [bsPlugins.Manipulator]: 'Manipulator' - } - } - } -} - -const utilObjects = [ - 'Util', - 'Sanitizer' -] - -const domObjects = [ - 'Data', - 'EventHandler', - 'Manipulator', - 'Polyfill', - 'SelectorEngine' -] - -const build = async plugin => { - console.log(`Building ${plugin} plugin...`) - - const { external, globals } = getConfigByPluginKey(plugin) - const pluginFilename = path.basename(bsPlugins[plugin]) - let pluginPath = rootPath - - if (utilObjects.includes(plugin)) { - pluginPath = `${rootPath}/util/` - } - - if (domObjects.includes(plugin)) { - pluginPath = `${rootPath}/dom/` - } - - const bundle = await rollup.rollup({ - input: bsPlugins[plugin], - plugins, - external - }) - - await bundle.write({ - banner: banner(pluginFilename), - format: 'umd', - name: plugin, - sourcemap: true, - globals, - file: path.resolve(__dirname, `${pluginPath}/${pluginFilename}`) - }) - - console.log(`Building ${plugin} plugin... Done!`) -} - -const main = async () => { - try { - await Promise.all(Object.keys(bsPlugins).map(plugin => build(plugin))) - } catch (error) { - console.error(error) - - process.exit(1) - } -} - -main() diff --git a/build/build-plugins.mjs b/build/build-plugins.mjs new file mode 100644 index 000000000000..b5a88d7c062e --- /dev/null +++ b/build/build-plugins.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +/*! + * Script to build our plugins to use them separately. + * Copyright 2020-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { babel } from '@rollup/plugin-babel' +import { globby } from 'globby' +import { rollup } from 'rollup' +import banner from './banner.mjs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/') +const jsFiles = await globby(`${sourcePath}/**/*.js`) + +// Array which holds the resolved plugins +const resolvedPlugins = [] + +// Trims the "js" extension and uppercases => first letter, hyphens, backslashes & slashes +const filenameToEntity = filename => filename.replace('.js', '') + .replace(/(?:^|-|\/|\\)[a-z]/g, str => str.slice(-1).toUpperCase()) + +for (const file of jsFiles) { + resolvedPlugins.push({ + src: file, + dist: file.replace('src', 'dist'), + fileName: path.basename(file), + className: filenameToEntity(path.basename(file)) + // safeClassName: filenameToEntity(path.relative(sourcePath, file)) + }) +} + +const build = async plugin => { + /** + * @type {import('rollup').GlobalsOption} + */ + const globals = {} + + const bundle = await rollup({ + input: plugin.src, + plugins: [ + babel({ + // Only transpile our source code + exclude: 'node_modules/**', + // Include the helpers in each file, at most one copy of each + babelHelpers: 'bundled' + }) + ], + external(source) { + // Pattern to identify local files + const pattern = /^(\.{1,2})\// + + // It's not a local file, e.g a Node.js package + if (!pattern.test(source)) { + globals[source] = source + return true + } + + const usedPlugin = resolvedPlugins.find(plugin => { + return plugin.src.includes(source.replace(pattern, '')) + }) + + if (!usedPlugin) { + throw new Error(`Source ${source} is not mapped!`) + } + + // We can change `Index` with `UtilIndex` etc if we use + // `safeClassName` instead of `className` everywhere + globals[path.normalize(usedPlugin.src)] = usedPlugin.className + return true + } + }) + + await bundle.write({ + banner: banner(plugin.fileName), + format: 'umd', + name: plugin.className, + sourcemap: true, + globals, + generatedCode: 'es2015', + file: plugin.dist + }) + + console.log(`Built ${plugin.className}`) +} + +(async () => { + try { + const basename = path.basename(__filename) + const timeLabel = `[${basename}] finished` + + console.log('Building individual plugins...') + console.time(timeLabel) + + await Promise.all(Object.values(resolvedPlugins).map(plugin => build(plugin))) + + console.timeEnd(timeLabel) + } catch (error) { + console.error(error) + process.exit(1) + } +})() diff --git a/build/change-version.js b/build/change-version.js deleted file mode 100644 index b8a640fa8ee3..000000000000 --- a/build/change-version.js +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to update version number references in the project. - * Copyright 2017-2020 The Bootstrap Authors - * Copyright 2017-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const fs = require('fs') -const path = require('path') -const sh = require('shelljs') - -sh.config.fatal = true - -// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37 -function regExpQuote(string) { - return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&') -} - -function regExpQuoteReplacement(string) { - return string.replace(/\$/g, '$$') -} - -const DRY_RUN = false - -function walkAsync(directory, excludedDirectories, fileCallback, errback) { - if (excludedDirectories.has(path.parse(directory).base)) { - return - } - - fs.readdir(directory, (err, names) => { - if (err) { - errback(err) - return - } - - names.forEach(name => { - const filepath = path.join(directory, name) - fs.lstat(filepath, (err, stats) => { - if (err) { - process.nextTick(errback, err) - return - } - - if (stats.isDirectory()) { - process.nextTick(walkAsync, filepath, excludedDirectories, fileCallback, errback) - } else if (stats.isFile()) { - process.nextTick(fileCallback, filepath) - } - }) - }) - }) -} - -function replaceRecursively(directory, excludedDirectories, allowedExtensions, original, replacement) { - original = new RegExp(regExpQuote(original), 'g') - replacement = regExpQuoteReplacement(replacement) - const updateFile = DRY_RUN ? - filepath => { - if (allowedExtensions.has(path.parse(filepath).ext)) { - console.log(`FILE: ${filepath}`) - } else { - console.log(`EXCLUDED:${filepath}`) - } - } : - filepath => { - if (allowedExtensions.has(path.parse(filepath).ext)) { - sh.sed('-i', original, replacement, filepath) - } - } - - walkAsync(directory, excludedDirectories, updateFile, err => { - console.error('ERROR while traversing directory!:') - console.error(err) - process.exit(1) - }) -} - -function main(args) { - if (args.length !== 2) { - console.error('USAGE: change-version old_version new_version') - console.error('Got arguments:', args) - process.exit(1) - } - - const oldVersion = args[0] - const newVersion = args[1] - const EXCLUDED_DIRS = new Set([ - '.git', - '_gh_pages', - 'node_modules', - 'vendor' - ]) - const INCLUDED_EXTENSIONS = new Set([ - // This extension allowlist is how we avoid modifying binary files - '', - '.css', - '.html', - '.js', - '.json', - '.md', - '.scss', - '.txt', - '.yml' - ]) - replaceRecursively('.', EXCLUDED_DIRS, INCLUDED_EXTENSIONS, oldVersion, newVersion) -} - -main(process.argv.slice(2)) diff --git a/build/change-version.mjs b/build/change-version.mjs new file mode 100644 index 000000000000..c1433d5780e6 --- /dev/null +++ b/build/change-version.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/*! + * Script to update version number references in the project. + * Copyright 2017-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +import { execFile } from 'node:child_process' +import fs from 'node:fs/promises' +import process from 'node:process' + +const VERBOSE = process.argv.includes('--verbose') +const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run') + +// These are the files we only care about replacing the version +const FILES = [ + 'README.md', + 'config.yml', + 'js/src/base-component.js', + 'package.js', + 'scss/mixins/_banner.scss', + 'site/data/docs-versions.yml' +] + +// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37 +function regExpQuote(string) { + return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&') +} + +function regExpQuoteReplacement(string) { + return string.replace(/\$/g, '$$') +} + +async function replaceRecursively(file, oldVersion, newVersion) { + const originalString = await fs.readFile(file, 'utf8') + const newString = originalString + .replace( + new RegExp(regExpQuote(oldVersion), 'g'), + regExpQuoteReplacement(newVersion) + ) + // Also replace the version used by the rubygem, + // which is using periods (`.`) instead of hyphens (`-`) + .replace( + new RegExp(regExpQuote(oldVersion.replace(/-/g, '.')), 'g'), + regExpQuoteReplacement(newVersion.replace(/-/g, '.')) + ) + + // No need to move any further if the strings are identical + if (originalString === newString) { + return + } + + if (VERBOSE) { + console.log(`Found ${oldVersion} in ${file}`) + } + + if (DRY_RUN) { + return + } + + await fs.writeFile(file, newString, 'utf8') +} + +function bumpNpmVersion(newVersion) { + if (DRY_RUN) { + return + } + + execFile('npm', ['version', newVersion, '--no-git-tag'], { shell: true }, error => { + if (error) { + console.error(error) + process.exit(1) + } + }) +} + +function showUsage(args) { + console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]') + console.error('Got arguments:', args) + process.exit(1) +} + +async function main(args) { + let [oldVersion, newVersion] = args + + if (!oldVersion || !newVersion) { + showUsage(args) + } + + // Strip any leading `v` from arguments because + // otherwise we will end up with duplicate `v`s + [oldVersion, newVersion] = [oldVersion, newVersion].map(arg => { + return arg.startsWith('v') ? arg.slice(1) : arg + }) + + if (oldVersion === newVersion) { + showUsage(args) + } + + bumpNpmVersion(newVersion) + + try { + await Promise.all( + FILES.map(file => replaceRecursively(file, oldVersion, newVersion)) + ) + } catch (error) { + console.error(error) + process.exit(1) + } +} + +main(process.argv.slice(2)) diff --git a/build/docs-prep.sh b/build/docs-prep.sh new file mode 100755 index 000000000000..357768abea7a --- /dev/null +++ b/build/docs-prep.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default branch suffix +BRANCH_SUFFIX="release" + +# Check if a custom version parameter was provided +if [ $# -eq 1 ]; then + BRANCH_SUFFIX="$1" +fi + +# Branch name to create +NEW_BRANCH="gh-pages-${BRANCH_SUFFIX}" + +# Get the current docs version from config +DOCS_VERSION=$(node -p "require('js-yaml').load(require('fs').readFileSync('config.yml', 'utf8')).docs_version") + +# Function to print colored messages +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" + exit 1 +} + +print_info() { + echo -e "${BLUE}ℹ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +# Function to execute command with error handling +execute() { + print_info "Running: $1" + eval $1 + if [ $? -ne 0 ]; then + print_error "Failed to execute: $1" + else + print_success "Successfully executed: $1" + fi +} + +# Check if /tmp/_site directory exists from a previous run +if [ -d "/tmp/_site" ]; then + print_warning "Found existing /tmp/_site directory. Removing it…" + rm -rf /tmp/_site +fi + +# Main process +print_info "Starting documentation deployment process…" + +# Step 1: Build documentation +print_info "Building documentation with npm run docs…" +npm run docs +if [ $? -ne 0 ]; then + print_error "Documentation build failed!" +fi +print_success "Documentation built successfully" + +# Step 2: Move _site to /tmp/ +print_info "Moving _site to temporary location…" +execute "mv _site /tmp/" + +# Step 3: Switch to gh-pages branch +print_info "Checking out gh-pages branch…" +git checkout gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to checkout gh-pages branch. Make sure it exists." +fi +print_success "Switched to gh-pages branch" + +git reset --hard origin/gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to reset to origin/gh-pages. Check your git configuration." +fi +print_success "Reset to origin/gh-pages" + +git pull origin gh-pages +if [ $? -ne 0 ]; then + print_error "Failed to pull from origin/gh-pages. Check your network connection and git configuration." +fi +print_success "Pulled latest changes from origin/gh-pages" + +# Step 4: Create a new branch for the update +print_info "Checking if branch ${NEW_BRANCH} exists and deleting it if it does…" +if git show-ref --verify --quiet refs/heads/${NEW_BRANCH}; then + execute "git branch -D ${NEW_BRANCH}" +else + print_info "Branch ${NEW_BRANCH} does not exist, proceeding with creation…" +fi +print_info "Creating new branch ${NEW_BRANCH}…" +execute "git checkout -b ${NEW_BRANCH}" + +# Step 5: Move all root-level files from Astro build +find /tmp/_site -maxdepth 1 -type f -exec mv {} . \; + +# Step 6: Move all top-level directories except 'docs' (which needs special handling) +find /tmp/_site -maxdepth 1 -type d ! -name "_site" ! -name "docs" -exec sh -c 'dir=$(basename "$1"); rm -rf "$dir"; mv "$1" .' _ {} \; + +# Step 7: Handle docs directory specially +if [ -d "/tmp/_site/docs" ]; then + # Replace only the current version's docs + if [ -d "docs/$DOCS_VERSION" ]; then + rm -rf "docs/$DOCS_VERSION" + fi + mv "/tmp/_site/docs/$DOCS_VERSION" "docs/" + + # Handle docs root files + find /tmp/_site/docs -maxdepth 1 -type f -exec mv {} docs/ \; + + # Handle special docs directories (getting-started, versions) + for special_dir in getting-started versions; do + if [ -d "/tmp/_site/docs/$special_dir" ]; then + rm -rf "docs/$special_dir" + mv "/tmp/_site/docs/$special_dir" "docs/" + fi + done +fi + +# Clean up remaining files in /tmp/_site if any +if [ -d "/tmp/_site" ]; then + remaining_files=$(find /tmp/_site -type f | wc -l) + remaining_dirs=$(find /tmp/_site -type d | wc -l) + if [ $remaining_files -gt 0 ] || [ $remaining_dirs -gt 1 ]; then + print_warning "There are still some files or directories in /tmp/_site that weren't moved." + print_warning "You may want to inspect /tmp/_site to see if anything important was missed." + else + print_info "Cleaning up temporary directory…" + rm -rf /tmp/_site + print_success "Temporary directory cleaned up" + fi +fi + +# Step 10: Remove empty site directory if it exists +if [ -d "site" ]; then + print_info "Removing empty site directory…" + execute "rm -rf site" +fi + +print_success "Docs prep complete!" +print_info "Review changes before committing and pushing." +print_info "Next steps:" +print_info " 1. Run a local server to review changes" +print_info " 2. Check browser and web inspector for any errors" +print_info " 3. git add ." +print_info " 4. git commit -m \"Update documentation\"" +print_info " 5. git push origin ${NEW_BRANCH}" diff --git a/build/generate-sri.js b/build/generate-sri.js deleted file mode 100644 index cfbf3777161e..000000000000 --- a/build/generate-sri.js +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to generate SRI hashes for use in our docs. - * Remember to use the same vendor files as the CDN ones, - * otherwise the hashes won't match! - * - * Copyright 2017-2020 The Bootstrap Authors - * Copyright 2017-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const crypto = require('crypto') -const fs = require('fs') -const path = require('path') -const sh = require('shelljs') - -sh.config.fatal = true - -const configFile = path.join(__dirname, '../config.yml') - -// Array of objects which holds the files to generate SRI hashes for. -// `file` is the path from the root folder -// `configPropertyName` is the config.yml variable's name of the file -const files = [ - { - file: 'dist/css/bootstrap.min.css', - configPropertyName: 'css_hash' - }, - { - file: 'dist/js/bootstrap.min.js', - configPropertyName: 'js_hash' - }, - { - file: 'dist/js/bootstrap.bundle.min.js', - configPropertyName: 'js_bundle_hash' - }, - { - file: 'node_modules/popper.js/dist/umd/popper.min.js', - configPropertyName: 'popper_hash' - } -] - -files.forEach(file => { - fs.readFile(file.file, 'utf8', (err, data) => { - if (err) { - throw err - } - - const algo = 'sha384' - const hash = crypto.createHash(algo).update(data, 'utf8').digest('base64') - const integrity = `${algo}-${hash}` - - console.log(`${file.configPropertyName}: ${integrity}`) - - sh.sed('-i', new RegExp(`(\\s${file.configPropertyName}:\\s+"|')(\\S+)("|')`), `$1${integrity}$3`, configFile) - }) -}) diff --git a/build/generate-sri.mjs b/build/generate-sri.mjs new file mode 100644 index 000000000000..3d9ce7f5b148 --- /dev/null +++ b/build/generate-sri.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +/*! + * Script to generate SRI hashes for use in our docs. + * Remember to use the same vendor files as the CDN ones, + * otherwise the hashes won't match! + * + * Copyright 2017-2026 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ + +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import sh from 'shelljs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +sh.config.fatal = true + +const configFile = path.join(__dirname, '../config.yml') + +// Array of objects which holds the files to generate SRI hashes for. +// `file` is the path from the root folder +// `configPropertyName` is the config.yml variable's name of the file +const files = [ + { + file: 'dist/css/bootstrap.min.css', + configPropertyName: 'css_hash' + }, + { + file: 'dist/css/bootstrap.rtl.min.css', + configPropertyName: 'css_rtl_hash' + }, + { + file: 'dist/js/bootstrap.min.js', + configPropertyName: 'js_hash' + }, + { + file: 'dist/js/bootstrap.bundle.min.js', + configPropertyName: 'js_bundle_hash' + }, + { + file: 'node_modules/@popperjs/core/dist/umd/popper.min.js', + configPropertyName: 'popper_hash' + } +] + +for (const { file, configPropertyName } of files) { + fs.readFile(file, 'utf8', (error, data) => { + if (error) { + throw error + } + + const algorithm = 'sha384' + const hash = crypto.createHash(algorithm).update(data, 'utf8').digest('base64') + const integrity = `${algorithm}-${hash}` + + console.log(`${configPropertyName}: ${integrity}`) + + sh.sed('-i', new RegExp(`^(\\s+${configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile) + }) +} diff --git a/build/postcss.config.js b/build/postcss.config.js deleted file mode 100644 index 56b7d94cc1fa..000000000000 --- a/build/postcss.config.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -module.exports = ctx => ({ - map: ctx.file.dirname.includes('examples') ? - false : - { - inline: false, - annotation: true, - sourcesContent: true - }, - plugins: { - autoprefixer: { - cascade: false - } - } -}) diff --git a/build/postcss.config.mjs b/build/postcss.config.mjs new file mode 100644 index 000000000000..7717cfc3f1f6 --- /dev/null +++ b/build/postcss.config.mjs @@ -0,0 +1,17 @@ +const mapConfig = { + inline: false, + annotation: true, + sourcesContent: true +} + +export default context => { + return { + map: context.file.dirname.includes('examples') ? false : mapConfig, + plugins: { + autoprefixer: { + cascade: false + }, + rtlcss: context.env === 'RTL' + } + } +} diff --git a/build/rollup.config.js b/build/rollup.config.js deleted file mode 100644 index 1ee67455232f..000000000000 --- a/build/rollup.config.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict' - -const path = require('path') -const { babel } = require('@rollup/plugin-babel') -const { nodeResolve } = require('@rollup/plugin-node-resolve') -const banner = require('./banner.js') - -const BUNDLE = process.env.BUNDLE === 'true' -const ESM = process.env.ESM === 'true' - -let fileDest = `bootstrap${ESM ? '.esm' : ''}` -const external = ['popper.js'] -const plugins = [ - babel({ - // Only transpile our source code - exclude: 'node_modules/**', - // Include the helpers in the bundle, at most one copy of each - babelHelpers: 'bundled' - }) -] -const globals = { - 'popper.js': 'Popper' -} - -if (BUNDLE) { - fileDest += '.bundle' - // Remove last entry in external array to bundle Popper - external.pop() - delete globals['popper.js'] - plugins.push(nodeResolve()) -} - -const rollupConfig = { - input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), - output: { - banner, - file: path.resolve(__dirname, `../dist/js/${fileDest}.js`), - format: ESM ? 'esm' : 'umd', - globals - }, - external, - plugins -} - -if (!ESM) { - rollupConfig.output.name = 'bootstrap' -} - -module.exports = rollupConfig diff --git a/build/rollup.config.mjs b/build/rollup.config.mjs new file mode 100644 index 000000000000..dd6c7d13e66f --- /dev/null +++ b/build/rollup.config.mjs @@ -0,0 +1,59 @@ +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { babel } from '@rollup/plugin-babel' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import replace from '@rollup/plugin-replace' +import banner from './banner.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const BUNDLE = process.env.BUNDLE === 'true' +const ESM = process.env.ESM === 'true' + +let destinationFile = `bootstrap${ESM ? '.esm' : ''}` +const external = ['@popperjs/core'] +const plugins = [ + babel({ + // Only transpile our source code + exclude: 'node_modules/**', + // Include the helpers in the bundle, at most one copy of each + babelHelpers: 'bundled' + }) +] +const globals = { + '@popperjs/core': 'Popper' +} + +if (BUNDLE) { + destinationFile += '.bundle' + // Remove last entry in external array to bundle Popper + external.pop() + delete globals['@popperjs/core'] + plugins.push( + replace({ + 'process.env.NODE_ENV': '"production"', + preventAssignment: true + }), + nodeResolve() + ) +} + +const rollupConfig = { + input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), + output: { + banner: banner(), + file: path.resolve(__dirname, `../dist/js/${destinationFile}.js`), + format: ESM ? 'esm' : 'umd', + globals, + generatedCode: 'es2015' + }, + external, + plugins +} + +if (!ESM) { + rollupConfig.output.name = 'bootstrap' +} + +export default rollupConfig diff --git a/build/ship.sh b/build/ship.sh deleted file mode 100644 index da5a03bcd15c..000000000000 --- a/build/ship.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# -# Usage -# --------------- -# 1. Clone second version of Bootstrap in sibling directory named `bs-docs`. -# 2. Within `bs-docs` copy, switch to `gh-pages` branch. -# 3. Pull latest, re-bundle, re-npm. -# 4. Run script. - -red=$'\e[1;31m' -green=$'\e[1;32m' -#blue=$'\e[1;34m' -magenta=$'\e[1;35m' -#cyan=$'\e[1;36m' -end=$'\e[0m' - -# Get current version from package.json -current_version=$(node -p "require('./package.json').version") - -if [[ $# -lt 1 ]]; then - printf "\n%s⚠️ Shipping aborted. You must specify a version.\n%s" "$red" "$end" - exit 1 -fi - -# Pulling latest changes, just to be sure -printf "\n%s=======================================================%s" "$magenta" "$end" -printf "\n%sPulling latest changes...%s" "$magenta" "$end" -printf "\n%s=======================================================\n\n%s" "$magenta" "$end" -git pull origin main - -# Update version number -printf "\n%s=======================================================%s" "$magenta" "$end" -printf "\n%sUpdating version number...%s" "$magenta" "$end" -printf "\n%s=======================================================\n%s" "$magenta" "$end" -npm run release-version "$current_version" "$1" - -# Build release -printf "\n%s=======================================================%s" "$magenta" "$end" -printf "\n%sBuilding release...%s" "$magenta" "$end" -printf "\n%s=======================================================\n%s" "$magenta" "$end" -npm run release - -# Copy the contents of the built docs site over to `bs-docs` repo -printf "\n%s=======================================================%s" "$magenta" "$end" -printf "\n%sCopy it over...%s" "$magenta" "$end" -printf "\n%s=======================================================\n%s" "$magenta" "$end" -cp -rf _gh_pages/. ../bs-docs/ -printf "\nDone!\n" - -printf "\n%s=======================================================%s" "$green" "$end" -printf "\n%sSuccess, $1 is ready to review and publish.%s" "$green" "$end" -printf "\n%s=======================================================\n\n%s" "$green" "$end" diff --git a/build/svgo.yml b/build/svgo.yml deleted file mode 100644 index 67940d393ee3..000000000000 --- a/build/svgo.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Usage: -# install svgo globally: `npm i -g svgo` -# svgo --config=build/svgo.yml --input=foo.svg - -# https://github.com/svg/svgo/blob/master/docs/how-it-works/en.md -# replace default config - -multipass: true -#full: true - -# https://github.com/svg/svgo/blob/master/lib/svgo/js2svg.js#L6 for more config options - -js2svg: - pretty: true - indent: 2 - -plugins: -# - addAttributesToSVGElement: -# attributes: -# - focusable: false - - cleanupAttrs: true - - cleanupEnableBackground: true - - cleanupIDs: true - - cleanupListOfValues: true - - cleanupNumericValues: true - - collapseGroups: true - - convertColors: true - - convertPathData: true - - convertShapeToPath: true - - convertStyleToAttrs: true - - convertTransform: true - - inlineStyles: true - - mergePaths: true - - minifyStyles: true - - moveElemsAttrsToGroup: true - - moveGroupAttrsToElems: true - - removeAttrs: - attrs: - - "data-name" - - removeComments: true - - removeDesc: true - - removeDoctype: true - - removeEditorsNSData: true - - removeEmptyAttrs: true - - removeEmptyContainers: true - - removeEmptyText: true - - removeHiddenElems: true - - removeMetadata: true - - removeNonInheritableGroupAttrs: true - - removeTitle: false - - removeUnknownsAndDefaults: - keepRoleAttr: true - - removeUnusedNS: true - - removeUselessDefs: true - - removeUselessStrokeAndFill: true - - removeViewBox: false - - removeXMLNS: false - - removeXMLProcInst: true - - sortAttrs: true diff --git a/build/vnu-jar.js b/build/vnu-jar.js deleted file mode 100644 index 90c1a12ab9c5..000000000000 --- a/build/vnu-jar.js +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node - -/*! - * Script to run vnu-jar if Java is available. - * Copyright 2017-2020 The Bootstrap Authors - * Copyright 2017-2020 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ - -'use strict' - -const childProcess = require('child_process') -const vnu = require('vnu-jar') - -childProcess.exec('java -version', (error, stdout, stderr) => { - if (error) { - console.error('Skipping vnu-jar test; Java is missing.') - return - } - - const is32bitJava = !/64-Bit/.test(stderr) - - // vnu-jar accepts multiple ignores joined with a `|`. - // Also note that the ignores are regular expressions. - const ignores = [ - // "autocomplete" is included in - + diff --git a/js/tests/integration/rollup.bundle-modularity.js b/js/tests/integration/rollup.bundle-modularity.js index a8670ca8ce48..63d6515251cc 100644 --- a/js/tests/integration/rollup.bundle-modularity.js +++ b/js/tests/integration/rollup.bundle-modularity.js @@ -1,7 +1,7 @@ -/* eslint-env node */ +'use strict' const commonjs = require('@rollup/plugin-commonjs') -const configRollup = require('./rollup.bundle') +const configRollup = require('./rollup.bundle.js') const config = { ...configRollup, diff --git a/js/tests/integration/rollup.bundle.js b/js/tests/integration/rollup.bundle.js index 9e2ed26c1c03..8b3c578a255a 100644 --- a/js/tests/integration/rollup.bundle.js +++ b/js/tests/integration/rollup.bundle.js @@ -1,7 +1,8 @@ -/* eslint-env node */ +'use strict' const { babel } = require('@rollup/plugin-babel') const { nodeResolve } = require('@rollup/plugin-node-resolve') +const replace = require('@rollup/plugin-replace') module.exports = { input: 'js/tests/integration/bundle.js', @@ -10,6 +11,10 @@ module.exports = { format: 'iife' }, plugins: [ + replace({ + 'process.env.NODE_ENV': '"production"', + preventAssignment: true + }), nodeResolve(), babel({ exclude: 'node_modules/**', diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index e87ee1cc71f0..36bf7f2df8fe 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -1,20 +1,18 @@ -/* eslint-env node */ +'use strict' -const path = require('path') +const path = require('node:path') const ip = require('ip') const { babel } = require('@rollup/plugin-babel') const istanbul = require('rollup-plugin-istanbul') const { nodeResolve } = require('@rollup/plugin-node-resolve') +const replace = require('@rollup/plugin-replace') +const { browsers } = require('./browsers.js') -const { - browsers, - browsersKeys -} = require('./browsers') +const ENV = process.env +const BROWSERSTACK = Boolean(ENV.BROWSERSTACK) +const DEBUG = Boolean(ENV.DEBUG) +const JQUERY_TEST = Boolean(ENV.JQUERY) -const { env } = process -const browserStack = env.BROWSER === 'true' -const debug = env.DEBUG === 'true' -const jQueryTest = env.JQUERY === 'true' const frameworks = [ 'jasmine' ] @@ -29,44 +27,53 @@ const reporters = ['dots'] const detectBrowsers = { usePhantomJS: false, postDetection(availableBrowser) { - if (env.CI === true || availableBrowser.includes('Chrome')) { - return debug ? ['Chrome'] : ['ChromeHeadless'] + // On CI just use Chrome + if (ENV.CI === true) { + return ['ChromeHeadless'] } - if (availableBrowser.includes('Firefox')) { - return debug ? ['Firefox'] : ['FirefoxHeadless'] + if (availableBrowser.includes('Chrome')) { + return DEBUG ? ['Chrome'] : ['ChromeHeadless'] } - throw new Error('Please install Firefox or Chrome') - } -} + if (availableBrowser.includes('Chromium')) { + return DEBUG ? ['Chromium'] : ['ChromiumHeadless'] + } + + if (availableBrowser.includes('Firefox')) { + return DEBUG ? ['Firefox'] : ['FirefoxHeadless'] + } -const customLaunchers = { - FirefoxHeadless: { - base: 'Firefox', - flags: ['-headless'] + throw new Error('Please install Chrome, Chromium or Firefox') } } -const conf = { +const config = { basePath: '../..', port: 9876, colors: true, autoWatch: false, singleRun: true, - concurrency: Infinity, + concurrency: Number.POSITIVE_INFINITY, client: { clearContext: false }, files: [ 'node_modules/hammer-simulator/index.js', - { pattern: 'js/tests/unit/**/!(jquery).spec.js', watched: !browserStack } + { + pattern: 'js/tests/unit/**/!(jquery).spec.js', + watched: !BROWSERSTACK + } ], preprocessors: { 'js/tests/unit/**/*.spec.js': ['rollup'] }, rollupPreprocessor: { plugins: [ + replace({ + 'process.env.NODE_ENV': '"dev"', + preventAssignment: true + }), istanbul({ exclude: [ 'node_modules/**', @@ -85,36 +92,39 @@ const conf = { output: { format: 'iife', name: 'bootstrapTest', - sourcemap: 'inline' + sourcemap: 'inline', + generatedCode: 'es2015' } } } -if (browserStack) { - conf.hostname = ip.address() - conf.browserStack = { - username: env.BROWSER_STACK_USERNAME, - accessKey: env.BROWSER_STACK_ACCESS_KEY, - build: `bootstrap-${new Date().toISOString()}`, +if (BROWSERSTACK) { + config.hostname = ip.address() + config.browserStack = { + username: ENV.BROWSER_STACK_USERNAME, + accessKey: ENV.BROWSER_STACK_ACCESS_KEY, + build: `bootstrap-${ENV.GITHUB_SHA ? `${ENV.GITHUB_SHA.slice(0, 7)}-` : ''}${new Date().toISOString()}`, project: 'Bootstrap', retryLimit: 2 } plugins.push('karma-browserstack-launcher', 'karma-jasmine-html-reporter') - conf.customLaunchers = browsers - conf.browsers = browsersKeys + config.customLaunchers = browsers + config.browsers = Object.keys(browsers) reporters.push('BrowserStack', 'kjhtml') -} else if (jQueryTest) { +} else if (JQUERY_TEST) { frameworks.push('detectBrowsers') plugins.push( 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-detect-browsers' ) - conf.customLaunchers = customLaunchers - conf.detectBrowsers = detectBrowsers - conf.files = [ + config.detectBrowsers = detectBrowsers + config.files = [ 'node_modules/jquery/dist/jquery.slim.min.js', - { pattern: 'js/tests/unit/jquery.spec.js', watched: false } + { + pattern: 'js/tests/unit/jquery.spec.js', + watched: false + } ] } else { frameworks.push('detectBrowsers') @@ -125,47 +135,35 @@ if (browserStack) { 'karma-coverage-istanbul-reporter' ) reporters.push('coverage-istanbul') - conf.customLaunchers = customLaunchers - conf.detectBrowsers = detectBrowsers - conf.coverageIstanbulReporter = { + config.detectBrowsers = detectBrowsers + config.coverageIstanbulReporter = { dir: path.resolve(__dirname, '../coverage/'), reports: ['lcov', 'text-summary'], thresholds: { emitWarning: false, global: { statements: 90, - branches: 90, + branches: 89, functions: 90, lines: 90 - }, - each: { - overrides: { - 'js/src/dom/polyfill.js': { - statements: 39, - lines: 37, - branches: 19, - functions: 50 - } - } } } } - if (debug) { - conf.hostname = ip.address() + if (DEBUG) { + config.hostname = ip.address() plugins.push('karma-jasmine-html-reporter') reporters.push('kjhtml') - conf.singleRun = false - conf.autoWatch = true + config.singleRun = false + config.autoWatch = true } } -conf.frameworks = frameworks -conf.plugins = plugins -conf.reporters = reporters +config.frameworks = frameworks +config.plugins = plugins +config.reporters = reporters module.exports = karmaConfig => { - // possible values: karmaConfig.LOG_DISABLE || karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN || karmaConfig.LOG_INFO || karmaConfig.LOG_DEBUG - conf.logLevel = karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN - karmaConfig.set(conf) + config.logLevel = karmaConfig.LOG_ERROR + karmaConfig.set(config) } diff --git a/js/tests/unit/.eslintrc.json b/js/tests/unit/.eslintrc.json deleted file mode 100644 index e7f8d5d2a9f7..000000000000 --- a/js/tests/unit/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "root": true, - "extends": [ - "../../../.eslintrc.json" - ], - "overrides": [ - { - "files": [ - "**/*.spec.js" - ], - "env": { - "jasmine": true - } - } - ] -} diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index 9372215b6507..97cc3cc5325b 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -1,8 +1,6 @@ -import Alert from '../../src/alert' -import { getTransitionDurationFromElement } from '../../src/util/index' - -/** Test helpers */ -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import Alert from '../../src/alert.js' +import { getTransitionDurationFromElement } from '../../src/util/index.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Alert', () => { let fixtureEl @@ -15,98 +13,116 @@ describe('Alert', () => { clearFixture() }) + it('should take care of element either passed as a CSS selector or DOM element', () => { + fixtureEl.innerHTML = '
' + + const alertEl = fixtureEl.querySelector('.alert') + const alertBySelector = new Alert('.alert') + const alertByElement = new Alert(alertEl) + + expect(alertBySelector._element).toEqual(alertEl) + expect(alertByElement._element).toEqual(alertEl) + }) + it('should return version', () => { - expect(typeof Alert.VERSION).toEqual('string') + expect(Alert.VERSION).toEqual(jasmine.any(String)) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Alert.DATA_KEY).toEqual('bs.alert') + }) }) describe('data-api', () => { it('should close an alert without instantiating it manually', () => { fixtureEl.innerHTML = [ '
', - ' ', + ' ', '
' ].join('') const button = document.querySelector('button') button.click() - expect(document.querySelectorAll('.alert').length).toEqual(0) + expect(document.querySelectorAll('.alert')).toHaveSize(0) }) it('should close an alert without instantiating it manually with the parent selector', () => { fixtureEl.innerHTML = [ '
', - ' ', + ' ', '
' ].join('') const button = document.querySelector('button') button.click() - expect(document.querySelectorAll('.alert').length).toEqual(0) + expect(document.querySelectorAll('.alert')).toHaveSize(0) }) }) describe('close', () => { - it('should close an alert', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - fixtureEl.innerHTML = '
' + it('should close an alert', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert').length).toEqual(0) - expect(spy).not.toHaveBeenCalled() - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + resolve() + }) - alert.close() + alert.close() + }) }) - it('should close alert with fade class', done => { - fixtureEl.innerHTML = '
' + it('should close alert with fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('transitionend', () => { - expect().nothing() - }) + alertEl.addEventListener('transitionend', () => { + expect().nothing() + }) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert').length).toEqual(0) - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + resolve() + }) - alert.close() + alert.close() + }) }) - it('should not remove alert if close event is prevented', done => { - fixtureEl.innerHTML = '
' - - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + it('should not remove alert if close event is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '
' - const endTest = () => { - setTimeout(() => { - expect(alert._removeElement).not.toHaveBeenCalled() - done() - }, 10) - } + const getAlert = () => document.querySelector('.alert') + const alertEl = getAlert() + const alert = new Alert(alertEl) - spyOn(alert, '_removeElement') + alertEl.addEventListener('close.bs.alert', event => { + event.preventDefault() + setTimeout(() => { + expect(getAlert()).not.toBeNull() + resolve() + }, 10) + }) - alertEl.addEventListener('close.bs.alert', event => { - event.preventDefault() - endTest() - }) + alertEl.addEventListener('closed.bs.alert', () => { + reject(new Error('should not fire closed event')) + }) - alertEl.addEventListener('closed.bs.alert', () => { - endTest() + alert.close() }) - - alert.close() }) }) @@ -117,7 +133,7 @@ describe('Alert', () => { const alertEl = document.querySelector('.alert') const alert = new Alert(alertEl) - expect(Alert.getInstance(alertEl)).toBeDefined() + expect(Alert.getInstance(alertEl)).not.toBeNull() alert.dispose() @@ -132,14 +148,14 @@ describe('Alert', () => { const alertEl = fixtureEl.querySelector('.alert') const alert = new Alert(alertEl) - spyOn(alert, 'close') + const spy = spyOn(alert, 'close') jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] jQueryMock.fn.alert.call(jQueryMock, 'close') - expect(alert.close).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create new alert instance and call close', () => { @@ -150,9 +166,9 @@ describe('Alert', () => { jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] + expect(Alert.getInstance(alertEl)).toBeNull() jQueryMock.fn.alert.call(jQueryMock, 'close') - expect(Alert.getInstance(alertEl)).toBeDefined() expect(fixtureEl.querySelector('.alert')).toBeNull() }) @@ -166,8 +182,78 @@ describe('Alert', () => { jQueryMock.fn.alert.call(jQueryMock) - expect(Alert.getInstance(alertEl)).toBeDefined() + expect(Alert.getInstance(alertEl)).not.toBeNull() expect(fixtureEl.querySelector('.alert')).not.toBeNull() }) + + it('should throw an error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.alert.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw an error on protected method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = '_getConfig' + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.alert.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + }) + + describe('getInstance', () => { + it('should return alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const alert = new Alert(div) + + expect(Alert.getInstance(div)).toEqual(alert) + expect(Alert.getInstance(div)).toBeInstanceOf(Alert) + }) + + it('should return null when there is no alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Alert.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const alert = new Alert(div) + + expect(Alert.getOrCreateInstance(div)).toEqual(alert) + expect(Alert.getInstance(div)).toEqual(Alert.getOrCreateInstance(div, {})) + expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert) + }) + + it('should return new instance when there is no alert instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Alert.getInstance(div)).toBeNull() + expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert) + }) }) }) diff --git a/js/tests/unit/base-component.spec.js b/js/tests/unit/base-component.spec.js new file mode 100644 index 000000000000..5b7d52e2331a --- /dev/null +++ b/js/tests/unit/base-component.spec.js @@ -0,0 +1,168 @@ +import BaseComponent from '../../src/base-component.js' +import EventHandler from '../../src/dom/event-handler.js' +import { noop } from '../../src/util/index.js' +import { clearFixture, getFixture } from '../helpers/fixture.js' + +class DummyClass extends BaseComponent { + constructor(element) { + super(element) + + EventHandler.on(this._element, `click${DummyClass.EVENT_KEY}`, noop) + } + + static get NAME() { + return 'dummy' + } +} + +describe('Base Component', () => { + let fixtureEl + const name = 'dummy' + let element + let instance + const createInstance = () => { + fixtureEl.innerHTML = '
' + element = fixtureEl.querySelector('#foo') + instance = new DummyClass(element) + } + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('Static Methods', () => { + describe('VERSION', () => { + it('should return version', () => { + expect(DummyClass.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(DummyClass.DATA_KEY).toEqual(`bs.${name}`) + }) + }) + + describe('NAME', () => { + it('should throw an Error if it is not initialized', () => { + expect(() => { + // eslint-disable-next-line no-unused-expressions + BaseComponent.NAME + }).toThrowError(Error) + }) + + it('should return plugin NAME', () => { + expect(DummyClass.NAME).toEqual(name) + }) + }) + + describe('EVENT_KEY', () => { + it('should return plugin event key', () => { + expect(DummyClass.EVENT_KEY).toEqual(`.bs.${name}`) + }) + }) + }) + + describe('Public Methods', () => { + describe('constructor', () => { + it('should accept element, either passed as a CSS selector or DOM element', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const el = fixtureEl.querySelector('#foo') + const elInstance = new DummyClass(el) + const selectorInstance = new DummyClass('#bar') + + expect(elInstance._element).toEqual(el) + expect(selectorInstance._element).toEqual(fixtureEl.querySelector('#bar')) + }) + + it('should not initialize and add element record to Data (caching), if argument `element` is not an HTML element', () => { + fixtureEl.innerHTML = '' + + const el = fixtureEl.querySelector('#foo') + const elInstance = new DummyClass(el) + const selectorInstance = new DummyClass('#bar') + + expect(elInstance._element).not.toBeDefined() + expect(selectorInstance._element).not.toBeDefined() + }) + }) + + describe('dispose', () => { + it('should dispose an component', () => { + createInstance() + expect(DummyClass.getInstance(element)).not.toBeNull() + + instance.dispose() + + expect(DummyClass.getInstance(element)).toBeNull() + expect(instance._element).toBeNull() + }) + + it('should de-register element event listeners', () => { + createInstance() + const spy = spyOn(EventHandler, 'off') + + instance.dispose() + + expect(spy).toHaveBeenCalledWith(element, DummyClass.EVENT_KEY) + }) + }) + + describe('getInstance', () => { + it('should return an instance', () => { + createInstance() + + expect(DummyClass.getInstance(element)).toEqual(instance) + expect(DummyClass.getInstance(element)).toBeInstanceOf(DummyClass) + }) + + it('should accept element, either passed as a CSS selector, jQuery element, or DOM element', () => { + createInstance() + + expect(DummyClass.getInstance('#foo')).toEqual(instance) + expect(DummyClass.getInstance(element)).toEqual(instance) + + const fakejQueryObject = { + 0: element, + jquery: 'foo' + } + + expect(DummyClass.getInstance(fakejQueryObject)).toEqual(instance) + }) + + it('should return null when there is no instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(DummyClass.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return an instance', () => { + createInstance() + + expect(DummyClass.getOrCreateInstance(element)).toEqual(instance) + expect(DummyClass.getInstance(element)).toEqual(DummyClass.getOrCreateInstance(element, {})) + expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass) + }) + + it('should return new instance when there is no alert instance', () => { + fixtureEl.innerHTML = '
' + element = fixtureEl.querySelector('#foo') + + expect(DummyClass.getInstance(element)).toBeNull() + expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass) + }) + }) + }) +}) diff --git a/js/tests/unit/button.spec.js b/js/tests/unit/button.spec.js index ac32b4b89761..6624fee7c98e 100644 --- a/js/tests/unit/button.spec.js +++ b/js/tests/unit/button.spec.js @@ -1,11 +1,5 @@ -import Button from '../../src/button' - -/** Test helpers */ -import { - getFixture, - clearFixture, - jQueryMock -} from '../helpers/fixture' +import Button from '../../src/button.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Button', () => { let fixtureEl @@ -18,64 +12,80 @@ describe('Button', () => { clearFixture() }) + it('should take care of element either passed as a CSS selector or DOM element', () => { + fixtureEl.innerHTML = '' + const buttonEl = fixtureEl.querySelector('[data-bs-toggle="button"]') + const buttonBySelector = new Button('[data-bs-toggle="button"]') + const buttonByElement = new Button(buttonEl) + + expect(buttonBySelector._element).toEqual(buttonEl) + expect(buttonByElement._element).toEqual(buttonEl) + }) + describe('VERSION', () => { it('should return plugin version', () => { expect(Button.VERSION).toEqual(jasmine.any(String)) }) }) + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Button.DATA_KEY).toEqual('bs.button') + }) + }) + describe('data-api', () => { it('should toggle active class on click', () => { fixtureEl.innerHTML = [ - '', - '' + '', + '' ].join('') const btn = fixtureEl.querySelector('.btn') const divTest = fixtureEl.querySelector('.test') const btnTestParent = fixtureEl.querySelector('.testParent') - expect(btn.classList.contains('active')).toEqual(false) + expect(btn).not.toHaveClass('active') btn.click() - expect(btn.classList.contains('active')).toEqual(true) + expect(btn).toHaveClass('active') btn.click() - expect(btn.classList.contains('active')).toEqual(false) + expect(btn).not.toHaveClass('active') divTest.click() - expect(btnTestParent.classList.contains('active')).toEqual(true) + expect(btnTestParent).toHaveClass('active') }) }) describe('toggle', () => { it('should toggle aria-pressed', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) expect(btnEl.getAttribute('aria-pressed')).toEqual('false') - expect(btnEl.classList.contains('active')).toEqual(false) + expect(btnEl).not.toHaveClass('active') button.toggle() expect(btnEl.getAttribute('aria-pressed')).toEqual('true') - expect(btnEl.classList.contains('active')).toEqual(true) + expect(btnEl).toHaveClass('active') }) }) describe('dispose', () => { it('should dispose a button', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) - expect(Button.getInstance(btnEl)).toBeDefined() + expect(Button.getInstance(btnEl)).not.toBeNull() button.dispose() @@ -85,23 +95,23 @@ describe('Button', () => { describe('jQueryInterface', () => { it('should handle config passed and toggle existing button', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) - spyOn(button, 'toggle') + const spy = spyOn(button, 'toggle') jQueryMock.fn.button = Button.jQueryInterface jQueryMock.elements = [btnEl] jQueryMock.fn.button.call(jQueryMock, 'toggle') - expect(button.toggle).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create new button instance and call toggle', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') @@ -110,12 +120,12 @@ describe('Button', () => { jQueryMock.fn.button.call(jQueryMock, 'toggle') - expect(Button.getInstance(btnEl)).toBeDefined() - expect(btnEl.classList.contains('active')).toEqual(true) + expect(Button.getInstance(btnEl)).not.toBeNull() + expect(btnEl).toHaveClass('active') }) it('should just create a button instance without calling toggle', () => { - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' const btnEl = fixtureEl.querySelector('.btn') @@ -124,8 +134,50 @@ describe('Button', () => { jQueryMock.fn.button.call(jQueryMock) - expect(Button.getInstance(btnEl)).toBeDefined() - expect(btnEl.classList.contains('active')).toEqual(false) + expect(Button.getInstance(btnEl)).not.toBeNull() + expect(btnEl).not.toHaveClass('active') + }) + }) + + describe('getInstance', () => { + it('should return button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const button = new Button(div) + + expect(Button.getInstance(div)).toEqual(button) + expect(Button.getInstance(div)).toBeInstanceOf(Button) + }) + + it('should return null when there is no button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Button.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const button = new Button(div) + + expect(Button.getOrCreateInstance(div)).toEqual(button) + expect(Button.getInstance(div)).toEqual(Button.getOrCreateInstance(div, {})) + expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button) + }) + + it('should return new instance when there is no button instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Button.getInstance(div)).toBeNull() + expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button) }) }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 61ca52cb2923..2960eb5ce59f 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -1,8 +1,10 @@ -import Carousel from '../../src/carousel' -import EventHandler from '../../src/dom/event-handler' - -/** Test helpers */ -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import Carousel from '../../src/carousel.js' +import EventHandler from '../../src/dom/event-handler.js' +import { isRTL, noop } from '../../src/util/index.js' +import Swipe from '../../src/util/swipe.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Carousel', () => { const { Simulator, PointerEvent } = window @@ -13,7 +15,7 @@ describe('Carousel', () => { const stylesCarousel = document.createElement('style') stylesCarousel.type = 'text/css' - stylesCarousel.appendChild(document.createTextNode(cssStyleCarousel)) + stylesCarousel.append(document.createTextNode(cssStyleCarousel)) const clearPointerEvents = () => { window.PointerEvent = null @@ -45,95 +47,166 @@ describe('Carousel', () => { }) }) + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Carousel.DATA_KEY).toEqual('bs.carousel') + }) + }) + describe('constructor', () => { - it('should go to next item if right arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should take care of element either passed as a CSS selector or DOM element', () => { + fixtureEl.innerHTML = '' const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carouselBySelector = new Carousel('#myCarousel') + const carouselByElement = new Carousel(carouselEl) - spyOn(carousel, '_keydown').and.callThrough() + expect(carouselBySelector._element).toEqual(carouselEl) + expect(carouselByElement._element).toEqual(carouselEl) + }) - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) + it('should start cycling if `ride`===`carousel`', () => { + fixtureEl.innerHTML = '' - const keydown = createEvent('keydown') - keydown.key = 'ArrowRight' + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).not.toBeNull() + }) + + it('should not start cycling if `ride`!==`carousel`', () => { + fixtureEl.innerHTML = '' + + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).toBeNull() + }) - carouselEl.dispatchEvent(keydown) + it('should go to next item if right arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + const spy = spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(spy).toHaveBeenCalled() + resolve() + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + carouselEl.dispatchEvent(keydown) + }) }) - it('should go to previous item if left arrow key is pressed', done => { + it('should ignore keyboard events if data-bs-keyboard=false', () => { fixtureEl.innerHTML = [ - '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') - dropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') - expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') - done() - }) + expect(document.activeElement).toEqual(input, 'input still focused') - triggerDropdown.click() + textarea.focus() + textarea.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) + + triggerDropdown.click() + }) }) - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - dropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('d-none')).toEqual(false, '.d-none not focused') - expect(document.activeElement.style.display === 'none').toEqual(false, '"display: none" not focused') - expect(document.activeElement.style.visibility === 'hidden').toEqual(false, '"visibility: hidden" not focused') + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - done() + triggerDropdown.click() }) + }) - triggerDropdown.click() + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydown) + + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') + + resolve() + }) + + triggerDropdown.click() + }) }) - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') - dropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - done() - }) + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should focus on the first element when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const item1 = fixtureEl.querySelector('#item1') + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) + }) - dropdown.addEventListener('shown.bs.dropdown', () => { const keydown = createEvent('keydown') keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) + }) - document.activeElement.dispatchEvent(keydown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) - done() + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) }) - - triggerDropdown.click() }) - it('should not close the dropdown if the user clicks on a text field', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - input.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - dropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - input.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should not close the dropdown if the user clicks on a textarea', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - textarea.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - dropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should ignore keyboard events for s and ', - ' ', - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - const keydownSpace = createEvent('keydown') - keydownSpace.key = 'Space' + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) + + triggerDropdown.click() + }) + }) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + it('should ignore keyboard events for s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) + } - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) - dropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - input.focus() - input.dispatchEvent(keydownSpace) + test('Space', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key ArrowUp + test('ArrowUp', input) - textarea.focus() - textarea.dispatchEvent(keydownSpace) + test('ArrowUp', textarea) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + // Key ArrowDown + test('ArrowDown', input) - // Key ArrowUp - input.focus() - input.dispatchEvent(keydownArrowUp) + test('ArrowDown', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - textarea.focus() - textarea.dispatchEvent(keydownArrowUp) + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + triggerDropdown.click() + }) + }) - // Key ArrowDown - input.focus() - input.dispatchEvent(keydownArrowDown) + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + + const spy = spyOn(dropdown, 'toggle') + + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) + + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) - expect(document.activeElement).toEqual(input, 'input still focused') + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) - textarea.focus() - textarea.dispatchEvent(keydownArrowDown) + it('should not propagate escape key events if dropdown is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).not.toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.click() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) + + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) + + toggle.click() + }) + }) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) + + dropdownToggle.click() + }) + }) - expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') - done() + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) + + dropdownToggle.click() }) + }) - triggerDropdown.click() + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.click() + }) }) - it('should not open dropdown if escape key was pressed on the toggle', done => { + it('should be able to identify clicked dropdown, no matter the markup order', () => { fixtureEl.innerHTML = [ - '
', - ' + +
+
+

التلميحات

+ دليل الإستخدام +
+ +
+ تلميح يظهر في الأعلى + + + + + `} /> +
+
+ + + + + + + diff --git a/site/src/assets/examples/cheatsheet/cheatsheet.css b/site/src/assets/examples/cheatsheet/cheatsheet.css new file mode 100644 index 000000000000..5721a028e9cc --- /dev/null +++ b/site/src/assets/examples/cheatsheet/cheatsheet.css @@ -0,0 +1,163 @@ +body { + scroll-behavior: smooth; +} + +/** + * Bootstrap "Journal code" icon + * @link https://icons.getbootstrap.com/icons/journal-code/ + */ +.bd-heading a::before { + display: inline-block; + width: 1em; + height: 1em; + margin-right: .25rem; + content: ""; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M4 1h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1H2a2 2 0 0 1 2-2z'/%3E%3Cpath d='M2 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2z'/%3E%3Cpath fill-rule='evenodd' d='M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z'/%3E%3C/svg%3E"); + background-size: 1em; +} + +/* stylelint-disable-next-line selector-max-universal */ +.bd-heading + div > * + * { + margin-top: 3rem; +} + +/* Table of contents */ +.bd-aside a { + padding: .1875rem .5rem; + margin-top: .125rem; + margin-left: .3125rem; + color: var(--bs-body-color); +} + +.bd-aside a:hover, +.bd-aside a:focus { + color: var(--bs-body-color); + background-color: rgba(121, 82, 179, .1); +} + +.bd-aside .active { + font-weight: 600; + color: var(--bs-body-color); +} + +.bd-aside .btn { + padding: .25rem .5rem; + font-weight: 600; + color: var(--bs-body-color); +} + +.bd-aside .btn:hover, +.bd-aside .btn:focus { + color: var(--bs-body-color); + background-color: rgba(121, 82, 179, .1); +} + +.bd-aside .btn:focus { + box-shadow: 0 0 0 1px rgba(121, 82, 179, .7); +} + +.bd-aside .btn::before { + width: 1.25em; + line-height: 0; + content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ccc' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + transition: transform .35s ease; + + /* rtl:raw: + transform: rotate(180deg) translateX(-2px); + */ + transform-origin: .5em 50%; +} + +.bd-aside .btn[aria-expanded="true"]::before { + transform: rotate(90deg)/* rtl:ignore */; +} + + +/* Examples */ +.scrollspy-example { + height: 200px; +} + +[id="modal"] .bd-example .btn, +[id="buttons"] .bd-example .btn, +[id="tooltips"] .bd-example .btn, +[id="popovers"] .bd-example .btn, +[id="dropdowns"] .bd-example .btn-group, +[id="dropdowns"] .bd-example .dropdown, +[id="dropdowns"] .bd-example .dropup, +[id="dropdowns"] .bd-example .dropend, +[id="dropdowns"] .bd-example .dropstart { + margin: 0 1rem 1rem 0; +} + +/* Layout */ +@media (min-width: 1200px) { + body { + display: grid; + grid-template-rows: auto; + grid-template-columns: 1fr 4fr 1fr; + gap: 1rem; + } + + .bd-header { + position: fixed; + top: 0; + /* rtl:begin:ignore */ + right: 0; + left: 0; + /* rtl:end:ignore */ + z-index: 1030; + grid-column: 1 / span 3; + } + + .bd-aside, + .bd-cheatsheet { + padding-top: 4rem; + } + + /** + * 1. Too bad only Firefox supports subgrids ATM + */ + .bd-cheatsheet, + .bd-cheatsheet section, + .bd-cheatsheet article { + display: inherit; /* 1 */ + grid-template-rows: auto; + grid-template-columns: 1fr 4fr; + grid-column: 1 / span 2; + gap: inherit; /* 1 */ + } + + .bd-aside { + grid-area: 1 / 3; + scroll-margin-top: 4rem; + } + + .bd-cheatsheet section, + .bd-cheatsheet section > h2 { + top: 2rem; + scroll-margin-top: 2rem; + } + + .bd-cheatsheet section > h2::before { + position: absolute; + /* rtl:begin:ignore */ + top: 0; + right: 0; + bottom: -2rem; + left: 0; + /* rtl:end:ignore */ + z-index: -1; + content: ""; + } + + .bd-cheatsheet article, + .bd-cheatsheet .bd-heading { + top: 8rem; + scroll-margin-top: 8rem; + } + + .bd-cheatsheet .bd-heading { + z-index: 1; + } +} diff --git a/site/src/assets/examples/cheatsheet/cheatsheet.js b/site/src/assets/examples/cheatsheet/cheatsheet.js new file mode 100644 index 000000000000..e25a89e75d6f --- /dev/null +++ b/site/src/assets/examples/cheatsheet/cheatsheet.js @@ -0,0 +1,73 @@ +/* global bootstrap: false */ + +(() => { + 'use strict' + + // Tooltip and popover demos + document.querySelectorAll('.tooltip-demo') + .forEach(tooltip => { + new bootstrap.Tooltip(tooltip, { + selector: '[data-bs-toggle="tooltip"]' + }) + }) + + document.querySelectorAll('[data-bs-toggle="popover"]') + .forEach(popover => { + new bootstrap.Popover(popover) + }) + + document.querySelectorAll('.toast') + .forEach(toastNode => { + const toast = new bootstrap.Toast(toastNode, { + autohide: false + }) + + toast.show() + }) + + // Disable empty links and submit buttons + document.querySelectorAll('[href="#"], [type="submit"]') + .forEach(link => { + link.addEventListener('click', event => { + event.preventDefault() + }) + }) + + function setActiveItem() { + const { hash } = window.location + + if (hash === '') { + return + } + + const link = document.querySelector(`.bd-aside a[href="${hash}"]`) + + if (!link) { + return + } + + const active = document.querySelector('.bd-aside .active') + const parent = link.parentNode.parentNode.previousElementSibling + + link.classList.add('active') + + if (parent.classList.contains('collapsed')) { + parent.click() + } + + if (!active) { + return + } + + const expanded = active.parentNode.parentNode.previousElementSibling + + active.classList.remove('active') + + if (expanded && parent !== expanded) { + expanded.click() + } + } + + setActiveItem() + window.addEventListener('hashchange', setActiveItem) +})() diff --git a/site/src/assets/examples/cheatsheet/cheatsheet.rtl.css b/site/src/assets/examples/cheatsheet/cheatsheet.rtl.css new file mode 100644 index 000000000000..416e39fcaf66 --- /dev/null +++ b/site/src/assets/examples/cheatsheet/cheatsheet.rtl.css @@ -0,0 +1,156 @@ +body { + scroll-behavior: smooth; +} + +/** + * Bootstrap "Journal code" icon + * @link https://icons.getbootstrap.com/icons/journal-code/ + */ +.bd-heading a::before { + display: inline-block; + width: 1em; + height: 1em; + margin-left: .25rem; + content: ""; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M4 1h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1H2a2 2 0 0 1 2-2z'/%3E%3Cpath d='M2 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H2z'/%3E%3Cpath fill-rule='evenodd' d='M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z'/%3E%3C/svg%3E"); + background-size: 1em; +} + +/* stylelint-disable-next-line selector-max-universal */ +.bd-heading + div > * + * { + margin-top: 3rem; +} + +/* Table of contents */ +.bd-aside a { + padding: .1875rem .5rem; + margin-top: .125rem; + margin-right: .3125rem; + color: var(--bs-body-color); +} + +.bd-aside a:hover, +.bd-aside a:focus { + color: var(--bs-body-color); + background-color: rgba(121, 82, 179, .1); +} + +.bd-aside .active { + font-weight: 600; + color: var(--bs-body-color); +} + +.bd-aside .btn { + padding: .25rem .5rem; + font-weight: 600; + color: var(--bs-body-color); +} + +.bd-aside .btn:hover, +.bd-aside .btn:focus { + color: var(--bs-body-color); + background-color: rgba(121, 82, 179, .1); +} + +.bd-aside .btn:focus { + box-shadow: 0 0 0 1px rgba(121, 82, 179, .7); +} + +.bd-aside .btn::before { + width: 1.25em; + line-height: 0; + content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ccc' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + transition: transform .35s ease; + transform: rotate(180deg) translateX(-2px); + transform-origin: .5em 50%; +} + +.bd-aside .btn[aria-expanded="true"]::before { + transform: rotate(90deg); +} + + +/* Examples */ +.scrollspy-example { + height: 200px; +} + +[id="modal"] .bd-example .btn, +[id="buttons"] .bd-example .btn, +[id="tooltips"] .bd-example .btn, +[id="popovers"] .bd-example .btn, +[id="dropdowns"] .bd-example .btn-group, +[id="dropdowns"] .bd-example .dropdown, +[id="dropdowns"] .bd-example .dropup, +[id="dropdowns"] .bd-example .dropend, +[id="dropdowns"] .bd-example .dropstart { + margin: 0 0 1rem 1rem; +} + +/* Layout */ +@media (min-width: 1200px) { + body { + display: grid; + grid-template-rows: auto; + grid-template-columns: 1fr 4fr 1fr; + gap: 1rem; + } + + .bd-header { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; + grid-column: 1 / span 3; + } + + .bd-aside, + .bd-cheatsheet { + padding-top: 4rem; + } + + /** + * 1. Too bad only Firefox supports subgrids ATM + */ + .bd-cheatsheet, + .bd-cheatsheet section, + .bd-cheatsheet article { + display: inherit; /* 1 */ + grid-template-rows: auto; + grid-template-columns: 1fr 4fr; + grid-column: 1 / span 2; + gap: inherit; /* 1 */ + } + + .bd-aside { + grid-area: 1 / 3; + scroll-margin-top: 4rem; + } + + .bd-cheatsheet section, + .bd-cheatsheet section > h2 { + top: 2rem; + scroll-margin-top: 2rem; + } + + .bd-cheatsheet section > h2::before { + position: absolute; + top: 0; + right: 0; + bottom: -2rem; + left: 0; + z-index: -1; + content: ""; + } + + .bd-cheatsheet article, + .bd-cheatsheet .bd-heading { + top: 8rem; + scroll-margin-top: 8rem; + } + + .bd-cheatsheet .bd-heading { + z-index: 1; + } +} diff --git a/site/src/assets/examples/cheatsheet/index.astro b/site/src/assets/examples/cheatsheet/index.astro new file mode 100644 index 000000000000..3081df2b9c23 --- /dev/null +++ b/site/src/assets/examples/cheatsheet/index.astro @@ -0,0 +1,1562 @@ +--- +import { getData } from '@libs/data' +import { getVersionedDocsPath } from '@libs/path' +import Example from '@shortcodes/Example.astro' +import Placeholder from '@shortcodes/Placeholder.astro' + +export const title = 'Cheatsheet' +export const extra_css = ['cheatsheet.css'] +export const extra_js = [{ src: 'cheatsheet.js' }] +export const body_class = 'bg-body-tertiary' +--- + +
+
+

+ Bootstrap + Cheatsheet +

+ RTL cheatsheet +
+
+ +
+
+

Contents

+ +
+
+

Typography

+ Documentation +
+ +
+ Display 1

+

Display 2

+

Display 3

+

Display 4

+

Display 5

+

Display 6

`} /> + + Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+

Heading 5

+

Heading 6

`} /> + + + This is a lead paragraph. It stands out from regular paragraphs. +

`} /> + + You can use the mark tag to highlight text.

+

This line of text is meant to be treated as deleted text.

+

This line of text is meant to be treated as no longer accurate.

+

This line of text is meant to be treated as an addition to the document.

+

This line of text will render as underlined.

+

This line of text is meant to be treated as fine print.

+

This line rendered as bold text.

+

This line rendered as italicized text.

`} /> + + `} /> + + +

A well-known quote, contained in a blockquote element.

+
Someone famous in Source Title
+ `} /> + + +
  • This is a list.
  • +
  • It appears completely unstyled.
  • +
  • Structurally, it's still a list.
  • +
  • However, this style only applies to immediate child elements.
  • +
  • Nested lists: +
      +
    • are unaffected by this style
    • +
    • will still show a bullet
    • +
    • and have appropriate left margin
    • +
    +
  • +
  • This may still come in handy in some situations.
  • + `} /> + + +
  • This is a list item.
  • +
  • And another one.
  • +
  • But they're displayed inline.
  • + `} /> +
    +
    + +
    +
    +

    Tables

    + Documentation +
    +
    + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + @mdo + + + 2 + Jacob + Thornton + @fat + + + 3 + John + Doe + @social + + + `} /> + + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + @mdo + + + 2 + Jacob + Thornton + @fat + + + 3 + John + Doe + @social + + + `} /> + + + + + Class + Heading + Heading + + + + + Default + Cell + Cell + `, + ...getData('theme-colors').map((themeColor) => ` + ${themeColor.title} + Cell + Cell + `), + ` + `]} /> + + + + + # + First + Last + Handle + + + + + 1 + Mark + Otto + @mdo + + + 2 + Jacob + Thornton + @fat + + + 3 + John + Doe + @social + + + `} /> +
    +
    + +
    +
    +

    Figures

    + Documentation +
    + +
    + + +
    A caption for the above image.
    + `} /> +
    +
    +
    + +
    +

    Forms

    + +
    +
    +

    Overview

    + Documentation +
    + +
    + +
    + + +
    We'll never share your email with anyone else.
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + Radios buttons +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + + `} /> +
    +
    +
    +
    +

    Disabled forms

    + Documentation +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + Disabled radios buttons +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + `} /> +
    +
    +
    +
    +

    Sizing

    + Documentation +
    + +
    + + +
    +
    + +
    +
    + +
    `} /> + + + +
    +
    + +
    +
    + +
    + `} /> + + +
    +
    +

    Input group

    + Documentation +
    + +
    + + @ + +
    +
    + + @example.com +
    + +
    + https://example.com/users/ + +
    +
    + $ + + .00 +
    +
    + With textarea + +
    + `} /> + +
    +
    +
    +

    Floating labels

    + Documentation +
    + +
    + +
    + + +
    +
    + + +
    + + `} /> +
    +
    +
    +
    +

    Validation

    + Documentation +
    + +
    + +
    + + +
    + Looks good! +
    +
    +
    + + +
    + Looks good! +
    +
    +
    + +
    + @ + +
    + Please choose a username. +
    +
    +
    +
    + + +
    + Please provide a valid city. +
    +
    +
    + + +
    + Please select a valid state. +
    +
    +
    + + +
    + Please provide a valid zip. +
    +
    +
    +
    + + +
    + You must agree before submitting. +
    +
    +
    +
    + +
    + + `} /> +
    +
    + + +
    +

    Components

    + +
    +
    +

    Accordion

    + Documentation +
    + +
    + +
    +

    + +

    +
    +
    + This is the first item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
    +
    +
    +
    +

    + +

    +
    +
    + This is the second item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
    +
    +
    +
    +

    + +

    +
    +
    + This is the third item's accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
    +
    +
    +
    + `} /> + +
    +
    +
    +

    Alerts

    + Documentation +
    + +
    + ` + + `)} /> + + +

    Well done!

    +

    Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

    +
    +

    Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

    +
    + `} /> + +
    +
    +
    +

    Badge

    + Documentation +
    + +
    + Example heading New

    +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    + `} /> + + ` + ${themeColor.title} + `)} /> +
    +
    + +
    +
    +

    Buttons

    + Documentation +
    +
    + ` + + `), + ``]} /> + + ` + + `)} /> + + Small button + + + `} /> +
    +
    +
    +
    +

    Button group

    + Documentation +
    + +
    + +
    + + + + +
    +
    + + + +
    +
    + +
    +
    + `} /> + +
    +
    +
    +

    Card

    + Documentation +
    + +
    + +
    +
    + +
    +
    Card title
    +

    Some quick example text to build on the card title and make up the bulk of the card's content.

    + Go somewhere +
    +
    +
    +
    +
    +
    + Featured +
    +
    +
    Card title
    +

    Some quick example text to build on the card title and make up the bulk of the card's content.

    + Go somewhere +
    + +
    +
    +
    +
    +
    +
    Card title
    +

    Some quick example text to build on the card title and make up the bulk of the card's content.

    +
    +
      +
    • An item
    • +
    • A second item
    • +
    • A third item
    • +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    Card title
    +

    This is a wider card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

    +

    Last updated 3 mins ago

    +
    +
    +
    +
    +
    +
    + `} /> + +
    + + + + + + + +
    +
    +

    Popovers

    + Documentation +
    + +
    + Click to toggle popover + `} /> + + + Popover on top + + + + + `} /> +
    +
    +
    +
    +

    Progress

    + Documentation +
    + +
    + +
    0%
    +
    +
    +
    25%
    +
    +
    +
    50%
    +
    +
    +
    75%
    +
    +
    +
    100%
    +
    + `} /> + + +
    +
    +
    +
    +
    +
    + + `} /> + +
    +
    +
    +

    Scrollspy

    + Documentation +
    + +
    + + Navbar + + +
    +

    First heading

    +

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    +

    Second heading

    +

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    +

    Third heading

    +

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    +

    Fourth heading

    +

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    +

    Fifth heading

    +

    This is some placeholder content for the scrollspy page. Note that as you scroll down the page, the appropriate navigation link is highlighted. It's repeated throughout the component example. We keep adding some more example copy here to emphasize the scrolling and highlighting.

    +
    + `} /> +
    +
    +
    +
    +

    Spinners

    + Documentation +
    + +
    + ` +
    + Loading... +
    + `)} /> + + ` +
    + Loading... +
    + `)} /> +
    +
    +
    +
    +

    Toasts

    + Documentation +
    + +
    + +
    + + Bootstrap + 11 mins ago + +
    +
    + Hello, world! This is a toast message. +
    +
    + `} /> + +
    +
    +
    +

    Tooltips

    + Documentation +
    + +
    + Tooltip on top + + + + + `} /> +
    +
    +
    + + + + + + + diff --git a/site/src/assets/examples/checkout-rtl/index.astro b/site/src/assets/examples/checkout-rtl/index.astro new file mode 100644 index 000000000000..1900f2ce22ba --- /dev/null +++ b/site/src/assets/examples/checkout-rtl/index.astro @@ -0,0 +1,231 @@ +--- +import { getVersionedDocsPath } from '@libs/path' + +export const title = 'مثال إتمام الشراء' +export const direction = 'rtl' +export const extra_css = ['../checkout/checkout.css'] +export const extra_js = [{ src: '../checkout/checkout.js' }] +export const body_class = 'bg-body-tertiary' +--- + +
    +
    +
    + +

    نموذج إتمام الشراء

    +

    فيما يلي مثال على نموذج تم إنشاؤه بالكامل باستخدام عناصر تحكم النموذج في Bootstrap. لكل مجموعة نماذج مطلوبة حالة تحقق يمكن تشغيلها بمحاولة إرسال النموذج دون استكماله.

    +
    + +
    +
    +

    + عربة التسوق + 3 +

    +
      +
    • +
      +
      اسم المنتج
      + وصف مختصر +
      + $12 +
    • +
    • +
      +
      المنتج الثاني
      + وصف مختصر +
      + $8 +
    • +
    • +
      +
      البند الثالث
      + وصف مختصر +
      + $5 +
    • +
    • +
      +
      رمز ترويجي
      + EXAMPLECODE +
      + -$5 +
    • +
    • + مجموع (USD) + $20 +
    • +
    + +
    +
    + + +
    +
    +
    +
    +

    عنوان الفوترة

    +
    +
    +
    + + +
    + يرجى إدخال اسم أول صحيح. +
    +
    + +
    + + +
    + يرجى إدخال اسم عائلة صحيح. +
    +
    + +
    + +
    + @ + +
    + اسم المستخدم الخاص بك مطلوب. +
    +
    +
    + +
    + + +
    + يرجى إدخال عنوان بريد إلكتروني صحيح لتصلكم تحديثات الشحن. +
    +
    + +
    + + +
    + يرجى إدخال عنوان الشحن الخاص بك. +
    +
    + +
    + + +
    + +
    + + +
    + يرجى اختيار بلد صحيح. +
    +
    + +
    + + +
    + يرجى اختيار اسم منطقة صحيح. +
    +
    + +
    + + +
    + الرمز البريدي مطلوب. +
    +
    +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +

    طريقة الدفع

    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + + الاسم الكامل كما هو معروض على البطاقة +
    + الاسم على البطاقة مطلوب +
    +
    + +
    + + +
    + رقم بطاقة الائتمان مطلوب +
    +
    + +
    + + +
    + تاريخ انتهاء الصلاحية مطلوب +
    +
    + +
    + + +
    + رمز الحماية مطلوب +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    diff --git a/site/content/docs/5.0/examples/checkout/form-validation.css b/site/src/assets/examples/checkout/checkout.css similarity index 100% rename from site/content/docs/5.0/examples/checkout/form-validation.css rename to site/src/assets/examples/checkout/checkout.css diff --git a/site/src/assets/examples/checkout/checkout.js b/site/src/assets/examples/checkout/checkout.js new file mode 100644 index 000000000000..30ea0aa6b1e0 --- /dev/null +++ b/site/src/assets/examples/checkout/checkout.js @@ -0,0 +1,19 @@ +// Example starter JavaScript for disabling form submissions if there are invalid fields +(() => { + 'use strict' + + // Fetch all the forms we want to apply custom Bootstrap validation styles to + const forms = document.querySelectorAll('.needs-validation') + + // Loop over them and prevent submission + Array.from(forms).forEach(form => { + form.addEventListener('submit', event => { + if (!form.checkValidity()) { + event.preventDefault() + event.stopPropagation() + } + + form.classList.add('was-validated') + }, false) + }) +})() diff --git a/site/content/docs/5.0/examples/checkout/index.html b/site/src/assets/examples/checkout/index.astro similarity index 84% rename from site/content/docs/5.0/examples/checkout/index.html rename to site/src/assets/examples/checkout/index.astro index bc7db176905f..3ddc2ae36c92 100644 --- a/site/content/docs/5.0/examples/checkout/index.html +++ b/site/src/assets/examples/checkout/index.astro @@ -1,50 +1,49 @@ --- -layout: examples -title: Checkout example -extra_css: - - "form-validation.css" -extra_js: - - src: "form-validation.js" -body_class: "bg-light" +import { getVersionedDocsPath } from '@libs/path' + +export const title = 'Checkout example' +export const extra_css = ['checkout.css'] +export const extra_js = [{ src: 'checkout.js' }] +export const body_class = 'bg-body-tertiary' ---
    - -

    Checkout form

    + +

    Checkout form

    Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.

    -
    +

    - Your cart - 3 + Your cart + 3

    • Product name
      - Brief description + Brief description
      - $12 + $12
    • Second product
      - Brief description + Brief description
      - $8 + $8
    • Third item
      - Brief description + Brief description
      - $5 + $5
    • -
    • +
    • Promo code
      EXAMPLECODE @@ -86,7 +85,7 @@

      Billing address

      -
      +
      @
      @@ -96,7 +95,7 @@

      Billing address

      - +
      Please enter a valid email address for shipping updates. @@ -112,7 +111,7 @@

      Billing address

      - +
      @@ -182,7 +181,7 @@

      Payment

      - Full name as displayed on card + Full name as displayed on card
      Name on card is required
      @@ -215,14 +214,14 @@

      Payment


      - +
    -
    -

    © 2017–{{< year >}} Company Name

    +
    +

    © 2017–{new Date().getFullYear()} Company Name

    • Privacy
    • Terms
    • diff --git a/site/content/docs/5.0/examples/cover/cover.css b/site/src/assets/examples/cover/cover.css similarity index 83% rename from site/content/docs/5.0/examples/cover/cover.css rename to site/src/assets/examples/cover/cover.css index 87afc3be9483..2e7aef8f88c3 100644 --- a/site/content/docs/5.0/examples/cover/cover.css +++ b/site/src/assets/examples/cover/cover.css @@ -4,9 +4,9 @@ /* Custom default button */ -.btn-secondary, -.btn-secondary:hover, -.btn-secondary:focus { +.btn-light, +.btn-light:hover, +.btn-light:focus { color: #333; text-shadow: none; /* Prevent inheritance from `body` */ } @@ -31,10 +31,7 @@ body { */ .nav-masthead .nav-link { - padding: .25rem 0; - font-weight: 700; color: rgba(255, 255, 255, .5); - background-color: transparent; border-bottom: .25rem solid transparent; } diff --git a/site/src/assets/examples/cover/index.astro b/site/src/assets/examples/cover/index.astro new file mode 100644 index 000000000000..3af731501617 --- /dev/null +++ b/site/src/assets/examples/cover/index.astro @@ -0,0 +1,31 @@ +--- +export const title = 'Cover Template' +export const extra_css = ['cover.css'] +export const html_class = 'h-100' +export const body_class = 'd-flex h-100 text-center text-bg-dark' +--- + +
      +
      +
      +

      Cover

      + +
      +
      + +
      +

      Cover your page.

      +

      Cover is a one-page template for building simple and beautiful home pages. Download, edit the text, and add your own fullscreen background photo to make it your own.

      +

      + Learn more +

      +
      + + +
      diff --git a/site/src/assets/examples/dashboard-rtl/dashboard.js b/site/src/assets/examples/dashboard-rtl/dashboard.js new file mode 100644 index 000000000000..590eb3e38486 --- /dev/null +++ b/site/src/assets/examples/dashboard-rtl/dashboard.js @@ -0,0 +1,48 @@ +/* globals Chart:false */ + +(() => { + 'use strict' + + // Graphs + const ctx = document.getElementById('myChart') + new Chart(ctx, { + type: 'line', + data: { + labels: [ + 'الأحد', + 'الإثنين', + 'الثلاثاء', + 'الأربعاء', + 'الخميس', + 'الجمعة', + 'السبت' + ], + datasets: [{ + data: [ + 15339, + 21345, + 18483, + 24003, + 23489, + 24092, + 12034 + ], + lineTension: 0, + backgroundColor: 'transparent', + borderColor: '#007bff', + borderWidth: 4, + pointBackgroundColor: '#007bff' + }] + }, + options: { + plugins: { + legend: { + display: false + }, + tooltip: { + boxPadding: 3 + } + } + } + }) +})() diff --git a/site/src/assets/examples/dashboard-rtl/index.astro b/site/src/assets/examples/dashboard-rtl/index.astro new file mode 100644 index 000000000000..d94c71b15cd0 --- /dev/null +++ b/site/src/assets/examples/dashboard-rtl/index.astro @@ -0,0 +1,330 @@ +--- +export const title = 'قالب لوحة القيادة' +export const direction = 'rtl' +export const extra_css = ['../dashboard/dashboard.rtl.css'] +export const extra_js = [ + { src: 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js', integrity: 'sha384-eI7PSr3L1XLISH8JdDII5YN/njoSsxfbrkCTnJrzXt+ENP5MOVBxD+l6sEG4zoLp'}, + { src: 'dashboard.js'} +] +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + +
      +
      +

      لوحة القيادة

      +
      +
      + + +
      + +
      +
      + + + +

      عنوان القسم

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      #عنوانعنوانعنوانعنوان
      1,001بياناتعشوائيةتثريالجدول
      1,002تثريمبهةتصميمتنسيق
      1,003عشوائيةغنيةقيمةمفيدة
      1,003معلوماتتثريتوضيحيةعشوائية
      1,004الجدولبياناتتنسيققيمة
      1,005قيمةمبهةالجدولتثري
      1,006قيمةتوضيحيةغنيةعشوائية
      1,007تثريمفيدةمعلوماتمبهة
      1,008بياناتعشوائيةتثريالجدول
      1,009تثريمبهةتصميمتنسيق
      1,010عشوائيةغنيةقيمةمفيدة
      1,011معلوماتتثريتوضيحيةعشوائية
      1,012الجدولتثريتنسيققيمة
      1,013قيمةمبهةالجدولتصميم
      1,014قيمةتوضيحيةغنيةعشوائية
      1,015بياناتمفيدةمعلوماتالجدول
      +
      +
      +
      +
      diff --git a/site/src/assets/examples/dashboard/dashboard.css b/site/src/assets/examples/dashboard/dashboard.css new file mode 100644 index 000000000000..154940c90b0b --- /dev/null +++ b/site/src/assets/examples/dashboard/dashboard.css @@ -0,0 +1,48 @@ +.bi { + display: inline-block; + width: 1rem; + height: 1rem; +} + +/* + * Sidebar + */ + +@media (min-width: 768px) { + .sidebar .offcanvas-lg { + position: -webkit-sticky; + position: sticky; + top: 48px; + } + .navbar-search { + display: block; + } +} + +.sidebar .nav-link { + font-size: .875rem; + font-weight: 500; +} + +.sidebar .nav-link.active { + color: #2470dc; +} + +.sidebar-heading { + font-size: .75rem; +} + +/* + * Navbar + */ + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); +} + +.navbar .form-control { + padding: .75rem 1rem; +} diff --git a/site/content/docs/5.0/examples/dashboard/dashboard.js b/site/src/assets/examples/dashboard/dashboard.js similarity index 62% rename from site/content/docs/5.0/examples/dashboard/dashboard.js rename to site/src/assets/examples/dashboard/dashboard.js index d3f54992850e..0bae98736b8f 100644 --- a/site/content/docs/5.0/examples/dashboard/dashboard.js +++ b/site/src/assets/examples/dashboard/dashboard.js @@ -1,14 +1,11 @@ -/* globals Chart:false, feather:false */ +/* globals Chart:false */ -(function () { +(() => { 'use strict' - feather.replace() - // Graphs - var ctx = document.getElementById('myChart') - // eslint-disable-next-line no-unused-vars - var myChart = new Chart(ctx, { + const ctx = document.getElementById('myChart') + new Chart(ctx, { type: 'line', data: { labels: [ @@ -38,15 +35,13 @@ }] }, options: { - scales: { - yAxes: [{ - ticks: { - beginAtZero: false - } - }] - }, - legend: { - display: false + plugins: { + legend: { + display: false + }, + tooltip: { + boxPadding: 3 + } } } }) diff --git a/site/src/assets/examples/dashboard/dashboard.rtl.css b/site/src/assets/examples/dashboard/dashboard.rtl.css new file mode 100644 index 000000000000..5c8a7e25719c --- /dev/null +++ b/site/src/assets/examples/dashboard/dashboard.rtl.css @@ -0,0 +1,48 @@ +.bi { + display: inline-block; + width: 1rem; + height: 1rem; +} + +/* + * Sidebar + */ + +@media (min-width: 768px) { + .sidebar .offcanvas-lg { + position: -webkit-sticky; + position: sticky; + top: 48px; + } + .navbar-search { + display: block; + } +} + +.sidebar .nav-link { + font-size: .875rem; + font-weight: 500; +} + +.sidebar .nav-link.active { + color: #2470dc; +} + +.sidebar-heading { + font-size: .75rem; +} + +/* + * Navbar + */ + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, .25); +} + +.navbar .form-control { + padding: .75rem 1rem; +} diff --git a/site/src/assets/examples/dashboard/index.astro b/site/src/assets/examples/dashboard/index.astro new file mode 100644 index 000000000000..73b47aa5ff92 --- /dev/null +++ b/site/src/assets/examples/dashboard/index.astro @@ -0,0 +1,329 @@ +--- +export const title = 'Dashboard Template' +export const extra_css = ['dashboard.css'] +export const extra_js = [ + { src: 'https://cdn.jsdelivr.net/npm/chart.js@4.3.2/dist/chart.umd.js', integrity: 'sha384-eI7PSr3L1XLISH8JdDII5YN/njoSsxfbrkCTnJrzXt+ENP5MOVBxD+l6sEG4zoLp'}, + { src: 'dashboard.js'} +] +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      + + +
      +
      +

      Dashboard

      +
      +
      + + +
      + +
      +
      + + + +

      Section title

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      #HeaderHeaderHeaderHeader
      1,001randomdataplaceholdertext
      1,002placeholderirrelevantvisuallayout
      1,003datarichdashboardtabular
      1,003informationplaceholderillustrativedata
      1,004textrandomlayoutdashboard
      1,005dashboardirrelevanttextplaceholder
      1,006dashboardillustrativerichdata
      1,007placeholdertabularinformationirrelevant
      1,008randomdataplaceholdertext
      1,009placeholderirrelevantvisuallayout
      1,010datarichdashboardtabular
      1,011informationplaceholderillustrativedata
      1,012textplaceholderlayoutdashboard
      1,013dashboardirrelevanttextvisual
      1,014dashboardillustrativerichdata
      1,015randomtabularinformationtext
      +
      +
      +
      +
      diff --git a/site/src/assets/examples/dropdowns/dropdowns.css b/site/src/assets/examples/dropdowns/dropdowns.css new file mode 100644 index 000000000000..f633e2cd8373 --- /dev/null +++ b/site/src/assets/examples/dropdowns/dropdowns.css @@ -0,0 +1,71 @@ +.dropdown-item-danger { + color: var(--bs-red); +} +.dropdown-item-danger:hover, +.dropdown-item-danger:focus { + color: #fff; + background-color: var(--bs-red); +} +.dropdown-item-danger.active { + background-color: var(--bs-red); +} + +.btn-hover-light { + color: var(--bs-body-color); + background-color: var(--bs-body-bg); +} +.btn-hover-light:hover, +.btn-hover-light:focus { + color: var(--bs-link-hover-color); + background-color: var(--bs-tertiary-bg); +} + +.cal-month, +.cal-days, +.cal-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; +} +.cal-month-name { + grid-column-start: 2; + grid-column-end: 7; + text-align: center; +} +.cal-weekday, +.cal-btn { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + height: 3rem; + padding: 0; +} +.cal-btn:not([disabled]) { + font-weight: 500; + color: var(--bs-emphasis-color); +} +.cal-btn:hover, +.cal-btn:focus { + background-color: var(--bs-secondary-bg); +} +.cal-btn[disabled] { + border: 0; + opacity: .5; +} + +.w-220px { + width: 220px; +} + +.w-280px { + width: 280px; +} + +.w-340px { + width: 340px; +} + +.opacity-10 { + opacity: .1; +} diff --git a/site/src/assets/examples/dropdowns/index.astro b/site/src/assets/examples/dropdowns/index.astro new file mode 100644 index 000000000000..812109e41688 --- /dev/null +++ b/site/src/assets/examples/dropdowns/index.astro @@ -0,0 +1,459 @@ +--- +export const title = 'Dropdowns' +export const extra_css = ['dropdowns.css'] +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + +
      + +
      + + + +
      + +
      + + diff --git a/site/src/assets/examples/features/features.css b/site/src/assets/examples/features/features.css new file mode 100644 index 000000000000..debc26364745 --- /dev/null +++ b/site/src/assets/examples/features/features.css @@ -0,0 +1,26 @@ +.feature-icon { + width: 4rem; + height: 4rem; + border-radius: .75rem; +} + +.icon-square { + width: 3rem; + height: 3rem; + border-radius: .75rem; +} + +.text-shadow-1 { text-shadow: 0 .125rem .25rem rgba(0, 0, 0, .25); } +.text-shadow-2 { text-shadow: 0 .25rem .5rem rgba(0, 0, 0, .25); } +.text-shadow-3 { text-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .25); } + +.card-cover { + background-repeat: no-repeat; + background-position: center center; + background-size: cover; +} + +.feature-icon-small { + width: 3rem; + height: 3rem; +} diff --git a/site/src/assets/examples/features/index.astro b/site/src/assets/examples/features/index.astro new file mode 100644 index 000000000000..7a3a7640d6bc --- /dev/null +++ b/site/src/assets/examples/features/index.astro @@ -0,0 +1,337 @@ +--- +export const title = 'Features' +export const extra_css = ['features.css'] +--- + + + + Bootstrap + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +

      Features examples

      + + + +
      + +
      +

      Hanging icons

      +
      +
      +
      + +
      +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

      + + Primary button + +
      +
      +
      +
      + +
      +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

      + + Primary button + +
      +
      +
      +
      + +
      +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

      + + Primary button + +
      +
      +
      +
      + +
      + +
      +

      Custom cards

      + +
      +
      +
      +
      +

      Short title, long jacket

      +
        +
      • + Bootstrap +
      • +
      • + + Earth +
      • +
      • + + 3d +
      • +
      +
      +
      +
      + +
      +
      +
      +

      Much longer title that wraps to multiple lines

      +
        +
      • + Bootstrap +
      • +
      • + + Pakistan +
      • +
      • + + 4d +
      • +
      +
      +
      +
      + +
      +
      +
      +

      Another longer title belongs here

      +
        +
      • + Bootstrap +
      • +
      • + + California +
      • +
      • + + 5d +
      • +
      +
      +
      +
      +
      +
      + +
      + +
      +

      Icon grid

      + +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      +
      + +
      + +
      +

      Features with title

      + +
      +
      +

      Left-aligned title explaining these awesome features

      +

      Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words.

      + Primary button +
      + +
      +
      +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      + +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      + +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      + +
      +
      + +
      +

      Featured title

      +

      Paragraph of text beneath the heading to explain the heading.

      +
      +
      +
      +
      +
      +
      diff --git a/site/src/assets/examples/features/unsplash-photo-1.jpg b/site/src/assets/examples/features/unsplash-photo-1.jpg new file mode 100644 index 000000000000..283acd0b4cad Binary files /dev/null and b/site/src/assets/examples/features/unsplash-photo-1.jpg differ diff --git a/site/src/assets/examples/features/unsplash-photo-2.jpg b/site/src/assets/examples/features/unsplash-photo-2.jpg new file mode 100644 index 000000000000..81eae64d8ef7 Binary files /dev/null and b/site/src/assets/examples/features/unsplash-photo-2.jpg differ diff --git a/site/src/assets/examples/features/unsplash-photo-3.jpg b/site/src/assets/examples/features/unsplash-photo-3.jpg new file mode 100644 index 000000000000..0f401d1e12d1 Binary files /dev/null and b/site/src/assets/examples/features/unsplash-photo-3.jpg differ diff --git a/site/src/assets/examples/footers/index.astro b/site/src/assets/examples/footers/index.astro new file mode 100644 index 000000000000..9aaaa9f6884e --- /dev/null +++ b/site/src/assets/examples/footers/index.astro @@ -0,0 +1,179 @@ +--- +export const title = 'Footers' +--- + + + + Bootstrap + + + + + + + + + + +
      + +
      + +
      + +
      +
      +
      + + + + © {new Date().getFullYear()} Company, Inc +
      + + +
      +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      diff --git a/site/src/assets/examples/grid/grid.css b/site/src/assets/examples/grid/grid.css new file mode 100644 index 000000000000..cbc7c311fb1b --- /dev/null +++ b/site/src/assets/examples/grid/grid.css @@ -0,0 +1,13 @@ +.themed-grid-col { + padding-top: .75rem; + padding-bottom: .75rem; + background-color: rgba(112.520718, 44.062154, 249.437846, .15); + border: 1px solid rgba(112.520718, 44.062154, 249.437846, .3); +} + +.themed-container { + padding: .75rem; + margin-bottom: 1.5rem; + background-color: rgba(112.520718, 44.062154, 249.437846, .15); + border: 1px solid rgba(112.520718, 44.062154, 249.437846, .3); +} diff --git a/site/content/docs/5.0/examples/grid/index.html b/site/src/assets/examples/grid/index.astro similarity index 81% rename from site/content/docs/5.0/examples/grid/index.html rename to site/src/assets/examples/grid/index.astro index f26829b4f97c..2c01d8de9dbf 100644 --- a/site/content/docs/5.0/examples/grid/index.html +++ b/site/src/assets/examples/grid/index.astro @@ -1,10 +1,7 @@ --- -layout: examples -title: Grid Template -extra_css: - - "grid.css" -body_class: "py-4" -include_js: false +export const title = 'Grid Template' +export const extra_css = ['grid.css'] +export const body_class = 'py-4' ---
      @@ -17,37 +14,37 @@

      Bootstrap grid examples

      Five grid tiers

      There are five tiers to the Bootstrap grid system, one for each range of devices we support. Each tier starts at a minimum viewport size and automatically applies to the larger devices unless overridden.

      -
      +
      .col-4
      .col-4
      .col-4
      -
      +
      .col-sm-4
      .col-sm-4
      .col-sm-4
      -
      +
      .col-md-4
      .col-md-4
      .col-md-4
      -
      +
      .col-lg-4
      .col-lg-4
      .col-lg-4
      -
      +
      .col-xl-4
      .col-xl-4
      .col-xl-4
      -
      +
      .col-xxl-4
      .col-xxl-4
      .col-xxl-4
      @@ -55,7 +52,7 @@

      Five grid tiers

      Three equal columns

      Get three equal-width columns starting at desktops and scaling to large desktops. On mobile devices, tablets and below, the columns will automatically stack.

      -
      +
      .col-md-4
      .col-md-4
      .col-md-4
      @@ -63,7 +60,7 @@

      Three equal columns

      Three equal columns alternative

      By using the .row-cols-* classes, you can easily create a grid with equal columns.

      -
      +
      .col child of .row-cols-md-3
      .col child of .row-cols-md-3
      .col child of .row-cols-md-3
      @@ -71,7 +68,7 @@

      Three equal columns alternative

      Three unequal columns

      Get three columns starting at desktops and scaling to large desktops of various widths. Remember, grid columns should add up to twelve for a single horizontal block. More than that, and columns start stacking no matter the viewport.

      -
      +
      .col-md-3
      .col-md-6
      .col-md-3
      @@ -79,7 +76,7 @@

      Three unequal columns

      Two columns

      Get two columns starting at desktops and scaling to large desktops.

      -
      +
      .col-md-8
      .col-md-4
      @@ -94,7 +91,7 @@

      Full width, single column

      Two columns with two nested columns

      Per the documentation, nesting is easy—just put a row of columns within an existing column. This gives you two columns starting at desktops and scaling to large desktops, with another two (equal widths) within the larger column.

      At mobile device sizes, tablets and down, these columns and their nested columns will stack.

      -
      +
      .col-md-8 @@ -110,18 +107,18 @@

      Two columns with two nested columns


      Mixed: mobile and desktop

      -

      The Bootstrap v4 grid system has five tiers of classes: xs (extra small, this class infix is not used), sm (small), md (medium), lg (large), and xl (extra large). You can use nearly any combination of these classes to create more dynamic and flexible layouts.

      -

      Each tier of classes scales up, meaning if you plan on setting the same widths for md, lg and xl, you only need to specify md.

      -
      +

      The Bootstrap v5 grid system has six tiers of classes: xs (extra small, this class infix is not used), sm (small), md (medium), lg (large), xl (x-large), and xxl (xx-large). You can use nearly any combination of these classes to create more dynamic and flexible layouts.

      +

      Each tier of classes scales up, meaning if you plan on setting the same widths for md, lg, xl and xxl, you only need to specify md.

      +
      .col-md-8
      .col-6 .col-md-4
      -
      +
      .col-6 .col-md-4
      .col-6 .col-md-4
      .col-6 .col-md-4
      -
      +
      .col-6
      .col-6
      @@ -129,11 +126,11 @@

      Mixed: mobile and desktop


      Mixed: mobile, tablet, and desktop

      -
      +
      .col-sm-6 .col-lg-8
      .col-6 .col-lg-4
      -
      +
      .col-6 .col-sm-4
      .col-6 .col-sm-4
      .col-6 .col-sm-4
      @@ -143,7 +140,7 @@

      Mixed: mobile, tablet, and desktop

      Gutters

      With .gx-* classes, the horizontal gutters can be adjusted.

      -
      +
      .col with .gx-4 gutters
      .col with .gx-4 gutters
      .col with .gx-4 gutters
      @@ -152,7 +149,7 @@

      Gutters

      .col with .gx-4 gutters

      Use the .gy-* classes to control the vertical gutters.

      -
      +
      .col with .gy-4 gutters
      .col with .gy-4 gutters
      .col with .gy-4 gutters
      @@ -161,7 +158,7 @@

      Gutters

      .col with .gy-4 gutters

      With .g-* classes, the gutters in both directions can be adjusted.

      -
      +
      .col with .g-3 gutters
      .col with .g-3 gutters
      .col with .g-3 gutters
      @@ -178,11 +175,11 @@

      Containers

      Additional classes added in Bootstrap v4.4 allow containers that are 100% wide until a particular breakpoint. v5 adds a new xxl breakpoint.

      -
      .container
      -
      .container-sm
      -
      .container-md
      -
      .container-lg
      -
      .container-xl
      -
      .container-xxl
      -
      .container-fluid
      +
      .container
      +
      .container-sm
      +
      .container-md
      +
      .container-lg
      +
      .container-xl
      +
      .container-xxl
      +
      .container-fluid
      diff --git a/site/src/assets/examples/headers/headers.css b/site/src/assets/examples/headers/headers.css new file mode 100644 index 000000000000..f887573febf5 --- /dev/null +++ b/site/src/assets/examples/headers/headers.css @@ -0,0 +1,15 @@ +.form-control-dark { + border-color: var(--bs-gray); +} +.form-control-dark:focus { + border-color: #fff; + box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25); +} + +.text-small { + font-size: 85%; +} + +.dropdown-toggle:not(:focus) { + outline: 0; +} diff --git a/site/src/assets/examples/headers/index.astro b/site/src/assets/examples/headers/index.astro new file mode 100644 index 000000000000..83f64cb0d704 --- /dev/null +++ b/site/src/assets/examples/headers/index.astro @@ -0,0 +1,294 @@ +--- +export const title = 'Headers' +export const extra_css = ['headers.css'] +--- + + + + Bootstrap + + + + + + + + + + + + + + + + + + + + + +
      +

      Headers examples

      + + + +
      + +
      +
      + +
      +
      + +
      + +
      +
      +
      + + + +
      + + + +
      + + +
      +
      +
      + +
      + +
      +
      +
      + + + + + + + + +
      + + +
      +
      +
      +
      + +
      + +
      +
      +
      + + + + + + + + + +
      +
      +
      + +
      + +
      +
      + + +
      + + + +
      +
      +
      + +
      +
      +
      +









      +
      +
      +









      +
      +
      +
      + +
      + + +
      + +
      + +
      + +
      + +
      +
      + + +
      + + +
      +
      +
      +
      + +
      +
      diff --git a/site/src/assets/examples/heroes/bootstrap-docs.png b/site/src/assets/examples/heroes/bootstrap-docs.png new file mode 100644 index 000000000000..a4e9b9864a27 Binary files /dev/null and b/site/src/assets/examples/heroes/bootstrap-docs.png differ diff --git a/site/static/docs/5.0/assets/img/bootstrap-themes@2x.png b/site/src/assets/examples/heroes/bootstrap-themes.png similarity index 100% rename from site/static/docs/5.0/assets/img/bootstrap-themes@2x.png rename to site/src/assets/examples/heroes/bootstrap-themes.png diff --git a/site/src/assets/examples/heroes/heroes.css b/site/src/assets/examples/heroes/heroes.css new file mode 100644 index 000000000000..e9deaf744f34 --- /dev/null +++ b/site/src/assets/examples/heroes/heroes.css @@ -0,0 +1,3 @@ +@media (min-width: 992px) { + .rounded-lg-3 { border-radius: .3rem; } +} diff --git a/site/src/assets/examples/heroes/index.astro b/site/src/assets/examples/heroes/index.astro new file mode 100644 index 000000000000..d4eb7480485d --- /dev/null +++ b/site/src/assets/examples/heroes/index.astro @@ -0,0 +1,125 @@ +--- +import { getVersionedDocsPath } from '@libs/path' + +export const title = 'Heroes' +export const extra_css = ['heroes.css'] +--- + +
      +

      Heroes examples

      + +
      + +

      Centered hero

      +
      +

      Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

      +
      + + +
      +
      +
      + +
      + +
      +

      Centered screenshot

      +
      +

      Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

      +
      + + +
      +
      +
      +
      + Example image +
      +
      +
      + +
      + +
      +
      +
      + Bootstrap Themes +
      +
      +

      Responsive left-aligned hero with image

      +

      Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

      +
      + + +
      +
      +
      +
      + +
      + +
      +
      +
      +

      Vertically centered hero sign-up form

      +

      Below is an example form built entirely with Bootstrap’s form controls. Each required form group has a validation state that can be triggered by attempting to submit the form without completing it.

      +
      +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      + +
      + By clicking Sign up, you agree to the terms of use. +
      +
      +
      +
      + +
      + +
      +
      +
      +

      Border hero with cropped image and shadows

      +

      Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

      +
      + + +
      +
      +
      + +
      +
      +
      + +
      + +
      +
      +

      Dark color hero

      +
      +

      Quickly design and customize responsive mobile-first sites with Bootstrap, the world’s most popular front-end open source toolkit, featuring Sass variables and mixins, responsive grid system, extensive prebuilt components, and powerful JavaScript plugins.

      +
      + + +
      +
      +
      +
      + +
      +
      diff --git a/site/src/assets/examples/jumbotron/index.astro b/site/src/assets/examples/jumbotron/index.astro new file mode 100644 index 000000000000..4de2b8da389a --- /dev/null +++ b/site/src/assets/examples/jumbotron/index.astro @@ -0,0 +1,43 @@ +--- +export const title = 'Jumbotron example' +--- + +
      +
      +
      + + Bootstrap + Jumbotron example + +
      + +
      +
      +

      Custom jumbotron

      +

      Using a series of utilities, you can create this jumbotron, just like the one in previous versions of Bootstrap. Check out the examples below for how you can remix and restyle it to your liking.

      + +
      +
      + +
      +
      +
      +

      Change the background

      +

      Swap the background-color utility and add a .text-* color utility to mix up the jumbotron look. Then, mix and match with additional component themes and more.

      + +
      +
      +
      +
      +

      Add borders

      +

      Or, keep it light and add a border for some added definition to the boundaries of your content. Be sure to look under the hood at the source HTML here as we've adjusted the alignment and sizing of both column's content for equal-height.

      + +
      +
      +
      + +
      + © {new Date().getFullYear()} +
      +
      +
      diff --git a/site/src/assets/examples/jumbotrons/index.astro b/site/src/assets/examples/jumbotrons/index.astro new file mode 100644 index 000000000000..fb9058fc0c4d --- /dev/null +++ b/site/src/assets/examples/jumbotrons/index.astro @@ -0,0 +1,79 @@ +--- +export const title = 'Jumbotrons' +export const extra_css = ['jumbotrons.css'] +--- + + + + Bootstrap + + + + + + + + + + + +
      +
      + +

      Jumbotron with icon

      +

      + This is a custom jumbotron featuring an SVG image at the top, some longer text that wraps early thanks to a responsive .col-* class, and a customized call to action. +

      +
      + + +
      +
      +
      + +
      + +
      +
      + + +

      Placeholder jumbotron

      +

      + This faded back jumbotron is useful for placeholder content. It's also a great way to add a bit of context to a page or section when no content is available and to encourage visitors to take a specific action. +

      + +
      +
      + +
      + +
      +
      +
      +

      Full-width jumbotron

      +

      + This takes the basic jumbotron above and makes its background edge-to-edge with a .container inside to align content. Similar to above, it's been recreated with built-in grid and utility classes. +

      +
      +
      +
      + +
      + +
      +
      +

      Basic jumbotron

      +

      + This is a simple Bootstrap jumbotron that sits within a .container, recreated with built-in utility classes. +

      +
      +
      + +
      diff --git a/site/src/assets/examples/jumbotrons/jumbotrons.css b/site/src/assets/examples/jumbotrons/jumbotrons.css new file mode 100644 index 000000000000..d7d065ed64b6 --- /dev/null +++ b/site/src/assets/examples/jumbotrons/jumbotrons.css @@ -0,0 +1 @@ +.border-dashed { --bs-border-style: dashed; } diff --git a/site/src/assets/examples/list-groups/index.astro b/site/src/assets/examples/list-groups/index.astro new file mode 100644 index 000000000000..220678f07ef4 --- /dev/null +++ b/site/src/assets/examples/list-groups/index.astro @@ -0,0 +1,222 @@ +--- +export const title = 'List groups' +export const extra_css = ['list-groups.css'] +--- + + + + + + + + + + + + + + + + + + + +
      + +
      +
      + + + +
      + +
      + + + +
      +
      + +
      + +
      +
      + + + + +
      +
      + +
      + +
      +
      + + + + + + + + + + + +
      +
      + +
      + +
      +
      +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      diff --git a/site/src/assets/examples/list-groups/list-groups.css b/site/src/assets/examples/list-groups/list-groups.css new file mode 100644 index 000000000000..b90cfa065533 --- /dev/null +++ b/site/src/assets/examples/list-groups/list-groups.css @@ -0,0 +1,63 @@ +.list-group { + width: 100%; + max-width: 460px; + margin-inline: 1.5rem; +} + +.form-check-input:checked + .form-checked-content { + opacity: .5; +} + +.form-check-input-placeholder { + border-style: dashed; +} +[contenteditable]:focus { + outline: 0; +} + +.list-group-checkable .list-group-item { + cursor: pointer; +} +.list-group-item-check { + position: absolute; + clip: rect(0, 0, 0, 0); +} +.list-group-item-check:hover + .list-group-item { + background-color: var(--bs-secondary-bg); +} +.list-group-item-check:checked + .list-group-item { + color: #fff; + background-color: var(--bs-primary); + border-color: var(--bs-primary); +} +.list-group-item-check[disabled] + .list-group-item, +.list-group-item-check:disabled + .list-group-item { + pointer-events: none; + filter: none; + opacity: .5; +} + +.list-group-radio .list-group-item { + cursor: pointer; + border-radius: .5rem; +} +.list-group-radio .form-check-input { + z-index: 2; + margin-top: -.5em; +} +.list-group-radio .list-group-item:hover, +.list-group-radio .list-group-item:focus { + background-color: var(--bs-secondary-bg); +} + +.list-group-radio .form-check-input:checked + .list-group-item { + background-color: var(--bs-body); + border-color: var(--bs-primary); + box-shadow: 0 0 0 2px var(--bs-primary); +} +.list-group-radio .form-check-input[disabled] + .list-group-item, +.list-group-radio .form-check-input:disabled + .list-group-item { + pointer-events: none; + filter: none; + opacity: .5; +} diff --git a/site/content/docs/5.0/examples/masonry/index.html b/site/src/assets/examples/masonry/index.astro similarity index 61% rename from site/content/docs/5.0/examples/masonry/index.html rename to site/src/assets/examples/masonry/index.astro index ceb3790ec9e2..b31254920707 100644 --- a/site/content/docs/5.0/examples/masonry/index.html +++ b/site/src/assets/examples/masonry/index.astro @@ -1,10 +1,11 @@ --- -layout: examples -title: Masonry example -extra_js: - - src: "https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js" - integrity: "sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" - async: true +export const title = 'Masonry example' +export const extra_js = [{ + src: 'https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js', + integrity: 'sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D', + async: true +}] +import Placeholder from "@shortcodes/Placeholder.astro" ---
      @@ -14,17 +15,17 @@

      Bootstrap and Masonry

      Masonry is not included in Bootstrap. Add it by including the JavaScript plugin manually, or using a CDN like so:

      
      -<script src="https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
      +<script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script>
         
      -

      By adding data-masonry='{"percentPosition": true }' to the .row wrapper, we can combine the powers of Bootstrap's responsive grid and Masonry's positioning.

      +

      By adding data-masonry='{"percentPosition": true }' to the .row wrapper, we can combine the powers of Bootstrap's responsive grid and Masonry's positioning.


      - {{< placeholder width="100%" height="200" class="card-img-top" text="Image cap" >}} +
      Card title that wraps to a new line

      This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.

      @@ -35,9 +36,9 @@
      Card title that wraps to a new line
      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

      +

      A well-known quote, contained in a blockquote element.

      -
      @@ -45,19 +46,19 @@
      Card title that wraps to a new line
      - {{< placeholder width="100%" height="200" class="card-img-top" text="Image cap" >}} +
      Card title

      This card has supporting text below as a natural lead-in to additional content.

      -

      Last updated 3 mins ago

      +

      Last updated 3 mins ago

      -
      +
      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat.

      +

      A well-known quote, contained in a blockquote element.

      - {{< placeholder width="100%" height="260" class="card-img" text="Card image" >}} +
      -
      +
      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.

      +

      A well-known quote, contained in a blockquote element.

      -
      @@ -96,7 +97,7 @@
      Card title
      Card title

      This is another card with title and supporting text below. This card has some additional content to make it slightly taller overall.

      -

      Last updated 3 mins ago

      +

      Last updated 3 mins ago

      diff --git a/site/src/assets/examples/modals/index.astro b/site/src/assets/examples/modals/index.astro new file mode 100644 index 000000000000..9514f6f1fb0b --- /dev/null +++ b/site/src/assets/examples/modals/index.astro @@ -0,0 +1,147 @@ +--- +export const title = 'Modals' +export const extra_css = ['modals.css'] +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + +
      + + + +
      + + + +
      diff --git a/site/src/assets/examples/modals/modals.css b/site/src/assets/examples/modals/modals.css new file mode 100644 index 000000000000..194e16aca6eb --- /dev/null +++ b/site/src/assets/examples/modals/modals.css @@ -0,0 +1,7 @@ +.modal-sheet .modal-dialog { + width: 380px; + transition: bottom .75s ease-in-out; +} +.modal-sheet .modal-footer { + padding-bottom: 2rem; +} diff --git a/site/content/docs/5.0/examples/navbar-bottom/index.html b/site/src/assets/examples/navbar-bottom/index.astro similarity index 50% rename from site/content/docs/5.0/examples/navbar-bottom/index.html rename to site/src/assets/examples/navbar-bottom/index.astro index 49833a686775..251f475595f3 100644 --- a/site/content/docs/5.0/examples/navbar-bottom/index.html +++ b/site/src/assets/examples/navbar-bottom/index.astro @@ -1,35 +1,36 @@ --- -layout: examples -title: Bottom navbar example +import { getVersionedDocsPath } from '@libs/path' + +export const title = 'Bottom navbar example' ---
      -
      +

      Bottom Navbar example

      This example is a quick exercise to illustrate how the bottom navbar works.

      - }}" role="button">View navbar docs » + View navbar docs »
      -
      + diff --git a/site/src/components/icons/BootstrapWhiteFillIcon.astro b/site/src/components/icons/BootstrapWhiteFillIcon.astro new file mode 100644 index 000000000000..ef40e6a42597 --- /dev/null +++ b/site/src/components/icons/BootstrapWhiteFillIcon.astro @@ -0,0 +1,18 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + + Bootstrap + + + diff --git a/site/src/components/icons/CircleSquareIcon.astro b/site/src/components/icons/CircleSquareIcon.astro new file mode 100644 index 000000000000..d71895069132 --- /dev/null +++ b/site/src/components/icons/CircleSquareIcon.astro @@ -0,0 +1,23 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + diff --git a/site/src/components/icons/DropletFillIcon.astro b/site/src/components/icons/DropletFillIcon.astro new file mode 100644 index 000000000000..d1fe5b519d99 --- /dev/null +++ b/site/src/components/icons/DropletFillIcon.astro @@ -0,0 +1,24 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + diff --git a/site/src/components/icons/GitHubIcon.astro b/site/src/components/icons/GitHubIcon.astro new file mode 100644 index 000000000000..faa01434d22d --- /dev/null +++ b/site/src/components/icons/GitHubIcon.astro @@ -0,0 +1,24 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + + GitHub + + + diff --git a/site/src/components/icons/HamburgerIcon.astro b/site/src/components/icons/HamburgerIcon.astro new file mode 100644 index 000000000000..8ff4730a8546 --- /dev/null +++ b/site/src/components/icons/HamburgerIcon.astro @@ -0,0 +1,23 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + diff --git a/site/src/components/icons/OpenCollectiveIcon.astro b/site/src/components/icons/OpenCollectiveIcon.astro new file mode 100644 index 000000000000..fc501641102c --- /dev/null +++ b/site/src/components/icons/OpenCollectiveIcon.astro @@ -0,0 +1,26 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + + Open Collective + + + + diff --git a/site/src/components/icons/Symbols.astro b/site/src/components/icons/Symbols.astro new file mode 100644 index 000000000000..06fe5297ca90 --- /dev/null +++ b/site/src/components/icons/Symbols.astro @@ -0,0 +1,149 @@ +--- + +--- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/components/icons/XIcon.astro b/site/src/components/icons/XIcon.astro new file mode 100644 index 000000000000..ea0f4bd8e1ee --- /dev/null +++ b/site/src/components/icons/XIcon.astro @@ -0,0 +1,23 @@ +--- +import type { SvgIconProps } from '@libs/icon' + +type Props = SvgIconProps + +const { class: className, height, width } = Astro.props +--- + + + X + + + diff --git a/site/src/components/shortcodes/AddedIn.astro b/site/src/components/shortcodes/AddedIn.astro new file mode 100644 index 000000000000..d9a26ce50e7c --- /dev/null +++ b/site/src/components/shortcodes/AddedIn.astro @@ -0,0 +1,16 @@ +--- +/* + * Outputs badge to identify the first version something was added + */ + +interface Props { + version: string +} + +const { version } = Astro.props +--- + +Added in v{version} diff --git a/site/src/components/shortcodes/BsTable.astro b/site/src/components/shortcodes/BsTable.astro new file mode 100644 index 000000000000..df80455ff0c0 --- /dev/null +++ b/site/src/components/shortcodes/BsTable.astro @@ -0,0 +1,16 @@ +--- +interface Props { + /** + * The CSS class to apply to the table. + * Note that the prop is not used in this component, but in a rehype plugin applying the classes to the table element + * directly on the HTML AST (HAST) generated by Astro. + * @default "table" + * @see src/libs/rehype.ts + */ + class?: string +} +--- + +
      + +
      diff --git a/site/src/components/shortcodes/Callout.astro b/site/src/components/shortcodes/Callout.astro new file mode 100644 index 000000000000..11243c848271 --- /dev/null +++ b/site/src/components/shortcodes/Callout.astro @@ -0,0 +1,44 @@ +--- +import { getCalloutByName } from '@libs/content' +import type { MarkdownInstance } from 'astro' + +interface Props { + /** + * The name of an existing callout to display located in `src/content/callouts`. + * This will override any content passed in via the default slot. + */ + name?: + | 'danger-async-methods' + | 'info-mediaqueries-breakpoints' + | 'info-npm-starter' + | 'info-prefersreducedmotion' + | 'info-sanitizer' + | 'warning-color-assistive-technologies' + | 'warning-data-bs-title-vs-title' + | 'warning-input-support' + /** + * The type of callout to display. One of `info`, `danger`, or `warning`. + * @default 'info' + */ + type?: 'danger' | 'info' | 'warning' +} + +const { name, type = 'info' } = Astro.props + +let Content: MarkdownInstance<{}>['Content'] | undefined + +if (name) { + const callout = await getCalloutByName(name) + + if (!callout) { + throw new Error(`Could not find callout with name '${name}'.`) + } + + const namedCallout = await callout.render() + Content = namedCallout.Content +} +--- + +
      + {Content ? : } +
      diff --git a/site/src/components/shortcodes/CalloutDeprecatedDarkVariants.astro b/site/src/components/shortcodes/CalloutDeprecatedDarkVariants.astro new file mode 100644 index 000000000000..4033900d96e0 --- /dev/null +++ b/site/src/components/shortcodes/CalloutDeprecatedDarkVariants.astro @@ -0,0 +1,19 @@ +--- +/* + * Outputs message about dark mode component variants being deprecated in v5.3. + */ + +interface Props { + component: string +} + +const { component } = Astro.props +--- + +
      +

      + Heads up! Dark variants for components were deprecated in v5.3.0 with the introduction of color modes. + Instead of adding .{component}-dark, set data-bs-theme="dark" on the root element, a parent + wrapper, or the component itself. +

      +
      diff --git a/site/src/components/shortcodes/Code.astro b/site/src/components/shortcodes/Code.astro new file mode 100644 index 000000000000..ba2c61165fed --- /dev/null +++ b/site/src/components/shortcodes/Code.astro @@ -0,0 +1,165 @@ +--- +import fs from 'node:fs' +import path from 'node:path' +import { Prism } from '@astrojs/prism' + +interface Props { + /** + * The CSS class(es) to be added to the `pre` HTML element when rendering code blocks in Markdown. + * Note that this prop is not used when the component is invoked directly. + */ + class?: string + /** + * The code to highlight. + * If an array is passed, elements will be joined with a new line. + */ + code?: string | string[] + /** + * The CSS class(es) to be added to the `div` wrapper HTML element. + */ + containerClass?: string + /** + * The language to use for highlighting. + * @see https://prismjs.com/#supported-languages + */ + lang?: string + /** + * If the `filePath` prop is defined, this prop can be used to specify a regex containing a match group to extract + * only a part of the file. + */ + fileMatch?: string + /** + * A path to the file containing the code to highlight relative to the root of the repository. + * This takes precedence over the `code` prop. + */ + filePath?: string + /** + * Defines if the `` component is nested inside an `` component or not. + * @default false + */ + nestedInExample?: boolean +} + +const { class: className, code, containerClass, fileMatch, filePath, lang, nestedInExample = false } = Astro.props + +let codeToDisplay = filePath + ? fs.readFileSync(path.join(process.cwd(), filePath), 'utf8') + : Array.isArray(code) + ? code.join('\n') + : code + +if (filePath && fileMatch && codeToDisplay) { + const match = codeToDisplay.match(new RegExp(fileMatch)) + + if (!match || !match[0]) { + throw new Error(`The file at ${filePath} does not contains a match for the regex '${fileMatch}'.`) + } + + codeToDisplay = match[0] +} +--- + + + +
      + { + nestedInExample + ? (<>) + : Astro.slots.has('pre') + ? ( + + ) + : ( +
      + +
      + ) + } +
      + { + codeToDisplay && lang ? ( + + ) : ( + /* prettier-ignore */
      + ) + } +
      +
      diff --git a/site/src/components/shortcodes/DeprecatedIn.astro b/site/src/components/shortcodes/DeprecatedIn.astro new file mode 100644 index 000000000000..50ba42b95d0d --- /dev/null +++ b/site/src/components/shortcodes/DeprecatedIn.astro @@ -0,0 +1,17 @@ +--- +/* + * Outputs badge to identify the version something was deprecated + */ + +interface Props { + version: string +} + +const { version } = Astro.props +--- + + + Deprecated in v{version} + diff --git a/site/src/components/shortcodes/Example.astro b/site/src/components/shortcodes/Example.astro new file mode 100644 index 000000000000..044e1af4d408 --- /dev/null +++ b/site/src/components/shortcodes/Example.astro @@ -0,0 +1,103 @@ +--- +import { replacePlaceholdersInHtml } from '@libs/placeholder' +import Code from '@components/shortcodes/Code.astro' + +interface Props { + /** + * Defines if extra JS snippet should be added to StackBlitz or not. + * @default false + */ + addStackblitzJs?: boolean + /** + * The example code. + * If an array is passed, elements will be joined with a new line. + */ + code: string | string[] + /** + * The CSS class(es) to be added to the preview wrapping `div` element. + */ + class?: string + /** + * The preview wrapping `div` element ID. + */ + id?: string + /** + * Language used to display the code. + * @default 'html' + */ + lang?: string + /** + * Defines if the markup should be visible or not. + * @default true + */ + showMarkup?: boolean + /** + * Defines if the preview should be visible or not. + * @default true + */ + showPreview?: boolean +} + +const { + addStackblitzJs = false, + code, + class: className, + id, + lang = 'html', + showMarkup = true, + showPreview = true +} = Astro.props + +let markup = Array.isArray(code) ? code.join('\n') : code +markup = replacePlaceholdersInHtml(markup) + +const simplifiedMarkup = markup + .replace( + //g, + (match, classes) => `...` + ) + .replace( + //g, + (match, classes) => `...` + ) +--- + +
      + { + showPreview && ( +
      + +
      + ) + } + + { + showMarkup && ( + <> + {showPreview && ( +
      + {lang} +
      + + +
      +
      + )} + + + ) + } +
      diff --git a/site/src/components/shortcodes/GuideFooter.mdx b/site/src/components/shortcodes/GuideFooter.mdx new file mode 100644 index 000000000000..426a71f271c2 --- /dev/null +++ b/site/src/components/shortcodes/GuideFooter.mdx @@ -0,0 +1,3 @@ +
      + +_See something wrong or out of date here? Please [open an issue on GitHub]([[config:repo]]/issues/new/choose). Need help troubleshooting? [Search or start a discussion]([[config:repo]]/discussions) on GitHub._ diff --git a/site/src/components/shortcodes/JsDataAttributes.mdx b/site/src/components/shortcodes/JsDataAttributes.mdx new file mode 100644 index 000000000000..b7c6c0a7a858 --- /dev/null +++ b/site/src/components/shortcodes/JsDataAttributes.mdx @@ -0,0 +1,5 @@ +As options can be passed via data attributes or JavaScript, you can append an option name to `data-bs-`, as in `data-bs-animation="{value}"`. Make sure to change the case type of the option name from “_camelCase_” to “_kebab-case_” when passing the options via data attributes. For example, use `data-bs-custom-class="beautifier"` instead of `data-bs-customClass="beautifier"`. + +As of Bootstrap 5.2.0, all components support an **experimental** reserved data attribute `data-bs-config` that can house simple component configuration as a JSON string. When an element has `data-bs-config='{"delay":0, "title":123}'` and `data-bs-title="456"` attributes, the final `title` value will be `456` and the separate data attributes will override values given on `data-bs-config`. In addition, existing data attributes are able to house JSON values like `data-bs-delay='{"show":0,"hide":150}'`. + +The final configuration object is the merged result of `data-bs-config`, `data-bs-`, and `js object` where the latest given key-value overrides the others. diff --git a/site/src/components/shortcodes/JsDismiss.astro b/site/src/components/shortcodes/JsDismiss.astro new file mode 100644 index 000000000000..d1da8fc4d6ac --- /dev/null +++ b/site/src/components/shortcodes/JsDismiss.astro @@ -0,0 +1,29 @@ +--- +import Code from '@shortcodes/Code.astro' + +interface Props { + name: string +} + +const { name } = Astro.props +--- + +

      + Dismissal can be achieved with the data-bs-dismiss attribute on a button within the {name} as demonstrated below: +

      + +`} + lang="html" +/> + +

      + or on a button outside the {name} using the additional data-bs-target as demonstrated below: +

      + +`} + lang="html" +/> diff --git a/site/src/components/shortcodes/JsDocs.astro b/site/src/components/shortcodes/JsDocs.astro new file mode 100644 index 000000000000..4430f21f885f --- /dev/null +++ b/site/src/components/shortcodes/JsDocs.astro @@ -0,0 +1,69 @@ +--- +import fs from 'node:fs' +import { getConfig } from '@libs/config' +import Code from '@shortcodes/Code.astro' + +// Prints everything between `// js-docs-start "name"` and `// js-docs-end "name"` +// comments in the docs. + +interface Props { + /** + * Reference name used to find the content to display within the content of the `file` prop. + */ + name: string + /** + * File path that contains the content to display relative to the root of the repository. + */ + file: string +} + +const { name, file } = Astro.props + +if (!name || !file) { + throw new Error( + `Missing required parameter(s) for the '' component, expected both 'name' and 'file' but got 'name: ${name}' and 'file: ${file}'.` + ) +} + +let content: string + +try { + const fileContent = fs.readFileSync(file, 'utf8') + + const matches = fileContent.match(new RegExp(`\/\/ js-docs-start ${name}\n((?:.|\n)*)\/\/ js-docs-end ${name}`, 'm')) + + if (!matches || !matches[1]) { + throw new Error( + `Failed to find the content named '${name}', make sure that '// js-docs-start ${name}' and '// js-docs-end ${name}' are defined.` + ) + } + + content = matches[1] + + // Fix the indentation by removing extra spaces at the beginning of each line + const lines = content.split('\n') + const spaceCounts = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^ */)[0].length) + const minSpaces = spaceCounts.length ? Math.min(...spaceCounts) : 0 + content = lines.map((line) => line.slice(minSpaces)).join('\n') +} catch (error) { + throw new Error(`Failed to find the content to render in the '' component at '${file}'.`, { + cause: error + }) +} +--- + + +
      + + {file} + +
      + +
      +
      +
      diff --git a/site/src/components/shortcodes/Placeholder.astro b/site/src/components/shortcodes/Placeholder.astro new file mode 100644 index 000000000000..3ebde32bcb31 --- /dev/null +++ b/site/src/components/shortcodes/Placeholder.astro @@ -0,0 +1,27 @@ +--- +import { getPlaceholder, type PlaceholderOptions } from '@libs/placeholder' + +type Props = Partial + +const { + options: { background, color, showText, showTitle, text, title }, + props, + type +} = getPlaceholder(Astro.props) +--- + +{ + type === 'img' ? ( + + ) : ( + + {showTitle && {title}} + + {showText && ( + + {text} + + )} + + ) +} diff --git a/site/src/components/shortcodes/ScssDocs.astro b/site/src/components/shortcodes/ScssDocs.astro new file mode 100644 index 000000000000..4eaa8ca01f50 --- /dev/null +++ b/site/src/components/shortcodes/ScssDocs.astro @@ -0,0 +1,71 @@ +--- +import fs from 'node:fs' +import { getConfig } from '@libs/config' +import Code from '@shortcodes/Code.astro' + +// Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"` +// comments in the docs. + +interface Props { + /** + * Reference name used to find the content to display within the content of the `file` prop. + */ + name: string + /** + * File path that contains the content to display relative to the root of the repository. + */ + file: string +} + +const { name, file } = Astro.props + +if (!name || !file) { + throw new Error( + `Missing required parameter(s) for the '' component, expected both 'name' and 'file' but got 'name: ${name}' and 'file: ${file}'.` + ) +} + +let content: string + +try { + const fileContent = fs.readFileSync(file, 'utf8') + + const matches = fileContent.match( + new RegExp(`\/\/ scss-docs-start ${name}\n((?:.|\n)*)\/\/ scss-docs-end ${name}`, 'm') + ) + + if (!matches || !matches[1]) { + throw new Error( + `Failed to find the content named '${name}', make sure that '// scss-docs-start ${name}' and '// scss-docs-end ${name}' are defined.` + ) + } + + content = matches[1].replaceAll(' !default', '') + + // Fix the indentation by removing extra spaces at the beginning of each line + const lines = content.split('\n') + const spaceCounts = lines.filter((line) => line.trim().length > 0).map((line) => line.match(/^ */)[0].length) + const minSpaces = spaceCounts.length ? Math.min(...spaceCounts) : 0 + content = lines.map((line) => line.slice(minSpaces)).join('\n') +} catch (error) { + throw new Error(`Failed to find the content to render in the '' component at '${file}'.`, { + cause: error + }) +} +--- + + +
      + + {file} + +
      + +
      +
      +
      diff --git a/site/src/components/shortcodes/Table.astro b/site/src/components/shortcodes/Table.astro new file mode 100644 index 000000000000..622aff9c88d2 --- /dev/null +++ b/site/src/components/shortcodes/Table.astro @@ -0,0 +1,32 @@ +--- +import Code from '@shortcodes/Code.astro' +import Example from '@shortcodes/Example.astro' +import * as tableContent from '@shortcodes/TableContent.md' + +interface Props { + /** + * Any class(es) to be added to the `` element (both in the example and code snippet). + */ + class?: string + /** + * Show a simplified version in the example code snippet by replacing the table content inside `
      ` & `
      ` + * with `...`. + * @default true + */ + simplified?: boolean +} + +const { class: className, simplified = true } = Astro.props + +const tableCode = ` +${simplified ? ' ...' : await tableContent.compiledContent()} +` + +const exampleCode = ` +${await tableContent.compiledContent()} +` +--- + + + + diff --git a/site/layouts/partials/table-content.html b/site/src/components/shortcodes/TableContent.md similarity index 87% rename from site/layouts/partials/table-content.html rename to site/src/components/shortcodes/TableContent.md index 71fca1d4885e..cee54c6d32a4 100644 --- a/site/layouts/partials/table-content.html +++ b/site/src/components/shortcodes/TableContent.md @@ -21,7 +21,8 @@ 3 - Larry the Bird - @twitter + John + Doe + @social diff --git a/site/src/content/callouts/danger-async-methods.md b/site/src/content/callouts/danger-async-methods.md new file mode 100644 index 000000000000..7b7a654b72d8 --- /dev/null +++ b/site/src/content/callouts/danger-async-methods.md @@ -0,0 +1 @@ +**All API methods are asynchronous and start a transition.** They return to the caller as soon as the transition is started, but before it ends. In addition, a method call on a transitioning component will be ignored. [Learn more in our JavaScript docs.](/docs/[[config:docs_version]]/getting-started/javascript/#asynchronous-functions-and-transitions) diff --git a/site/src/content/callouts/info-mediaqueries-breakpoints.md b/site/src/content/callouts/info-mediaqueries-breakpoints.md new file mode 100644 index 000000000000..52be67386a9d --- /dev/null +++ b/site/src/content/callouts/info-mediaqueries-breakpoints.md @@ -0,0 +1 @@ +**Why subtract .02px?** Browsers don’t currently support [range context queries](https://www.w3.org/TR/mediaqueries-4/#range-context), so we work around the limitations of [`min-` and `max-` prefixes](https://www.w3.org/TR/mediaqueries-4/#mq-min-max) and viewports with fractional widths (which can occur under certain conditions on high-dpi devices, for instance) by using values with higher precision. diff --git a/site/src/content/callouts/info-npm-starter.md b/site/src/content/callouts/info-npm-starter.md new file mode 100644 index 000000000000..cc4a50e68fd1 --- /dev/null +++ b/site/src/content/callouts/info-npm-starter.md @@ -0,0 +1 @@ +**Get started with Bootstrap via npm with our starter project!** Head to the [Sass & JS example](https://github.com/twbs/examples/tree/main/sass-js) template repository to see how to build and customize Bootstrap in your own npm project. Includes Sass compiler, Autoprefixer, Stylelint, PurgeCSS, and Bootstrap Icons. diff --git a/site/layouts/partials/callout-info-prefersreducedmotion.md b/site/src/content/callouts/info-prefersreducedmotion.md similarity index 50% rename from site/layouts/partials/callout-info-prefersreducedmotion.md rename to site/src/content/callouts/info-prefersreducedmotion.md index d258fbe4820d..49d81ef8ba93 100644 --- a/site/layouts/partials/callout-info-prefersreducedmotion.md +++ b/site/src/content/callouts/info-prefersreducedmotion.md @@ -1 +1 @@ -The animation effect of this component is dependent on the `prefers-reduced-motion` media query. See the [reduced motion section of our accessibility documentation](/docs/{{ .Site.Params.docs_version }}/getting-started/accessibility/#reduced-motion). +The animation effect of this component is dependent on the `prefers-reduced-motion` media query. See the [reduced motion section of our accessibility documentation](/docs/[[config:docs_version]]/getting-started/accessibility/#reduced-motion). diff --git a/site/src/content/callouts/info-sanitizer.md b/site/src/content/callouts/info-sanitizer.md new file mode 100644 index 000000000000..516975b3206c --- /dev/null +++ b/site/src/content/callouts/info-sanitizer.md @@ -0,0 +1 @@ +By default, this component uses the built-in content sanitizer, which strips out any HTML elements that are not explicitly allowed. See the [sanitizer section in our JavaScript documentation](/docs/[[config:docs_version]]/getting-started/javascript/#sanitizer) for more details. diff --git a/site/src/content/callouts/warning-color-assistive-technologies.md b/site/src/content/callouts/warning-color-assistive-technologies.md new file mode 100644 index 000000000000..8afa62ee8cdb --- /dev/null +++ b/site/src/content/callouts/warning-color-assistive-technologies.md @@ -0,0 +1 @@ +**Accessibility tip:** Using color to add meaning only provides a visual indication, which will not be conveyed to users of assistive technologies like screen readers. Please ensure the meaning is obvious from the content itself (e.g., the visible text with a [_sufficient_ color contrast](/docs/[[config:docs_version]]/getting-started/accessibility/#color-contrast)) or is included through alternative means, such as additional text hidden with the `.visually-hidden` class. diff --git a/site/src/content/callouts/warning-data-bs-title-vs-title.md b/site/src/content/callouts/warning-data-bs-title-vs-title.md new file mode 100644 index 000000000000..e932f22ad971 --- /dev/null +++ b/site/src/content/callouts/warning-data-bs-title-vs-title.md @@ -0,0 +1 @@ +Feel free to use either `title` or `data-bs-title` in your HTML. When `title` is used, Popper will replace it automatically with `data-bs-title` when the element is rendered. diff --git a/site/src/content/callouts/warning-input-support.md b/site/src/content/callouts/warning-input-support.md new file mode 100644 index 000000000000..f9d9c0abd67d --- /dev/null +++ b/site/src/content/callouts/warning-input-support.md @@ -0,0 +1 @@ +Some date inputs types are [not fully supported](https://caniuse.com/input-datetime) by the latest versions of Safari and Firefox. diff --git a/site/src/content/config.ts b/site/src/content/config.ts new file mode 100644 index 000000000000..387a0052ebef --- /dev/null +++ b/site/src/content/config.ts @@ -0,0 +1,45 @@ +import { z, defineCollection } from 'astro:content' + +const docsSchema = z.object({ + added: z + .object({ + show_badge: z.boolean().optional(), + version: z.string() + }) + .optional(), + aliases: z.string().or(z.string().array()).optional(), + description: z.string(), + direction: z.literal('rtl').optional(), + extra_js: z + .object({ + async: z.boolean().optional(), + src: z.string() + }) + .array() + .optional(), + sections: z + .object({ + description: z.string(), + title: z.string() + }) + .array() + .optional(), + thumbnail: z.string().optional(), + title: z.string(), + toc: z.boolean().optional() +}) + +const docsCollection = defineCollection({ + schema: docsSchema +}) + +const calloutsSchema = z.object({}) + +const calloutsCollection = defineCollection({ + schema: calloutsSchema +}) + +export const collections = { + docs: docsCollection, + callouts: calloutsCollection +} diff --git a/site/src/content/docs/about/brand.mdx b/site/src/content/docs/about/brand.mdx new file mode 100644 index 000000000000..0b1716221308 --- /dev/null +++ b/site/src/content/docs/about/brand.mdx @@ -0,0 +1,41 @@ +--- +title: Brand guidelines +description: Documentation and examples for Bootstrap’s logo and brand usage guidelines. +toc: true +--- + +Have a need for Bootstrap’s brand resources? Great! We have only a few guidelines we follow, and in turn ask you to follow as well. + +## Logo + +When referencing Bootstrap, use our logo mark. Do not modify our logos in any way. Do not use Bootstrap’s branding for your own open or closed source projects. + +
      + Bootstrap +
      + +Our logo mark is also available in black and white. All rules for our primary logo apply to these as well. + +
      +
      + Bootstrap +
      +
      + Bootstrap +
      +
      + +## Name + +Bootstrap should always be referred to as just **Bootstrap**. No capital _s_. + +
      +
      +
      Bootstrap
      + Correct +
      +
      +
      BootStrap
      + Incorrect +
      +
      diff --git a/site/content/docs/5.0/about/license.md b/site/src/content/docs/about/license.mdx similarity index 58% rename from site/content/docs/5.0/about/license.md rename to site/src/content/docs/about/license.mdx index 07e60e00aab4..6479df67c999 100644 --- a/site/content/docs/5.0/about/license.md +++ b/site/src/content/docs/about/license.mdx @@ -1,15 +1,13 @@ --- -layout: docs title: License FAQs -description: Commonly asked questions about Bootstrap's open source license. -group: about +description: Commonly asked questions about Bootstrap’s open source license. --- -Bootstrap is released under the MIT license and is copyright {{< year >}} Twitter. Boiled down to smaller chunks, it can be described with the following conditions. +Bootstrap is released under the MIT license and is copyright {new Date().getFullYear()}. Boiled down to smaller chunks, it can be described with the following conditions. ## It requires you to: -- Keep the license and copyright notice included in Bootstrap's CSS and JavaScript files when you use them in your works +- Keep the license and copyright notice included in Bootstrap’s CSS and JavaScript files when you use them in your works ## It permits you to: @@ -23,12 +21,12 @@ Bootstrap is released under the MIT license and is copyright {{< year >}} Twitte - Hold the authors and license owners liable for damages as Bootstrap is provided without warranty - Hold the creators or copyright holders of Bootstrap liable - Redistribute any piece of Bootstrap without proper attribution -- Use any marks owned by Twitter in any way that might state or imply that Twitter endorses your distribution -- Use any marks owned by Twitter in any way that might state or imply that you created the Twitter software in question +- Use any marks owned by Bootstrap in any way that might state or imply that Bootstrap endorses your distribution +- Use any marks owned by Bootstrap in any way that might state or imply that you created the Bootstrap software in question ## It does not require you to: - Include the source of Bootstrap itself, or of any modifications you may have made to it, in any redistribution you may assemble that includes it - Submit changes that you make to Bootstrap back to the Bootstrap project (though such feedback is encouraged) -The full Bootstrap license is located [in the project repository]({{< param repo >}}/blob/v{{< param current_version >}}/LICENSE) for more information. +The full Bootstrap license is located [in the project repository]([[config:repo]]/blob/v[[config:current_version]]/LICENSE) for more information. diff --git a/site/src/content/docs/about/overview.mdx b/site/src/content/docs/about/overview.mdx new file mode 100644 index 000000000000..efd16c543ed3 --- /dev/null +++ b/site/src/content/docs/about/overview.mdx @@ -0,0 +1,27 @@ +--- +title: About Bootstrap +description: Learn more about the team maintaining Bootstrap, how and why the project started, and how to get involved. +aliases: + - "/about/" + - "/docs/[[config:docs_version]]/about/" +--- + +## Team + +Bootstrap is maintained by a [small team of developers](https://github.com/orgs/twbs/people) on GitHub. We’re actively looking to grow this team and would love to hear from you if you’re excited about CSS at scale, writing and maintaining vanilla JavaScript plugins, and improving build tooling processes for frontend code. + +## History + +Originally created by a designer and a developer at Twitter, Bootstrap has become one of the most popular front-end frameworks and open source projects in the world. + +Bootstrap was created at Twitter in mid-2010 by [@mdo](https://x.com/mdo) and [@fat](https://x.com/fat). Prior to being an open-sourced framework, Bootstrap was known as _Twitter Blueprint_. A few months into development, Twitter held its [first Hack Week](https://blog.x.com/engineering/en_us/a/2010/hack-week) and the project exploded as developers of all skill levels jumped in without any external guidance. It served as the style guide for internal tools development at the company for over a year before its public release, and continues to do so today. + +Originally [released](https://blog.x.com/developer/en_us/a/2011/bootstrap-twitter) on , we’ve since had over [twenty releases]([[config:repo]]/releases), including two major rewrites with v2 and v3. With Bootstrap 2, we added responsive functionality to the entire framework as an optional stylesheet. Building on that with Bootstrap 3, we rewrote the library once more to make it responsive by default with a mobile first approach. + +With Bootstrap 4, we once again rewrote the project to account for two key architectural changes: a migration to Sass and the move to CSS’s flexbox. Our intention is to help in a small way to move the web development community forward by pushing for newer CSS properties, fewer dependencies, and new technologies across more modern browsers. + +Our latest release, Bootstrap 5, focuses on improving v4’s codebase with as few major breaking changes as possible. We improved existing features and components, removed support for older browsers, dropped jQuery for regular JavaScript, and embraced more future-friendly technologies like CSS custom properties as part of our tooling. + +## Get involved + +Get involved with Bootstrap development by [opening an issue]([[config:repo]]/issues/new/choose) or submitting a pull request. Read our [contributing guidelines]([[config:repo]]/blob/v[[config:current_version]]/.github/CONTRIBUTING.md) for information on how we develop. diff --git a/site/src/content/docs/about/team.mdx b/site/src/content/docs/about/team.mdx new file mode 100644 index 000000000000..46b03d87746c --- /dev/null +++ b/site/src/content/docs/about/team.mdx @@ -0,0 +1,23 @@ +--- +title: Team +description: An overview of the founding team and core contributors to Bootstrap. +--- + +import { getData } from '@libs/data' + +Bootstrap is maintained by the founding team and a small group of invaluable core contributors, with the massive support and involvement of our community. + +
      + {getData('core-team').map((member) => { + return ( + + {`@${member.user}`} + + {member.name} @{member.user} + + + ) + })} +
      + +Get involved with Bootstrap development by [opening an issue]([[config:repo]]/issues/new/choose) or submitting a pull request. Read our [contributing guidelines]([[config:repo]]/blob/v[[config:current_version]]/.github/CONTRIBUTING.md) for information on how we develop. diff --git a/site/src/content/docs/about/translations.mdx b/site/src/content/docs/about/translations.mdx new file mode 100644 index 000000000000..7db4ab846cc7 --- /dev/null +++ b/site/src/content/docs/about/translations.mdx @@ -0,0 +1,20 @@ +--- +title: Translations +description: Links to community-translated Bootstrap documentation sites. +--- + +import { getData } from '@libs/data' + +Community members have translated Bootstrap’s documentation into various languages. None are officially supported and they may not always be up-to-date. + + + +**We don’t help organize or host translations, we just link to them.** + +Finished a new or better translation? Open a pull request to add it to our list. diff --git a/site/src/content/docs/components/accordion.mdx b/site/src/content/docs/components/accordion.mdx new file mode 100644 index 000000000000..e8df6d88b625 --- /dev/null +++ b/site/src/content/docs/components/accordion.mdx @@ -0,0 +1,240 @@ +--- +title: Accordion +description: Build vertically collapsing accordions in combination with our Collapse JavaScript plugin. +aliases: + - "/components/" + - "/docs/[[config:docs_version]]/components/" +toc: true +--- + +## How it works + +The accordion uses [collapse]([[docsref:/components/collapse]]) internally to make it collapsible. + + + +## Example + +Click the accordions below to expand/collapse the accordion content. + +To render an accordion that’s expanded by default: +- add the `.show` class on the `.accordion-collapse` element. +- drop the `.collapsed` class from the `.accordion-button` element and set its `aria-expanded` attribute to `true`. + + +
      +

      + +

      +
      +
      + This is the first item’s accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      +

      + +

      +
      +
      + This is the second item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      +

      + +

      +
      +
      + This is the third item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      `} /> + +### Flush + +Add `.accordion-flush` to remove some borders and rounded corners to render accordions edge-to-edge with their parent container. + + +
      +

      + +

      +
      +
      Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the first item’s accordion body.
      +
      +
      +
      +

      + +

      +
      +
      Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the second item’s accordion body. Let’s imagine this being filled with some actual content.
      +
      +
      +
      +

      + +

      +
      +
      Placeholder content for this accordion, which is intended to demonstrate the .accordion-flush class. This is the third item’s accordion body. Nothing more exciting happening here in terms of content, but just filling up the space to make it look, at least at first glance, a bit more representative of how this would look in a real-world application.
      +
      +
      +
      `} /> + +### Always open + +Omit the `data-bs-parent` attribute on each `.accordion-collapse` to make accordion items stay open when another item is opened. + + +
      +

      + +

      +
      +
      + This is the first item’s accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      +

      + +

      +
      +
      + This is the second item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      +

      + +

      +
      +
      + This is the third item’s accordion body. It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It’s also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow. +
      +
      +
      +
      `} /> + +## Accessibility + +Please read the [collapse accessibility section]([[docsref:/components/collapse#accessibility]]) for more information. + +## CSS + +### Variables + + + +As part of Bootstrap’s evolving CSS variables approach, accordions now use local CSS variables on `.accordion` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + + + +### Sass variables + + + +## Usage + +The collapse plugin utilizes a few classes to handle the heavy lifting: + +- `.collapse` hides the content +- `.collapse.show` shows the content +- `.collapsing` is added when the transition starts, and removed when it finishes + +These classes can be found in `_transitions.scss`. + +### Via data attributes + +Just add `data-bs-toggle="collapse"` and a `data-bs-target` to the element to automatically assign control of one or more collapsible elements. The `data-bs-target` attribute accepts a CSS selector to apply the collapse to. Be sure to add the class `collapse` to the collapsible element. If you’d like it to default open, add the additional class `show`. + +To add accordion group management to a collapsible area, add the data attribute `data-bs-parent="#selector"`. + +### Via JavaScript + +Enable manually with: + +```js +const accordionCollapseElementList = document.querySelectorAll('#myAccordion .collapse') +const accordionCollapseList = [...accordionCollapseElementList].map(accordionCollapseEl => new bootstrap.Collapse(accordionCollapseEl)) +``` + +### Options + + + + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +`parent` | selector, DOM element | `null` | If parent is provided, then all collapsible elements under the specified parent will be closed when this collapsible item is shown. (similar to traditional accordion behavior - this is dependent on the `card` class). The attribute has to be set on the target collapsible area. | +`toggle` | boolean | `true` | Toggles the collapsible element on invocation. | + + +### Methods + + + +Activates your content as a collapsible element. Accepts an optional options `object`. + +You can create a collapse instance with the constructor, for example: + +```js +const bsCollapse = new bootstrap.Collapse('#myCollapse', { + toggle: false +}) +``` + + +| Method | Description | +| --- | --- | +| `dispose` | Destroys an element’s collapse. (Removes stored data on the DOM element) | +| `getInstance` | Static method which allows you to get the collapse instance associated to a DOM element, you can use it like this: `bootstrap.Collapse.getInstance(element)`. | +| `getOrCreateInstance` | Static method which returns a collapse instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Collapse.getOrCreateInstance(element)`. | +| `hide` | Hides a collapsible element. **Returns to the caller before the collapsible element has actually been hidden** (e.g., before the `hidden.bs.collapse` event occurs). | +| `show` | Shows a collapsible element. **Returns to the caller before the collapsible element has actually been shown** (e.g., before the `shown.bs.collapse` event occurs). | +| `toggle` | Toggles a collapsible element to shown or hidden. **Returns to the caller before the collapsible element has actually been shown or hidden** (i.e. before the `shown.bs.collapse` or `hidden.bs.collapse` event occurs). | + + +### Events + +Bootstrap’s collapse class exposes a few events for hooking into collapse functionality. + + +| Event type | Description | +| --- | --- | +| `hide.bs.collapse` | This event is fired immediately when the `hide` method has been called. | +| `hidden.bs.collapse` | This event is fired when a collapse element has been hidden from the user (will wait for CSS transitions to complete). | +| `show.bs.collapse` | This event fires immediately when the `show` instance method is called. | +| `shown.bs.collapse` | This event is fired when a collapse element has been made visible to the user (will wait for CSS transitions to complete). | + + +```js +const myCollapsible = document.getElementById('myCollapsible') +myCollapsible.addEventListener('hidden.bs.collapse', event => { + // do something... +}) +``` diff --git a/site/src/content/docs/components/alerts.mdx b/site/src/content/docs/components/alerts.mdx new file mode 100644 index 000000000000..e25604462b26 --- /dev/null +++ b/site/src/content/docs/components/alerts.mdx @@ -0,0 +1,218 @@ +--- +title: Alerts +description: Provide contextual feedback messages for typical user actions with the handful of available and flexible alert messages. +toc: true +--- + +import { getData } from '@libs/data' + +## Examples + +Alerts are available for any length of text, as well as an optional close button. For proper styling, use one of the eight **required** contextual classes (e.g., `.alert-success`). For inline dismissal, use the [alerts JavaScript plugin](#dismissing). + + +**Heads up!** As of v5.3.0, the `alert-variant()` Sass mixin is deprecated. Alert variants now have their CSS variables overridden in [a Sass loop](#sass-loops). + + + ``)} /> + + + +### Live example + +Click the button below to show an alert (hidden with inline styles to start), then dismiss (and destroy) it with the built-in close button. + +
      +`} /> + +We use the following JavaScript to trigger our live alert demo: + + + +### Link color + +Use the `.alert-link` utility class to quickly provide matching colored links within any alert. + + ``)} /> + +### Additional content + +Alerts can also contain additional HTML elements like headings, paragraphs and dividers. + + +

      Well done!

      +

      Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.

      +
      +

      Whenever you need to, be sure to use margin utilities to keep things nice and tidy.

      +
      `} /> + +### Icons + +Similarly, you can use [flexbox utilities]([[docsref:/utilities/flex]]) and [Bootstrap Icons]([[config:icons]]) to create alerts with icons. Depending on your icons and content, you may want to add more utilities or custom styles. + + + + + +
      + An example alert with an icon +
      +
    `} /> + +Need more than one icon for your alerts? Consider using more Bootstrap Icons and making a local SVG sprite like so to easily reference the same icons repeatedly. + + + + + + + + + + + + + + + + + `} /> + +### Dismissing + +Using the alert JavaScript plugin, it’s possible to dismiss any alert inline. Here’s how: + +- Be sure you’ve loaded the alert plugin, or the compiled Bootstrap JavaScript. +- Add a [close button]([[docsref:/components/close-button]]) and the `.alert-dismissible` class, which adds extra padding to the right of the alert and positions the close button. +- On the close button, add the `data-bs-dismiss="alert"` attribute, which triggers the JavaScript functionality. Be sure to use the ` + `} /> + + +When an alert is dismissed, the element is completely removed from the page structure. If a keyboard user dismisses the alert using the close button, their focus will suddenly be lost and, depending on the browser, reset to the start of the page/document. For this reason, we recommend including additional JavaScript that listens for the `closed.bs.alert` event and programmatically sets `focus()` to the most appropriate location in the page. If you’re planning to move focus to a non-interactive element that normally does not receive focus, make sure to add `tabindex="-1"` to the element. + + +## CSS + +### Variables + + + +As part of Bootstrap’s evolving CSS variables approach, alerts now use local CSS variables on `.alert` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + + + +### Sass variables + + + +### Sass mixins + + + + + +### Sass loops + +Loop that generates the modifier classes with an overriding of CSS variables. + + + +## JavaScript behavior + +### Initialize + +Initialize elements as alerts + +```js +const alertList = document.querySelectorAll('.alert') +const alerts = [...alertList].map(element => new bootstrap.Alert(element)) +``` + + +For the sole purpose of dismissing an alert, it isn’t necessary to initialize the component manually via the JS API. By making use of `data-bs-dismiss="alert"`, the component will be initialized automatically and properly dismissed. + +See the [triggers](#triggers) section for more details. + + +### Triggers + + + +**Note that closing an alert will remove it from the DOM.** + +### Methods + +You can create an alert instance with the alert constructor, for example: + +```js +const bsAlert = new bootstrap.Alert('#myAlert') +``` + +This makes an alert listen for click events on descendant elements which have the `data-bs-dismiss="alert"` attribute. (Not necessary when using the data-api’s auto-initialization.) + + +| Method | Description | +| --- | --- | +| `close` | Closes an alert by removing it from the DOM. If the `.fade` and `.show` classes are present on the element, the alert will fade out before it is removed. | +| `dispose` | Destroys an element’s alert. (Removes stored data on the DOM element) | +| `getInstance` | Static method which allows you to get the alert instance associated to a DOM element. For example: `bootstrap.Alert.getInstance(alert)`. | +| `getOrCreateInstance` | Static method which returns an alert instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Alert.getOrCreateInstance(element)`. | + + +Basic usage: + +```js +const alert = bootstrap.Alert.getOrCreateInstance('#myAlert') +alert.close() +``` + +### Events + +Bootstrap’s alert plugin exposes a few events for hooking into alert functionality. + + +| Event | Description | +| --- | --- | +| `close.bs.alert` | Fires immediately when the `close` instance method is called. | +| `closed.bs.alert` | Fired when the alert has been closed and CSS transitions have completed. | + + +```js +const myAlert = document.getElementById('myAlert') +myAlert.addEventListener('closed.bs.alert', event => { + // do something, for instance, explicitly move focus to the most appropriate element, + // so it doesn’t get lost/reset to the start of the page + // document.getElementById('...').focus() +}) +``` diff --git a/site/src/content/docs/components/badge.mdx b/site/src/content/docs/components/badge.mdx new file mode 100644 index 000000000000..b3e574b6d899 --- /dev/null +++ b/site/src/content/docs/components/badge.mdx @@ -0,0 +1,83 @@ +--- +title: Badges +description: Documentation and examples for badges, our small count and labeling component. +toc: true +--- + +import { getData } from '@libs/data' + +## Examples + +Badges scale to match the size of the immediate parent element by using relative font sizing and `em` units. As of v5, badges no longer have focus or hover styles for links. + +### Headings + +Example heading New +

    Example heading New

    +

    Example heading New

    +

    Example heading New

    +
    Example heading New
    +
    Example heading New
    `} /> + +### Buttons + +Badges can be used as part of links or buttons to provide a counter. + + + Notifications 4 + `} /> + +Note that depending on how they are used, badges may be confusing for users of screen readers and similar assistive technologies. While the styling of badges provides a visual cue as to their purpose, these users will simply be presented with the content of the badge. Depending on the specific situation, these badges may seem like random additional words or numbers at the end of a sentence, link, or button. + +Unless the context is clear (as with the “Notifications” example, where it is understood that the “4” is the number of notifications), consider including additional context with a visually hidden piece of additional text. + +### Positioned + +Use utilities to modify a `.badge` and position it in the corner of a link or button. + + + Inbox + + 99+ + unread messages + + `} /> + +You can also replace the `.badge` class with a few more utilities without a count for a more generic indicator. + + + Profile + + New alerts + + `} /> + +## Background colors + + + +Set a `background-color` with contrasting foreground `color` with [our `.text-bg-{color}` helpers]([[docsref:helpers/color-background]]). Previously it was required to manually pair your choice of [`.text-{color}`]([[docsref:/utilities/colors]]) and [`.bg-{color}`]([[docsref:/utilities/background]]) utilities for styling, which you still may use if you prefer. + + `${themeColor.title}`)} /> + + + +## Pill badges + +Use the `.rounded-pill` utility class to make badges more rounded with a larger `border-radius`. + + `${themeColor.title}`)} /> + +## CSS + +### Variables + + + +As part of Bootstrap’s evolving CSS variables approach, badges now use local CSS variables on `.badge` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + + + +### Sass variables + + diff --git a/site/src/content/docs/components/breadcrumb.mdx b/site/src/content/docs/components/breadcrumb.mdx new file mode 100644 index 000000000000..50cceb1cc190 --- /dev/null +++ b/site/src/content/docs/components/breadcrumb.mdx @@ -0,0 +1,98 @@ +--- +title: Breadcrumb +description: Indicate the current page’s location within a navigational hierarchy that automatically adds separators via CSS. +toc: true +--- + +## Example + +Use an ordered or unordered list with linked list items to create a minimally styled breadcrumb. Use our utilities to add additional styles as desired. + + + + + + + + `} /> + +## Dividers + +Dividers are automatically added in CSS through [`::before`](https://developer.mozilla.org/en-US/docs/Web/CSS/::before) and [`content`](https://developer.mozilla.org/en-US/docs/Web/CSS/content). They can be changed by modifying a local CSS custom property `--bs-breadcrumb-divider`, or through the `$breadcrumb-divider` Sass variable — and `$breadcrumb-divider-flipped` for its RTL counterpart, if needed. We default to our Sass variable, which is set as a fallback to the custom property. This way, you get a global divider that you can override without recompiling CSS at any time. + + + + `} /> + +When modifying via Sass, the [quote](https://sass-lang.com/documentation/modules/string/#quote) function is required to generate the quotes around a string. For example, using `>` as the divider, you can use this: + +```scss +$breadcrumb-divider: quote(">"); +``` + +It’s also possible to use an **embedded SVG icon**. Apply it via our CSS custom property, or use the Sass variable. + + +**Inlined SVG requires properly escaped characters.** Some reserved characters, such as `<`, `>` and `#`, must be URL-encoded or escaped. We do this with the `$breadcrumb-divider` variable using our [`escape-svg()` Sass function]([[docsref:/customize/sass#escape-svg]]). When customizing the CSS variable, you must handle this yourself. Read [Kevin Weber’s explanations on CodePen](https://codepen.io/kevinweber/pen/dXWoRw ) for more info. + + + + + `} /> + +```scss +$breadcrumb-divider: url("data:image/svg+xml,"); +``` + +You can also remove the divider setting `--bs-breadcrumb-divider: '';` (empty strings in CSS custom properties counts as a value), or setting the Sass variable to `$breadcrumb-divider: none;`. + + + + `} /> + + +```scss +$breadcrumb-divider: none; +``` + +## Accessibility + +Since breadcrumbs provide a navigation, it’s a good idea to add a meaningful label such as `aria-label="breadcrumb"` to describe the type of navigation provided in the ``} /> + +## Directions + + +**Directions are flipped in RTL mode.** As such, `.dropstart` will appear on the right side. + + +### Centered + +Make the dropdown menu centered below the toggle with `.dropdown-center` on the parent element. + + + + + `} /> + +### Dropup + +Trigger dropdown menus above elements by adding `.dropup` to the parent element. + + + + + +
    + + + +
    `} /> + +```html + +
    + + +
    + + +
    + + + +
    +``` + +### Dropup centered + +Make the dropup menu centered above the toggle with `.dropup-center` on the parent element. + + + + + `} /> + +### Dropend + +Trigger dropdown menus at the right of the elements by adding `.dropend` to the parent element. + + + + + +
    + + + +
    `} /> + +```html + +
    + + +
    + + +
    + + + +
    +``` + +### Dropstart + +Trigger dropdown menus at the left of the elements by adding `.dropstart` to the parent element. + + + + + +
    + + + +
    `} /> + +```html + +
    + + +
    + + +
    + + + +
    +``` + +## Menu items + +You can use `` or ` + + `} /> + +You can also create non-interactive dropdown items with `.dropdown-item-text`. Feel free to style further with custom CSS or text utilities. + + +
  • Dropdown item text
  • +
  • Action
  • +
  • Another action
  • +
  • Something else here
  • + `} /> + +### Active + +Add `.active` to items in the dropdown to **style them as active**. To convey the active state to assistive technologies, use the `aria-current` attribute — using the `page` value for the current page, or `true` for the current item in a set. + + +
  • Regular link
  • +
  • Active link
  • +
  • Another link
  • + `} /> + +### Disabled + +Add `.disabled` to items in the dropdown to **style them as disabled**. + + +
  • Regular link
  • +
  • Disabled link
  • +
  • Another link
  • + `} /> + +## Menu alignment + +By default, a dropdown menu is automatically positioned 100% from the top and along the left side of its parent. You can change this with the directional `.drop*` classes, but you can also control them with additional modifier classes. + +Add `.dropdown-menu-end` to a `.dropdown-menu` to right align the dropdown menu. Directions are mirrored when using Bootstrap in RTL, meaning `.dropdown-menu-end` will appear on the left side. + + +**Heads up!** Dropdowns are positioned thanks to Popper except when they are contained in a navbar. + + + + + + `} /> + +### Responsive alignment + +If you want to use responsive alignment, disable dynamic positioning by adding the `data-bs-display="static"` attribute and use the responsive variation classes. + +To align **right** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu{-sm|-md|-lg|-xl|-xxl}-end`. + + + + + `} /> + +To align **left** the dropdown menu with the given breakpoint or larger, add `.dropdown-menu-end` and `.dropdown-menu{-sm|-md|-lg|-xl|-xxl}-start`. + + + + + `} /> + +Note that you don’t need to add a `data-bs-display="static"` attribute to dropdown buttons in navbars, since Popper isn’t used in navbars. + +### Alignment options + +Taking most of the options shown above, here’s a small kitchen sink demo of various dropdown alignment options in one place. + + + + + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    `} /> + +## Menu content + +### Headers + +Add a header to label sections of actions in any dropdown menu. + + +
  • +
  • Action
  • +
  • Another action
  • + `} /> + +### Dividers + +Separate groups of related menu items with a divider. + + +
  • Action
  • +
  • Another action
  • +
  • Something else here
  • +
  • +
  • Separated link
  • + `} /> + +### Text + +Place any freeform text within a dropdown menu with text and use [spacing utilities]([[docsref:/utilities/spacing]]). Note that you’ll likely need additional sizing styles to constrain the menu width. + + +

    + Some example text that’s free-flowing within the dropdown menu. +

    +

    + And this is more example text. +

    + `} /> + +### Forms + +Put a form within a dropdown menu, or make it into a dropdown menu, and use [margin or padding utilities]([[docsref:/utilities/spacing]]) to give it the negative space you require. + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + +
    + + New around here? Sign up + Forgot password? + `} /> + + + + + `} /> + +## Dropdown options + +Use `data-bs-offset` or `data-bs-reference` to change the location of the dropdown. + + + +
    + + + +
    + `} /> + +### Auto close behavior + +By default, the dropdown menu is closed when clicking inside or outside the dropdown menu. You can use the `autoClose` option to change this behavior of the dropdown. + + + + + + +
    + + +
    + +
    + + +
    + +
    + + +
    `} /> + +## CSS + +### Variables + + + +As part of Bootstrap’s evolving CSS variables approach, dropdowns now use local CSS variables on `.dropdown-menu` for enhanced real-time customization. Values for the CSS variables are set via Sass, so Sass customization is still supported, too. + + + + +Dropdown items include at least one variable that is not set on `.dropdown`. This allows you to provide a new value while Bootstrap defaults to a fallback value. + +- `--bs-dropdown-item-border-radius` + + +Customization through CSS variables can be seen on the `.dropdown-menu-dark` class where we override specific values without adding duplicate CSS selectors. + + + +### Sass variables + +Variables for all dropdowns: + + + +Variables for the [dark dropdown](#dark-dropdowns): + + + +Variables for the CSS-based carets that indicate a dropdown’s interactivity: + + + +### Sass mixins + +Mixins are used to generate the CSS-based carets and can be found in `scss/mixins/_caret.scss`. + + + +## Usage + +Via data attributes or JavaScript, the dropdown plugin toggles hidden content (dropdown menus) by toggling the `.show` class on the parent `.dropdown-menu`. The `data-bs-toggle="dropdown"` attribute is relied on for closing dropdown menus at an application level, so it’s a good idea to always use it. + + +On touch-enabled devices, opening a dropdown adds empty `mouseover` handlers to the immediate children of the `` element. This admittedly ugly hack is necessary to work around a [quirk in iOs’ event delegation](https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html), which would otherwise prevent a tap anywhere outside of the dropdown from triggering the code that closes the dropdown. Once the dropdown is closed, these additional empty `mouseover` handlers are removed. + + +### Via data attributes + +Add `data-bs-toggle="dropdown"` to a link or button to toggle a dropdown. + +```html + +``` + +### Via JavaScript + + +Dropdowns must have `data-bs-toggle="dropdown"` on their trigger element, regardless of whether you call your dropdown via JavaScript or use the data-api. + + +Call the dropdowns via JavaScript: + +```js +const dropdownElementList = document.querySelectorAll('.dropdown-toggle') +const dropdownList = [...dropdownElementList].map(dropdownToggleEl => new bootstrap.Dropdown(dropdownToggleEl)) +``` + +### Options + + + + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `autoClose` | boolean, string | `true` | Configure the auto close behavior of the dropdown:
    • `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.
    • `false` - the dropdown will be closed by clicking the toggle button and manually calling `hide` or `toggle` method. (Also will not be closed by pressing Esc key)
    • `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.
    • `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
    Note: the dropdown can always be closed with the Esc key. | +| `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the dropdown menu (applies only to Popper’s preventOverflow modifier). By default it’s `clippingParents` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Popper’s [detectOverflow docs](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/utils/detect-overflow.md/#boundary). | +| `display` | string | `'dynamic'` | By default, we use Popper for dynamic positioning. Disable this with `static`. | +| `offset` | array, string, function | `[0, 2]` | Offset of the dropdown relative to its target. You can pass a string in data attributes with comma separated values like: `data-bs-offset="10,20"`. When a function is used to determine the offset, it is called with an object containing the popper placement, the reference, and popper rects as its first argument. The triggering element DOM node is passed as the second argument. The function must return an array with two numbers: [skidding](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/modifiers/offset.md/#skidding-1), [distance](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/modifiers/offset.md/#distance-1). For more information refer to Popper’s [offset docs](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/modifiers/offset.md/#options). | +| `popperConfig` | null, object, function | `null` | To change Bootstrap’s default Popper config, see [Popper’s configuration](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/constructors.md/#options). When a function is used to create the Popper configuration, it’s called with an object that contains the Bootstrap’s default Popper configuration. It helps you use and merge the default with your own configuration. The function must return a configuration object for Popper. | +| `reference` | string, element, object | `'toggle'` | Reference element of the dropdown menu. Accepts the values of `'toggle'`, `'parent'`, an HTMLElement reference or an object providing `getBoundingClientRect`. For more information refer to Popper’s [constructor docs](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/constructors.md/#createpopper) and [virtual element docs](https://github.com/floating-ui/popper-docs/blob/main/docs/v2/virtual-elements.md/). | +
    + +#### Using function with `popperConfig` + +```js +const dropdown = new bootstrap.Dropdown(element, { + popperConfig(defaultBsPopperConfig) { + // const newPopperConfig = {...} + // use defaultBsPopperConfig if needed... + // return newPopperConfig + } +}) +``` + +### Methods + + +| Method | Description | +| --- | --- | +| `dispose` | Destroys an element’s dropdown. (Removes stored data on the DOM element) | +| `getInstance` | Static method which allows you to get the dropdown instance associated to a DOM element, you can use it like this: `bootstrap.Dropdown.getInstance(element)`. | +| `getOrCreateInstance` | Static method which returns a dropdown instance associated to a DOM element or create a new one in case it wasn’t initialized. You can use it like this: `bootstrap.Dropdown.getOrCreateInstance(element)`. | +| `hide` | Hides the dropdown menu of a given navbar or tabbed navigation. | +| `show` | Shows the dropdown menu of a given navbar or tabbed navigation. | +| `toggle` | Toggles the dropdown menu of a given navbar or tabbed navigation. | +| `update` | Updates the position of an element’s dropdown. | + + +### Events + +All dropdown events are fired at the toggling element and then bubbled up. So you can also add event listeners on the `.dropdown-menu`’s parent element. `hide.bs.dropdown` and `hidden.bs.dropdown` events have a `clickEvent` property (only when the original Event type is `click`) that contains an Event Object for the click event. + + +| Event type | Description | +| --- | --- | +| `hide.bs.dropdown` | Fires immediately when the `hide` instance method has been called. | +| `hidden.bs.dropdown` | Fired when the dropdown has finished being hidden from the user and CSS transitions have completed. | +| `show.bs.dropdown` | Fires immediately when the `show` instance method is called. | +| `shown.bs.dropdown` | Fired when the dropdown has been made visible to the user and CSS transitions have completed. | + + +```js +const myDropdown = document.getElementById('myDropdown') +myDropdown.addEventListener('show.bs.dropdown', event => { + // do something... +}) +``` diff --git a/site/src/content/docs/components/list-group.mdx b/site/src/content/docs/components/list-group.mdx new file mode 100644 index 000000000000..59827ddd41bb --- /dev/null +++ b/site/src/content/docs/components/list-group.mdx @@ -0,0 +1,448 @@ +--- +title: List group +description: List groups are a flexible and powerful component for displaying a series of content. Modify and extend them to support just about any content within. +toc: true +--- + +import { getData } from '@libs/data' + +## Basic example + +The most basic list group is an unordered list with list items and the proper classes. Build upon it with the options that follow, or with your own CSS as needed. + + +
  • An item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Active items + +Add `.active` to a `.list-group-item` to indicate the current active selection. + + +
  • An active item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Links and buttons + +Use ``s or ` + + + + + `} /> + +## Flush + +Add `.list-group-flush` to remove some borders and rounded corners to render list group items edge-to-edge in a parent container (e.g., cards). + + +
  • An item
  • +
  • A second item
  • +
  • A third item
  • +
  • A fourth item
  • +
  • And a fifth one
  • + `} /> + +## Numbered + +Add the `.list-group-numbered` modifier class (and optionally use an `
      ` element) to opt into numbered list group items. Numbers are generated via CSS (as opposed to a `
        `s default browser styling) for better placement inside list group items and to allow for better customization. + +Numbers are generated by `counter-reset` on the `
          `, and then styled and placed with a `::before` pseudo-element on the `
        1. ` with `counter-increment` and `content`. + + +
        2. A list item
        3. +
        4. A list item
        5. +
        6. A list item
        7. +
        `} /> + +These work great with custom content as well. + + +
      1. +
        +
        Subheading
        + Content for list item +
        + 14 +
      2. +
      3. +
        +
        Subheading
        + Content for list item +
        + 14 +
      4. +
      5. +
        +
        Subheading
        + Content for list item +
        + 14 +
      6. +
      `} /> + +## Horizontal + +Add `.list-group-horizontal` to change the layout of list group items from vertical to horizontal across all breakpoints. Alternatively, choose a responsive variant `.list-group-horizontal-{sm|md|lg|xl|xxl}` to make a list group horizontal starting at that breakpoint’s `min-width`. Currently **horizontal list groups cannot be combined with flush list groups.** + +**ProTip:** Want equal-width list group items when horizontal? Add `.flex-fill` to each list group item. + + `
        +
      • An item
      • +
      • A second item
      • +
      • A third item
      • +
      `)} /> + +## Variants + + +**Heads up!** As of v5.3.0, the `list-group-item-variant()` Sass mixin is deprecated. List group item variants now have their CSS variables overridden in [a Sass loop](#sass-loops). + + +Use contextual classes to style list items with a stateful background and color. + + +
    1. A simple default list group item
    2. + `, + ...getData('theme-colors').map((themeColor) => `
    3. A simple ${themeColor.name} list group item
    4. `), + `` + ]} /> + +### For links and buttons + +Contextual classes also work with `.list-group-item-action` for `
      ` and ` + + + + + + + + +```html + +``` + + +In the above static example, we use `
      `, to avoid issues with the heading hierarchy in the documentation page. Structurally, however, a modal dialog represents its own separate document/context, so the `.modal-title` should ideally be an `

      `. If necessary, you can use the [font size utilities]([[docsref:/utilities/text#font-size]]) to control the heading’s appearance. All the following live examples use this approach. + + +### Live demo + +Toggle a working modal demo by clicking the button below. It will slide down and fade in from the top of the page. + + + +Launch demo modal +`} /> + +```html + + + + + +``` + +### Static backdrop + +When backdrop is set to static, the modal will not close when clicking outside of it. Click the button below to try it. + + + +Launch static backdrop modal +`} /> + +```html + + + + + +``` + +### Scrolling long content + +When modals become too long for the user’s viewport or device, they scroll independent of the page itself. Try the demo below to see what we mean. + + + +Launch demo modal +`} /> + +You can also create a scrollable modal that allows scrolling the modal body by adding `.modal-dialog-scrollable` to `.modal-dialog`. + + + +Launch demo modal +`} /> + +```html + + +``` + +### Vertically centered + +Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal. + +