diff --git a/.changeset/brown-symbols-create.md b/.changeset/brown-symbols-create.md new file mode 100644 index 0000000000..d4a62b1280 --- /dev/null +++ b/.changeset/brown-symbols-create.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': minor +--- + +Techdocs CLI serve supports automatic refresh, relying on `mkdocs` `watch` feature. diff --git a/.changeset/brown-turkeys-send.md b/.changeset/brown-turkeys-send.md new file mode 100644 index 0000000000..87a1d03b01 --- /dev/null +++ b/.changeset/brown-turkeys-send.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Log before provider-orphaning eviction happens diff --git a/.changeset/bui-themer-bui-palette-additions.md b/.changeset/bui-themer-bui-palette-additions.md new file mode 100644 index 0000000000..645641b787 --- /dev/null +++ b/.changeset/bui-themer-bui-palette-additions.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mui-to-bui': minor +--- + +This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. diff --git a/.changeset/create-app-1759243273.md b/.changeset/create-app-1759243273.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1759243273.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/create-app-1759849206.md b/.changeset/create-app-1759849206.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1759849206.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/cuddly-mugs-act.md b/.changeset/cuddly-mugs-act.md new file mode 100644 index 0000000000..1b939d8196 --- /dev/null +++ b/.changeset/cuddly-mugs-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. diff --git a/.changeset/curvy-bobcats-melt.md b/.changeset/curvy-bobcats-melt.md new file mode 100644 index 0000000000..a9a5d58e5c --- /dev/null +++ b/.changeset/curvy-bobcats-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Don't change loading to false until we've actually got some log state diff --git a/.changeset/dependabot-eaf5987.md b/.changeset/dependabot-eaf5987.md new file mode 100644 index 0000000000..bf1475613a --- /dev/null +++ b/.changeset/dependabot-eaf5987.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 diff --git a/.changeset/eighty-cows-care.md b/.changeset/eighty-cows-care.md new file mode 100644 index 0000000000..d4c9fafa4d --- /dev/null +++ b/.changeset/eighty-cows-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': minor +--- + +**BREAKING**: Added a new `PasswordField` component. As part of this change, the `password` and `search` types have been removed from `TextField`. diff --git a/.changeset/famous-loops-tickle.md b/.changeset/famous-loops-tickle.md new file mode 100644 index 0000000000..ae8c9d80ee --- /dev/null +++ b/.changeset/famous-loops-tickle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +The configmaps added to be rendered diff --git a/.changeset/few-weeks-create.md b/.changeset/few-weeks-create.md new file mode 100644 index 0000000000..c9de7cb4be --- /dev/null +++ b/.changeset/few-weeks-create.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Making href mandatory in tabs that are part of a Header component diff --git a/.changeset/flat-peas-run.md b/.changeset/flat-peas-run.md new file mode 100644 index 0000000000..bb075ce910 --- /dev/null +++ b/.changeset/flat-peas-run.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-catalog-graph': patch +--- + +Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. diff --git a/.changeset/forty-crabs-travel.md b/.changeset/forty-crabs-travel.md new file mode 100644 index 0000000000..83e281aa51 --- /dev/null +++ b/.changeset/forty-crabs-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-signals': patch +--- + +Remove `app-root-element:signals/signals-display` which was not doing anything useful diff --git a/.changeset/full-chefs-roll.md b/.changeset/full-chefs-roll.md new file mode 100644 index 0000000000..0a13f116ec --- /dev/null +++ b/.changeset/full-chefs-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. diff --git a/.changeset/fuzzy-trams-kick.md b/.changeset/fuzzy-trams-kick.md new file mode 100644 index 0000000000..b433eac803 --- /dev/null +++ b/.changeset/fuzzy-trams-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +fix(home): correct `clearAll` logic to properly handle `deletable` flag diff --git a/.changeset/giant-weeks-jump.md b/.changeset/giant-weeks-jump.md new file mode 100644 index 0000000000..1b11bd1490 --- /dev/null +++ b/.changeset/giant-weeks-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Add `initialRelationAggregation` and `showAggregateMembersToggle` options to `EntityMembersListCard` as well to `EntityOwnershipCard` diff --git a/.changeset/heavy-cooks-divide.md b/.changeset/heavy-cooks-divide.md new file mode 100644 index 0000000000..181cd01075 --- /dev/null +++ b/.changeset/heavy-cooks-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-slack': minor +--- + +Adds username as optional config in order to send Slack notifications with a specific username in the case when using one Slack App for more than just Backstage. diff --git a/.changeset/modern-cats-double.md b/.changeset/modern-cats-double.md new file mode 100644 index 0000000000..0c5681aaad --- /dev/null +++ b/.changeset/modern-cats-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bump `better-sqlite3` to the latest version diff --git a/.changeset/modern-pugs-appear.md b/.changeset/modern-pugs-appear.md new file mode 100644 index 0000000000..8b502fb52d --- /dev/null +++ b/.changeset/modern-pugs-appear.md @@ -0,0 +1,19 @@ +--- +'@backstage/cli': patch +--- + +Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + +**Example usage:** + +Consider the following plugin dev folder structure: + +``` +dev/ + index.tsx + alpha/ + index.ts +``` + +- The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; +- Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. diff --git a/.changeset/nasty-moose-rescue.md b/.changeset/nasty-moose-rescue.md new file mode 100644 index 0000000000..875c19e9a4 --- /dev/null +++ b/.changeset/nasty-moose-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too. diff --git a/.changeset/pre.json b/.changeset/pre.json index 03b4d762e2..458c97896e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -203,37 +203,74 @@ "@backstage/plugin-techdocs-react": "1.3.3", "@backstage/plugin-user-settings": "0.8.26", "@backstage/plugin-user-settings-backend": "0.3.6", - "@backstage/plugin-user-settings-common": "0.0.1" + "@backstage/plugin-user-settings-common": "0.0.1", + "@backstage/plugin-mui-to-bui": "0.1.0" }, "changesets": [ "brave-teeth-reply", "brown-falcons-own", + "brown-turkeys-send", + "bui-themer-bui-palette-additions", "calm-trains-tie", "clever-papers-watch", "cold-coats-show", "cool-baboons-count", "create-app-1758639549", "create-app-1758718573", + "create-app-1759243273", + "create-app-1759849206", + "cuddly-mugs-act", + "curvy-bobcats-melt", + "dependabot-eaf5987", "eager-toes-start", + "famous-loops-tickle", "fast-heads-brake", "fast-queens-guess", + "few-weeks-create", "five-olives-bet", + "flat-peas-run", + "forty-crabs-travel", + "full-chefs-roll", + "fuzzy-trams-kick", + "giant-weeks-jump", + "heavy-cooks-divide", "hungry-crews-fetch", "itchy-falcons-leave", "kind-places-reply", "legal-eagles-jog", + "modern-pugs-appear", + "moody-singers-deny", + "nasty-moose-rescue", "nice-readers-judge", "public-sites-admire", + "public-wombats-say", "rare-states-pay", "ready-poems-change", "ready-pots-arrive", + "red-dodos-work", "red-times-bet", + "sad-women-rule", "salty-words-wash", + "shiny-candles-hide", "short-aliens-invite", + "silent-mice-play", "six-cooks-battle", + "slimy-signs-agree", "solid-bikes-leave", "tame-hairs-smash", "tender-cups-tap", - "wet-spiders-wait" + "thin-hoops-bathe", + "thirty-rules-press", + "tidy-coats-know", + "tired-mice-cheer", + "tough-clocks-attack", + "twelve-guests-sit", + "twelve-oranges-grin", + "two-emus-like", + "unified-theme-attr-stack", + "warm-items-look", + "wet-spiders-wait", + "wide-flies-jog", + "yarn-plugin-integration" ] } diff --git a/.changeset/public-wombats-say.md b/.changeset/public-wombats-say.md new file mode 100644 index 0000000000..b8a72e8436 --- /dev/null +++ b/.changeset/public-wombats-say.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Fixed dependency graph automatically scrolling forever diff --git a/.changeset/red-dodos-work.md b/.changeset/red-dodos-work.md new file mode 100644 index 0000000000..1bc92d2d59 --- /dev/null +++ b/.changeset/red-dodos-work.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added new `catalog:validate-entity` action to actions registry. + +This action can be used to validate entities against the software catalog. +This is useful for validating `catalog-info.yaml` file changes locally using the +Backstage MCP server. diff --git a/.changeset/ripe-yaks-brake.md b/.changeset/ripe-yaks-brake.md new file mode 100644 index 0000000000..3b3097011b --- /dev/null +++ b/.changeset/ripe-yaks-brake.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Implemented AbortController request cancellation for overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start. diff --git a/.changeset/sad-women-rule.md b/.changeset/sad-women-rule.md new file mode 100644 index 0000000000..04bbbe1eed --- /dev/null +++ b/.changeset/sad-women-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Add react router for internal routing for ButtonLinks diff --git a/.changeset/shiny-candles-hide.md b/.changeset/shiny-candles-hide.md new file mode 100644 index 0000000000..7ec4a8062e --- /dev/null +++ b/.changeset/shiny-candles-hide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-notifications-backend-module-email': patch +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-notifications': patch +--- + +Removed unused dependencies diff --git a/.changeset/silent-mice-play.md b/.changeset/silent-mice-play.md new file mode 100644 index 0000000000..aa14cf69e3 --- /dev/null +++ b/.changeset/silent-mice-play.md @@ -0,0 +1,9 @@ +--- +'@backstage/cli': patch +--- + +Remove unused @octokit modules from cli package + +- @octokit/graphql +- @octokit/graphql-schema +- @octokit/oauth-app diff --git a/.changeset/slimy-signs-agree.md b/.changeset/slimy-signs-agree.md new file mode 100644 index 0000000000..c4307f0bd7 --- /dev/null +++ b/.changeset/slimy-signs-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Fixed exclude entity reference not working in notification sending diff --git a/.changeset/thin-hoops-bathe.md b/.changeset/thin-hoops-bathe.md new file mode 100644 index 0000000000..508d548431 --- /dev/null +++ b/.changeset/thin-hoops-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Remove auto selection of tabs for tabs that all have href defined diff --git a/.changeset/thirty-rules-press.md b/.changeset/thirty-rules-press.md new file mode 100644 index 0000000000..d9968317cb --- /dev/null +++ b/.changeset/thirty-rules-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': minor +--- + +Add a new `externalTokenHandlersServiceRef` to allow custom external token validations diff --git a/.changeset/tidy-coats-know.md b/.changeset/tidy-coats-know.md new file mode 100644 index 0000000000..e641c12784 --- /dev/null +++ b/.changeset/tidy-coats-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it diff --git a/.changeset/tired-mice-cheer.md b/.changeset/tired-mice-cheer.md new file mode 100644 index 0000000000..929aae0f59 --- /dev/null +++ b/.changeset/tired-mice-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Avoid overriding onChange when spreading props diff --git a/.changeset/tough-clocks-attack.md b/.changeset/tough-clocks-attack.md new file mode 100644 index 0000000000..caf3b33fe4 --- /dev/null +++ b/.changeset/tough-clocks-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Using react router for internal links in the Menu component diff --git a/.changeset/twelve-guests-sit.md b/.changeset/twelve-guests-sit.md new file mode 100644 index 0000000000..0d75e07f0c --- /dev/null +++ b/.changeset/twelve-guests-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +Fixes calculation of CPU utilization in the PodTable diff --git a/.changeset/twelve-oranges-grin.md b/.changeset/twelve-oranges-grin.md new file mode 100644 index 0000000000..ab1ede0a0d --- /dev/null +++ b/.changeset/twelve-oranges-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved. diff --git a/.changeset/two-emus-like.md b/.changeset/two-emus-like.md new file mode 100644 index 0000000000..e8af7a7fa0 --- /dev/null +++ b/.changeset/two-emus-like.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': patch +--- + +Ensure that the text rendering has react keys for all elements diff --git a/.changeset/unified-theme-attr-stack.md b/.changeset/unified-theme-attr-stack.md new file mode 100644 index 0000000000..003e05018f --- /dev/null +++ b/.changeset/unified-theme-attr-stack.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered. diff --git a/.changeset/warm-items-look.md b/.changeset/warm-items-look.md new file mode 100644 index 0000000000..b8a2abd61d --- /dev/null +++ b/.changeset/warm-items-look.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-github-org': patch +'@backstage/plugin-catalog-backend-module-gitlab-org': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-kubernetes-cluster': patch +'@backstage/plugin-kubernetes': patch +--- + +Removed unused dependencies diff --git a/.changeset/wide-flies-jog.md b/.changeset/wide-flies-jog.md new file mode 100644 index 0000000000..4fbafe66a1 --- /dev/null +++ b/.changeset/wide-flies-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added missing form fields for the new frontend system. diff --git a/.changeset/yarn-plugin-integration.md b/.changeset/yarn-plugin-integration.md new file mode 100644 index 0000000000..2847e89273 --- /dev/null +++ b/.changeset/yarn-plugin-integration.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4f0e0ac47d..98073c5128 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ The following files contain guidelines for the project: Before any of these commands can be run, you need to run `yarn install` in the project root. - Build: There is no need to build the project during development, and it is verified automatically in the CI pipeline. -- Test: Use `yarn test ` in the project root to run tests. The path can be either a single file or a directory, and be omitted to run tests for all changed files. +- Test: Use `yarn test --no-watch ` in the project root to run tests. The path can be either a single file or a directory. Always provide a path, avoid running all tests. - Type checking: Use `yarn tsc` in the project root to run the type checker. - Code formatting: Use `yarn prettier --write ` to format code. - Lint: Use `yarn lint --fix` in the project root to run the linter. diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 8f3dcb65ff..db3f3adf2c 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -282,6 +282,7 @@ modularization monorepo Monorepo monorepos +monospace morgan msgraph msw diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml index 0c9f110b4c..18511b936d 100644 --- a/.github/workflows/verify_chromatic.yml +++ b/.github/workflows/verify_chromatic.yml @@ -5,7 +5,7 @@ on: push: branches: - master - pull_request: + pull_request_target: paths: - '.github/workflows/verify_chromatic.yml' - '.storybook/**' @@ -31,6 +31,8 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + # For pull_request_target, we need to check out the PR branch + ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }} fetch-depth: 10000 # Required to retrieve git history - name: Use node.js ${{ matrix.node-version }} @@ -63,7 +65,7 @@ jobs: packages/ui/src/**/*.css - name: Prepare Chromatic Message - if: github.event_name == 'pull_request' && steps.chromatic.outputs.url + if: github.event_name == 'pull_request_target' && steps.chromatic.outputs.url id: prepare-message run: | if [ "${{ steps.chromatic.outputs.changeCount }}" = "0" ] || [ -z "${{ steps.chromatic.outputs.changeCount }}" ]; then @@ -73,7 +75,7 @@ jobs: fi - name: Post Chromatic Link in PR Comment - if: github.event_name == 'pull_request' && steps.chromatic.outputs.url + if: github.event_name == 'pull_request_target' && steps.chromatic.outputs.url uses: mshick/add-pr-comment@v2 with: message: | diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 080d14a743..64ce9e7b12 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -41,11 +41,10 @@ jobs: with: python-version: '3.9' - - name: install dependencies - run: yarn install --immutable - - - name: generate types - run: yarn tsc + - name: yarn install + uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17 + with: + cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: build techdocs-cli working-directory: packages/techdocs-cli diff --git a/docs-ui/public/theme-backstage.css b/docs-ui/public/theme-backstage.css index 98b0f62b47..9d9c282f5c 100644 --- a/docs-ui/public/theme-backstage.css +++ b/docs-ui/public/theme-backstage.css @@ -1,2 +1,2 @@ /*! modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize */ -@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-SearchField{&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{padding:0;transition:width .3s ease-in-out;&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=small]{width:2rem;height:2rem}&[data-size=medium] .bui-Input{padding-left:0;&::placeholder{opacity:0}}&[data-size=small] .bui-Input{padding-left:0;&::placeholder{opacity:0}}&[data-size=small] .bui-InputIcon{left:var(--bui-space-2)}&[data-size=medium] .bui-InputIcon{left:10px}}}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file +@layer base{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:100%;tab-size:4;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;line-height:1.15}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{border-color:currentColor}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}}.bui-p{padding:var(--p)}.bui-p-0\.5{padding:var(--bui-space-0_5)}.bui-p-1{padding:var(--bui-space-1)}.bui-p-1\.5{padding:var(--bui-space-1_5)}.bui-p-2{padding:var(--bui-space-2)}.bui-p-3{padding:var(--bui-space-3)}.bui-p-4{padding:var(--bui-space-4)}.bui-p-5{padding:var(--bui-space-5)}.bui-p-6{padding:var(--bui-space-6)}.bui-p-7{padding:var(--bui-space-7)}.bui-p-8{padding:var(--bui-space-8)}.bui-p-9{padding:var(--bui-space-9)}.bui-p-10{padding:var(--bui-space-10)}.bui-p-11{padding:var(--bui-space-11)}.bui-p-12{padding:var(--bui-space-12)}.bui-p-13{padding:var(--bui-space-13)}.bui-p-14{padding:var(--bui-space-14)}@media (width>=640px){.xs\:bui-p{padding:var(--p-xs)}.xs\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xs\:bui-p-1{padding:var(--bui-space-1)}.xs\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xs\:bui-p-2{padding:var(--bui-space-2)}.xs\:bui-p-3{padding:var(--bui-space-3)}.xs\:bui-p-4{padding:var(--bui-space-4)}.xs\:bui-p-5{padding:var(--bui-space-5)}.xs\:bui-p-6{padding:var(--bui-space-6)}.xs\:bui-p-7{padding:var(--bui-space-7)}.xs\:bui-p-8{padding:var(--bui-space-8)}.xs\:bui-p-9{padding:var(--bui-space-9)}.xs\:bui-p-10{padding:var(--bui-space-10)}.xs\:bui-p-11{padding:var(--bui-space-11)}.xs\:bui-p-12{padding:var(--bui-space-12)}.xs\:bui-p-13{padding:var(--bui-space-13)}.xs\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-p{padding:var(--p-sm)}.sm\:bui-p-0\.5{padding:var(--bui-space-0_5)}.sm\:bui-p-1{padding:var(--bui-space-1)}.sm\:bui-p-1\.5{padding:var(--bui-space-1_5)}.sm\:bui-p-2{padding:var(--bui-space-2)}.sm\:bui-p-3{padding:var(--bui-space-3)}.sm\:bui-p-4{padding:var(--bui-space-4)}.sm\:bui-p-5{padding:var(--bui-space-5)}.sm\:bui-p-6{padding:var(--bui-space-6)}.sm\:bui-p-7{padding:var(--bui-space-7)}.sm\:bui-p-8{padding:var(--bui-space-8)}.sm\:bui-p-9{padding:var(--bui-space-9)}.sm\:bui-p-10{padding:var(--bui-space-10)}.sm\:bui-p-11{padding:var(--bui-space-11)}.sm\:bui-p-12{padding:var(--bui-space-12)}.sm\:bui-p-13{padding:var(--bui-space-13)}.sm\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-p{padding:var(--p-md)}.md\:bui-p-0\.5{padding:var(--bui-space-0_5)}.md\:bui-p-1{padding:var(--bui-space-1)}.md\:bui-p-1\.5{padding:var(--bui-space-1_5)}.md\:bui-p-2{padding:var(--bui-space-2)}.md\:bui-p-3{padding:var(--bui-space-3)}.md\:bui-p-4{padding:var(--bui-space-4)}.md\:bui-p-5{padding:var(--bui-space-5)}.md\:bui-p-6{padding:var(--bui-space-6)}.md\:bui-p-7{padding:var(--bui-space-7)}.md\:bui-p-8{padding:var(--bui-space-8)}.md\:bui-p-9{padding:var(--bui-space-9)}.md\:bui-p-10{padding:var(--bui-space-10)}.md\:bui-p-11{padding:var(--bui-space-11)}.md\:bui-p-12{padding:var(--bui-space-12)}.md\:bui-p-13{padding:var(--bui-space-13)}.md\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-p{padding:var(--p-lg)}.lg\:bui-p-0\.5{padding:var(--bui-space-0_5)}.lg\:bui-p-1{padding:var(--bui-space-1)}.lg\:bui-p-1\.5{padding:var(--bui-space-1_5)}.lg\:bui-p-2{padding:var(--bui-space-2)}.lg\:bui-p-3{padding:var(--bui-space-3)}.lg\:bui-p-4{padding:var(--bui-space-4)}.lg\:bui-p-5{padding:var(--bui-space-5)}.lg\:bui-p-6{padding:var(--bui-space-6)}.lg\:bui-p-7{padding:var(--bui-space-7)}.lg\:bui-p-8{padding:var(--bui-space-8)}.lg\:bui-p-9{padding:var(--bui-space-9)}.lg\:bui-p-10{padding:var(--bui-space-10)}.lg\:bui-p-11{padding:var(--bui-space-11)}.lg\:bui-p-12{padding:var(--bui-space-12)}.lg\:bui-p-13{padding:var(--bui-space-13)}.lg\:bui-p-14{padding:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-p{padding:var(--p-xl)}.xl\:bui-p-0\.5{padding:var(--bui-space-0_5)}.xl\:bui-p-1{padding:var(--bui-space-1)}.xl\:bui-p-1\.5{padding:var(--bui-space-1_5)}.xl\:bui-p-2{padding:var(--bui-space-2)}.xl\:bui-p-3{padding:var(--bui-space-3)}.xl\:bui-p-4{padding:var(--bui-space-4)}.xl\:bui-p-5{padding:var(--bui-space-5)}.xl\:bui-p-6{padding:var(--bui-space-6)}.xl\:bui-p-7{padding:var(--bui-space-7)}.xl\:bui-p-8{padding:var(--bui-space-8)}.xl\:bui-p-9{padding:var(--bui-space-9)}.xl\:bui-p-10{padding:var(--bui-space-10)}.xl\:bui-p-11{padding:var(--bui-space-11)}.xl\:bui-p-12{padding:var(--bui-space-12)}.xl\:bui-p-13{padding:var(--bui-space-13)}.xl\:bui-p-14{padding:var(--bui-space-14)}}.bui-pl{padding-left:var(--pl)}.bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.bui-pl-1{padding-left:var(--bui-space-1)}.bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.bui-pl-2{padding-left:var(--bui-space-2)}.bui-pl-3{padding-left:var(--bui-space-3)}.bui-pl-4{padding-left:var(--bui-space-4)}.bui-pl-5{padding-left:var(--bui-space-5)}.bui-pl-6{padding-left:var(--bui-space-6)}.bui-pl-7{padding-left:var(--bui-space-7)}.bui-pl-8{padding-left:var(--bui-space-8)}.bui-pl-9{padding-left:var(--bui-space-9)}.bui-pl-10{padding-left:var(--bui-space-10)}.bui-pl-11{padding-left:var(--bui-space-11)}.bui-pl-12{padding-left:var(--bui-space-12)}.bui-pl-13{padding-left:var(--bui-space-13)}.bui-pl-14{padding-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pl{padding-left:var(--pl-xs)}.xs\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xs\:bui-pl-1{padding-left:var(--bui-space-1)}.xs\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xs\:bui-pl-2{padding-left:var(--bui-space-2)}.xs\:bui-pl-3{padding-left:var(--bui-space-3)}.xs\:bui-pl-4{padding-left:var(--bui-space-4)}.xs\:bui-pl-5{padding-left:var(--bui-space-5)}.xs\:bui-pl-6{padding-left:var(--bui-space-6)}.xs\:bui-pl-7{padding-left:var(--bui-space-7)}.xs\:bui-pl-8{padding-left:var(--bui-space-8)}.xs\:bui-pl-9{padding-left:var(--bui-space-9)}.xs\:bui-pl-10{padding-left:var(--bui-space-10)}.xs\:bui-pl-11{padding-left:var(--bui-space-11)}.xs\:bui-pl-12{padding-left:var(--bui-space-12)}.xs\:bui-pl-13{padding-left:var(--bui-space-13)}.xs\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pl{padding-left:var(--pl-sm)}.sm\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.sm\:bui-pl-1{padding-left:var(--bui-space-1)}.sm\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.sm\:bui-pl-2{padding-left:var(--bui-space-2)}.sm\:bui-pl-3{padding-left:var(--bui-space-3)}.sm\:bui-pl-4{padding-left:var(--bui-space-4)}.sm\:bui-pl-5{padding-left:var(--bui-space-5)}.sm\:bui-pl-6{padding-left:var(--bui-space-6)}.sm\:bui-pl-7{padding-left:var(--bui-space-7)}.sm\:bui-pl-8{padding-left:var(--bui-space-8)}.sm\:bui-pl-9{padding-left:var(--bui-space-9)}.sm\:bui-pl-10{padding-left:var(--bui-space-10)}.sm\:bui-pl-11{padding-left:var(--bui-space-11)}.sm\:bui-pl-12{padding-left:var(--bui-space-12)}.sm\:bui-pl-13{padding-left:var(--bui-space-13)}.sm\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pl{padding-left:var(--pl-md)}.md\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.md\:bui-pl-1{padding-left:var(--bui-space-1)}.md\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.md\:bui-pl-2{padding-left:var(--bui-space-2)}.md\:bui-pl-3{padding-left:var(--bui-space-3)}.md\:bui-pl-4{padding-left:var(--bui-space-4)}.md\:bui-pl-5{padding-left:var(--bui-space-5)}.md\:bui-pl-6{padding-left:var(--bui-space-6)}.md\:bui-pl-7{padding-left:var(--bui-space-7)}.md\:bui-pl-8{padding-left:var(--bui-space-8)}.md\:bui-pl-9{padding-left:var(--bui-space-9)}.md\:bui-pl-10{padding-left:var(--bui-space-10)}.md\:bui-pl-11{padding-left:var(--bui-space-11)}.md\:bui-pl-12{padding-left:var(--bui-space-12)}.md\:bui-pl-13{padding-left:var(--bui-space-13)}.md\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pl{padding-left:var(--pl-lg)}.lg\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.lg\:bui-pl-1{padding-left:var(--bui-space-1)}.lg\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.lg\:bui-pl-2{padding-left:var(--bui-space-2)}.lg\:bui-pl-3{padding-left:var(--bui-space-3)}.lg\:bui-pl-4{padding-left:var(--bui-space-4)}.lg\:bui-pl-5{padding-left:var(--bui-space-5)}.lg\:bui-pl-6{padding-left:var(--bui-space-6)}.lg\:bui-pl-7{padding-left:var(--bui-space-7)}.lg\:bui-pl-8{padding-left:var(--bui-space-8)}.lg\:bui-pl-9{padding-left:var(--bui-space-9)}.lg\:bui-pl-10{padding-left:var(--bui-space-10)}.lg\:bui-pl-11{padding-left:var(--bui-space-11)}.lg\:bui-pl-12{padding-left:var(--bui-space-12)}.lg\:bui-pl-13{padding-left:var(--bui-space-13)}.lg\:bui-pl-14{padding-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pl{padding-left:var(--pl-xl)}.xl\:bui-pl-0\.5{padding-left:var(--bui-space-0_5)}.xl\:bui-pl-1{padding-left:var(--bui-space-1)}.xl\:bui-pl-1\.5{padding-left:var(--bui-space-1_5)}.xl\:bui-pl-2{padding-left:var(--bui-space-2)}.xl\:bui-pl-3{padding-left:var(--bui-space-3)}.xl\:bui-pl-4{padding-left:var(--bui-space-4)}.xl\:bui-pl-5{padding-left:var(--bui-space-5)}.xl\:bui-pl-6{padding-left:var(--bui-space-6)}.xl\:bui-pl-7{padding-left:var(--bui-space-7)}.xl\:bui-pl-8{padding-left:var(--bui-space-8)}.xl\:bui-pl-9{padding-left:var(--bui-space-9)}.xl\:bui-pl-10{padding-left:var(--bui-space-10)}.xl\:bui-pl-11{padding-left:var(--bui-space-11)}.xl\:bui-pl-12{padding-left:var(--bui-space-12)}.xl\:bui-pl-13{padding-left:var(--bui-space-13)}.xl\:bui-pl-14{padding-left:var(--bui-space-14)}}.bui-pr{padding-right:var(--pr)}.bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.bui-pr-1{padding-right:var(--bui-space-1)}.bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.bui-pr-2{padding-right:var(--bui-space-2)}.bui-pr-3{padding-right:var(--bui-space-3)}.bui-pr-4{padding-right:var(--bui-space-4)}.bui-pr-5{padding-right:var(--bui-space-5)}.bui-pr-6{padding-right:var(--bui-space-6)}.bui-pr-7{padding-right:var(--bui-space-7)}.bui-pr-8{padding-right:var(--bui-space-8)}.bui-pr-9{padding-right:var(--bui-space-9)}.bui-pr-10{padding-right:var(--bui-space-10)}.bui-pr-11{padding-right:var(--bui-space-11)}.bui-pr-12{padding-right:var(--bui-space-12)}.bui-pr-13{padding-right:var(--bui-space-13)}.bui-pr-14{padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pr{padding-right:var(--pr-xs)}.xs\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xs\:bui-pr-1{padding-right:var(--bui-space-1)}.xs\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xs\:bui-pr-2{padding-right:var(--bui-space-2)}.xs\:bui-pr-3{padding-right:var(--bui-space-3)}.xs\:bui-pr-4{padding-right:var(--bui-space-4)}.xs\:bui-pr-5{padding-right:var(--bui-space-5)}.xs\:bui-pr-6{padding-right:var(--bui-space-6)}.xs\:bui-pr-7{padding-right:var(--bui-space-7)}.xs\:bui-pr-8{padding-right:var(--bui-space-8)}.xs\:bui-pr-9{padding-right:var(--bui-space-9)}.xs\:bui-pr-10{padding-right:var(--bui-space-10)}.xs\:bui-pr-11{padding-right:var(--bui-space-11)}.xs\:bui-pr-12{padding-right:var(--bui-space-12)}.xs\:bui-pr-13{padding-right:var(--bui-space-13)}.xs\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pr{padding-right:var(--pr-sm)}.sm\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.sm\:bui-pr-1{padding-right:var(--bui-space-1)}.sm\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.sm\:bui-pr-2{padding-right:var(--bui-space-2)}.sm\:bui-pr-3{padding-right:var(--bui-space-3)}.sm\:bui-pr-4{padding-right:var(--bui-space-4)}.sm\:bui-pr-5{padding-right:var(--bui-space-5)}.sm\:bui-pr-6{padding-right:var(--bui-space-6)}.sm\:bui-pr-7{padding-right:var(--bui-space-7)}.sm\:bui-pr-8{padding-right:var(--bui-space-8)}.sm\:bui-pr-9{padding-right:var(--bui-space-9)}.sm\:bui-pr-10{padding-right:var(--bui-space-10)}.sm\:bui-pr-11{padding-right:var(--bui-space-11)}.sm\:bui-pr-12{padding-right:var(--bui-space-12)}.sm\:bui-pr-13{padding-right:var(--bui-space-13)}.sm\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pr{padding-right:var(--pr-md)}.md\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.md\:bui-pr-1{padding-right:var(--bui-space-1)}.md\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.md\:bui-pr-2{padding-right:var(--bui-space-2)}.md\:bui-pr-3{padding-right:var(--bui-space-3)}.md\:bui-pr-4{padding-right:var(--bui-space-4)}.md\:bui-pr-5{padding-right:var(--bui-space-5)}.md\:bui-pr-6{padding-right:var(--bui-space-6)}.md\:bui-pr-7{padding-right:var(--bui-space-7)}.md\:bui-pr-8{padding-right:var(--bui-space-8)}.md\:bui-pr-9{padding-right:var(--bui-space-9)}.md\:bui-pr-10{padding-right:var(--bui-space-10)}.md\:bui-pr-11{padding-right:var(--bui-space-11)}.md\:bui-pr-12{padding-right:var(--bui-space-12)}.md\:bui-pr-13{padding-right:var(--bui-space-13)}.md\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pr{padding-right:var(--pr-lg)}.lg\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.lg\:bui-pr-1{padding-right:var(--bui-space-1)}.lg\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.lg\:bui-pr-2{padding-right:var(--bui-space-2)}.lg\:bui-pr-3{padding-right:var(--bui-space-3)}.lg\:bui-pr-4{padding-right:var(--bui-space-4)}.lg\:bui-pr-5{padding-right:var(--bui-space-5)}.lg\:bui-pr-6{padding-right:var(--bui-space-6)}.lg\:bui-pr-7{padding-right:var(--bui-space-7)}.lg\:bui-pr-8{padding-right:var(--bui-space-8)}.lg\:bui-pr-9{padding-right:var(--bui-space-9)}.lg\:bui-pr-10{padding-right:var(--bui-space-10)}.lg\:bui-pr-11{padding-right:var(--bui-space-11)}.lg\:bui-pr-12{padding-right:var(--bui-space-12)}.lg\:bui-pr-13{padding-right:var(--bui-space-13)}.lg\:bui-pr-14{padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pr{padding-right:var(--pr-xl)}.xl\:bui-pr-0\.5{padding-right:var(--bui-space-0_5)}.xl\:bui-pr-1{padding-right:var(--bui-space-1)}.xl\:bui-pr-1\.5{padding-right:var(--bui-space-1_5)}.xl\:bui-pr-2{padding-right:var(--bui-space-2)}.xl\:bui-pr-3{padding-right:var(--bui-space-3)}.xl\:bui-pr-4{padding-right:var(--bui-space-4)}.xl\:bui-pr-5{padding-right:var(--bui-space-5)}.xl\:bui-pr-6{padding-right:var(--bui-space-6)}.xl\:bui-pr-7{padding-right:var(--bui-space-7)}.xl\:bui-pr-8{padding-right:var(--bui-space-8)}.xl\:bui-pr-9{padding-right:var(--bui-space-9)}.xl\:bui-pr-10{padding-right:var(--bui-space-10)}.xl\:bui-pr-11{padding-right:var(--bui-space-11)}.xl\:bui-pr-12{padding-right:var(--bui-space-12)}.xl\:bui-pr-13{padding-right:var(--bui-space-13)}.xl\:bui-pr-14{padding-right:var(--bui-space-14)}}.bui-pt{padding-top:var(--pt)}.bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.bui-pt-1{padding-top:var(--bui-space-1)}.bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.bui-pt-2{padding-top:var(--bui-space-2)}.bui-pt-3{padding-top:var(--bui-space-3)}.bui-pt-4{padding-top:var(--bui-space-4)}.bui-pt-5{padding-top:var(--bui-space-5)}.bui-pt-6{padding-top:var(--bui-space-6)}.bui-pt-7{padding-top:var(--bui-space-7)}.bui-pt-8{padding-top:var(--bui-space-8)}.bui-pt-9{padding-top:var(--bui-space-9)}.bui-pt-10{padding-top:var(--bui-space-10)}.bui-pt-11{padding-top:var(--bui-space-11)}.bui-pt-12{padding-top:var(--bui-space-12)}.bui-pt-13{padding-top:var(--bui-space-13)}.bui-pt-14{padding-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pt{padding-top:var(--pt-xs)}.xs\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xs\:bui-pt-1{padding-top:var(--bui-space-1)}.xs\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xs\:bui-pt-2{padding-top:var(--bui-space-2)}.xs\:bui-pt-3{padding-top:var(--bui-space-3)}.xs\:bui-pt-4{padding-top:var(--bui-space-4)}.xs\:bui-pt-5{padding-top:var(--bui-space-5)}.xs\:bui-pt-6{padding-top:var(--bui-space-6)}.xs\:bui-pt-7{padding-top:var(--bui-space-7)}.xs\:bui-pt-8{padding-top:var(--bui-space-8)}.xs\:bui-pt-9{padding-top:var(--bui-space-9)}.xs\:bui-pt-10{padding-top:var(--bui-space-10)}.xs\:bui-pt-11{padding-top:var(--bui-space-11)}.xs\:bui-pt-12{padding-top:var(--bui-space-12)}.xs\:bui-pt-13{padding-top:var(--bui-space-13)}.xs\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pt{padding-top:var(--pt-sm)}.sm\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.sm\:bui-pt-1{padding-top:var(--bui-space-1)}.sm\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.sm\:bui-pt-2{padding-top:var(--bui-space-2)}.sm\:bui-pt-3{padding-top:var(--bui-space-3)}.sm\:bui-pt-4{padding-top:var(--bui-space-4)}.sm\:bui-pt-5{padding-top:var(--bui-space-5)}.sm\:bui-pt-6{padding-top:var(--bui-space-6)}.sm\:bui-pt-7{padding-top:var(--bui-space-7)}.sm\:bui-pt-8{padding-top:var(--bui-space-8)}.sm\:bui-pt-9{padding-top:var(--bui-space-9)}.sm\:bui-pt-10{padding-top:var(--bui-space-10)}.sm\:bui-pt-11{padding-top:var(--bui-space-11)}.sm\:bui-pt-12{padding-top:var(--bui-space-12)}.sm\:bui-pt-13{padding-top:var(--bui-space-13)}.sm\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pt{padding-top:var(--pt-md)}.md\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.md\:bui-pt-1{padding-top:var(--bui-space-1)}.md\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.md\:bui-pt-2{padding-top:var(--bui-space-2)}.md\:bui-pt-3{padding-top:var(--bui-space-3)}.md\:bui-pt-4{padding-top:var(--bui-space-4)}.md\:bui-pt-5{padding-top:var(--bui-space-5)}.md\:bui-pt-6{padding-top:var(--bui-space-6)}.md\:bui-pt-7{padding-top:var(--bui-space-7)}.md\:bui-pt-8{padding-top:var(--bui-space-8)}.md\:bui-pt-9{padding-top:var(--bui-space-9)}.md\:bui-pt-10{padding-top:var(--bui-space-10)}.md\:bui-pt-11{padding-top:var(--bui-space-11)}.md\:bui-pt-12{padding-top:var(--bui-space-12)}.md\:bui-pt-13{padding-top:var(--bui-space-13)}.md\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pt{padding-top:var(--pt-lg)}.lg\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.lg\:bui-pt-1{padding-top:var(--bui-space-1)}.lg\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.lg\:bui-pt-2{padding-top:var(--bui-space-2)}.lg\:bui-pt-3{padding-top:var(--bui-space-3)}.lg\:bui-pt-4{padding-top:var(--bui-space-4)}.lg\:bui-pt-5{padding-top:var(--bui-space-5)}.lg\:bui-pt-6{padding-top:var(--bui-space-6)}.lg\:bui-pt-7{padding-top:var(--bui-space-7)}.lg\:bui-pt-8{padding-top:var(--bui-space-8)}.lg\:bui-pt-9{padding-top:var(--bui-space-9)}.lg\:bui-pt-10{padding-top:var(--bui-space-10)}.lg\:bui-pt-11{padding-top:var(--bui-space-11)}.lg\:bui-pt-12{padding-top:var(--bui-space-12)}.lg\:bui-pt-13{padding-top:var(--bui-space-13)}.lg\:bui-pt-14{padding-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pt{padding-top:var(--pt-xl)}.xl\:bui-pt-0\.5{padding-top:var(--bui-space-0_5)}.xl\:bui-pt-1{padding-top:var(--bui-space-1)}.xl\:bui-pt-1\.5{padding-top:var(--bui-space-1_5)}.xl\:bui-pt-2{padding-top:var(--bui-space-2)}.xl\:bui-pt-3{padding-top:var(--bui-space-3)}.xl\:bui-pt-4{padding-top:var(--bui-space-4)}.xl\:bui-pt-5{padding-top:var(--bui-space-5)}.xl\:bui-pt-6{padding-top:var(--bui-space-6)}.xl\:bui-pt-7{padding-top:var(--bui-space-7)}.xl\:bui-pt-8{padding-top:var(--bui-space-8)}.xl\:bui-pt-9{padding-top:var(--bui-space-9)}.xl\:bui-pt-10{padding-top:var(--bui-space-10)}.xl\:bui-pt-11{padding-top:var(--bui-space-11)}.xl\:bui-pt-12{padding-top:var(--bui-space-12)}.xl\:bui-pt-13{padding-top:var(--bui-space-13)}.xl\:bui-pt-14{padding-top:var(--bui-space-14)}}.bui-pb{padding-bottom:var(--pb)}.bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.bui-pb-1{padding-bottom:var(--bui-space-1)}.bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.bui-pb-2{padding-bottom:var(--bui-space-2)}.bui-pb-3{padding-bottom:var(--bui-space-3)}.bui-pb-4{padding-bottom:var(--bui-space-4)}.bui-pb-5{padding-bottom:var(--bui-space-5)}.bui-pb-6{padding-bottom:var(--bui-space-6)}.bui-pb-7{padding-bottom:var(--bui-space-7)}.bui-pb-8{padding-bottom:var(--bui-space-8)}.bui-pb-9{padding-bottom:var(--bui-space-9)}.bui-pb-10{padding-bottom:var(--bui-space-10)}.bui-pb-11{padding-bottom:var(--bui-space-11)}.bui-pb-12{padding-bottom:var(--bui-space-12)}.bui-pb-13{padding-bottom:var(--bui-space-13)}.bui-pb-14{padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-pb{padding-bottom:var(--pb-xs)}.xs\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xs\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xs\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xs\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xs\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xs\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xs\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xs\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xs\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xs\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xs\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xs\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xs\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xs\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xs\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xs\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-pb{padding-bottom:var(--pb-sm)}.sm\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.sm\:bui-pb-1{padding-bottom:var(--bui-space-1)}.sm\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.sm\:bui-pb-2{padding-bottom:var(--bui-space-2)}.sm\:bui-pb-3{padding-bottom:var(--bui-space-3)}.sm\:bui-pb-4{padding-bottom:var(--bui-space-4)}.sm\:bui-pb-5{padding-bottom:var(--bui-space-5)}.sm\:bui-pb-6{padding-bottom:var(--bui-space-6)}.sm\:bui-pb-7{padding-bottom:var(--bui-space-7)}.sm\:bui-pb-8{padding-bottom:var(--bui-space-8)}.sm\:bui-pb-9{padding-bottom:var(--bui-space-9)}.sm\:bui-pb-10{padding-bottom:var(--bui-space-10)}.sm\:bui-pb-11{padding-bottom:var(--bui-space-11)}.sm\:bui-pb-12{padding-bottom:var(--bui-space-12)}.sm\:bui-pb-13{padding-bottom:var(--bui-space-13)}.sm\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-pb{padding-bottom:var(--pb-md)}.md\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.md\:bui-pb-1{padding-bottom:var(--bui-space-1)}.md\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.md\:bui-pb-2{padding-bottom:var(--bui-space-2)}.md\:bui-pb-3{padding-bottom:var(--bui-space-3)}.md\:bui-pb-4{padding-bottom:var(--bui-space-4)}.md\:bui-pb-5{padding-bottom:var(--bui-space-5)}.md\:bui-pb-6{padding-bottom:var(--bui-space-6)}.md\:bui-pb-7{padding-bottom:var(--bui-space-7)}.md\:bui-pb-8{padding-bottom:var(--bui-space-8)}.md\:bui-pb-9{padding-bottom:var(--bui-space-9)}.md\:bui-pb-10{padding-bottom:var(--bui-space-10)}.md\:bui-pb-11{padding-bottom:var(--bui-space-11)}.md\:bui-pb-12{padding-bottom:var(--bui-space-12)}.md\:bui-pb-13{padding-bottom:var(--bui-space-13)}.md\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-pb{padding-bottom:var(--pb-lg)}.lg\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.lg\:bui-pb-1{padding-bottom:var(--bui-space-1)}.lg\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.lg\:bui-pb-2{padding-bottom:var(--bui-space-2)}.lg\:bui-pb-3{padding-bottom:var(--bui-space-3)}.lg\:bui-pb-4{padding-bottom:var(--bui-space-4)}.lg\:bui-pb-5{padding-bottom:var(--bui-space-5)}.lg\:bui-pb-6{padding-bottom:var(--bui-space-6)}.lg\:bui-pb-7{padding-bottom:var(--bui-space-7)}.lg\:bui-pb-8{padding-bottom:var(--bui-space-8)}.lg\:bui-pb-9{padding-bottom:var(--bui-space-9)}.lg\:bui-pb-10{padding-bottom:var(--bui-space-10)}.lg\:bui-pb-11{padding-bottom:var(--bui-space-11)}.lg\:bui-pb-12{padding-bottom:var(--bui-space-12)}.lg\:bui-pb-13{padding-bottom:var(--bui-space-13)}.lg\:bui-pb-14{padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-pb{padding-bottom:var(--pb-xl)}.xl\:bui-pb-0\.5{padding-bottom:var(--bui-space-0_5)}.xl\:bui-pb-1{padding-bottom:var(--bui-space-1)}.xl\:bui-pb-1\.5{padding-bottom:var(--bui-space-1_5)}.xl\:bui-pb-2{padding-bottom:var(--bui-space-2)}.xl\:bui-pb-3{padding-bottom:var(--bui-space-3)}.xl\:bui-pb-4{padding-bottom:var(--bui-space-4)}.xl\:bui-pb-5{padding-bottom:var(--bui-space-5)}.xl\:bui-pb-6{padding-bottom:var(--bui-space-6)}.xl\:bui-pb-7{padding-bottom:var(--bui-space-7)}.xl\:bui-pb-8{padding-bottom:var(--bui-space-8)}.xl\:bui-pb-9{padding-bottom:var(--bui-space-9)}.xl\:bui-pb-10{padding-bottom:var(--bui-space-10)}.xl\:bui-pb-11{padding-bottom:var(--bui-space-11)}.xl\:bui-pb-12{padding-bottom:var(--bui-space-12)}.xl\:bui-pb-13{padding-bottom:var(--bui-space-13)}.xl\:bui-pb-14{padding-bottom:var(--bui-space-14)}}.bui-py{padding-top:var(--py);padding-bottom:var(--py)}.bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-py{padding-top:var(--py-xs);padding-bottom:var(--py-xs)}.xs\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xs\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xs\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xs\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xs\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xs\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xs\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xs\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xs\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xs\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xs\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xs\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xs\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xs\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xs\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xs\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-py{padding-top:var(--py-sm);padding-bottom:var(--py-sm)}.sm\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.sm\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.sm\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.sm\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.sm\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.sm\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.sm\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.sm\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.sm\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.sm\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.sm\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.sm\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.sm\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.sm\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.sm\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.sm\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-py{padding-top:var(--py-md);padding-bottom:var(--py-md)}.md\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.md\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.md\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.md\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.md\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.md\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.md\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.md\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.md\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.md\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.md\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.md\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.md\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.md\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.md\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.md\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-py{padding-top:var(--py-lg);padding-bottom:var(--py-lg)}.lg\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.lg\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.lg\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.lg\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.lg\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.lg\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.lg\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.lg\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.lg\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.lg\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.lg\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.lg\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.lg\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.lg\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.lg\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.lg\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-py{padding-top:var(--py-xl);padding-bottom:var(--py-xl)}.xl\:bui-py-0\.5{padding-top:var(--bui-space-0_5);padding-bottom:var(--bui-space-0_5)}.xl\:bui-py-1{padding-top:var(--bui-space-1);padding-bottom:var(--bui-space-1)}.xl\:bui-py-1\.5{padding-top:var(--bui-space-1_5);padding-bottom:var(--bui-space-1_5)}.xl\:bui-py-2{padding-top:var(--bui-space-2);padding-bottom:var(--bui-space-2)}.xl\:bui-py-3{padding-top:var(--bui-space-3);padding-bottom:var(--bui-space-3)}.xl\:bui-py-4{padding-top:var(--bui-space-4);padding-bottom:var(--bui-space-4)}.xl\:bui-py-5{padding-top:var(--bui-space-5);padding-bottom:var(--bui-space-5)}.xl\:bui-py-6{padding-top:var(--bui-space-6);padding-bottom:var(--bui-space-6)}.xl\:bui-py-7{padding-top:var(--bui-space-7);padding-bottom:var(--bui-space-7)}.xl\:bui-py-8{padding-top:var(--bui-space-8);padding-bottom:var(--bui-space-8)}.xl\:bui-py-9{padding-top:var(--bui-space-9);padding-bottom:var(--bui-space-9)}.xl\:bui-py-10{padding-top:var(--bui-space-10);padding-bottom:var(--bui-space-10)}.xl\:bui-py-11{padding-top:var(--bui-space-11);padding-bottom:var(--bui-space-11)}.xl\:bui-py-12{padding-top:var(--bui-space-12);padding-bottom:var(--bui-space-12)}.xl\:bui-py-13{padding-top:var(--bui-space-13);padding-bottom:var(--bui-space-13)}.xl\:bui-py-14{padding-top:var(--bui-space-14);padding-bottom:var(--bui-space-14)}}.bui-px{padding-left:var(--px);padding-right:var(--px)}.bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-px{padding-left:var(--px-xs);padding-right:var(--px-xs)}.xs\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xs\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xs\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xs\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xs\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xs\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xs\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xs\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xs\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xs\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xs\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xs\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xs\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xs\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xs\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xs\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-px{padding-left:var(--px-sm);padding-right:var(--px-sm)}.sm\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.sm\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.sm\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.sm\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.sm\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.sm\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.sm\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.sm\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.sm\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.sm\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.sm\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.sm\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.sm\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.sm\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.sm\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.sm\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-px{padding-left:var(--px-md);padding-right:var(--px-md)}.md\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.md\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.md\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.md\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.md\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.md\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.md\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.md\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.md\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.md\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.md\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.md\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.md\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.md\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.md\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.md\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-px{padding-left:var(--px-lg);padding-right:var(--px-lg)}.lg\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.lg\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.lg\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.lg\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.lg\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.lg\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.lg\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.lg\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.lg\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.lg\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.lg\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.lg\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.lg\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.lg\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.lg\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.lg\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-px{padding-left:var(--px-xl);padding-right:var(--px-xl)}.xl\:bui-px-0\.5{padding-left:var(--bui-space-0_5);padding-right:var(--bui-space-0_5)}.xl\:bui-px-1{padding-left:var(--bui-space-1);padding-right:var(--bui-space-1)}.xl\:bui-px-1\.5{padding-left:var(--bui-space-1_5);padding-right:var(--bui-space-1_5)}.xl\:bui-px-2{padding-left:var(--bui-space-2);padding-right:var(--bui-space-2)}.xl\:bui-px-3{padding-left:var(--bui-space-3);padding-right:var(--bui-space-3)}.xl\:bui-px-4{padding-left:var(--bui-space-4);padding-right:var(--bui-space-4)}.xl\:bui-px-5{padding-left:var(--bui-space-5);padding-right:var(--bui-space-5)}.xl\:bui-px-6{padding-left:var(--bui-space-6);padding-right:var(--bui-space-6)}.xl\:bui-px-7{padding-left:var(--bui-space-7);padding-right:var(--bui-space-7)}.xl\:bui-px-8{padding-left:var(--bui-space-8);padding-right:var(--bui-space-8)}.xl\:bui-px-9{padding-left:var(--bui-space-9);padding-right:var(--bui-space-9)}.xl\:bui-px-10{padding-left:var(--bui-space-10);padding-right:var(--bui-space-10)}.xl\:bui-px-11{padding-left:var(--bui-space-11);padding-right:var(--bui-space-11)}.xl\:bui-px-12{padding-left:var(--bui-space-12);padding-right:var(--bui-space-12)}.xl\:bui-px-13{padding-left:var(--bui-space-13);padding-right:var(--bui-space-13)}.xl\:bui-px-14{padding-left:var(--bui-space-14);padding-right:var(--bui-space-14)}}.bui-m{margin:var(--m)}.bui-m-0\.5{margin:var(--bui-space-0_5)}.bui-m-1{margin:var(--bui-space-1)}.bui-m-1\.5{margin:var(--bui-space-1_5)}.bui-m-2{margin:var(--bui-space-2)}.bui-m-3{margin:var(--bui-space-3)}.bui-m-4{margin:var(--bui-space-4)}.bui-m-5{margin:var(--bui-space-5)}.bui-m-6{margin:var(--bui-space-6)}.bui-m-7{margin:var(--bui-space-7)}.bui-m-8{margin:var(--bui-space-8)}.bui-m-9{margin:var(--bui-space-9)}.bui-m-10{margin:var(--bui-space-10)}.bui-m-11{margin:var(--bui-space-11)}.bui-m-12{margin:var(--bui-space-12)}.bui-m-13{margin:var(--bui-space-13)}.bui-m-14{margin:var(--bui-space-14)}@media (width>=640px){.xs\:bui-m{margin:var(--p-xs)}.xs\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xs\:bui-m-1{margin:var(--bui-space-1)}.xs\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xs\:bui-m-2{margin:var(--bui-space-2)}.xs\:bui-m-3{margin:var(--bui-space-3)}.xs\:bui-m-4{margin:var(--bui-space-4)}.xs\:bui-m-5{margin:var(--bui-space-5)}.xs\:bui-m-6{margin:var(--bui-space-6)}.xs\:bui-m-7{margin:var(--bui-space-7)}.xs\:bui-m-8{margin:var(--bui-space-8)}.xs\:bui-m-9{margin:var(--bui-space-9)}.xs\:bui-m-10{margin:var(--bui-space-10)}.xs\:bui-m-11{margin:var(--bui-space-11)}.xs\:bui-m-12{margin:var(--bui-space-12)}.xs\:bui-m-13{margin:var(--bui-space-13)}.xs\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-m{margin:var(--p-sm)}.sm\:bui-m-0\.5{margin:var(--bui-space-0_5)}.sm\:bui-m-1{margin:var(--bui-space-1)}.sm\:bui-m-1\.5{margin:var(--bui-space-1_5)}.sm\:bui-m-2{margin:var(--bui-space-2)}.sm\:bui-m-3{margin:var(--bui-space-3)}.sm\:bui-m-4{margin:var(--bui-space-4)}.sm\:bui-m-5{margin:var(--bui-space-5)}.sm\:bui-m-6{margin:var(--bui-space-6)}.sm\:bui-m-7{margin:var(--bui-space-7)}.sm\:bui-m-8{margin:var(--bui-space-8)}.sm\:bui-m-9{margin:var(--bui-space-9)}.sm\:bui-m-10{margin:var(--bui-space-10)}.sm\:bui-m-11{margin:var(--bui-space-11)}.sm\:bui-m-12{margin:var(--bui-space-12)}.sm\:bui-m-13{margin:var(--bui-space-13)}.sm\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-m{margin:var(--p-md)}.md\:bui-m-0\.5{margin:var(--bui-space-0_5)}.md\:bui-m-1{margin:var(--bui-space-1)}.md\:bui-m-1\.5{margin:var(--bui-space-1_5)}.md\:bui-m-2{margin:var(--bui-space-2)}.md\:bui-m-3{margin:var(--bui-space-3)}.md\:bui-m-4{margin:var(--bui-space-4)}.md\:bui-m-5{margin:var(--bui-space-5)}.md\:bui-m-6{margin:var(--bui-space-6)}.md\:bui-m-7{margin:var(--bui-space-7)}.md\:bui-m-8{margin:var(--bui-space-8)}.md\:bui-m-9{margin:var(--bui-space-9)}.md\:bui-m-10{margin:var(--bui-space-10)}.md\:bui-m-11{margin:var(--bui-space-11)}.md\:bui-m-12{margin:var(--bui-space-12)}.md\:bui-m-13{margin:var(--bui-space-13)}.md\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-m{margin:var(--p-lg)}.lg\:bui-m-0\.5{margin:var(--bui-space-0_5)}.lg\:bui-m-1{margin:var(--bui-space-1)}.lg\:bui-m-1\.5{margin:var(--bui-space-1_5)}.lg\:bui-m-2{margin:var(--bui-space-2)}.lg\:bui-m-3{margin:var(--bui-space-3)}.lg\:bui-m-4{margin:var(--bui-space-4)}.lg\:bui-m-5{margin:var(--bui-space-5)}.lg\:bui-m-6{margin:var(--bui-space-6)}.lg\:bui-m-7{margin:var(--bui-space-7)}.lg\:bui-m-8{margin:var(--bui-space-8)}.lg\:bui-m-9{margin:var(--bui-space-9)}.lg\:bui-m-10{margin:var(--bui-space-10)}.lg\:bui-m-11{margin:var(--bui-space-11)}.lg\:bui-m-12{margin:var(--bui-space-12)}.lg\:bui-m-13{margin:var(--bui-space-13)}.lg\:bui-m-14{margin:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-m{margin:var(--p-xl)}.xl\:bui-m-0\.5{margin:var(--bui-space-0_5)}.xl\:bui-m-1{margin:var(--bui-space-1)}.xl\:bui-m-1\.5{margin:var(--bui-space-1_5)}.xl\:bui-m-2{margin:var(--bui-space-2)}.xl\:bui-m-3{margin:var(--bui-space-3)}.xl\:bui-m-4{margin:var(--bui-space-4)}.xl\:bui-m-5{margin:var(--bui-space-5)}.xl\:bui-m-6{margin:var(--bui-space-6)}.xl\:bui-m-7{margin:var(--bui-space-7)}.xl\:bui-m-8{margin:var(--bui-space-8)}.xl\:bui-m-9{margin:var(--bui-space-9)}.xl\:bui-m-10{margin:var(--bui-space-10)}.xl\:bui-m-11{margin:var(--bui-space-11)}.xl\:bui-m-12{margin:var(--bui-space-12)}.xl\:bui-m-13{margin:var(--bui-space-13)}.xl\:bui-m-14{margin:var(--bui-space-14)}}.bui-ml{margin-left:var(--ml)}.bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.bui-ml-1{margin-left:var(--bui-space-1)}.bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.bui-ml-2{margin-left:var(--bui-space-2)}.bui-ml-3{margin-left:var(--bui-space-3)}.bui-ml-4{margin-left:var(--bui-space-4)}.bui-ml-5{margin-left:var(--bui-space-5)}.bui-ml-6{margin-left:var(--bui-space-6)}.bui-ml-7{margin-left:var(--bui-space-7)}.bui-ml-8{margin-left:var(--bui-space-8)}.bui-ml-9{margin-left:var(--bui-space-9)}.bui-ml-10{margin-left:var(--bui-space-10)}.bui-ml-11{margin-left:var(--bui-space-11)}.bui-ml-12{margin-left:var(--bui-space-12)}.bui-ml-13{margin-left:var(--bui-space-13)}.bui-ml-14{margin-left:var(--bui-space-14)}@media (width>=640px){.xs\:bui-ml{margin-left:var(--pl-xs)}.xs\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xs\:bui-ml-1{margin-left:var(--bui-space-1)}.xs\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xs\:bui-ml-2{margin-left:var(--bui-space-2)}.xs\:bui-ml-3{margin-left:var(--bui-space-3)}.xs\:bui-ml-4{margin-left:var(--bui-space-4)}.xs\:bui-ml-5{margin-left:var(--bui-space-5)}.xs\:bui-ml-6{margin-left:var(--bui-space-6)}.xs\:bui-ml-7{margin-left:var(--bui-space-7)}.xs\:bui-ml-8{margin-left:var(--bui-space-8)}.xs\:bui-ml-9{margin-left:var(--bui-space-9)}.xs\:bui-ml-10{margin-left:var(--bui-space-10)}.xs\:bui-ml-11{margin-left:var(--bui-space-11)}.xs\:bui-ml-12{margin-left:var(--bui-space-12)}.xs\:bui-ml-13{margin-left:var(--bui-space-13)}.xs\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-ml{margin-left:var(--pl-sm)}.sm\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.sm\:bui-ml-1{margin-left:var(--bui-space-1)}.sm\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.sm\:bui-ml-2{margin-left:var(--bui-space-2)}.sm\:bui-ml-3{margin-left:var(--bui-space-3)}.sm\:bui-ml-4{margin-left:var(--bui-space-4)}.sm\:bui-ml-5{margin-left:var(--bui-space-5)}.sm\:bui-ml-6{margin-left:var(--bui-space-6)}.sm\:bui-ml-7{margin-left:var(--bui-space-7)}.sm\:bui-ml-8{margin-left:var(--bui-space-8)}.sm\:bui-ml-9{margin-left:var(--bui-space-9)}.sm\:bui-ml-10{margin-left:var(--bui-space-10)}.sm\:bui-ml-11{margin-left:var(--bui-space-11)}.sm\:bui-ml-12{margin-left:var(--bui-space-12)}.sm\:bui-ml-13{margin-left:var(--bui-space-13)}.sm\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-ml{margin-left:var(--pl-md)}.md\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.md\:bui-ml-1{margin-left:var(--bui-space-1)}.md\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.md\:bui-ml-2{margin-left:var(--bui-space-2)}.md\:bui-ml-3{margin-left:var(--bui-space-3)}.md\:bui-ml-4{margin-left:var(--bui-space-4)}.md\:bui-ml-5{margin-left:var(--bui-space-5)}.md\:bui-ml-6{margin-left:var(--bui-space-6)}.md\:bui-ml-7{margin-left:var(--bui-space-7)}.md\:bui-ml-8{margin-left:var(--bui-space-8)}.md\:bui-ml-9{margin-left:var(--bui-space-9)}.md\:bui-ml-10{margin-left:var(--bui-space-10)}.md\:bui-ml-11{margin-left:var(--bui-space-11)}.md\:bui-ml-12{margin-left:var(--bui-space-12)}.md\:bui-ml-13{margin-left:var(--bui-space-13)}.md\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-ml{margin-left:var(--pl-lg)}.lg\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.lg\:bui-ml-1{margin-left:var(--bui-space-1)}.lg\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.lg\:bui-ml-2{margin-left:var(--bui-space-2)}.lg\:bui-ml-3{margin-left:var(--bui-space-3)}.lg\:bui-ml-4{margin-left:var(--bui-space-4)}.lg\:bui-ml-5{margin-left:var(--bui-space-5)}.lg\:bui-ml-6{margin-left:var(--bui-space-6)}.lg\:bui-ml-7{margin-left:var(--bui-space-7)}.lg\:bui-ml-8{margin-left:var(--bui-space-8)}.lg\:bui-ml-9{margin-left:var(--bui-space-9)}.lg\:bui-ml-10{margin-left:var(--bui-space-10)}.lg\:bui-ml-11{margin-left:var(--bui-space-11)}.lg\:bui-ml-12{margin-left:var(--bui-space-12)}.lg\:bui-ml-13{margin-left:var(--bui-space-13)}.lg\:bui-ml-14{margin-left:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-ml{margin-left:var(--pl-xl)}.xl\:bui-ml-0\.5{margin-left:var(--bui-space-0_5)}.xl\:bui-ml-1{margin-left:var(--bui-space-1)}.xl\:bui-ml-1\.5{margin-left:var(--bui-space-1_5)}.xl\:bui-ml-2{margin-left:var(--bui-space-2)}.xl\:bui-ml-3{margin-left:var(--bui-space-3)}.xl\:bui-ml-4{margin-left:var(--bui-space-4)}.xl\:bui-ml-5{margin-left:var(--bui-space-5)}.xl\:bui-ml-6{margin-left:var(--bui-space-6)}.xl\:bui-ml-7{margin-left:var(--bui-space-7)}.xl\:bui-ml-8{margin-left:var(--bui-space-8)}.xl\:bui-ml-9{margin-left:var(--bui-space-9)}.xl\:bui-ml-10{margin-left:var(--bui-space-10)}.xl\:bui-ml-11{margin-left:var(--bui-space-11)}.xl\:bui-ml-12{margin-left:var(--bui-space-12)}.xl\:bui-ml-13{margin-left:var(--bui-space-13)}.xl\:bui-ml-14{margin-left:var(--bui-space-14)}}.bui-mr{margin-right:var(--mr)}.bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.bui-mr-1{margin-right:var(--bui-space-1)}.bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.bui-mr-2{margin-right:var(--bui-space-2)}.bui-mr-3{margin-right:var(--bui-space-3)}.bui-mr-4{margin-right:var(--bui-space-4)}.bui-mr-5{margin-right:var(--bui-space-5)}.bui-mr-6{margin-right:var(--bui-space-6)}.bui-mr-7{margin-right:var(--bui-space-7)}.bui-mr-8{margin-right:var(--bui-space-8)}.bui-mr-9{margin-right:var(--bui-space-9)}.bui-mr-10{margin-right:var(--bui-space-10)}.bui-mr-11{margin-right:var(--bui-space-11)}.bui-mr-12{margin-right:var(--bui-space-12)}.bui-mr-13{margin-right:var(--bui-space-13)}.bui-mr-14{margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mr{margin-right:var(--pr-xs)}.xs\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xs\:bui-mr-1{margin-right:var(--bui-space-1)}.xs\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xs\:bui-mr-2{margin-right:var(--bui-space-2)}.xs\:bui-mr-3{margin-right:var(--bui-space-3)}.xs\:bui-mr-4{margin-right:var(--bui-space-4)}.xs\:bui-mr-5{margin-right:var(--bui-space-5)}.xs\:bui-mr-6{margin-right:var(--bui-space-6)}.xs\:bui-mr-7{margin-right:var(--bui-space-7)}.xs\:bui-mr-8{margin-right:var(--bui-space-8)}.xs\:bui-mr-9{margin-right:var(--bui-space-9)}.xs\:bui-mr-10{margin-right:var(--bui-space-10)}.xs\:bui-mr-11{margin-right:var(--bui-space-11)}.xs\:bui-mr-12{margin-right:var(--bui-space-12)}.xs\:bui-mr-13{margin-right:var(--bui-space-13)}.xs\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mr{margin-right:var(--pr-sm)}.sm\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.sm\:bui-mr-1{margin-right:var(--bui-space-1)}.sm\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.sm\:bui-mr-2{margin-right:var(--bui-space-2)}.sm\:bui-mr-3{margin-right:var(--bui-space-3)}.sm\:bui-mr-4{margin-right:var(--bui-space-4)}.sm\:bui-mr-5{margin-right:var(--bui-space-5)}.sm\:bui-mr-6{margin-right:var(--bui-space-6)}.sm\:bui-mr-7{margin-right:var(--bui-space-7)}.sm\:bui-mr-8{margin-right:var(--bui-space-8)}.sm\:bui-mr-9{margin-right:var(--bui-space-9)}.sm\:bui-mr-10{margin-right:var(--bui-space-10)}.sm\:bui-mr-11{margin-right:var(--bui-space-11)}.sm\:bui-mr-12{margin-right:var(--bui-space-12)}.sm\:bui-mr-13{margin-right:var(--bui-space-13)}.sm\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mr{margin-right:var(--pr-md)}.md\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.md\:bui-mr-1{margin-right:var(--bui-space-1)}.md\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.md\:bui-mr-2{margin-right:var(--bui-space-2)}.md\:bui-mr-3{margin-right:var(--bui-space-3)}.md\:bui-mr-4{margin-right:var(--bui-space-4)}.md\:bui-mr-5{margin-right:var(--bui-space-5)}.md\:bui-mr-6{margin-right:var(--bui-space-6)}.md\:bui-mr-7{margin-right:var(--bui-space-7)}.md\:bui-mr-8{margin-right:var(--bui-space-8)}.md\:bui-mr-9{margin-right:var(--bui-space-9)}.md\:bui-mr-10{margin-right:var(--bui-space-10)}.md\:bui-mr-11{margin-right:var(--bui-space-11)}.md\:bui-mr-12{margin-right:var(--bui-space-12)}.md\:bui-mr-13{margin-right:var(--bui-space-13)}.md\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mr{margin-right:var(--pr-lg)}.lg\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.lg\:bui-mr-1{margin-right:var(--bui-space-1)}.lg\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.lg\:bui-mr-2{margin-right:var(--bui-space-2)}.lg\:bui-mr-3{margin-right:var(--bui-space-3)}.lg\:bui-mr-4{margin-right:var(--bui-space-4)}.lg\:bui-mr-5{margin-right:var(--bui-space-5)}.lg\:bui-mr-6{margin-right:var(--bui-space-6)}.lg\:bui-mr-7{margin-right:var(--bui-space-7)}.lg\:bui-mr-8{margin-right:var(--bui-space-8)}.lg\:bui-mr-9{margin-right:var(--bui-space-9)}.lg\:bui-mr-10{margin-right:var(--bui-space-10)}.lg\:bui-mr-11{margin-right:var(--bui-space-11)}.lg\:bui-mr-12{margin-right:var(--bui-space-12)}.lg\:bui-mr-13{margin-right:var(--bui-space-13)}.lg\:bui-mr-14{margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mr{margin-right:var(--pr-xl)}.xl\:bui-mr-0\.5{margin-right:var(--bui-space-0_5)}.xl\:bui-mr-1{margin-right:var(--bui-space-1)}.xl\:bui-mr-1\.5{margin-right:var(--bui-space-1_5)}.xl\:bui-mr-2{margin-right:var(--bui-space-2)}.xl\:bui-mr-3{margin-right:var(--bui-space-3)}.xl\:bui-mr-4{margin-right:var(--bui-space-4)}.xl\:bui-mr-5{margin-right:var(--bui-space-5)}.xl\:bui-mr-6{margin-right:var(--bui-space-6)}.xl\:bui-mr-7{margin-right:var(--bui-space-7)}.xl\:bui-mr-8{margin-right:var(--bui-space-8)}.xl\:bui-mr-9{margin-right:var(--bui-space-9)}.xl\:bui-mr-10{margin-right:var(--bui-space-10)}.xl\:bui-mr-11{margin-right:var(--bui-space-11)}.xl\:bui-mr-12{margin-right:var(--bui-space-12)}.xl\:bui-mr-13{margin-right:var(--bui-space-13)}.xl\:bui-mr-14{margin-right:var(--bui-space-14)}}.bui-mt{margin-top:var(--mt)}.bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.bui-mt-1{margin-top:var(--bui-space-1)}.bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.bui-mt-2{margin-top:var(--bui-space-2)}.bui-mt-3{margin-top:var(--bui-space-3)}.bui-mt-4{margin-top:var(--bui-space-4)}.bui-mt-5{margin-top:var(--bui-space-5)}.bui-mt-6{margin-top:var(--bui-space-6)}.bui-mt-7{margin-top:var(--bui-space-7)}.bui-mt-8{margin-top:var(--bui-space-8)}.bui-mt-9{margin-top:var(--bui-space-9)}.bui-mt-10{margin-top:var(--bui-space-10)}.bui-mt-11{margin-top:var(--bui-space-11)}.bui-mt-12{margin-top:var(--bui-space-12)}.bui-mt-13{margin-top:var(--bui-space-13)}.bui-mt-14{margin-top:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mt{margin-top:var(--pt-xs)}.xs\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xs\:bui-mt-1{margin-top:var(--bui-space-1)}.xs\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xs\:bui-mt-2{margin-top:var(--bui-space-2)}.xs\:bui-mt-3{margin-top:var(--bui-space-3)}.xs\:bui-mt-4{margin-top:var(--bui-space-4)}.xs\:bui-mt-5{margin-top:var(--bui-space-5)}.xs\:bui-mt-6{margin-top:var(--bui-space-6)}.xs\:bui-mt-7{margin-top:var(--bui-space-7)}.xs\:bui-mt-8{margin-top:var(--bui-space-8)}.xs\:bui-mt-9{margin-top:var(--bui-space-9)}.xs\:bui-mt-10{margin-top:var(--bui-space-10)}.xs\:bui-mt-11{margin-top:var(--bui-space-11)}.xs\:bui-mt-12{margin-top:var(--bui-space-12)}.xs\:bui-mt-13{margin-top:var(--bui-space-13)}.xs\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mt{margin-top:var(--pt-sm)}.sm\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.sm\:bui-mt-1{margin-top:var(--bui-space-1)}.sm\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.sm\:bui-mt-2{margin-top:var(--bui-space-2)}.sm\:bui-mt-3{margin-top:var(--bui-space-3)}.sm\:bui-mt-4{margin-top:var(--bui-space-4)}.sm\:bui-mt-5{margin-top:var(--bui-space-5)}.sm\:bui-mt-6{margin-top:var(--bui-space-6)}.sm\:bui-mt-7{margin-top:var(--bui-space-7)}.sm\:bui-mt-8{margin-top:var(--bui-space-8)}.sm\:bui-mt-9{margin-top:var(--bui-space-9)}.sm\:bui-mt-10{margin-top:var(--bui-space-10)}.sm\:bui-mt-11{margin-top:var(--bui-space-11)}.sm\:bui-mt-12{margin-top:var(--bui-space-12)}.sm\:bui-mt-13{margin-top:var(--bui-space-13)}.sm\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mt{margin-top:var(--pt-md)}.md\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.md\:bui-mt-1{margin-top:var(--bui-space-1)}.md\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.md\:bui-mt-2{margin-top:var(--bui-space-2)}.md\:bui-mt-3{margin-top:var(--bui-space-3)}.md\:bui-mt-4{margin-top:var(--bui-space-4)}.md\:bui-mt-5{margin-top:var(--bui-space-5)}.md\:bui-mt-6{margin-top:var(--bui-space-6)}.md\:bui-mt-7{margin-top:var(--bui-space-7)}.md\:bui-mt-8{margin-top:var(--bui-space-8)}.md\:bui-mt-9{margin-top:var(--bui-space-9)}.md\:bui-mt-10{margin-top:var(--bui-space-10)}.md\:bui-mt-11{margin-top:var(--bui-space-11)}.md\:bui-mt-12{margin-top:var(--bui-space-12)}.md\:bui-mt-13{margin-top:var(--bui-space-13)}.md\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mt{margin-top:var(--pt-lg)}.lg\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.lg\:bui-mt-1{margin-top:var(--bui-space-1)}.lg\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.lg\:bui-mt-2{margin-top:var(--bui-space-2)}.lg\:bui-mt-3{margin-top:var(--bui-space-3)}.lg\:bui-mt-4{margin-top:var(--bui-space-4)}.lg\:bui-mt-5{margin-top:var(--bui-space-5)}.lg\:bui-mt-6{margin-top:var(--bui-space-6)}.lg\:bui-mt-7{margin-top:var(--bui-space-7)}.lg\:bui-mt-8{margin-top:var(--bui-space-8)}.lg\:bui-mt-9{margin-top:var(--bui-space-9)}.lg\:bui-mt-10{margin-top:var(--bui-space-10)}.lg\:bui-mt-11{margin-top:var(--bui-space-11)}.lg\:bui-mt-12{margin-top:var(--bui-space-12)}.lg\:bui-mt-13{margin-top:var(--bui-space-13)}.lg\:bui-mt-14{margin-top:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mt{margin-top:var(--pt-xl)}.xl\:bui-mt-0\.5{margin-top:var(--bui-space-0_5)}.xl\:bui-mt-1{margin-top:var(--bui-space-1)}.xl\:bui-mt-1\.5{margin-top:var(--bui-space-1_5)}.xl\:bui-mt-2{margin-top:var(--bui-space-2)}.xl\:bui-mt-3{margin-top:var(--bui-space-3)}.xl\:bui-mt-4{margin-top:var(--bui-space-4)}.xl\:bui-mt-5{margin-top:var(--bui-space-5)}.xl\:bui-mt-6{margin-top:var(--bui-space-6)}.xl\:bui-mt-7{margin-top:var(--bui-space-7)}.xl\:bui-mt-8{margin-top:var(--bui-space-8)}.xl\:bui-mt-9{margin-top:var(--bui-space-9)}.xl\:bui-mt-10{margin-top:var(--bui-space-10)}.xl\:bui-mt-11{margin-top:var(--bui-space-11)}.xl\:bui-mt-12{margin-top:var(--bui-space-12)}.xl\:bui-mt-13{margin-top:var(--bui-space-13)}.xl\:bui-mt-14{margin-top:var(--bui-space-14)}}.bui-mb{margin-bottom:var(--mb)}.bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.bui-mb-1{margin-bottom:var(--bui-space-1)}.bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.bui-mb-2{margin-bottom:var(--bui-space-2)}.bui-mb-3{margin-bottom:var(--bui-space-3)}.bui-mb-4{margin-bottom:var(--bui-space-4)}.bui-mb-5{margin-bottom:var(--bui-space-5)}.bui-mb-6{margin-bottom:var(--bui-space-6)}.bui-mb-7{margin-bottom:var(--bui-space-7)}.bui-mb-8{margin-bottom:var(--bui-space-8)}.bui-mb-9{margin-bottom:var(--bui-space-9)}.bui-mb-10{margin-bottom:var(--bui-space-10)}.bui-mb-11{margin-bottom:var(--bui-space-11)}.bui-mb-12{margin-bottom:var(--bui-space-12)}.bui-mb-13{margin-bottom:var(--bui-space-13)}.bui-mb-14{margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mb{margin-bottom:var(--pb-xs)}.xs\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xs\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xs\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xs\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xs\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xs\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xs\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xs\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xs\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xs\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xs\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xs\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xs\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xs\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xs\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xs\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mb{margin-bottom:var(--pb-sm)}.sm\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.sm\:bui-mb-1{margin-bottom:var(--bui-space-1)}.sm\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.sm\:bui-mb-2{margin-bottom:var(--bui-space-2)}.sm\:bui-mb-3{margin-bottom:var(--bui-space-3)}.sm\:bui-mb-4{margin-bottom:var(--bui-space-4)}.sm\:bui-mb-5{margin-bottom:var(--bui-space-5)}.sm\:bui-mb-6{margin-bottom:var(--bui-space-6)}.sm\:bui-mb-7{margin-bottom:var(--bui-space-7)}.sm\:bui-mb-8{margin-bottom:var(--bui-space-8)}.sm\:bui-mb-9{margin-bottom:var(--bui-space-9)}.sm\:bui-mb-10{margin-bottom:var(--bui-space-10)}.sm\:bui-mb-11{margin-bottom:var(--bui-space-11)}.sm\:bui-mb-12{margin-bottom:var(--bui-space-12)}.sm\:bui-mb-13{margin-bottom:var(--bui-space-13)}.sm\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mb{margin-bottom:var(--pb-md)}.md\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.md\:bui-mb-1{margin-bottom:var(--bui-space-1)}.md\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.md\:bui-mb-2{margin-bottom:var(--bui-space-2)}.md\:bui-mb-3{margin-bottom:var(--bui-space-3)}.md\:bui-mb-4{margin-bottom:var(--bui-space-4)}.md\:bui-mb-5{margin-bottom:var(--bui-space-5)}.md\:bui-mb-6{margin-bottom:var(--bui-space-6)}.md\:bui-mb-7{margin-bottom:var(--bui-space-7)}.md\:bui-mb-8{margin-bottom:var(--bui-space-8)}.md\:bui-mb-9{margin-bottom:var(--bui-space-9)}.md\:bui-mb-10{margin-bottom:var(--bui-space-10)}.md\:bui-mb-11{margin-bottom:var(--bui-space-11)}.md\:bui-mb-12{margin-bottom:var(--bui-space-12)}.md\:bui-mb-13{margin-bottom:var(--bui-space-13)}.md\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mb{margin-bottom:var(--pb-lg)}.lg\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.lg\:bui-mb-1{margin-bottom:var(--bui-space-1)}.lg\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.lg\:bui-mb-2{margin-bottom:var(--bui-space-2)}.lg\:bui-mb-3{margin-bottom:var(--bui-space-3)}.lg\:bui-mb-4{margin-bottom:var(--bui-space-4)}.lg\:bui-mb-5{margin-bottom:var(--bui-space-5)}.lg\:bui-mb-6{margin-bottom:var(--bui-space-6)}.lg\:bui-mb-7{margin-bottom:var(--bui-space-7)}.lg\:bui-mb-8{margin-bottom:var(--bui-space-8)}.lg\:bui-mb-9{margin-bottom:var(--bui-space-9)}.lg\:bui-mb-10{margin-bottom:var(--bui-space-10)}.lg\:bui-mb-11{margin-bottom:var(--bui-space-11)}.lg\:bui-mb-12{margin-bottom:var(--bui-space-12)}.lg\:bui-mb-13{margin-bottom:var(--bui-space-13)}.lg\:bui-mb-14{margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mb{margin-bottom:var(--pb-xl)}.xl\:bui-mb-0\.5{margin-bottom:var(--bui-space-0_5)}.xl\:bui-mb-1{margin-bottom:var(--bui-space-1)}.xl\:bui-mb-1\.5{margin-bottom:var(--bui-space-1_5)}.xl\:bui-mb-2{margin-bottom:var(--bui-space-2)}.xl\:bui-mb-3{margin-bottom:var(--bui-space-3)}.xl\:bui-mb-4{margin-bottom:var(--bui-space-4)}.xl\:bui-mb-5{margin-bottom:var(--bui-space-5)}.xl\:bui-mb-6{margin-bottom:var(--bui-space-6)}.xl\:bui-mb-7{margin-bottom:var(--bui-space-7)}.xl\:bui-mb-8{margin-bottom:var(--bui-space-8)}.xl\:bui-mb-9{margin-bottom:var(--bui-space-9)}.xl\:bui-mb-10{margin-bottom:var(--bui-space-10)}.xl\:bui-mb-11{margin-bottom:var(--bui-space-11)}.xl\:bui-mb-12{margin-bottom:var(--bui-space-12)}.xl\:bui-mb-13{margin-bottom:var(--bui-space-13)}.xl\:bui-mb-14{margin-bottom:var(--bui-space-14)}}.bui-my{margin-top:var(--my);margin-bottom:var(--my)}.bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}@media (width>=640px){.xs\:bui-my{margin-top:var(--py-xs);margin-bottom:var(--py-xs)}.xs\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xs\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xs\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xs\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xs\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xs\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xs\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xs\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xs\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xs\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xs\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xs\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xs\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xs\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xs\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xs\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-my{margin-top:var(--py-sm);margin-bottom:var(--py-sm)}.sm\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.sm\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.sm\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.sm\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.sm\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.sm\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.sm\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.sm\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.sm\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.sm\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.sm\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.sm\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.sm\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.sm\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.sm\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.sm\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-my{margin-top:var(--py-md);margin-bottom:var(--py-md)}.md\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.md\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.md\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.md\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.md\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.md\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.md\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.md\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.md\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.md\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.md\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.md\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.md\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.md\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.md\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.md\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-my{margin-top:var(--py-lg);margin-bottom:var(--py-lg)}.lg\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.lg\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.lg\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.lg\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.lg\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.lg\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.lg\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.lg\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.lg\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.lg\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.lg\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.lg\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.lg\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.lg\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.lg\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.lg\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-my{margin-top:var(--py-xl);margin-bottom:var(--py-xl)}.xl\:bui-my-0\.5{margin-top:var(--bui-space-0_5);margin-bottom:var(--bui-space-0_5)}.xl\:bui-my-1{margin-top:var(--bui-space-1);margin-bottom:var(--bui-space-1)}.xl\:bui-my-1\.5{margin-top:var(--bui-space-1_5);margin-bottom:var(--bui-space-1_5)}.xl\:bui-my-2{margin-top:var(--bui-space-2);margin-bottom:var(--bui-space-2)}.xl\:bui-my-3{margin-top:var(--bui-space-3);margin-bottom:var(--bui-space-3)}.xl\:bui-my-4{margin-top:var(--bui-space-4);margin-bottom:var(--bui-space-4)}.xl\:bui-my-5{margin-top:var(--bui-space-5);margin-bottom:var(--bui-space-5)}.xl\:bui-my-6{margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6)}.xl\:bui-my-7{margin-top:var(--bui-space-7);margin-bottom:var(--bui-space-7)}.xl\:bui-my-8{margin-top:var(--bui-space-8);margin-bottom:var(--bui-space-8)}.xl\:bui-my-9{margin-top:var(--bui-space-9);margin-bottom:var(--bui-space-9)}.xl\:bui-my-10{margin-top:var(--bui-space-10);margin-bottom:var(--bui-space-10)}.xl\:bui-my-11{margin-top:var(--bui-space-11);margin-bottom:var(--bui-space-11)}.xl\:bui-my-12{margin-top:var(--bui-space-12);margin-bottom:var(--bui-space-12)}.xl\:bui-my-13{margin-top:var(--bui-space-13);margin-bottom:var(--bui-space-13)}.xl\:bui-my-14{margin-top:var(--bui-space-14);margin-bottom:var(--bui-space-14)}}.bui-mx{margin-left:var(--mx);margin-right:var(--mx)}.bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}@media (width>=640px){.xs\:bui-mx{margin-left:var(--px-xs);margin-right:var(--px-xs)}.xs\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xs\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xs\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xs\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xs\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xs\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xs\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xs\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xs\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xs\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xs\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xs\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xs\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xs\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xs\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xs\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-mx{margin-left:var(--px-sm);margin-right:var(--px-sm)}.sm\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.sm\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.sm\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.sm\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.sm\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.sm\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.sm\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.sm\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.sm\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.sm\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.sm\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.sm\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.sm\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.sm\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.sm\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.sm\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-mx{margin-left:var(--px-md);margin-right:var(--px-md)}.md\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.md\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.md\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.md\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.md\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.md\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.md\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.md\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.md\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.md\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.md\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.md\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.md\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.md\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.md\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.md\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-mx{margin-left:var(--px-lg);margin-right:var(--px-lg)}.lg\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.lg\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.lg\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.lg\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.lg\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.lg\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.lg\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.lg\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.lg\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.lg\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.lg\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.lg\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.lg\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.lg\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.lg\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.lg\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-mx{margin-left:var(--px-xl);margin-right:var(--px-xl)}.xl\:bui-mx-0\.5{margin-left:var(--bui-space-0_5);margin-right:var(--bui-space-0_5)}.xl\:bui-mx-1{margin-left:var(--bui-space-1);margin-right:var(--bui-space-1)}.xl\:bui-mx-1\.5{margin-left:var(--bui-space-1_5);margin-right:var(--bui-space-1_5)}.xl\:bui-mx-2{margin-left:var(--bui-space-2);margin-right:var(--bui-space-2)}.xl\:bui-mx-3{margin-left:var(--bui-space-3);margin-right:var(--bui-space-3)}.xl\:bui-mx-4{margin-left:var(--bui-space-4);margin-right:var(--bui-space-4)}.xl\:bui-mx-5{margin-left:var(--bui-space-5);margin-right:var(--bui-space-5)}.xl\:bui-mx-6{margin-left:var(--bui-space-6);margin-right:var(--bui-space-6)}.xl\:bui-mx-7{margin-left:var(--bui-space-7);margin-right:var(--bui-space-7)}.xl\:bui-mx-8{margin-left:var(--bui-space-8);margin-right:var(--bui-space-8)}.xl\:bui-mx-9{margin-left:var(--bui-space-9);margin-right:var(--bui-space-9)}.xl\:bui-mx-10{margin-left:var(--bui-space-10);margin-right:var(--bui-space-10)}.xl\:bui-mx-11{margin-left:var(--bui-space-11);margin-right:var(--bui-space-11)}.xl\:bui-mx-12{margin-left:var(--bui-space-12);margin-right:var(--bui-space-12)}.xl\:bui-mx-13{margin-left:var(--bui-space-13);margin-right:var(--bui-space-13)}.xl\:bui-mx-14{margin-left:var(--bui-space-14);margin-right:var(--bui-space-14)}}.bui-display-none{display:none}.bui-display-inline{display:inline}.bui-display-inline-block{display:inline-block}.bui-display-block{display:block}@media (width>=640px){.xs\:bui-display-none{display:none}.xs\:bui-display-inline{display:inline}.xs\:bui-display-inline-block{display:inline-block}.xs\:bui-display-block{display:block}}@media (width>=768px){.sm\:bui-display-none{display:none}.sm\:bui-display-inline{display:inline}.sm\:bui-display-inline-block{display:inline-block}.sm\:bui-display-block{display:block}}@media (width>=1024px){.md\:bui-display-none{display:none}.md\:bui-display-inline{display:inline}.md\:bui-display-inline-block{display:inline-block}.md\:bui-display-block{display:block}}@media (width>=1280px){.lg\:bui-display-none{display:none}.lg\:bui-display-inline{display:inline}.lg\:bui-display-inline-block{display:inline-block}.lg\:bui-display-block{display:block}}@media (width>=1536px){.xl\:bui-display-none{display:none}.xl\:bui-display-inline{display:inline}.xl\:bui-display-inline-block{display:inline-block}.xl\:bui-display-block{display:block}}.bui-w{width:var(--width)}.bui-min-w{min-width:var(--min-width)}.bui-max-w{max-width:var(--max-width)}@media (width>=640px){.xs\:bui-w{width:var(--width)}.xs\:bui-min-w{min-width:var(--min-width)}.xs\:bui-max-w{max-width:var(--max-width)}}@media (width>=768px){.sm\:bui-w{width:var(--width)}.sm\:bui-min-w{min-width:var(--min-width)}.sm\:bui-max-w{max-width:var(--max-width)}}@media (width>=1024px){.md\:bui-w{width:var(--width)}.md\:bui-min-w{min-width:var(--min-width)}.md\:bui-max-w{max-width:var(--max-width)}}@media (width>=1280px){.lg\:bui-w{width:var(--width)}.lg\:bui-min-w{min-width:var(--min-width)}.lg\:bui-max-w{max-width:var(--max-width)}}@media (width>=1536px){.xl\:bui-w{width:var(--width)}.xl\:bui-min-w{min-width:var(--min-width)}.xl\:bui-max-w{max-width:var(--max-width)}}.bui-h{height:var(--height)}.bui-min-h{min-height:var(--min-height)}.bui-max-h{max-height:var(--max-height)}@media (width>=640px){.xs\:bui-h{height:var(--height)}.xs\:bui-min-h{min-height:var(--min-height)}.xs\:bui-max-h{max-height:var(--max-height)}}@media (width>=768px){.sm\:bui-h{height:var(--height)}.sm\:bui-min-h{min-height:var(--min-height)}.sm\:bui-max-h{max-height:var(--max-height)}}@media (width>=1024px){.md\:bui-h{height:var(--height)}.md\:bui-min-h{min-height:var(--min-height)}.md\:bui-max-h{max-height:var(--max-height)}}@media (width>=1280px){.lg\:bui-h{height:var(--height)}.lg\:bui-min-h{min-height:var(--min-height)}.lg\:bui-max-h{max-height:var(--max-height)}}@media (width>=1536px){.xl\:bui-h{height:var(--height)}.xl\:bui-min-h{min-height:var(--min-height)}.xl\:bui-max-h{max-height:var(--max-height)}}.bui-position-absolute{position:absolute}.bui-position-fixed{position:fixed}.bui-position-sticky{position:sticky}.bui-position-relative{position:relative}.bui-position-static{position:static}@media (width>=640px){.xs\:bui-position-absolute{position:absolute}.xs\:bui-position-fixed{position:fixed}.xs\:bui-position-sticky{position:sticky}.xs\:bui-position-relative{position:relative}.xs\:bui-position-static{position:static}}@media (width>=768px){.sm\:bui-position-absolute{position:absolute}.sm\:bui-position-fixed{position:fixed}.sm\:bui-position-sticky{position:sticky}.sm\:bui-position-relative{position:relative}.sm\:bui-position-static{position:static}}@media (width>=1024px){.md\:bui-position-absolute{position:absolute}.md\:bui-position-fixed{position:fixed}.md\:bui-position-sticky{position:sticky}.md\:bui-position-relative{position:relative}.md\:bui-position-static{position:static}}@media (width>=1280px){.lg\:bui-position-absolute{position:absolute}.lg\:bui-position-fixed{position:fixed}.lg\:bui-position-sticky{position:sticky}.lg\:bui-position-relative{position:relative}.lg\:bui-position-static{position:static}}@media (width>=1536px){.xl\:bui-position-absolute{position:absolute}.xl\:bui-position-fixed{position:fixed}.xl\:bui-position-sticky{position:sticky}.xl\:bui-position-relative{position:relative}.xl\:bui-position-static{position:static}}.bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.bui-col-span-1{grid-column:span 1/span 1}.bui-col-span-2{grid-column:span 2/span 2}.bui-col-span-3{grid-column:span 3/span 3}.bui-col-span-4{grid-column:span 4/span 4}.bui-col-span-5{grid-column:span 5/span 5}.bui-col-span-6{grid-column:span 6/span 6}.bui-col-span-7{grid-column:span 7/span 7}.bui-col-span-8{grid-column:span 8/span 8}.bui-col-span-9{grid-column:span 9/span 9}.bui-col-span-10{grid-column:span 10/span 10}.bui-col-span-11{grid-column:span 11/span 11}.bui-col-span-12{grid-column:span 12/span 12}.bui-col-span-auto{grid-column:span auto/span auto}.bui-col-start-1{grid-column-start:1}.bui-col-start-2{grid-column-start:2}.bui-col-start-3{grid-column-start:3}.bui-col-start-4{grid-column-start:4}.bui-col-start-5{grid-column-start:5}.bui-col-start-6{grid-column-start:6}.bui-col-start-7{grid-column-start:7}.bui-col-start-8{grid-column-start:8}.bui-col-start-9{grid-column-start:9}.bui-col-start-10{grid-column-start:10}.bui-col-start-11{grid-column-start:11}.bui-col-start-12{grid-column-start:12}.bui-col-start-13{grid-column-start:13}.bui-col-start-auto{grid-column-start:auto}.bui-col-end-1{grid-column-end:1}.bui-col-end-2{grid-column-end:2}.bui-col-end-3{grid-column-end:3}.bui-col-end-4{grid-column-end:4}.bui-col-end-5{grid-column-end:5}.bui-col-end-6{grid-column-end:6}.bui-col-end-7{grid-column-end:7}.bui-col-end-8{grid-column-end:8}.bui-col-end-9{grid-column-end:9}.bui-col-end-10{grid-column-end:10}.bui-col-end-11{grid-column-end:11}.bui-col-end-12{grid-column-end:12}.bui-col-end-13{grid-column-end:13}.bui-col-end-auto{grid-column-end:auto}.bui-row-span-1{grid-row:span 1/span 1}.bui-row-span-2{grid-row:span 2/span 2}.bui-row-span-3{grid-row:span 3/span 3}.bui-row-span-4{grid-row:span 4/span 4}.bui-row-span-5{grid-row:span 5/span 5}.bui-row-span-6{grid-row:span 6/span 6}.bui-row-span-7{grid-row:span 7/span 7}.bui-row-span-8{grid-row:span 8/span 8}.bui-row-span-9{grid-row:span 9/span 9}.bui-row-span-10{grid-row:span 10/span 10}.bui-row-span-11{grid-row:span 11/span 11}.bui-row-span-12{grid-row:span 12/span 12}.bui-row-span-auto{grid-row:span auto/span auto}@media (width>=640px){.xs\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xs\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xs\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xs\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xs\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xs\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xs\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xs\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xs\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xs\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xs\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xs\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xs\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xs\:bui-col-span-1{grid-column:span 1/span 1}.xs\:bui-col-span-2{grid-column:span 2/span 2}.xs\:bui-col-span-3{grid-column:span 3/span 3}.xs\:bui-col-span-4{grid-column:span 4/span 4}.xs\:bui-col-span-5{grid-column:span 5/span 5}.xs\:bui-col-span-6{grid-column:span 6/span 6}.xs\:bui-col-span-7{grid-column:span 7/span 7}.xs\:bui-col-span-8{grid-column:span 8/span 8}.xs\:bui-col-span-9{grid-column:span 9/span 9}.xs\:bui-col-span-10{grid-column:span 10/span 10}.xs\:bui-col-span-11{grid-column:span 11/span 11}.xs\:bui-col-span-12{grid-column:span 12/span 12}.xs\:bui-col-span-auto{grid-column:span auto/span auto}.xs\:bui-col-start-1{grid-column-start:1}.xs\:bui-col-start-2{grid-column-start:2}.xs\:bui-col-start-3{grid-column-start:3}.xs\:bui-col-start-4{grid-column-start:4}.xs\:bui-col-start-5{grid-column-start:5}.xs\:bui-col-start-6{grid-column-start:6}.xs\:bui-col-start-7{grid-column-start:7}.xs\:bui-col-start-8{grid-column-start:8}.xs\:bui-col-start-9{grid-column-start:9}.xs\:bui-col-start-10{grid-column-start:10}.xs\:bui-col-start-11{grid-column-start:11}.xs\:bui-col-start-12{grid-column-start:12}.xs\:bui-col-start-13{grid-column-start:13}.xs\:bui-col-start-auto{grid-column-start:auto}.xs\:bui-col-end-1{grid-column-end:1}.xs\:bui-col-end-2{grid-column-end:2}.xs\:bui-col-end-3{grid-column-end:3}.xs\:bui-col-end-4{grid-column-end:4}.xs\:bui-col-end-5{grid-column-end:5}.xs\:bui-col-end-6{grid-column-end:6}.xs\:bui-col-end-7{grid-column-end:7}.xs\:bui-col-end-8{grid-column-end:8}.xs\:bui-col-end-9{grid-column-end:9}.xs\:bui-col-end-10{grid-column-end:10}.xs\:bui-col-end-11{grid-column-end:11}.xs\:bui-col-end-12{grid-column-end:12}.xs\:bui-col-end-13{grid-column-end:13}.xs\:bui-col-end-auto{grid-column-end:auto}.xs\:bui-row-span-1{grid-row:span 1/span 1}.xs\:bui-row-span-2{grid-row:span 2/span 2}.xs\:bui-row-span-3{grid-row:span 3/span 3}.xs\:bui-row-span-4{grid-row:span 4/span 4}.xs\:bui-row-span-5{grid-row:span 5/span 5}.xs\:bui-row-span-6{grid-row:span 6/span 6}.xs\:bui-row-span-7{grid-row:span 7/span 7}.xs\:bui-row-span-8{grid-row:span 8/span 8}.xs\:bui-row-span-9{grid-row:span 9/span 9}.xs\:bui-row-span-10{grid-row:span 10/span 10}.xs\:bui-row-span-11{grid-row:span 11/span 11}.xs\:bui-row-span-12{grid-row:span 12/span 12}.xs\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=768px){.sm\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.sm\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.sm\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.sm\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.sm\:bui-col-span-1{grid-column:span 1/span 1}.sm\:bui-col-span-2{grid-column:span 2/span 2}.sm\:bui-col-span-3{grid-column:span 3/span 3}.sm\:bui-col-span-4{grid-column:span 4/span 4}.sm\:bui-col-span-5{grid-column:span 5/span 5}.sm\:bui-col-span-6{grid-column:span 6/span 6}.sm\:bui-col-span-7{grid-column:span 7/span 7}.sm\:bui-col-span-8{grid-column:span 8/span 8}.sm\:bui-col-span-9{grid-column:span 9/span 9}.sm\:bui-col-span-10{grid-column:span 10/span 10}.sm\:bui-col-span-11{grid-column:span 11/span 11}.sm\:bui-col-span-12{grid-column:span 12/span 12}.sm\:bui-col-span-auto{grid-column:span auto/span auto}.sm\:bui-col-start-1{grid-column-start:1}.sm\:bui-col-start-2{grid-column-start:2}.sm\:bui-col-start-3{grid-column-start:3}.sm\:bui-col-start-4{grid-column-start:4}.sm\:bui-col-start-5{grid-column-start:5}.sm\:bui-col-start-6{grid-column-start:6}.sm\:bui-col-start-7{grid-column-start:7}.sm\:bui-col-start-8{grid-column-start:8}.sm\:bui-col-start-9{grid-column-start:9}.sm\:bui-col-start-10{grid-column-start:10}.sm\:bui-col-start-11{grid-column-start:11}.sm\:bui-col-start-12{grid-column-start:12}.sm\:bui-col-start-13{grid-column-start:13}.sm\:bui-col-start-auto{grid-column-start:auto}.sm\:bui-col-end-1{grid-column-end:1}.sm\:bui-col-end-2{grid-column-end:2}.sm\:bui-col-end-3{grid-column-end:3}.sm\:bui-col-end-4{grid-column-end:4}.sm\:bui-col-end-5{grid-column-end:5}.sm\:bui-col-end-6{grid-column-end:6}.sm\:bui-col-end-7{grid-column-end:7}.sm\:bui-col-end-8{grid-column-end:8}.sm\:bui-col-end-9{grid-column-end:9}.sm\:bui-col-end-10{grid-column-end:10}.sm\:bui-col-end-11{grid-column-end:11}.sm\:bui-col-end-12{grid-column-end:12}.sm\:bui-col-end-13{grid-column-end:13}.sm\:bui-col-end-auto{grid-column-end:auto}.sm\:bui-row-span-1{grid-row:span 1/span 1}.sm\:bui-row-span-2{grid-row:span 2/span 2}.sm\:bui-row-span-3{grid-row:span 3/span 3}.sm\:bui-row-span-4{grid-row:span 4/span 4}.sm\:bui-row-span-5{grid-row:span 5/span 5}.sm\:bui-row-span-6{grid-row:span 6/span 6}.sm\:bui-row-span-7{grid-row:span 7/span 7}.sm\:bui-row-span-8{grid-row:span 8/span 8}.sm\:bui-row-span-9{grid-row:span 9/span 9}.sm\:bui-row-span-10{grid-row:span 10/span 10}.sm\:bui-row-span-11{grid-row:span 11/span 11}.sm\:bui-row-span-12{grid-row:span 12/span 12}.sm\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1024px){.md\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.md\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.md\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.md\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.md\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.md\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.md\:bui-col-span-1{grid-column:span 1/span 1}.md\:bui-col-span-2{grid-column:span 2/span 2}.md\:bui-col-span-3{grid-column:span 3/span 3}.md\:bui-col-span-4{grid-column:span 4/span 4}.md\:bui-col-span-5{grid-column:span 5/span 5}.md\:bui-col-span-6{grid-column:span 6/span 6}.md\:bui-col-span-7{grid-column:span 7/span 7}.md\:bui-col-span-8{grid-column:span 8/span 8}.md\:bui-col-span-9{grid-column:span 9/span 9}.md\:bui-col-span-10{grid-column:span 10/span 10}.md\:bui-col-span-11{grid-column:span 11/span 11}.md\:bui-col-span-12{grid-column:span 12/span 12}.md\:bui-col-span-auto{grid-column:span auto/span auto}.md\:bui-col-start-1{grid-column-start:1}.md\:bui-col-start-2{grid-column-start:2}.md\:bui-col-start-3{grid-column-start:3}.md\:bui-col-start-4{grid-column-start:4}.md\:bui-col-start-5{grid-column-start:5}.md\:bui-col-start-6{grid-column-start:6}.md\:bui-col-start-7{grid-column-start:7}.md\:bui-col-start-8{grid-column-start:8}.md\:bui-col-start-9{grid-column-start:9}.md\:bui-col-start-10{grid-column-start:10}.md\:bui-col-start-11{grid-column-start:11}.md\:bui-col-start-12{grid-column-start:12}.md\:bui-col-start-13{grid-column-start:13}.md\:bui-col-start-auto{grid-column-start:auto}.md\:bui-col-end-1{grid-column-end:1}.md\:bui-col-end-2{grid-column-end:2}.md\:bui-col-end-3{grid-column-end:3}.md\:bui-col-end-4{grid-column-end:4}.md\:bui-col-end-5{grid-column-end:5}.md\:bui-col-end-6{grid-column-end:6}.md\:bui-col-end-7{grid-column-end:7}.md\:bui-col-end-8{grid-column-end:8}.md\:bui-col-end-9{grid-column-end:9}.md\:bui-col-end-10{grid-column-end:10}.md\:bui-col-end-11{grid-column-end:11}.md\:bui-col-end-12{grid-column-end:12}.md\:bui-col-end-13{grid-column-end:13}.md\:bui-col-end-auto{grid-column-end:auto}.md\:bui-row-span-1{grid-row:span 1/span 1}.md\:bui-row-span-2{grid-row:span 2/span 2}.md\:bui-row-span-3{grid-row:span 3/span 3}.md\:bui-row-span-4{grid-row:span 4/span 4}.md\:bui-row-span-5{grid-row:span 5/span 5}.md\:bui-row-span-6{grid-row:span 6/span 6}.md\:bui-row-span-7{grid-row:span 7/span 7}.md\:bui-row-span-8{grid-row:span 8/span 8}.md\:bui-row-span-9{grid-row:span 9/span 9}.md\:bui-row-span-10{grid-row:span 10/span 10}.md\:bui-row-span-11{grid-row:span 11/span 11}.md\:bui-row-span-12{grid-row:span 12/span 12}.md\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1280px){.lg\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.lg\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.lg\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.lg\:bui-col-span-1{grid-column:span 1/span 1}.lg\:bui-col-span-2{grid-column:span 2/span 2}.lg\:bui-col-span-3{grid-column:span 3/span 3}.lg\:bui-col-span-4{grid-column:span 4/span 4}.lg\:bui-col-span-5{grid-column:span 5/span 5}.lg\:bui-col-span-6{grid-column:span 6/span 6}.lg\:bui-col-span-7{grid-column:span 7/span 7}.lg\:bui-col-span-8{grid-column:span 8/span 8}.lg\:bui-col-span-9{grid-column:span 9/span 9}.lg\:bui-col-span-10{grid-column:span 10/span 10}.lg\:bui-col-span-11{grid-column:span 11/span 11}.lg\:bui-col-span-12{grid-column:span 12/span 12}.lg\:bui-col-span-auto{grid-column:span auto/span auto}.lg\:bui-col-start-1{grid-column-start:1}.lg\:bui-col-start-2{grid-column-start:2}.lg\:bui-col-start-3{grid-column-start:3}.lg\:bui-col-start-4{grid-column-start:4}.lg\:bui-col-start-5{grid-column-start:5}.lg\:bui-col-start-6{grid-column-start:6}.lg\:bui-col-start-7{grid-column-start:7}.lg\:bui-col-start-8{grid-column-start:8}.lg\:bui-col-start-9{grid-column-start:9}.lg\:bui-col-start-10{grid-column-start:10}.lg\:bui-col-start-11{grid-column-start:11}.lg\:bui-col-start-12{grid-column-start:12}.lg\:bui-col-start-13{grid-column-start:13}.lg\:bui-col-start-auto{grid-column-start:auto}.lg\:bui-col-end-1{grid-column-end:1}.lg\:bui-col-end-2{grid-column-end:2}.lg\:bui-col-end-3{grid-column-end:3}.lg\:bui-col-end-4{grid-column-end:4}.lg\:bui-col-end-5{grid-column-end:5}.lg\:bui-col-end-6{grid-column-end:6}.lg\:bui-col-end-7{grid-column-end:7}.lg\:bui-col-end-8{grid-column-end:8}.lg\:bui-col-end-9{grid-column-end:9}.lg\:bui-col-end-10{grid-column-end:10}.lg\:bui-col-end-11{grid-column-end:11}.lg\:bui-col-end-12{grid-column-end:12}.lg\:bui-col-end-13{grid-column-end:13}.lg\:bui-col-end-auto{grid-column-end:auto}.lg\:bui-row-span-1{grid-row:span 1/span 1}.lg\:bui-row-span-2{grid-row:span 2/span 2}.lg\:bui-row-span-3{grid-row:span 3/span 3}.lg\:bui-row-span-4{grid-row:span 4/span 4}.lg\:bui-row-span-5{grid-row:span 5/span 5}.lg\:bui-row-span-6{grid-row:span 6/span 6}.lg\:bui-row-span-7{grid-row:span 7/span 7}.lg\:bui-row-span-8{grid-row:span 8/span 8}.lg\:bui-row-span-9{grid-row:span 9/span 9}.lg\:bui-row-span-10{grid-row:span 10/span 10}.lg\:bui-row-span-11{grid-row:span 11/span 11}.lg\:bui-row-span-12{grid-row:span 12/span 12}.lg\:bui-row-span-auto{grid-row:span auto/span auto}}@media (width>=1536px){.xl\:bui-columns-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:bui-columns-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:bui-columns-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:bui-columns-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:bui-columns-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:bui-columns-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:bui-columns-7{grid-template-columns:repeat(7,minmax(0,1fr))}.xl\:bui-columns-8{grid-template-columns:repeat(8,minmax(0,1fr))}.xl\:bui-columns-9{grid-template-columns:repeat(9,minmax(0,1fr))}.xl\:bui-columns-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:bui-columns-11{grid-template-columns:repeat(11,minmax(0,1fr))}.xl\:bui-columns-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:bui-columns-auto{grid-template-columns:repeat(auto-fit,minmax(0,1fr))}.xl\:bui-col-span-1{grid-column:span 1/span 1}.xl\:bui-col-span-2{grid-column:span 2/span 2}.xl\:bui-col-span-3{grid-column:span 3/span 3}.xl\:bui-col-span-4{grid-column:span 4/span 4}.xl\:bui-col-span-5{grid-column:span 5/span 5}.xl\:bui-col-span-6{grid-column:span 6/span 6}.xl\:bui-col-span-7{grid-column:span 7/span 7}.xl\:bui-col-span-8{grid-column:span 8/span 8}.xl\:bui-col-span-9{grid-column:span 9/span 9}.xl\:bui-col-span-10{grid-column:span 10/span 10}.xl\:bui-col-span-11{grid-column:span 11/span 11}.xl\:bui-col-span-12{grid-column:span 12/span 12}.xl\:bui-col-span-auto{grid-column:span auto/span auto}.xl\:bui-col-start-1{grid-column-start:1}.xl\:bui-col-start-2{grid-column-start:2}.xl\:bui-col-start-3{grid-column-start:3}.xl\:bui-col-start-4{grid-column-start:4}.xl\:bui-col-start-5{grid-column-start:5}.xl\:bui-col-start-6{grid-column-start:6}.xl\:bui-col-start-7{grid-column-start:7}.xl\:bui-col-start-8{grid-column-start:8}.xl\:bui-col-start-9{grid-column-start:9}.xl\:bui-col-start-10{grid-column-start:10}.xl\:bui-col-start-11{grid-column-start:11}.xl\:bui-col-start-12{grid-column-start:12}.xl\:bui-col-start-13{grid-column-start:13}.xl\:bui-col-start-auto{grid-column-start:auto}.xl\:bui-col-end-1{grid-column-end:1}.xl\:bui-col-end-2{grid-column-end:2}.xl\:bui-col-end-3{grid-column-end:3}.xl\:bui-col-end-4{grid-column-end:4}.xl\:bui-col-end-5{grid-column-end:5}.xl\:bui-col-end-6{grid-column-end:6}.xl\:bui-col-end-7{grid-column-end:7}.xl\:bui-col-end-8{grid-column-end:8}.xl\:bui-col-end-9{grid-column-end:9}.xl\:bui-col-end-10{grid-column-end:10}.xl\:bui-col-end-11{grid-column-end:11}.xl\:bui-col-end-12{grid-column-end:12}.xl\:bui-col-end-13{grid-column-end:13}.xl\:bui-col-end-auto{grid-column-end:auto}.xl\:bui-row-span-1{grid-row:span 1/span 1}.xl\:bui-row-span-2{grid-row:span 2/span 2}.xl\:bui-row-span-3{grid-row:span 3/span 3}.xl\:bui-row-span-4{grid-row:span 4/span 4}.xl\:bui-row-span-5{grid-row:span 5/span 5}.xl\:bui-row-span-6{grid-row:span 6/span 6}.xl\:bui-row-span-7{grid-row:span 7/span 7}.xl\:bui-row-span-8{grid-row:span 8/span 8}.xl\:bui-row-span-9{grid-row:span 9/span 9}.xl\:bui-row-span-10{grid-row:span 10/span 10}.xl\:bui-row-span-11{grid-row:span 11/span 11}.xl\:bui-row-span-12{grid-row:span 12/span 12}.xl\:bui-row-span-auto{grid-row:span auto/span auto}}.bui-gap{gap:var(--gap)}.bui-gap-0\.5{gap:var(--bui-space-0_5)}.bui-gap-1{gap:var(--bui-space-1)}.bui-gap-1\.5{gap:var(--bui-space-1_5)}.bui-gap-2{gap:var(--bui-space-2)}.bui-gap-3{gap:var(--bui-space-3)}.bui-gap-4{gap:var(--bui-space-4)}.bui-gap-5{gap:var(--bui-space-5)}.bui-gap-6{gap:var(--bui-space-6)}.bui-gap-7{gap:var(--bui-space-7)}.bui-gap-8{gap:var(--bui-space-8)}.bui-gap-9{gap:var(--bui-space-9)}.bui-gap-10{gap:var(--bui-space-10)}.bui-gap-11{gap:var(--bui-space-11)}.bui-gap-12{gap:var(--bui-space-12)}.bui-gap-13{gap:var(--bui-space-13)}.bui-gap-14{gap:var(--bui-space-14)}@media (width>=640px){.xs\:bui-gap{gap:var(--gap-xs)}.xs\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xs\:bui-gap-1{gap:var(--bui-space-1)}.xs\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xs\:bui-gap-2{gap:var(--bui-space-2)}.xs\:bui-gap-3{gap:var(--bui-space-3)}.xs\:bui-gap-4{gap:var(--bui-space-4)}.xs\:bui-gap-5{gap:var(--bui-space-5)}.xs\:bui-gap-6{gap:var(--bui-space-6)}.xs\:bui-gap-7{gap:var(--bui-space-7)}.xs\:bui-gap-8{gap:var(--bui-space-8)}.xs\:bui-gap-9{gap:var(--bui-space-9)}.xs\:bui-gap-10{gap:var(--bui-space-10)}.xs\:bui-gap-11{gap:var(--bui-space-11)}.xs\:bui-gap-12{gap:var(--bui-space-12)}.xs\:bui-gap-13{gap:var(--bui-space-13)}.xs\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=768px){.sm\:bui-gap{gap:var(--gap-sm)}.sm\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.sm\:bui-gap-1{gap:var(--bui-space-1)}.sm\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.sm\:bui-gap-2{gap:var(--bui-space-2)}.sm\:bui-gap-3{gap:var(--bui-space-3)}.sm\:bui-gap-4{gap:var(--bui-space-4)}.sm\:bui-gap-5{gap:var(--bui-space-5)}.sm\:bui-gap-6{gap:var(--bui-space-6)}.sm\:bui-gap-7{gap:var(--bui-space-7)}.sm\:bui-gap-8{gap:var(--bui-space-8)}.sm\:bui-gap-9{gap:var(--bui-space-9)}.sm\:bui-gap-10{gap:var(--bui-space-10)}.sm\:bui-gap-11{gap:var(--bui-space-11)}.sm\:bui-gap-12{gap:var(--bui-space-12)}.sm\:bui-gap-13{gap:var(--bui-space-13)}.sm\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1024px){.md\:bui-gap{gap:var(--gap-md)}.md\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.md\:bui-gap-1{gap:var(--bui-space-1)}.md\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.md\:bui-gap-2{gap:var(--bui-space-2)}.md\:bui-gap-3{gap:var(--bui-space-3)}.md\:bui-gap-4{gap:var(--bui-space-4)}.md\:bui-gap-5{gap:var(--bui-space-5)}.md\:bui-gap-6{gap:var(--bui-space-6)}.md\:bui-gap-7{gap:var(--bui-space-7)}.md\:bui-gap-8{gap:var(--bui-space-8)}.md\:bui-gap-9{gap:var(--bui-space-9)}.md\:bui-gap-10{gap:var(--bui-space-10)}.md\:bui-gap-11{gap:var(--bui-space-11)}.md\:bui-gap-12{gap:var(--bui-space-12)}.md\:bui-gap-13{gap:var(--bui-space-13)}.md\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1280px){.lg\:bui-gap{gap:var(--gap-lg)}.lg\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.lg\:bui-gap-1{gap:var(--bui-space-1)}.lg\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.lg\:bui-gap-2{gap:var(--bui-space-2)}.lg\:bui-gap-3{gap:var(--bui-space-3)}.lg\:bui-gap-4{gap:var(--bui-space-4)}.lg\:bui-gap-5{gap:var(--bui-space-5)}.lg\:bui-gap-6{gap:var(--bui-space-6)}.lg\:bui-gap-7{gap:var(--bui-space-7)}.lg\:bui-gap-8{gap:var(--bui-space-8)}.lg\:bui-gap-9{gap:var(--bui-space-9)}.lg\:bui-gap-10{gap:var(--bui-space-10)}.lg\:bui-gap-11{gap:var(--bui-space-11)}.lg\:bui-gap-12{gap:var(--bui-space-12)}.lg\:bui-gap-13{gap:var(--bui-space-13)}.lg\:bui-gap-14{gap:var(--bui-space-14)}}@media (width>=1536px){.xl\:bui-gap{gap:var(--gap-xl)}.xl\:bui-gap-0\.5{gap:var(--bui-space-0_5)}.xl\:bui-gap-1{gap:var(--bui-space-1)}.xl\:bui-gap-1\.5{gap:var(--bui-space-1_5)}.xl\:bui-gap-2{gap:var(--bui-space-2)}.xl\:bui-gap-3{gap:var(--bui-space-3)}.xl\:bui-gap-4{gap:var(--bui-space-4)}.xl\:bui-gap-5{gap:var(--bui-space-5)}.xl\:bui-gap-6{gap:var(--bui-space-6)}.xl\:bui-gap-7{gap:var(--bui-space-7)}.xl\:bui-gap-8{gap:var(--bui-space-8)}.xl\:bui-gap-9{gap:var(--bui-space-9)}.xl\:bui-gap-10{gap:var(--bui-space-10)}.xl\:bui-gap-11{gap:var(--bui-space-11)}.xl\:bui-gap-12{gap:var(--bui-space-12)}.xl\:bui-gap-13{gap:var(--bui-space-13)}.xl\:bui-gap-14{gap:var(--bui-space-14)}}.bui-align-start{align-items:start}.bui-align-center{align-items:center}.bui-align-end{align-items:end}.bui-align-baseline{align-items:baseline}.bui-align-stretch{align-items:stretch}.bui-jc-start{justify-content:start}.bui-jc-center{justify-content:center}.bui-jc-end{justify-content:end}.bui-jc-between{justify-content:space-between}.bui-fd-row{flex-direction:row}.bui-fd-column{flex-direction:column}.bui-fd-row-reverse{flex-direction:row-reverse}.bui-fd-column-reverse{flex-direction:column-reverse}@media (width>=640px){.xs\:bui-align-start{align-items:start}.xs\:bui-align-center{align-items:center}.xs\:bui-align-end{align-items:end}.xs\:bui-align-stretch{align-items:stretch}.xs\:bui-jc-start{justify-content:start}.xs\:bui-jc-center{justify-content:center}.xs\:bui-jc-end{justify-content:end}.xs\:bui-jc-between{justify-content:space-between}.xs\:bui-fd-row{flex-direction:row}.xs\:bui-fd-column{flex-direction:column}.xs\:bui-fd-row-reverse{flex-direction:row-reverse}.xs\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=768px){.sm\:bui-align-start{align-items:start}.sm\:bui-align-center{align-items:center}.sm\:bui-align-end{align-items:end}.sm\:bui-align-stretch{align-items:stretch}.sm\:bui-jc-start{justify-content:start}.sm\:bui-jc-center{justify-content:center}.sm\:bui-jc-end{justify-content:end}.sm\:bui-jc-between{justify-content:space-between}.sm\:bui-fd-row{flex-direction:row}.sm\:bui-fd-column{flex-direction:column}.sm\:bui-fd-row-reverse{flex-direction:row-reverse}.sm\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1024px){.md\:bui-align-start{align-items:start}.md\:bui-align-center{align-items:center}.md\:bui-align-end{align-items:end}.md\:bui-align-stretch{align-items:stretch}.md\:bui-jc-start{justify-content:start}.md\:bui-jc-center{justify-content:center}.md\:bui-jc-end{justify-content:end}.md\:bui-jc-between{justify-content:space-between}.md\:bui-fd-row{flex-direction:row}.md\:bui-fd-column{flex-direction:column}.md\:bui-fd-row-reverse{flex-direction:row-reverse}.md\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1280px){.lg\:bui-align-start{align-items:start}.lg\:bui-align-center{align-items:center}.lg\:bui-align-end{align-items:end}.lg\:bui-align-stretch{align-items:stretch}.lg\:bui-jc-start{justify-content:start}.lg\:bui-jc-center{justify-content:center}.lg\:bui-jc-end{justify-content:end}.lg\:bui-jc-between{justify-content:space-between}.lg\:bui-fd-row{flex-direction:row}.lg\:bui-fd-column{flex-direction:column}.lg\:bui-fd-row-reverse{flex-direction:row-reverse}.lg\:bui-fd-column-reverse{flex-direction:column-reverse}}@media (width>=1536px){.xl\:bui-align-start{align-items:start}.xl\:bui-align-center{align-items:center}.xl\:bui-align-end{align-items:end}.xl\:bui-align-stretch{align-items:stretch}.xl\:bui-jc-start{justify-content:start}.xl\:bui-jc-center{justify-content:center}.xl\:bui-jc-end{justify-content:end}.xl\:bui-jc-between{justify-content:space-between}.xl\:bui-fd-row{flex-direction:row}.xl\:bui-fd-column{flex-direction:column}.xl\:bui-fd-row-reverse{flex-direction:row-reverse}.xl\:bui-fd-column-reverse{flex-direction:column-reverse}}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}:root{--bui-font-regular:system-ui;--bui-font-monospace:ui-monospace,"Menlo","Monaco","Consolas","Liberation Mono","Courier New",monospace;--bui-font-weight-regular:400;--bui-font-weight-bold:600;--bui-font-size-1:.625rem;--bui-font-size-2:.75rem;--bui-font-size-3:.875rem;--bui-font-size-4:1rem;--bui-font-size-5:1.25rem;--bui-font-size-6:1.5rem;--bui-font-size-7:2rem;--bui-font-size-8:3rem;--bui-font-size-9:4rem;--bui-font-size-10:5.75rem;--bui-space:.25rem;--bui-space-0_5:calc(var(--bui-space)*.5);--bui-space-1:var(--bui-space);--bui-space-1_5:calc(var(--bui-space)*1.5);--bui-space-2:calc(var(--bui-space)*2);--bui-space-3:calc(var(--bui-space)*3);--bui-space-4:calc(var(--bui-space)*4);--bui-space-5:calc(var(--bui-space)*5);--bui-space-6:calc(var(--bui-space)*6);--bui-space-7:calc(var(--bui-space)*7);--bui-space-8:calc(var(--bui-space)*8);--bui-space-9:calc(var(--bui-space)*9);--bui-space-10:calc(var(--bui-space)*10);--bui-space-11:calc(var(--bui-space)*11);--bui-space-12:calc(var(--bui-space)*12);--bui-space-13:calc(var(--bui-space)*13);--bui-space-14:calc(var(--bui-space)*14);--bui-radius-1:calc(.125rem);--bui-radius-2:calc(.25rem);--bui-radius-3:calc(.5rem);--bui-radius-4:calc(.75rem);--bui-radius-5:calc(1rem);--bui-radius-6:calc(1.25rem);--bui-radius-full:9999px;--bui-black:#000;--bui-white:#fff;--bui-gray-1:#f8f8f8;--bui-gray-2:#ececec;--bui-gray-3:#d9d9d9;--bui-gray-4:#c1c1c1;--bui-gray-5:#9e9e9e;--bui-gray-6:#8c8c8c;--bui-gray-7:#757575;--bui-gray-8:#595959;--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1f5493;--bui-bg-solid-hover:#163a66;--bui-bg-solid-pressed:#0f2b4e;--bui-bg-solid-disabled:#ebebeb;--bui-bg-tint:transparent;--bui-bg-tint-hover:#1f549366;--bui-bg-tint-pressed:#1f549399;--bui-bg-tint-disabled:#ebebeb;--bui-bg-danger:#feebe7;--bui-bg-warning:#fff2b2;--bui-bg-success:#e6f6eb;--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#1f5493;--bui-fg-link-hover:#1f2d5c;--bui-fg-disabled:#9e9e9e;--bui-fg-solid:var(--bui-white);--bui-fg-solid-disabled:#9c9c9c;--bui-fg-tint:#1f5493;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#0000001a;--bui-border-hover:#0003;--bui-border-pressed:#0006;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#a0a0a03b;--bui-scrollbar-thumb:#a0a0a0;--bui-animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite}[data-theme-mode=dark]{--bui-gray-1:#191919;--bui-gray-2:#242424;--bui-gray-3:#373737;--bui-gray-4:#464646;--bui-gray-5:#575757;--bui-gray-6:#7b7b7b;--bui-gray-7:#9e9e9e;--bui-gray-8:#b4b4b4;--bui-bg:#333;--bui-bg-surface-1:#424242;--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#9cc9ff;--bui-bg-solid-hover:#83b9fd;--bui-bg-solid-pressed:#83b9fd;--bui-bg-solid-disabled:#222;--bui-bg-tint:transparent;--bui-bg-tint-hover:#9cc9ff1f;--bui-bg-tint-pressed:#9cc9ff29;--bui-bg-tint-disabled:transparent;--bui-bg-danger:#3b1219;--bui-bg-warning:#302008;--bui-bg-success:#132d21;--bui-fg-primary:var(--bui-white);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-link:#9cc9ff;--bui-fg-link-hover:#7eb5f7;--bui-fg-disabled:var(--bui-gray-7);--bui-fg-solid:#101821;--bui-fg-solid-disabled:var(--bui-gray-5);--bui-fg-tint:#9cc9ff;--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:#ffffff1f;--bui-border-hover:#fff6;--bui-border-pressed:#ffffff80;--bui-border-disabled:#fff3;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#1f5493;--bui-scrollbar:#3636363a;--bui-scrollbar-thumb:#575757}.bui-AvatarRoot{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-AvatarRoot[data-size=small]{width:1.5rem;height:1.5rem}.bui-AvatarRoot[data-size=medium]{width:2rem;height:2rem}.bui-AvatarRoot[data-size=large]{width:3rem;height:3rem}.bui-AvatarImage{object-fit:cover;width:100%;height:100%}.bui-AvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-Box{font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary)}.bui-Button{user-select:none;font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-bold);cursor:pointer;border-radius:var(--bui-radius-2);justify-content:center;align-items:center;gap:var(--bui-space-1_5);border:none;flex-shrink:0;padding:0;display:inline-flex;&[data-disabled=true]{cursor:not-allowed}}.bui-Button[data-variant=primary]{background-color:var(--bui-bg-solid);color:var(--bui-fg-solid);&:hover{background-color:var(--bui-bg-solid-hover);transition:background-color .15s}&:active{background-color:var(--bui-bg-solid-pressed)}&:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled=true]{background-color:var(--bui-bg-solid-disabled);color:var(--bui-fg-solid-disabled)}}.bui-Button[data-variant=secondary]{background-color:var(--bui-bg-surface-1);box-shadow:inset 0 0 0 1px var(--bui-border);color:var(--bui-fg-primary);&:hover{box-shadow:inset 0 0 0 1px var(--bui-border-hover);transition:box-shadow .15s}&:active{box-shadow:inset 0 0 0 1px var(--bui-border-pressed)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{box-shadow:inset 0 0 0 1px var(--bui-border-disabled);color:var(--bui-fg-disabled)}}.bui-Button[data-variant=tertiary]{color:var(--bui-fg-primary);background-color:#0000;&:hover{background-color:var(--bui-bg-surface-1);transition:background-color .2s}&:active{background-color:var(--bui-bg-surface-2)}&:focus-visible{box-shadow:inset 0 0 0 2px var(--bui-ring);outline:none;transition:none}&[data-disabled=true]{color:var(--bui-fg-disabled);background-color:#0000}}.bui-Button[data-size=medium]{font-size:var(--bui-font-size-4);padding:0 var(--bui-space-3);height:2.5rem}.bui-Button[data-size=small]{font-size:var(--bui-font-size-3);padding:0 var(--bui-space-2);height:2rem}.bui-Button[data-size=small] svg{width:1rem;height:1rem}.bui-Button[data-size=medium] svg{width:1.25rem;height:1.25rem}.bui-ButtonIcon{justify-content:center;align-items:center}.bui-ButtonIcon[data-size=small]{width:2rem;padding:0}.bui-ButtonIcon[data-size=medium]{width:2.5rem;padding:0}.bui-Card{gap:var(--bui-space-3);background-color:var(--bui-bg-surface-1);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-3);color:var(--bui-fg-primary);border:1px solid var(--bui-border);flex-direction:column;width:100%;min-height:0;display:flex;overflow:hidden}.bui-CardBody{flex:1;min-height:0;overflow:auto}.bui-CardHeader,.bui-CardFooter{padding-inline:var(--bui-space-3)}.bui-CheckboxRoot{width:1rem;height:1rem;box-shadow:inset 0 0 0 1px var(--bui-border);cursor:pointer;background-color:var(--bui-bg-surface-1);border:none;border-radius:2px;flex-shrink:0;justify-content:center;align-items:center;padding:0;transition:background-color .2s ease-in-out;display:flex}.bui-CheckboxRoot:focus-visible{outline:2px solid var(--bui-ring);outline-offset:2px;transition:none}.bui-CheckboxRoot[data-checked]{background-color:var(--bui-bg-solid);box-shadow:none;color:var(--bui-fg-solid)}.bui-CheckboxLabel{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);user-select:none;flex-direction:row;display:flex;&:hover{& .bui-CheckboxRoot:not([data-checked]){box-shadow:inset 0 0 0 1px var(--bui-border-hover)}}}.bui-CheckboxIndicator{color:var(--bui-fg-solid);justify-content:center;align-items:center;display:flex}.bui-CollapsiblePanel{height:var(--collapsible-panel-height);transition:all .15s ease-out;display:flex;overflow:hidden;&[data-starting-style],&[data-ending-style]{height:0}}.bui-Container{max-width:120rem;padding-inline:var(--bui-space-4);margin-inline:auto;transition:padding .2s ease-in-out}@media (width>=640px){.bui-Container{padding-inline:var(--bui-space-5)}}.bui-FieldError{color:var(--bui-fg-danger);font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);margin-top:var(--bui-space-2);display:inline-block}.bui-FieldLabelWrapper{margin-bottom:var(--bui-space-3);gap:var(--bui-space-1);flex-direction:column;display:flex}.bui-FieldLabel{color:var(--bui-fg-primary);cursor:pointer;font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);margin-right:auto}.bui-FieldSecondaryLabel{color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular);margin-left:var(--bui-space-1)}.bui-FieldDescription{font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);margin:0}.bui-Flex{min-width:0;display:flex}.bui-Grid{display:grid}.bui-HeaderToolbar{margin-bottom:var(--bui-space-6);&:before{content:"";background-color:var(--bui-bg);z-index:0;height:16px;position:absolute;top:0;left:0;right:0}&[data-has-tabs=true]{margin-bottom:0}}.bui-HeaderToolbarWrapper{z-index:1;background-color:var(--bui-bg-surface-1);padding-inline:var(--bui-space-5);border-bottom:1px solid var(--bui-border);color:var(--bui-fg-primary);flex-direction:row;justify-content:space-between;align-items:center;height:52px;display:flex;position:relative}.bui-HeaderToolbarContent{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-HeaderToolbarName{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-3);font-weight:var(--bui-font-weight-regular);flex-direction:row;flex-shrink:0;display:flex}.bui-HeaderToolbarIcon{width:16px;height:16px;color:var(--bui-fg-primary);& svg{width:100%;height:100%}}.bui-HeaderToolbarControls{right:var(--bui-space-5);align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.bui-HeaderTabsWrapper{margin-bottom:var(--bui-space-4);padding-inline:var(--bui-space-3);border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1)}.bui-HeaderPage{gap:var(--bui-space-1);margin-top:var(--bui-space-6);margin-bottom:var(--bui-space-6);flex-direction:column;display:flex}.bui-HeaderPageContent{flex-direction:row;justify-content:space-between;display:flex}.bui-HeaderPageTabsWrapper{margin-left:-8px}.bui-HeaderPageControls,.bui-HeaderPageBreadcrumbs{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:flex}.bui-Icon{width:1rem;height:1rem}.bui-Link{font-family:var(--bui-font-regular);cursor:pointer;margin:0;padding:0;text-decoration-line:none;display:inline-block;&:hover{text-underline-offset:calc(.025em + 2px);text-decoration-line:underline;text-decoration-style:solid;text-decoration-thickness:min(2px,max(1px,.05em));text-decoration-color:color-mix(in srgb,currentColor 30%,transparent)}}.bui-MenuPopover{border:1px solid var(--bui-border);border-radius:var(--bui-radius-2);background:var(--bui-bg-surface-1);color:var(--bui-fg-primary);outline:none;flex-direction:column;min-height:0;transition:transform .2s,opacity .2s;display:flex;overflow:hidden;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}&[data-placement=top]{--origin:translateY(8px)}&[data-placement=bottom]{--origin:translateY(-8px)}&[data-placement=right]{--origin:translateX(-8px)}&[data-placement=left]{--origin:translateX(8px)}}.bui-MenuContent{max-height:inherit;box-sizing:border-box;padding:var(--bui-space-1);outline:none;min-width:150px;overflow:auto}.bui-MenuPopover .bui-ScrollAreaRoot{flex-direction:column;flex:1;height:100%;min-height:0;display:flex}.bui-MenuPopover .bui-ScrollAreaScrollbar{margin-inline:var(--bui-space-1_5)}.bui-MenuItem{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&[data-focused],&[data-open]{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-color=danger]{color:var(--bui-fg-danger)}&[data-color=danger][data-focused]{background:var(--bui-bg-danger);color:var(--bui-fg-danger)}&[data-has-submenu]{&>.bui-MenuItemArrow{display:block}}}.bui-MenuItemListBox{height:2rem;padding-inline:var(--bui-space-2);border-radius:var(--bui-radius-2);cursor:default;color:var(--bui-fg-primary);font-size:var(--bui-font-size-3);justify-content:space-between;align-items:center;gap:var(--bui-space-6);outline:none;display:flex;&:hover{background:var(--bui-bg-surface-2);color:var(--bui-fg-primary)}&[data-selected] .bui-MenuItemListBoxCheck{&>svg{opacity:1;color:var(--bui-fg-primary)}}}.bui-MenuItemListBoxCheck{justify-content:center;align-items:center;width:1rem;height:1rem;display:flex;&>svg{opacity:0;width:1rem;height:1rem}}.bui-MenuItemContent{align-items:center;gap:var(--bui-space-2);display:flex;&>svg{width:1rem;height:1rem}}.bui-MenuItemArrow{width:1rem;height:1rem;display:none;&>svg{width:1rem;height:1rem}}.bui-MenuSection{&:first-child .bui-MenuSectionHeader{padding-top:0}}.bui-MenuSectionHeader{height:2rem;padding-top:var(--bui-space-3);padding-left:var(--bui-space-2);color:var(--bui-fg-primary);font-size:var(--bui-font-size-1);letter-spacing:.05rem;text-transform:uppercase;align-items:center;font-weight:700;display:flex}.bui-MenuSeparator{background:var(--bui-border);height:1px;margin-inline:var(--bui-space-1_5);margin-block:var(--bui-space-1)}.bui-MenuSearchField{font-family:var(--bui-font-regular);flex-shrink:0;width:100%;position:relative;&[data-empty]{& .bui-MenuSearchFieldClear{display:none}}}.bui-MenuSearchFieldInput{padding:0 var(--bui-space-3);border:none;border-bottom:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:2rem;cursor:inherit;outline:none;align-items:center;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}}.bui-MenuSearchFieldClear{right:var(--bui-space-2);cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;&>svg{width:1rem;height:1rem}}.bui-MenuEmptyState{padding:var(--bui-space-1);color:var(--bui-fg-secondary);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular)}.bui-Popover{background-color:var(--bui-bg-surface-1);border:1px solid var(--bui-border);border-radius:var(--bui-radius-3);padding-block:var(--bui-space-1);margin-right:12px;overflow:scroll;box-shadow:0 4px 12px #0000001a}.bui-RadioGroup{color:var(--bui-fg-primary);flex-direction:column;display:flex}.bui-RadioGroup[data-orientation=horizontal] .bui-RadioGroupContent{gap:var(--bui-space-4);flex-direction:row}.bui-RadioGroupContent{gap:var(--bui-space-2);flex-direction:column;display:flex}.bui-Radio{align-items:center;gap:var(--bui-space-2);font-size:var(--bui-font-size-2);color:var(--bui-fg-primary);forced-color-adjust:none;display:flex;position:relative;&:before{content:"";box-sizing:border-box;border:.125rem solid var(--bui-border);background:var(--bui-gray-1);border-radius:var(--bui-radius-full);width:1rem;height:1rem;transition:all .2s;display:block}&[data-pressed]:before{border-color:var(--bui-border)}&[data-selected]{&:before{border-color:var(--bui-bg-solid);border-width:.25rem}&[data-pressed]:before{border-color:var(--bui-bg-solid)}}&[data-focus-visible]:before{outline:2px solid var(--bui-ring);outline-offset:2px}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}&[data-invalid]:before,&[data-invalid][data-selected]:before{border-color:var(--bui-border-danger)}&[data-disabled][data-invalid]{color:var(--bui-fg-disabled);&:before{border-color:var(--bui-border-disabled);background:var(--bui-bg-disabled)}&[data-selected]:before{border-color:var(--bui-border-disabled)}}}.bui-Table{caption-side:bottom;border-collapse:collapse;width:100%}.bui-TableHeader{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out}.bui-TableHead{text-align:left;padding:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary)}.bui-TableHeadSortButton{cursor:pointer;user-select:none;align-items:center;gap:var(--bui-space-1);display:inline-flex;&:hover svg{opacity:.5}& svg{opacity:0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}&[data-sort-order=asc] svg{opacity:1;transform:rotate(0)}&[data-sort-order=desc] svg{opacity:1;transform:rotate(180deg)}}.bui-TableBody{color:var(--bui-fg-primary)}.bui-TableRow{border-bottom:1px solid var(--bui-border);transition:color .2s ease-in-out;&[data-react-aria-pressable=true]{cursor:pointer}}.bui-TableBody .bui-TableRow:hover{background-color:var(--bui-gray-2)}.bui-TableCell{padding:var(--bui-space-3);font-size:var(--bui-font-size-3);padding:var(--bui-space-3);font-size:var(--bui-font-size-3)}.bui-TableCellContentWrapper{align-items:center;gap:var(--bui-space-2);flex-direction:row;display:inline-flex}.bui-TableCellIcon,.bui-TableCellIcon svg{color:var(--bui-fg-primary);align-items:center;display:inline-flex}.bui-TableCellContent{gap:var(--bui-space-0_5);flex-direction:column;display:flex}.bui-TableCellProfile{gap:var(--bui-space-2);flex-direction:row;align-items:center;display:flex}.bui-TableCellProfileAvatar{vertical-align:middle;user-select:none;color:var(--bui-fg-primary);background-color:var(--bui-bg-surface-2);border-radius:100%;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;font-size:1rem;font-weight:500;line-height:1;display:inline-flex;overflow:hidden}.bui-TableCellProfileAvatarImage{object-fit:cover;width:100%;height:100%}.bui-TableCellProfileAvatarFallback{width:100%;height:100%;font-size:var(--bui-font-size-2);font-weight:var(--bui-font-weight-regular);box-shadow:inset 0 0 0 1px var(--bui-border);border-radius:var(--bui-radius-full);justify-content:center;align-items:center;display:flex}.bui-DataTablePagination{padding-top:var(--bui-space-5);justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--left{justify-content:space-between;align-items:center;display:flex}.bui-DataTablePagination--right{justify-content:space-between;align-items:center;gap:var(--bui-space-2);display:flex}.bui-DataTablePagination--select{min-width:10.5rem}.bui-Tabs{--active-tab-left:0px;--active-tab-right:0px;--active-tab-top:0px;--active-tab-bottom:0px;--active-tab-width:0px;--active-tab-height:0px;--active-transition-duration:0s;--hovered-tab-left:0px;--hovered-tab-right:0px;--hovered-tab-top:0px;--hovered-tab-bottom:0px;--hovered-tab-width:0px;--hovered-tab-height:0px;--hovered-tab-opacity:0;--hovered-transition-duration:0s}.bui-TabList{flex-direction:row;display:flex}.bui-TabListWrapper{position:relative}.bui-Tab{font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-secondary);cursor:pointer;z-index:2;height:36px;padding-inline:var(--bui-space-2);justify-content:center;align-items:center;display:flex;position:relative;&[data-selected=true]{color:var(--bui-fg-primary)}}.bui-TabActive{content:"";left:calc(var(--active-tab-left) + var(--bui-space-2));width:calc(var(--active-tab-width) - var(--bui-space-4));background-color:var(--bui-fg-primary);height:1px;transition:left var(--active-transition-duration)ease-out,opacity .15s ease-out,width var(--active-transition-duration)ease-out;opacity:1;border-radius:4px;position:absolute;bottom:-1px}.bui-TabHovered{content:"";left:var(--hovered-tab-left);top:calc(var(--hovered-tab-top) + 4px);width:var(--hovered-tab-width);height:calc(var(--hovered-tab-height) - 8px);background-color:var(--bui-gray-2);opacity:var(--hovered-tab-opacity);transition:left var(--hovered-transition-duration)ease-out,top var(--hovered-transition-duration)ease-out,width var(--hovered-transition-duration)ease-out,height var(--hovered-transition-duration)ease-out,opacity .15s ease-out;border-radius:4px;position:absolute}.bui-TabPanel{padding-inline:var(--bui-space-2);padding-top:var(--bui-space-4)}.bui-TagList{gap:var(--bui-space-2);flex-wrap:wrap;display:flex}.bui-Tag{color:var(--bui-fg-primary);background-color:var(--bui-gray-2);border-radius:var(--bui-radius-2);font-weight:var(--bui-font-weight-regular);justify-content:center;align-items:center;gap:var(--bui-space-1);box-sizing:border-box;transition-property:background-color,box-shadow,color;transition-duration:.2s;transition-timing-function:ease-in-out;display:flex}.bui-Tag[data-size=small]{height:26px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-1)}.bui-Tag[data-size=medium]{height:32px;padding:0 var(--bui-space-2);font-size:var(--bui-font-size-2)}.bui-Tag[data-hovered]{background-color:var(--bui-gray-3);cursor:pointer}.bui-Tag[data-focus-visible]{outline:2px solid var(--bui-ring);outline-offset:1px}.bui-Tag[data-selected]{box-shadow:inset 0 0 0 1px var(--bui-gray-8)}.bui-Tag[data-disabled]{color:var(--bui-fg-disabled);cursor:not-allowed}.bui-TagRemoveButton{cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;width:1rem;height:1rem;margin:0;padding:0}.bui-TagIcon{justify-content:center;align-items:center;transition:color .2s ease-in-out;display:flex;& svg{width:1rem;height:1rem}}.bui-Text{font-family:var(--bui-font-regular);margin:0;padding:0}.bui-Text[data-variant=title-large]{font-size:var(--bui-font-size-8);line-height:140%}.bui-Text[data-variant=title-medium]{font-size:var(--bui-font-size-7);line-height:140%}.bui-Text[data-variant=title-small]{font-size:var(--bui-font-size-6);line-height:140%}.bui-Text[data-variant=title-x-small]{font-size:var(--bui-font-size-5);line-height:140%}.bui-Text[data-variant=body-large]{font-size:var(--bui-font-size-4);line-height:140%}.bui-Text[data-variant=body-medium]{font-size:var(--bui-font-size-3);line-height:140%}.bui-Text[data-variant=body-small]{font-size:var(--bui-font-size-2);line-height:140%}.bui-Text[data-variant=body-x-small]{font-size:var(--bui-font-size-1);line-height:140%}.bui-Text[data-weight=regular]{font-weight:var(--bui-font-weight-regular)}.bui-Text[data-weight=bold]{font-weight:var(--bui-font-weight-bold)}.bui-Text[data-color=primary]{color:var(--bui-fg-primary)}.bui-Text[data-color=secondary]{color:var(--bui-fg-secondary)}.bui-Text[data-color=danger]{color:var(--bui-fg-danger)}.bui-Text[data-color=warning]{color:var(--bui-fg-warning)}.bui-Text[data-color=success]{color:var(--bui-fg-success)}.bui-Text[data-truncate]{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.bui-Text[data-as=span],.bui-Text[data-as=label],.bui-Text[data-as=strong],.bui-Text[data-as=em],.bui-Text[data-as=small]{display:inline-block}.bui-TextField{font-family:var(--bui-font-regular);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputWrapper{position:relative;&[data-size=small] .bui-Input{height:2rem}&[data-size=medium] .bui-Input{height:2.5rem}&[data-size=small] .bui-Input[data-icon]{padding-left:var(--bui-space-8)}&[data-size=medium] .bui-Input[data-icon]{padding-left:var(--bui-space-9)}}.bui-InputIcon{left:var(--bui-space-3);margin-right:var(--bui-space-1);color:var(--bui-fg-primary);pointer-events:none;flex-shrink:0;transition:left .2s ease-in-out;position:absolute;top:50%;transform:translateY(-50%);&[data-size=small],&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium],&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-Input{padding:0 var(--bui-space-3);border-radius:var(--bui-radius-2);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);width:100%;height:100%;cursor:inherit;align-items:center;transition:border-color .2s ease-in-out,outline-color .2s ease-in-out;display:flex;&::-webkit-search-cancel-button,&::-webkit-search-decoration{-webkit-appearance:none}&::placeholder{color:var(--bui-fg-secondary)}&[data-focused]{outline-color:var(--bui-border-pressed);outline-width:0}&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-disabled]{opacity:.5;cursor:not-allowed;border:1px solid var(--bui-border-disabled)}}.bui-PasswordField{font-family:var(--bui-font-regular);--bui-passwordmanager-icon-width:var(--bui-space-1);flex-direction:column;flex-shrink:0;width:100%;display:flex}.bui-InputVisibility{right:var(--bui-passwordmanager-icon-width);cursor:pointer;color:var(--bui-fg-primary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;display:flex;position:absolute;top:0;bottom:0;&[data-size=small]{width:2rem;height:2rem}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium]{width:2.5rem;height:2.5rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}}.bui-PasswordField .bui-InputWrapper[data-size=small] .bui-Input{padding-right:calc(2rem + var(--bui-passwordmanager-icon-width))}.bui-PasswordField .bui-InputWrapper[data-size=medium] .bui-Input{padding-right:calc(2.5rem + var(--bui-passwordmanager-icon-width))}.bui-SearchField{flex:1 0;&[data-empty]{& .bui-InputClear{display:none}}&[data-start-collapsed=true]{flex:0 auto;padding:0;transition:flex-basis .3s ease-in-out;&[data-collapsed=true]{flex-basis:200px}&[data-collapsed=false]{cursor:pointer;&[data-size=medium]{flex-basis:2.5rem;height:2.5rem}&[data-size=small]{flex-basis:2rem;height:2rem}&[data-size=medium] .bui-Input{&::placeholder{opacity:0}}&[data-size=small] .bui-Input{&::placeholder{opacity:0}}& .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:0}}}}}.bui-SearchField .bui-Input{transition:padding .3s ease-in-out,border-color .2s ease-in-out,outline-color .2s ease-in-out;&[data-hovered]{border-color:var(--bui-border-hover)}&[data-focused]{border-color:var(--bui-border-pressed);outline-width:0}}.bui-SearchField .bui-InputWrapper{& .bui-Input[data-icon]{padding-right:var(--bui-space-6)}}.bui-SearchField .bui-InputIcon{justify-content:center;display:flex;left:0;&[data-size=small]{width:var(--bui-space-8)}&[data-size=medium]{width:var(--bui-space-10)}}.bui-InputClear{cursor:pointer;color:var(--bui-fg-secondary);background-color:#0000;border:none;justify-content:center;align-items:center;margin:0;padding:0;transition:color .2s ease-in-out;display:flex;position:absolute;top:0;bottom:0;right:0}.bui-InputClear:hover{color:var(--bui-fg-primary)}.bui-InputClear[data-size=small]{width:2rem;height:2rem}.bui-InputClear[data-size=medium]{width:2.5rem;height:2.5rem}.bui-InputClear svg{width:1rem;height:1rem}.bui-Skeleton{animation:var(--bui-animate-pulse);background-color:var(--bui-bg-surface-2);border-radius:var(--bui-radius-2)}.bui-Skeleton[data-rounded=true]{border-radius:var(--bui-radius-full)}.bui-Tooltip{background:var(--bui-bg-surface-1);border:1px solid var(--bui-gray-3);forced-color-adjust:none;padding:var(--bui-space-2)var(--bui-space-3);max-width:240px;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);color:var(--bui-fg-primary);border-radius:4px;outline:none;transition:transform .2s,opacity .2s;transform:translate(0,0);box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;&[data-entering],&[data-exiting]{transform:var(--origin);opacity:0}--tooltip-offset:var(--bui-space-3)&[data-placement=top]{margin-bottom:var(--tooltip-offset);--origin:translateY(4px)}&[data-placement=right]{margin-left:var(--tooltip-offset);--origin:translateX(-4px)}&[data-placement=bottom]{margin-top:var(--tooltip-offset);--origin:translateY(-4px)}&[data-placement=left]{margin-right:var(--tooltip-offset);--origin:translateX(4px)}}.bui-TooltipArrow{& svg{display:block;& path:first-child{fill:var(--bui-bg-surface-1)}& path:nth-child(2){fill:var(--bui-gray-3)}--tooltip-arrow-overlap:-2px}&[data-placement=top] svg{margin-top:var(--tooltip-arrow-overlap)}&[data-placement=bottom] svg{margin-bottom:var(--tooltip-arrow-overlap);transform:rotate(180deg)}&[data-placement=right] svg{margin-right:var(--tooltip-arrow-overlap);transform:rotate(90deg)}&[data-placement=left] svg{margin-left:var(--tooltip-arrow-overlap);transform:rotate(-90deg)}}[data-theme=dark]{& .bui-Tooltip{background:var(--bui-bg-surface-2);box-shadow:none;border:1px solid var(--bui-gray-4)}& .bui-TooltipArrow{& svg path:first-child{fill:var(--bui-bg-surface-2)}& svg path:nth-child(2){fill:var(--bui-gray-4)}}}.bui-ScrollAreaRoot{box-sizing:border-box;width:100%}.bui-ScrollAreaViewport{overscroll-behavior:contain;height:100%}.bui-ScrollAreaContent{padding-block:.75rem;flex-direction:column;gap:1rem;padding-left:1rem;padding-right:1.5rem;display:flex}.bui-ScrollAreaScrollbar{background-color:var(--bui-scrollbar);opacity:0;border-radius:.375rem;justify-content:center;width:.25rem;margin:.5rem;transition:opacity .15s .3s;display:flex;&[data-hovering],&[data-scrolling]{opacity:1;transition-duration:75ms;transition-delay:0s}&:before{content:"";width:1.25rem;height:100%;position:absolute}}.bui-ScrollAreaThumb{border-radius:inherit;background-color:var(--bui-scrollbar-thumb);width:100%}.bui-Select[data-invalid]{& .bui-SelectTrigger{border-color:var(--bui-fg-danger)}}.bui-SelectTrigger{box-sizing:border-box;border-radius:var(--bui-radius-3);border:1px solid var(--bui-border);background-color:var(--bui-bg-surface-1);cursor:pointer;justify-content:space-between;align-items:center;gap:var(--bui-space-2);width:100%;display:flex;& svg{color:var(--bui-fg-secondary);flex-shrink:0}&[data-size=small]{height:2rem;padding-inline:var(--bui-space-3)}&[data-size=medium]{height:3rem;padding-inline:var(--bui-space-4)}&[data-size=small] svg{width:1rem;height:1rem}&[data-size=medium] svg{width:1.25rem;height:1.25rem}&::placeholder{color:var(--bui-fg-secondary)}&:hover{border-color:var(--bui-border-hover);transition:border-color .2s ease-in-out,outline-color .2s ease-in-out}&:focus-visible{border-color:var(--bui-border-pressed);outline:0}&[data-invalid]{border-color:var(--bui-fg-danger)}&[data-invalid]:hover,&[data-invalid]:focus-visible{border-width:2px}&[disabled]{cursor:not-allowed;border-color:var(--bui-border-disabled);color:var(--bui-fg-disabled)}&[disabled] .bui-SelectValue{color:var(--bui-fg-disabled)}&[data-popup-open] .bui-SelectIcon{transform:rotate(180deg)}}.bui-SelectValue{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:var(--bui-font-size-3);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);color:var(--bui-fg-primary);text-align:left;overflow:hidden;& .bui-SelectItemIndicator{display:none}&[disabled]{color:var(--bui-fg-disabled)}}.bui-SelectItem{width:var(--anchor-width);padding-block:var(--bui-space-2);padding-left:var(--bui-space-3);padding-right:var(--bui-space-4);color:var(--bui-fg-primary);border-radius:var(--bui-radius-3);cursor:pointer;user-select:none;font-size:var(--bui-font-size-3);align-items:center;gap:var(--bui-space-1);outline:none;grid-template-columns:1rem 1fr;grid-template-areas:"icon text";display:grid;position:relative;&[data-focused]{z-index:0;color:var(--bui-fg-primary);position:relative}&[data-focused]:before{content:"";z-index:-1;background-color:var(--bui-bg-tint-hover);border-radius:.25rem;position:absolute;inset-block:0;inset-inline:.25rem}&[data-disabled]{cursor:not-allowed;color:var(--bui-fg-disabled)}&[data-selected] .bui-SelectItemIndicator{opacity:1}}.bui-SelectItemIndicator{opacity:0;grid-area:icon;justify-content:center;align-items:center;transition:opacity .2s ease-in-out;display:flex}.bui-SelectItemLabel{flex:1;grid-area:text}.bui-Switch{align-items:center;gap:var(--bui-space-3);font-size:var(--bui-font-size-3);color:var(--bui-fg-primary);cursor:pointer;display:flex;position:relative;&[data-pressed] .bui-SwitchIndicator{&:before{background:var(--bui-fg-solid)}}&[data-selected]{& .bui-SwitchIndicator{background:var(--bui-bg-solid);&:before{background:var(--bui-fg-solid);transform:translate(100%)}}&[data-pressed]{& .indicator{background:var(--bui-gray-3)}}}&[data-focus-visible] .bui-SwitchIndicator{outline-offset:2px;outline:2px solid}}.bui-SwitchIndicator{background:var(--bui-gray-3);border:2px;border-radius:1.143rem;width:2rem;height:1.143rem;transition:all .2s;&:before{content:"";background:var(--bui-fg-solid);border-radius:16px;width:.857rem;height:.857rem;margin:.143rem;transition:all .2s;display:block}} \ No newline at end of file diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx index f31cf4524e..ffd3bc9dcb 100644 --- a/docs-ui/src/app/page.mdx +++ b/docs-ui/src/app/page.mdx @@ -85,6 +85,11 @@ building it incrementally with not conflict with the existing theming system. description="A search field component to help you search for items." href="/components/search-field" /> + - --bui-font-mono + --bui-font-monospace The monospace font for the theme. diff --git a/docs-ui/src/content/components/grid.props.ts b/docs-ui/src/content/components/grid.props.ts index 34d89785cc..6f037cd64f 100644 --- a/docs-ui/src/content/components/grid.props.ts +++ b/docs-ui/src/content/components/grid.props.ts @@ -43,12 +43,12 @@ export const gridItemPropDefs: Record = { values: [...columnsValues, 'full'], responsive: true, }, - start: { + colStart: { type: 'enum | string', values: [...columnsValues, 'auto'], responsive: true, }, - end: { + colEnd: { type: 'enum | string', values: [...columnsValues, 'auto'], responsive: true, @@ -60,7 +60,7 @@ export const gridItemPropDefs: Record = { export const gridUsageSnippet = `import { Grid } from '@backstage/ui'; -`; +`; export const gridDefaultSnippet = ` @@ -68,44 +68,44 @@ export const gridDefaultSnippet = ` `; -export const gridSimpleSnippet = ` +export const gridSimpleSnippet = ` Hello World Hello World Hello World -`; +`; -export const gridComplexSnippet = ` - - Hello World +export const gridComplexSnippet = ` + + Hello World - - Hello World + + Hello World -`; +`; -export const gridMixingRowsSnippet = ` - - Hello World +export const gridMixingRowsSnippet = ` + + Hello World - - Hello World + + Hello World - - Hello World + + Hello World -`; +`; -export const gridResponsiveSnippet = ` +export const gridResponsiveSnippet = ` - Hello World + - Hello World + Hello World -`; +`; -export const gridStartEndSnippet = ` - - Hello World +export const gridStartEndSnippet = ` + + Hello World -`; +`; diff --git a/docs-ui/src/content/components/password-field.mdx b/docs-ui/src/content/components/password-field.mdx new file mode 100644 index 0000000000..14fbc9e34d --- /dev/null +++ b/docs-ui/src/content/components/password-field.mdx @@ -0,0 +1,64 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { PasswordFieldSnippet } from '@/snippets/stories-snippets'; +import { + inputPropDefs, + passwordFieldUsageSnippet, + passwordFieldDefaultSnippet, + passwordFieldSizesSnippet, + passwordFieldDescriptionSnippet, +} from './password-field.props'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; + + + +} + code={passwordFieldDefaultSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Sizes + +We support two different sizes: `small`, `medium`. + +} + code={passwordFieldSizesSnippet} +/> + +### With description + +Here's a simple PasswordField with a description. + +} + code={passwordFieldDescriptionSnippet} +/> + + + + diff --git a/docs-ui/src/content/components/password-field.props.ts b/docs-ui/src/content/components/password-field.props.ts new file mode 100644 index 0000000000..493405b5fd --- /dev/null +++ b/docs-ui/src/content/components/password-field.props.ts @@ -0,0 +1,43 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const inputPropDefs: Record = { + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + }, + label: { + type: 'string', + }, + icon: { + type: 'enum', + values: ['ReactNode'], + }, + description: { + type: 'string', + }, + name: { + type: 'string', + required: true, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui'; + +`; + +export const passwordFieldDefaultSnippet = ``; + +export const passwordFieldSizesSnippet = ` + } /> + } /> +`; + +export const passwordFieldDescriptionSnippet = ``; diff --git a/docs-ui/src/content/components/table.props.ts b/docs-ui/src/content/components/table.props.ts index 1b40055849..d46c6b8ea7 100644 --- a/docs-ui/src/content/components/table.props.ts +++ b/docs-ui/src/content/components/table.props.ts @@ -212,6 +212,11 @@ export const cellPropDefs: Record = { description: "A string representation of the cell's contents, used for features like typeahead.", }, + leadingIcon: { + type: 'enum', + values: ['ReactNode'], + description: 'Optional icon to display before the cell content.', + }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx index 5aa5b8c7d9..5bba3384b5 100644 --- a/docs-ui/src/snippets/stories-snippets.tsx +++ b/docs-ui/src/snippets/stories-snippets.tsx @@ -28,6 +28,7 @@ import * as HeaderStories from '../../../packages/ui/src/components/Header/Heade import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories'; import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories'; import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories'; +import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories'; // Helper function to create snippet components // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -53,6 +54,8 @@ export const ContainerSnippet = createSnippetComponent(ContainerStories); export const GridSnippet = createSnippetComponent(GridStories); export const IconSnippet = createSnippetComponent(IconStories); export const TextFieldSnippet = createSnippetComponent(TextFieldStories); +export const PasswordFieldSnippet = + createSnippetComponent(PasswordFieldStories); export const TextSnippet = createSnippetComponent(TextStories); export const FlexSnippet = createSnippetComponent(FlexStories); export const SelectSnippet = createSnippetComponent(SelectStories); diff --git a/docs-ui/src/utils/changelog.ts b/docs-ui/src/utils/changelog.ts index a66997cf26..ef2e6c9fb9 100644 --- a/docs-ui/src/utils/changelog.ts +++ b/docs-ui/src/utils/changelog.ts @@ -25,7 +25,8 @@ export type Component = | 'radio-group' | 'card' | 'skeleton' - | 'header'; + | 'header' + | 'passwordfield'; export type Version = `${number}.${number}.${number}`; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index 5e7305da28..c1a69e459b 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -126,6 +126,11 @@ export const components: Page[] = [ slug: 'menu', status: 'alpha', }, + { + title: 'PasswordField', + slug: 'password-field', + status: 'alpha', + }, { title: 'RadioGroup', slug: 'radio-group', diff --git a/docs/assets/architecture-overview/backstage-front-back-arch.jpeg b/docs/assets/architecture-overview/backstage-front-back-arch.jpeg new file mode 100644 index 0000000000..1cca115c95 Binary files /dev/null and b/docs/assets/architecture-overview/backstage-front-back-arch.jpeg differ diff --git a/docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg new file mode 100644 index 0000000000..e8f5144c9a Binary files /dev/null and b/docs/assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg differ diff --git a/docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg new file mode 100644 index 0000000000..bf96367b17 Binary files /dev/null and b/docs/assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg differ diff --git a/docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg b/docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg new file mode 100644 index 0000000000..4b9fd1d35f Binary files /dev/null and b/docs/assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg differ diff --git a/docs/assets/technical-overview/backstage-ui-group-ownership.png b/docs/assets/technical-overview/backstage-ui-group-ownership.png new file mode 100644 index 0000000000..607aa7ad47 Binary files /dev/null and b/docs/assets/technical-overview/backstage-ui-group-ownership.png differ diff --git a/docs/assets/user-interface/css-classname-structure.png b/docs/assets/user-interface/css-classname-structure.png new file mode 100644 index 0000000000..c241b182cc Binary files /dev/null and b/docs/assets/user-interface/css-classname-structure.png differ diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index d35aed364d..1423fa92f7 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -414,9 +414,13 @@ Each entry has one or more of the following fields: ## Adding custom or logic for validation and issuing of tokens -The `pluginTokenHandlerDecoratorServiceRef` can be used to decorate the existing token handler without having to re-implement the entire `AuthService` implementation. +The `pluginTokenHandlerDecoratorServiceRef` and `externalTokenHandlersServiceRef` can be used to extend the existing token handler without having to re-implement the entire `AuthService` implementation. This is particularly useful when you want to add additional logic to the handler, such as logging or metrics or custom token validation. +### PluginTokenHandler decoration + +The `pluginTokenHandlerDecoratorServiceRef` can be used to decorate the default PluginTokenHandler used for create and verify tokens from plugins. + The `PluginTokenHandler` interface has two methods: - `issueToken`: This method is used to issue a token for a plugin. It takes in the `pluginId` and `targetPluginId` as arguments, and an optional `limitedUserToken` object which can be used to issue a token on behalf of another user. The method returns a promise that resolves to an object containing the issued token. @@ -439,3 +443,109 @@ const decoratedPluginTokenHandler = createServiceFactory({ }, }); ``` + +### Adding custom ExternalTokenHandler + +The `externalTokenHandlersServiceRef` can be used to add custom external token handlers to the default implementation. + +Your service factory must return an object with a `type` property that matches the token type in your configuration (e.g., 'custom', 'api-key'). When Backstage encounters tokens of this type, it calls your `initialize` method with all the configuration entries that match this type. Your factory can return either a single token handler or an array of handlers to process and validate these tokens. + +:::note Note + +During token verification, all the token handlers are tested. Consider this when adding many token handlers, as it may impact performance. + +::: + +For example, if we want to add a custom external token handler for the `custom` type: + +our config would look like this: + +```yaml title="in e.g. app-config.production.yaml" +backend: + auth: + externalAccess: + - type: custom + options: + customOptions: additional-value + accessRestrictions: + - plugin: events + - type: custom + options: + customOptions: another-value + accessRestrictions: + - plugin: events +``` + +And we can implement the custom token handler like this: + +```ts +import { + ExternalTokenHandler, + externalTokenHandlersServiceRef, + createExternalTokenHandler, +} from '@backstage/backend-defaults/auth'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; + +const customExternalTokenHandlers = createServiceFactory({ + service: externalTokenHandlersServiceRef, + deps: {}, + async factory() { + return createExternalTokenHandler({ + type: 'custom', + initialize({ options }) { + // Initialize your handler context from config + const customOptions = options.getString('customOptions'); + return { customOptions }; + }, + async verifyToken(token, context) { + // Your custom token validation logic here + // Return undefined if token is invalid + // Return { subject: 'your-subject' } if token is valid + + if (token === 'valid-token') { + return { subject: `custom:${context.customOptions}` }; + } + return undefined; + }, + }); + }, +}); +``` + +The `createExternalTokenHandler` helper simplifies creating external token handlers with the new API: + +- **`type`**: A string identifier for your token handler type that matches the config +- **`initialize`**: Called once for each config entry of this type, receives the config options and returns a context object that will be passed to `verifyToken` +- **`verifyToken`**: Called for each token verification with the token and context, returns the subject if valid or `undefined` if not + +```ts +// Example of a more complex handler with external API call +const apiTokenHandler = createExternalTokenHandler({ + type: 'api-validation', + initialize({ options }) { + const apiBaseUrl = options.getString('apiBaseUrl'); + const apiKey = options.getString('apiKey'); + return { apiBaseUrl, apiKey }; + }, + async verifyToken(token, { apiBaseUrl, apiKey }) { + try { + const response = await fetch(`${apiBaseUrl}/validate-token`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token }), + }); + + if (response.ok) { + const { userId } = await response.json(); + return { subject: `api:${userId}` }; + } + } catch (error) { + // Log error but don't throw - return undefined for invalid tokens + } + return undefined; + }, +}); +``` diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index cfcd606d92..022aab202b 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -223,6 +223,35 @@ export const customFooServiceFactory = createServiceFactory({ This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. +## Multiton + +By default the service reference will point to a singleton instance of the service. This mean if a new service factory uses this reference it will override the previous one. This is the most common use-case, but in some cases you may want to have multiple instances of the same service. +For some services, it is desirable to extend the functionality instead of overriding it. For example, some services could have many handlers to address specific events, and you may want to add a new handler instead of overriding the previous one. In this case, you can use the `multiton` option when creating the service reference: + +```ts +// example-service-ref.ts +import { createServiceRef } from '@backstage/backend-plugin-api'; + +export interface FooService { + foo(options: FooOptions): Promise; +} + +export const fooServiceRef = createServiceRef({ + id: 'example.foo', + multiton: true, // this service ref will be an array of instances +}); +``` + +When adding this `serviceRef` as a dependency to a factory, the factory will receive an array of instances instead of a single instance: + +```ts +deps: {fooServices: fooServiceRef}, + factory(fooServices) { + // fooServices is an array of instances + return new Bar(fooServices); + }, +``` + ## Service Factory Options Pattern :::note Note diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 5e065152a0..6de394f549 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -134,7 +134,13 @@ import { createAuthIntegrationRouter, createRateLimitMiddleware, } from '@backstage/backend-defaults/httpRouter'; -import { createServiceFactory } from '@backstage/backend-plugin-api'; +import PromiseRouter from 'express-promise-router'; +import { Handler } from 'express'; +import { + createServiceFactory, + coreServices, + HttpRouterServiceAuthPolicy, +} from '@backstage/backend-plugin-api'; const backend = createBackend(); diff --git a/docs/conf/user-interface/icons.md b/docs/conf/user-interface/icons.md new file mode 100644 index 0000000000..26a6f11096 --- /dev/null +++ b/docs/conf/user-interface/icons.md @@ -0,0 +1,124 @@ +--- +id: icons +title: Customizing Icons +sidebar_label: Icons +description: Customizing Icons +--- + +So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons + +## Custom Icons + +You can also customize the Project's _default_ icons. + +You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx). + +### Requirements + +- Files in `.svg` format +- React components created for the icons + +### Create React Component + +In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `customIcons.tsx` file. + +```tsx title="customIcons.tsx" +import { SvgIcon, SvgIconProps } from '@material-ui/core'; + +export const ExampleIcon = (props: SvgIconProps) => ( + + + +); +``` + +### Using the custom icon + +Supply your custom icon in `packages/app/src/App.tsx` + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-next-line */ +import { ExampleIcon } from './assets/icons/CustomIcons' + + +const app = createApp({ + apis, + components: { + {/* ... */} + }, + themes: [ + {/* ... */} + ], + /* highlight-add-start */ + icons: { + github: ExampleIcon, + }, + /* highlight-add-end */ + bindRoutes({ bind }) { + {/* ... */} + } +}) +``` + +## Adding Icons + +You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that: + +1. First you will want to open your `App.tsx` in `/packages/app/src` +2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';` +3. Next you want to add the icon like this to your `createApp`: + + ```tsx title="packages/app/src/App.tsx" + const app = createApp({ + apis: ..., + plugins: ..., + /* highlight-add-start */ + icons: { + alert: AlarmIcon, + }, + /* highlight-add-end */ + themes: ..., + components: ..., + }); + ``` + +4. Now we can reference `alert` for our icon in our entity links like this: + + ```yaml + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: artist-lookup + description: Artist Lookup + links: + - url: https://example.com/alert + title: Alerts + icon: alert + ``` + + And this is the result: + + ![Example Link with Alert icon](../../assets/getting-started/add-icons-links-example.png) + + Another way you can use these icons is from the `AppContext` like this: + + ```ts + import { useApp } from '@backstage/core-plugin-api'; + + const app = useApp(); + const alertIcon = app.getSystemIcon('alert'); + ``` + + You might want to use this method if you have an icon you want to use in several locations. + +:::note Note + +If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon` + +::: diff --git a/docs/conf/user-interface/index.md b/docs/conf/user-interface/index.md new file mode 100644 index 0000000000..81acfdf720 --- /dev/null +++ b/docs/conf/user-interface/index.md @@ -0,0 +1,621 @@ +--- +id: index +title: Customizing Your App's UI +sidebar_label: Introduction +description: Learn how to customize the look and feel of your Backstage app, including theming and branding options. +--- + +Backstage offers built-in support for both light and dark themes, making it easy to get started with a professional look and feel. But many teams want to go further—tailoring the interface to reflect their organization’s unique brand, identity, and experience. + +This section explores the different ways you can customize the appearance of your Backstage instance. You'll learn how the theming system is structured today, how to work with the two coexisting UI systems, and how to define themes that align with your visual language. + +## Theming architecture overview + +Backstage currently supports two parallel UI systems. The original theming and component model is built on Material UI (MUI), a popular React-based framework. More recently, Backstage introduced Backstage UI (BUI), a custom-designed, CSS-first system developed to meet the platform’s evolving needs. Both systems are supported today, with many parts of the ecosystem still using MUI while new components adopt BUI. + +
+
+

MUI (Legacy)

+
    +
  • Theming: JS-based with UnifiedThemeProvider
  • +
  • Coverage: Most existing plugins
  • +
  • Documentation: mui.com
  • +
+
+
+

Backstage UI (New)

+
    +
  • Theming: CSS variables and tokens
  • +
  • Coverage: Growing, focused on new work
  • +
  • Documentation: ui.backstage.io
  • +
+
+
+ +:::info +We recognize that maintaining two separate theming systems is not ideal. Because of the fundamental architectural differences between MUI and Backstage UI, it can be challenging to automate theme updates or know exactly which theme to modify for a given component. Our recommendation is to inspect the component’s code and check its class names: if you see a class name starting with `bui`, you should use the Backstage UI theming approach to style it. +::: + +## Creating custom themes + +During the transition to Backstage UI, you will need to maintain themes in two places: some components and plugins still rely on MUI, while others use Backstage UI. We are working on a plugin that will make help you convert your existing MUI theme into a Backstage UI CSS file you can add to your application. We'll update this page when the plugin is available but for now you can follow progress on this PR [#31140](https://github.com/backstage/backstage/pull/31140). + +```tsx title="packages/app/src/App.tsx" +/* highlight-add-start */ +import { lightTheme, darkTheme } from './themes'; // MUI themes +import './styles.css'; // Backstage UI (BUI) theme +/* highlight-add-end */ + +const app = createApp({ + apis, + components, + /* highlight-add-start */ + themes: [ + { + id: 'light', + title: 'Light theme', + variant: 'light', + icon: , + Provider: ({ children }) => ( + + ), + }, + { + id: 'dark', + title: 'Dark theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + ), + }, + ], + /* highlight-add-end */ +}); +``` + +| Name | Description | +| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | Each theme has a unique `id` | +| `title` | This will be shown in the settings page to select the right theme. | +| `variant` | This can be either `light` or `dark`. This is also referred to as `mode`. On the `body` of your app we are inserting a data attribute to set the theme based on this value: `data-theme-mode="light"`. | +| `icon` | This will be shown in the settings page as a visual element to complement the title. | +| `Provider` | This is needed to set the legacy theme with MUI only. This will be become redundant later on when we fully replace with BUI but for now you need to have it for MUI to work. BUI is based on CSS and don't rely on any global providers. | + +:::note +Your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). Be sure to provide both `light` and `dark` modes so users can choose their preference. +::: + +## Create a theme for Backstage UI (New) + +Backstage UI is built entirely using CSS. By default we are providing a default theme that include all our core CSS variables and component styles. To start customising Backstage UI to match your brand you need to create a new CSS file and import it directly in `packages/app/src/App.tsx`. All styles declared in this file will override the default styles. As your file grow you can organise it the way you want or even import multiple files. + +Backstage UI is using light by default under `:root` but you can target it more specifically using the data attribute for mode + +```css title="packages/app/src/styles.css" +:root { + /* Use :root to set styles for both light and dark themes */ + .bui-Button { + background-color: #000; + color: #fff; + } +} + +[data-theme-mode='light'] { + /* Light theme specific styles */ +  --bui-bg: #f8f8f8; + --bui-fg-primary: #000; +} + +[data-theme-mode='dark'] { + /* Dark theme specific styles */ +  --bui-bg: #333333; + --bui-fg-primary: #fff; +} +``` + +### CSS variables + +By adjusting just a few theme variables, you can easily transform the look and feel of your Backstage instance to align with your brand identity. All colors are defined using these variables, ensuring they adapt seamlessly to both light and dark modes. + +We recommend starting with a core set of CSS variables to quickly achieve a branded experience. You’ll also find a complete list of available variables below, giving you full flexibility to fine-tune the design to your needs. + +And if you’d like to go even further, you can target specific component class names for advanced customization. + +| Token Name | Description | +| -------------------- | ----------------------------------------------------------------------------------- | +| `--bui-bg` | This is used to define the background color of your app. It will only be used once. | +| `--bui-bg-surface-1` | We ar using this color to sit on top of `--bui-bg` mostly for `Card`, `Dialog`, ... | +| `--bui-bg-surface-2` | This is for content inside elevated components. This colour is less common. | +| `--bui-bg-solid` | This is used for main actions like primary buttons. | +| `--bui-fg-solid` | This is for texts or icons on top of a solid backgrounds. | +| `--bui-fg-primary` | Your primary text or icon colours. | +| `--bui-fg-secondary` | Your secondary text or icon colours. | +| `--bui-fg-link` | Used for links. | +| `--bui-border` | Main borders around surfaces like `Card`, `Dialog`, ... | +| `--bui-font-regular` | The main font of your app. | + +
+ All available CSS variables + +#### Base colors + +These colors are used for special purposes like ring, scrollbar, ... + +| Token Name | Description | +| -------------- | ----------------------------------------------------------------------- | +| `--bui-black` | Pure black color. This one should be the same in light and dark themes. | +| `--bui-white` | Pure white color. This one should be the same in light and dark themes. | +| `--bui-gray-1` | You can use these mostly for backgrounds colors. | +| `--bui-gray-2` | You can use these mostly for backgrounds colors. | +| `--bui-gray-3` | You can use these mostly for backgrounds colors. | +| `--bui-gray-4` | You can use these mostly for backgrounds colors. | +| `--bui-gray-5` | You can use these mostly for backgrounds colors. | +| `--bui-gray-6` | You can use these mostly for backgrounds colors. | +| `--bui-gray-7` | You can use these mostly for backgrounds colors. | +| `--bui-gray-8` | You can use these mostly for backgrounds colors. | + +#### Core background colors + +These colors are used for the background of your application. We are mostly using for now a single elevated background for panels. `--bui-bg` should mostly use as the main background color of your app. + +| Token Name | Description | +| ------------------------- | ------------------------------------------------ | +| `--bui-bg` | The background color of your Backstage instance. | +| `--bui-bg-surface-1` | Use for any panels or elevated surfaces. | +| `--bui-bg-surface-2` | Use for any panels or elevated surfaces. | +| `--bui-bg-solid` | Used for solid background colors. | +| `--bui-bg-solid-hover` | Used for solid background colors when hovered. | +| `--bui-bg-solid-pressed` | Used for solid background colors when pressed. | +| `--bui-bg-solid-disabled` | Used for solid background colors when disabled. | +| `--bui-bg-tint` | Used for tint background colors. | +| `--bui-bg-tint-hover` | Used for tint background colors when hovered. | +| `--bui-bg-tint-focus` | Used for tint background colors when active. | +| `--bui-bg-tint-disabled` | Used for tint background colors when disabled. | +| `--bui-bg-danger` | Used to show errors information. | +| `--bui-bg-warning` | Used to show warnings information. | +| `--bui-bg-success` | Used to show success information. | + +#### Foreground colors + +Foreground colours are meant to work in pair with a background colours. Typically this would work for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors are prefixed with `fg` to make it easier to identify. + +| Token Name | Description | +| ------------------------ | ----------------------------------------------------------------- | +| `--bui-fg-primary` | It should be used on top of main background surfaces. | +| `--bui-fg-secondary` | It should be used on top of main background surfaces. | +| `--bui-fg-link` | It should be used on top of main background surfaces. | +| `--bui-fg-link-hover` | It should be used on top of main background surfaces. | +| `--bui-fg-disabled` | It should be used on top of main background surfaces. | +| `--bui-fg-solid` | It should be used on top of solid background colors. | +| `--bui-fg-tint` | It should be used on top of tint background colors. | +| `--bui-fg-tint-disabled` | It should be used on top of tint background colors when disabled. | +| `--bui-fg-danger` | It should be used on top of danger background colors. | +| `--bui-fg-warning` | It should be used on top of warning background colors. | +| `--bui-fg-success` | It should be used on top of success background colors. | + +#### Border colors + +These border colors are mostly meant to be used as borders on top of any components with low contrast to help as a separator with the different background colors. + +| Token Name | Description | +| ----------------------- | --------------------------------------------------- | +| `--bui-border` | It should be used on top of `--bui-bg-surface-1`. | +| `--bui-border-hover` | Used when the component is interactive and hovered. | +| `--bui-border-pressed` | Used when the component is interactive and hovered. | +| `--bui-border-disabled` | Used when the component is disabled. | +| `--bui-border-danger` | It should be used on top of `--bui-bg-danger`. | +| `--bui-border-warning` | It should be used on top of `--bui-bg-warning`. | +| `--bui-border-success` | It should be used on top of `--bui-bg-success`. | + +#### Special colors + +These colors are used for special purposes like ring, scrollbar, ... + +| Token Name | Description | +| ----------------------- | --------------------------------- | +| `--bui-ring` | The color of the ring. | +| `--bui-scrollbar` | The color of the scrollbar. | +| `--bui-scrollbar-thumb` | The color of the scrollbar thumb. | + +#### Font families + +We have two fonts that we use across Backstage UI. The first one is the sans-serif font that we use for the body of the application. The second one is the monospace font that we use for code blocks and tables. + +| Token Name | Description | +| -------------------- | ---------------------------------- | +| `--bui-font-regular` | The sans-serif font for the theme. | +| `--bui-font-mono` | The monospace font for the theme. | + +#### Font weights + +We have two font weights that we use across Backstage UI. Regular or Bold. + +| Token Name | Description | +| --------------------------- | -------------------------------------- | +| `--bui-font-weight-regular` | The regular font weight for the theme. | +| `--bui-font-weight-bold` | The bold font weight for the theme. | + +#### Spacing + +We built a spacing system based on a single value `--bui-space`. This value is used to calculate the spacing for all the components. By default if you would like to increase or decrease the spacing between your components you can do it simply by updating `--bui-space` and it will apply to all spacing values. + +`--bui-space` is not used directly in any components but serve as an easy way to calculate the other values. + +| Token Name | Description | +| ------------- | ----------------------------------------------------------------- | +| `--bui-space` | The base unit for the spacing system. Default value is `0.25rem.` | + +#### Radius + +We use a radius system to make sure that the components have a consistent look and feel. + +| Token Name | Description | +| ------------------- | --------------------------------------------------------- | +| `--bui-radius-1` | The radius of the component. Default value is `0.125rem`. | +| `--bui-radius-2` | The radius of the component. Default value is `0.25rem`. | +| `--bui-radius-3` | The radius of the component. Default value is `0.5rem`. | +| `--bui-radius-4` | The radius of the component. Default value is `0.75rem`. | +| `--bui-radius-5` | The radius of the component. Default value is `1rem`. | +| `--bui-radius-6` | The radius of the component. Default value is `1.25rem`. | +| `--bui-radius-full` | The radius of the component. Default value is `9999px`. | + +
+ +### Component class names + +All Backstage UI components come with a set of CSS classes that you can use to style them. To make it easier to identify the class name you can use, we use a specific structure for the class names. + +![classname-structure](../../assets/user-interface/css-classname-structure.png) + +Every component has a unique prefix `.bui-` followed by the component name. Component props are represented using the `data-` attribute. That way, class names are easily identifiable. + +## Create a theme for MUI (Legacy) + +To customize the appearance of your Backstage app using the legacy MUI theming system, you can define your own theme by extending the built-in light or dark themes. This is done using the createUnifiedTheme utility provided by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. This function allows you to override key aspects of the theme—such as color palette, typography, spacing, and shape—while preserving Backstage’s base configuration and component compatibility. + +The example below shows how to create a new theme based on the default light theme: + +```ts title="packages/app/src/themes.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const lightTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', +}); + +export const darkTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.dark, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', +}); +``` + +You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the +[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. + +
+ Example of a custom MUI theme + +For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). + +```ts title="packages/app/src/themes.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + genPageTheme, + palettes, + shapes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: { + ...palettes.light, + primary: { + main: '#343b58', + }, + secondary: { + main: '#565a6e', + }, + error: { + main: '#8c4351', + }, + warning: { + main: '#8f5e15', + }, + info: { + main: '#34548a', + }, + success: { + main: '#485e30', + }, + background: { + default: '#d5d6db', + paper: '#d5d6db', + }, + banner: { + info: '#34548a', + error: '#8c4351', + text: '#343b58', + link: '#565a6e', + }, + errorBackground: '#8c4351', + warningBackground: '#8f5e15', + infoBackground: '#343b58', + navigation: { + background: '#343b58', + indicator: '#8f5e15', + color: '#d5d6db', + selectedColor: '#ffffff', + }, + }, + }), + defaultPageTheme: 'home', + fontFamily: 'Comic Sans MS', + /* below drives the header colors */ + pageTheme: { + home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + documentation: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave2, + }), + tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }), + service: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + website: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + library: genPageTheme({ + colors: ['#8c4351', '#343b58'], + shape: shapes.wave, + }), + other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), + }, +}); +``` + +
+ +
+ Custom Typography + +When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + htmlFontSize: 16, + fontFamily: 'Arial, sans-serif', + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + defaultTypography, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +
+ +
+ Custom Fonts + +To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder. + +You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/). + +After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array. + +```ts title="packages/app/src/theme/myTheme.ts" +import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; + +const myCustomFont = { + fontFamily: 'My-Custom-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Custom-Font'), + url(${MyCustomFont}) format('woff2'), + `, +}; + +export const myTheme = createUnifiedTheme({ + fontFamily: 'My-Custom-Font', + palette: palettes.light, + components: { + MuiCssBaseline: { + styleOverrides: { + '@font-face': [myCustomFont], + }, + }, + }, +}); +``` + +If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings. + +```ts title="packages/app/src/theme/myTheme.ts" +import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; +import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2'; + +const myCustomFont = { + fontFamily: 'My-Custom-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Custom-Font'), + url(${MyCustomFont}) format('woff2'), + `, +}; + +const myAwesomeFont = { + fontFamily: 'My-Awesome-Font', + fontStyle: 'normal', + fontDisplay: 'swap', + fontWeight: 300, + src: ` + local('My-Awesome-Font'), + url(${myAwesomeFont}) format('woff2'), + `, +}; + +export const myTheme = createUnifiedTheme({ + fontFamily: 'My-Custom-Font', + components: { + MuiCssBaseline: { + styleOverrides: { + '@font-face': [myCustomFont, myAwesomeFont], + }, + }, + }, + ...createBaseThemeOptions({ + palette: palettes.light, + typography: { + ...defaultTypography, + htmlFontSize: 16, + fontFamily: 'My-Custom-Font', + h1: { + fontSize: 72, + fontWeight: 700, + marginBottom: 10, + fontFamily: 'My-Awesome-Font', + }, + }, + defaultPageTheme: 'home', + }), +}); +``` + +
+ +
+ Overriding Backstage and Material UI components styles + +When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: + +```tsx +const useStyles = makeStyles( + theme => ({ + header: { + padding: theme.spacing(3), + boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)', + backgroundImage: theme.page.backgroundImage, + }, + }), + { name: 'BackstageHeader' }, +); +``` + +Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. + +Here's how you would do that: + +```ts title="packages/app/src/theme/myTheme.ts" +import { + createBaseThemeOptions, + createUnifiedTheme, + palettes, +} from '@backstage/theme'; + +export const myTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: palettes.light, + }), + fontFamily: 'Comic Sans MS', + defaultPageTheme: 'home', + components: { + BackstageHeader: { + styleOverrides: { + header: ({ theme }) => ({ + width: 'auto', + margin: '20px', + boxShadow: 'none', + borderBottom: `4px solid ${theme.palette.primary.main}`, + }), + }, + }, + }, +}); +``` + +
diff --git a/docs/conf/user-interface/logo.md b/docs/conf/user-interface/logo.md new file mode 100644 index 0000000000..35789d6330 --- /dev/null +++ b/docs/conf/user-interface/logo.md @@ -0,0 +1,25 @@ +--- +id: logo +title: Customizing Your Logo +sidebar_label: Logo +description: Learn how to customize your logo. +--- + +In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. + +In your frontend app, locate `src/components/Root/` folder. You'll find two components: + +- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. +- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. + +To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. + +You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: + +```tsx +import MyCustomLogoFull from './logo/my-company-logo.png'; + +const LogoFull = () => { + return ; +}; +``` diff --git a/docs/conf/user-interface/sidebar.md b/docs/conf/user-interface/sidebar.md new file mode 100644 index 0000000000..f66e6bd106 --- /dev/null +++ b/docs/conf/user-interface/sidebar.md @@ -0,0 +1,93 @@ +--- +id: sidebar +title: Customizing Your Sidebar +sidebar_label: Sidebar +description: Learn how to customize the look and feel of your Sidebar. +--- + +As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar. + +## Sidebar Sub-menu + +For this example we'll show you how you can expand the sidebar with a sub-menu: + +1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives +2. Then we want to add the following import for `useApp`: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + import { useApp } from '@backstage/core-plugin-api'; + ``` + +3. Then update the `@backstage/core-components` import like this: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + import { + Sidebar, + sidebarConfig, + SidebarDivider, + SidebarGroup, + SidebarItem, + SidebarPage, + SidebarScrollWrapper, + SidebarSpace, + useSidebarOpenState, + Link, + /* highlight-add-start */ + GroupIcon, + SidebarSubmenu, + SidebarSubmenuItem, + /* highlight-add-end */ + } from '@backstage/core-components'; + ``` + +4. Finally replace `` with this: + + ```tsx title="packages/app/src/components/Root/Root.tsx" + + + + + + + + + + + + + + ``` + +When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this: + +![Sidebar sub-menu example](./../../assets/getting-started/sidebar-submenu-example.png) + +You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar) diff --git a/docs/dls/component-design-guidelines.md b/docs/dls/component-design-guidelines.md index 35e2f987d2..fddffd9fd9 100644 --- a/docs/dls/component-design-guidelines.md +++ b/docs/dls/component-design-guidelines.md @@ -103,6 +103,4 @@ accessibility. [7]: https://v4.mui.com/components/cards/ [8]: https://v4.mui.com/customization/palette/#default-values [9]: https://v4.mui.com/customization/typography/ -[10]: https://backstage.io/docs/getting-started/app-custom-theme -[11]: https://backstage.io/docs/getting-started/app-custom-theme#overriding-backstage-and-material-ui-components-styles -[12]: https://v4.mui.com/customization/default-theme/#explore +[10]: https://backstage.io/docs/conf/user-interface diff --git a/docs/dls/design.md b/docs/dls/design.md index 7e2ae90528..e988d1032f 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -127,7 +127,7 @@ your own plugin for Backstage. **[Discord](https://discord.gg/backstage-687207715902193673)** - all design questions should be directed to the _#design_ channel. -**[Customize Backstage's look and feel](https://backstage.io/docs/getting-started/app-custom-theme)** - +**[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface)** - How to customize the look and feel of your Backstage instance by extending the theme. diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 7055db1138..f0823ac47e 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -116,7 +116,7 @@ indexBuilder.addCollator({ The default highlighting styling for matched terms in search results is your browsers default styles for the `` HTML tag. If you want to customize how highlighted terms look you can follow Backstage's guide on how to -[Customize the look-and-feel of your App](https://backstage.io/docs/getting-started/app-custom-theme) +[Customizing Your App's UI](https://backstage.io/docs/conf/user-interface) to create an override with your preferred styling. For example, using the new MUI V4+V5 unified theming method, the following will result diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index 1ef865039f..a828e6591a 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -571,10 +571,8 @@ Finally, entity predicates also support value operators that can be used in plac ```json { "filter": { - { - "kind": "component", - "spec.type": { "$in": ["service", "website"] } - }, + "kind": "component", + "spec.type": { "$in": ["service", "website"] } } } ``` @@ -695,6 +693,6 @@ filter: relations: $contains: type: ownedBy - target: + targetRef: $in: [group:default/admins, group:default/viewers] ``` diff --git a/docs/frontend-system/building-plugins/04-built-in-data-refs.md b/docs/frontend-system/building-plugins/04-built-in-data-refs.md index b69e4cb3f4..c01520d7bd 100644 --- a/docs/frontend-system/building-plugins/04-built-in-data-refs.md +++ b/docs/frontend-system/building-plugins/04-built-in-data-refs.md @@ -34,6 +34,14 @@ const examplePage = createExtension({ }); ``` +### `title` + +| id | type | +| :----------: | :------: | +| `core.title` | `string` | + +The `title` data reference can be used for defining the extension input/output of string titles. + ### `routePath` | id | type | diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md deleted file mode 100644 index 38f2fd4c98..0000000000 --- a/docs/getting-started/app-custom-theme.md +++ /dev/null @@ -1,613 +0,0 @@ ---- -id: app-custom-theme -title: Customize the look-and-feel of your App -description: Documentation on Customizing look and feel of the App ---- - -Backstage ships with a default theme with a light and dark mode variant. The themes are provided as a part of the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package, which also includes utilities for customizing the default theme, or creating completely new themes. - -## Creating a Custom Theme - -The easiest way to create a new theme is to use the `createUnifiedTheme` function exported by the [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override some basic parameters of the default theme such as the color palette and font. - -For example, you can create a new theme based on the default light theme like this: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - }), - fontFamily: 'Comic Sans MS', - defaultPageTheme: 'home', -}); -``` - -:::note Note - -we recommend creating a `theme` folder in `packages/app/src` to place your theme file to keep things nicely organized. - -::: - -You can also create a theme from scratch that matches the `BackstageTheme` type exported by [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). See the -[Material UI docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done. - -## Using your Custom Theme - -To add a custom theme to your Backstage app, you pass it as configuration to `createApp`. - -For example, adding the theme that we created in the previous section can be done like this: - -```tsx title="packages/app/src/App.tsx" -import { createApp } from '@backstage/app-defaults'; -import { ThemeProvider } from '@material-ui/core/styles'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import LightIcon from '@material-ui/icons/WbSunny'; -import { UnifiedThemeProvider} from '@backstage/theme'; -import { myTheme } from './themes/myTheme'; - -const app = createApp({ - apis: ..., - plugins: ..., - themes: [{ - id: 'my-theme', - title: 'My Custom Theme', - variant: 'light', - icon: , - Provider: ({ children }) => ( - - ), - }] -}) -``` - -Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). - -## Example of a custom theme - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - genPageTheme, - palettes, - shapes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: { - ...palettes.light, - primary: { - main: '#343b58', - }, - secondary: { - main: '#565a6e', - }, - error: { - main: '#8c4351', - }, - warning: { - main: '#8f5e15', - }, - info: { - main: '#34548a', - }, - success: { - main: '#485e30', - }, - background: { - default: '#d5d6db', - paper: '#d5d6db', - }, - banner: { - info: '#34548a', - error: '#8c4351', - text: '#343b58', - link: '#565a6e', - }, - errorBackground: '#8c4351', - warningBackground: '#8f5e15', - infoBackground: '#343b58', - navigation: { - background: '#343b58', - indicator: '#8f5e15', - color: '#d5d6db', - selectedColor: '#ffffff', - }, - }, - }), - defaultPageTheme: 'home', - fontFamily: 'Comic Sans MS', - /* below drives the header colors */ - pageTheme: { - home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - documentation: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave2, - }), - tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }), - service: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - website: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - library: genPageTheme({ - colors: ['#8c4351', '#343b58'], - shape: shapes.wave, - }), - other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }), - }, -}); -``` - -For a more complete example of a custom theme including Backstage and Material UI component overrides, see the [Aperture theme](https://github.com/backstage/demo/blob/master/packages/app/src/theme/aperture.ts) from the [Backstage demo site](https://demo.backstage.io). - -## Custom Typography - -When creating a custom theme you can also customize various aspects of the default typography, here's an example using simplified theme: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - htmlFontSize: 16, - fontFamily: 'Arial, sans-serif', - h1: { - fontSize: 54, - fontWeight: 700, - marginBottom: 10, - }, - h2: { - fontSize: 40, - fontWeight: 700, - marginBottom: 8, - }, - h3: { - fontSize: 32, - fontWeight: 700, - marginBottom: 6, - }, - h4: { - fontWeight: 700, - fontSize: 28, - marginBottom: 6, - }, - h5: { - fontWeight: 700, - fontSize: 24, - marginBottom: 4, - }, - h6: { - fontWeight: 700, - fontSize: 20, - marginBottom: 2, - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -If you wanted to only override a sub-set of the typography setting, for example just `h1` then you would do this: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - defaultTypography, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - ...defaultTypography, - htmlFontSize: 16, - fontFamily: 'Roboto, sans-serif', - h1: { - fontSize: 72, - fontWeight: 700, - marginBottom: 10, - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -## Custom Fonts - -To add custom fonts, you first need to store the font so that it can be imported. We suggest creating the `assets/fonts` directory in your front-end application `src` folder. - -You can then declare the font style following the `@font-face` syntax from [Material UI Typography](https://mui.com/material-ui/customization/typography/). - -After that you can then utilize the `styleOverrides` of `MuiCssBaseline` under components to add a font to the `@font-face` array. - -```ts title="packages/app/src/theme/myTheme.ts" -import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; - -const myCustomFont = { - fontFamily: 'My-Custom-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Custom-Font'), - url(${MyCustomFont}) format('woff2'), - `, -}; - -export const myTheme = createUnifiedTheme({ - fontFamily: 'My-Custom-Font', - palette: palettes.light, - components: { - MuiCssBaseline: { - styleOverrides: { - '@font-face': [myCustomFont], - }, - }, - }, -}); -``` - -If you want to utilize different or multiple fonts, then you can set the top level `fontFamily` to what you want for your body, and then override `fontFamily` in `typography` to control fonts for various headings. - -```ts title="packages/app/src/theme/myTheme.ts" -import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2'; -import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2'; - -const myCustomFont = { - fontFamily: 'My-Custom-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Custom-Font'), - url(${MyCustomFont}) format('woff2'), - `, -}; - -const myAwesomeFont = { - fontFamily: 'My-Awesome-Font', - fontStyle: 'normal', - fontDisplay: 'swap', - fontWeight: 300, - src: ` - local('My-Awesome-Font'), - url(${myAwesomeFont}) format('woff2'), - `, -}; - -export const myTheme = createUnifiedTheme({ - fontFamily: 'My-Custom-Font', - components: { - MuiCssBaseline: { - styleOverrides: { - '@font-face': [myCustomFont, myAwesomeFont], - }, - }, - }, - ...createBaseThemeOptions({ - palette: palettes.light, - typography: { - ...defaultTypography, - htmlFontSize: 16, - fontFamily: 'My-Custom-Font', - h1: { - fontSize: 72, - fontWeight: 700, - marginBottom: 10, - fontFamily: 'My-Awesome-Font', - }, - }, - defaultPageTheme: 'home', - }), -}); -``` - -## Overriding Backstage and Material UI components styles - -When creating a custom theme you would be applying different values to component's CSS rules that use the theme object. For example, a Backstage component's styles might look like this: - -```tsx -const useStyles = makeStyles( - theme => ({ - header: { - padding: theme.spacing(3), - boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)', - backgroundImage: theme.page.backgroundImage, - }, - }), - { name: 'BackstageHeader' }, -); -``` - -Notice how the `padding` is getting its value from `theme.spacing`, that means that setting a value for spacing in your custom theme would affect this component padding property and the same goes for `backgroundImage` which uses `theme.page.backgroundImage`. However, the `boxShadow` property doesn't reference any value from the theme, that means that creating a custom theme wouldn't be enough to alter the `box-shadow` property or to add css rules that aren't already defined like a margin. For these cases you should also create an override. - -Here's how you would do that: - -```ts title="packages/app/src/theme/myTheme.ts" -import { - createBaseThemeOptions, - createUnifiedTheme, - palettes, -} from '@backstage/theme'; - -export const myTheme = createUnifiedTheme({ - ...createBaseThemeOptions({ - palette: palettes.light, - }), - fontFamily: 'Comic Sans MS', - defaultPageTheme: 'home', - components: { - BackstageHeader: { - styleOverrides: { - header: ({ theme }) => ({ - width: 'auto', - margin: '20px', - boxShadow: 'none', - borderBottom: `4px solid ${theme.palette.primary.main}`, - }), - }, - }, - }, -}); -``` - -## Custom Logo - -In addition to a custom theme, you can also customize the logo displayed at the far top left of the site. - -In your frontend app, locate `src/components/Root/` folder. You'll find two components: - -- `LogoFull.tsx` - A larger logo used when the Sidebar navigation is opened. -- `LogoIcon.tsx` - A smaller logo used when the Sidebar navigation is closed. - -To replace the images, you can simply replace the relevant code in those components with raw SVG definitions. - -You can also use another web image format such as PNG by importing it. To do this, place your new image into a new subdirectory such as `src/components/Root/logo/my-company-logo.png`, and then add this code: - -```tsx -import MyCustomLogoFull from './logo/my-company-logo.png'; - -const LogoFull = () => { - return ; -}; -``` - -## Icons - -So far you've seen how to create your own theme and add your own logo, in the following sections you'll be shown how to override the existing icons and how to add more icons - -### Custom Icons - -You can also customize the Project's _default_ icons. - -You can change the following [icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx). - -#### Requirements - -- Files in `.svg` format -- React components created for the icons - -#### Create React Component - -In your front-end application, locate the `src` folder. We suggest creating the `assets/icons` directory and `CustomIcons.tsx` file. - -```tsx title="customIcons.tsx" -import { SvgIcon, SvgIconProps } from '@material-ui/core'; - -export const ExampleIcon = (props: SvgIconProps) => ( - - - -); -``` - -#### Using the custom icon - -Supply your custom icon in `packages/app/src/App.tsx` - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ExampleIcon } from './assets/icons/CustomIcons' - - -const app = createApp({ - apis, - components: { - {/* ... */} - }, - themes: [ - {/* ... */} - ], - /* highlight-add-start */ - icons: { - github: ExampleIcon, - }, - /* highlight-add-end */ - bindRoutes({ bind }) { - {/* ... */} - } -}) -``` - -### Adding Icons - -You can add more icons, if the [default icons](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx) do not fit your needs, so that they can be used in other places like for Links in your entities. For this example we'll be using icons from[Material UI](https://v4.mui.com/components/material-icons/) and specifically the `AlarmIcon`. Here's how to do that: - -1. First you will want to open your `App.tsx` in `/packages/app/src` -2. Then you want to import your icon, add this to the rest of your imports: `import AlarmIcon from '@material-ui/icons/Alarm';` -3. Next you want to add the icon like this to your `createApp`: - - ```tsx title="packages/app/src/App.tsx" - const app = createApp({ - apis: ..., - plugins: ..., - /* highlight-add-start */ - icons: { - alert: AlarmIcon, - }, - /* highlight-add-end */ - themes: ..., - components: ..., - }); - ``` - -4. Now we can reference `alert` for our icon in our entity links like this: - - ```yaml - apiVersion: backstage.io/v1alpha1 - kind: Component - metadata: - name: artist-lookup - description: Artist Lookup - links: - - url: https://example.com/alert - title: Alerts - icon: alert - ``` - - And this is the result: - - ![Example Link with Alert icon](../assets/getting-started/add-icons-links-example.png) - - Another way you can use these icons is from the `AppContext` like this: - - ```ts - import { useApp } from '@backstage/core-plugin-api'; - - const app = useApp(); - const alertIcon = app.getSystemIcon('alert'); - ``` - - You might want to use this method if you have an icon you want to use in several locations. - -:::note Note - -If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon` - -::: - -## Custom Sidebar - -As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar. - -### Sidebar Sub-menu - -For this example we'll show you how you can expand the sidebar with a sub-menu: - -1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives -2. Then we want to add the following import for `useApp`: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - import { useApp } from '@backstage/core-plugin-api'; - ``` - -3. Then update the `@backstage/core-components` import like this: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - import { - Sidebar, - sidebarConfig, - SidebarDivider, - SidebarGroup, - SidebarItem, - SidebarPage, - SidebarScrollWrapper, - SidebarSpace, - useSidebarOpenState, - Link, - /* highlight-add-start */ - GroupIcon, - SidebarSubmenu, - SidebarSubmenuItem, - /* highlight-add-end */ - } from '@backstage/core-components'; - ``` - -4. Finally replace `` with this: - - ```tsx title="packages/app/src/components/Root/Root.tsx" - - - - - - - - - - - - - - ``` - -When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this: - -![Sidebar sub-menu example](./../assets/getting-started/sidebar-submenu-example.png) - -You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar) - -## Custom Homepage - -In addition to a custom theme, a custom logo, you can also customize the -homepage of your app. Read the full guide on the [next page](homepage.md). - -## Migrating to Material UI v5 - -We now support Material UI v5 in Backstage. Check out our [migration guide](../tutorials/migrate-to-mui5.md) to get started. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 252884dc54..876af4f0a5 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -157,7 +157,7 @@ Choose the correct next steps for your user role, if you're likely to be deployi - Configuring Backstage - [Adding plugins](./configure-app-with-plugins.md) - - [Customizing the theme](./app-custom-theme.md) + - [Customizing Your App's UI](../conf/user-interface/index.md) - [Populating the homepage](./homepage.md) ### Developer diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 0fce7b864e..5e02c853fa 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -27,7 +27,7 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab Then add the following to your backend initialization: -```ts title="packages/backend/src/index.ts +```ts title="packages/backend/src/index.ts" // optional if you want HTTP endpoints to receive external events // backend.add(import('@backstage/plugin-events-backend')); // optional if you want to use AWS SQS instead of HTTP endpoints to receive external events @@ -49,7 +49,7 @@ Further documentation: - [Events Plugin](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [GitLab Module for the Events Plugin](https://github.com/backstage/backstage/blob/master/plugins/events-backend-module-gitlab/README.md) -### Installation with Legacy Backend System +### Installation with Legacy Backend System (skip if you are using Backstage v1.31.0 or later) #### Installation without Events Support diff --git a/docs/landing-page/doc-landing-page.md b/docs/landing-page/doc-landing-page.md new file mode 100644 index 0000000000..cb1b889851 --- /dev/null +++ b/docs/landing-page/doc-landing-page.md @@ -0,0 +1,109 @@ +--- +id: doc-landing-page +title: Documentation +description: Documentation landing page. +--- + +## Understand, try, and administer Backstage + + + + + + + + + + + + + + +
Understand BackstageTry BackstageAdministrationAdditional Features
Learn about Backstage.

+ +
Build a customizable Backstage app.
+ +
Configure, Deploy, & Upgrade.

+ +
Additional Backstage Features

+ +
+ +## Plugin Development, references, and how to contribute + + + + + + + + + + + + +
Plugin DevelopmentReferencesContribute to Backstage
Creating and Configuring Plugins

+ +
Glossary, FAQ, and other reference material.
+ +
Contributions are welcome and greatly appreciated!
+ +
diff --git a/docs/notifications/processors.md b/docs/notifications/processors.md index ad3e427364..c25b669ed6 100644 --- a/docs/notifications/processors.md +++ b/docs/notifications/processors.md @@ -148,6 +148,7 @@ notifications: - token: xoxb-XXXXXXXXX broadcastChannels: # Optional, if you wish to support broadcast notifications. - C12345678 + username: 'Backstage Bot' # Optional, defaults to the name of the Slack App. ``` Multiple instances can be added in the `slack` array, allowing you to have multiple configurations if you need to send diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 63e2c16e3c..de283beec0 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -10,285 +10,232 @@ Backstage is organized into three main components, each catering to different gr - Core - This includes the base functionality developed by core developers within the open-source project. - App - The app represents a deployed instance of a Backstage application, customized and maintained by app developers, typically a productivity team within an organization. It integrates core functionalities with additional plugins. -- Plugins - These provide additional functionalities to enhance the usefulness of your Backstage app. Plugins can be company-specific or open-sourced and reusable. At Spotify, we have over 100 plugins created by more than 50 different teams, significantly enriching the unified developer experience by incorporating contributions from various infrastructure teams. +- Plugins - These provide additional functionalities to enhance the usefulness of your Backstage app. Plugins can be company-specific or open-sourced and reusable. ## Overview -The following diagram shows how Backstage might look when deployed inside a -company which uses the Tech Radar plugin, the Lighthouse plugin, the CircleCI -plugin and the software catalog. +The following diagram shows a high level view of the overall architecture of Backstage. Running this architecture in a real environment typically involves +containerizing the components. Various commands are provided for accomplishing this. There are 3 main components in this architecture: -1. The core Backstage UI -2. The UI plugins and their backing services -3. Databases +- The [frontend](#frontend-building-blocks) includes the core Backstage [UI](#user-interface) which is an [extension](#extensions) that interacts directly with the user to present the information from the integrated core feature plugins, and other plugins added by a user. +- The [backend](#backend-building-blocks) includes the backend plugins, [core services](https://backstage.io/docs/backend-system/core-services/index), and other services. This is the server-side part of Backstage that is responsible for wiring things together. You can deploy more than one backend, and more than one backend container, depending on your need to scale and isolate individual features. +- Databases host your Backstage data. -Running this architecture in a real environment typically involves -containerizing the components. Various commands are provided for accomplishing -this. +![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-front-back-arch.jpeg) -![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-typical-architecture.png) +## Frontend building blocks -## User Interface +The architectural diagram provides an overview of the different building blocks and the other blocks that each of them interacts with. -The UI is a thin, client-side wrapper around a set of plugins. It provides some -core UI components and libraries for shared activities such as config -management. [[live demo](https://demo.backstage.io/catalog)] +### App + +This is the app instance itself that you create and use as the root of your Backstage frontend application. It does not have any direct functionality in and of itself, but is simply responsible for wiring things together. + +### Extensions + +[Extensions](../frontend-system/architecture/20-extensions.md) are the building blocks that build out both the visual and non-visual structure of the application. There are both built-in extensions provided by the app itself as well as extensions provided by plugins. Each extension is attached to a parent with which it shares data and can have any number of children of its own. It is up to the app to wire together all extensions into a single tree known as the app extension tree. It is from this structure that the entire app can then be instantiated and rendered. + +#### User Interface + +The UI is one of the extensions in the frontend. It is a thin, client-side wrapper around a set of plugins. It provides some core UI components and libraries for shared activities such as config management. [[live demo](https://demo.backstage.io/catalog)] ![UI with different components highlighted](../assets/architecture-overview/core-vs-plugin-components-highlighted.png) -Each plugin typically makes itself available in the UI on a dedicated URL. For -example, the Lighthouse plugin is registered with the UI on `/lighthouse`. -[[learn more](https://backstage.io/blog/2020/04/06/lighthouse-plugin)] +Each plugin typically makes itself available in the UI on a dedicated URL. For example, the Service Catalog plugin is registered with the UI on `/catalog`. -![The lighthouse plugin UI](../assets/architecture-overview/lighthouse-plugin.png) +### Frontend plugins -The CircleCI plugin is available on `/circleci`. +Plugins provide the actual features inside an app. The size of a plugin can range from a tiny component to an entire new system in which other plugins can be composed and integrated. Plugins can be completely standalone or built on top of each other to extend existing plugins and augment their features. Plugins can communicate with each other by composing their extensions or by sharing Utility APIs and routes. -![CircleCI Plugin UI](../assets/architecture-overview/circle-ci.png) +Backstage includes the following set of core plugins: -## Plugins and plugin backends +- [Software Catalog](../features/software-catalog/index.md) - A centralized system that contains metadata for all your software, such as services, websites, libraries, ML models, data pipelines, and so on. It can also contain metadata for the physical or virtual infrastructure needed to operate a piece of software. The software catalog can be viewed and searched through a UI. +- [Software Templates](../features/software-templates/index.md) - A tool to help you create components inside Backstage. A template can load skeletons of code, include some variables, and then publish the template to a location, such as GitHub. +- [TechDocs](https://backstage.io/docs/features/techdocs/) - A docs-like-code solution built into Backstage. Documentation is written in Markdown files which lives together with the code. +- [Kubernetes](../features/kubernetes/index.md) - A tool that allows developers to check the health of their services whether it is on a local host or in production. +- [Search](https://backstage.io/docs/features/search/) - Search for information in the Backstage ecosystem. You can customize the look and feel of each search result and use your own search engine. -Each plugin is a client side application which mounts itself on the UI. Plugins -are written in TypeScript or JavaScript. They each live in their own directory -in the `plugins` folder. For example, the source code for the catalog plugin -is available at -[plugins/catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog). +[Plugin architecture](#plugin-architecture) provides greater detail about the architecture of the plugins themselves. -### Installing plugins +### Extension Overrides -Plugins are typically installed as React components in your Backstage -application. For example, -[here](https://github.com/backstage/backstage/blob/master/packages/app/src/App.tsx) -is a file that imports many full-page plugins in the Backstage sample app. +In addition to the built-in extensions and extensions provided by plugins, it is also possible to install [extension overrides](../frontend-system/architecture/25-extension-overrides.md). This is a collection of extensions with high priority that can replace existing extensions. They can for example be used to override an individual extension provided by a plugin, or install a completely new extension, such as a new app theme. -An example of one of these plugin components is the `CatalogIndexPage`, which is -a full-page view that allows you to browse entities in the Backstage catalog. It -is installed in the app by importing it and adding it as an element like this: +### Utility APIs -```tsx -import { CatalogIndexPage } from '@backstage/plugin-catalog'; +[Utility APIs](../api/utility-apis.md) provide functionality that makes it easier to build plugins, make it possible for plugins to share functionality with other plugins, as well as serve as a customization point for integrators to change the behaviour of the app. Each Utility API is defined by a TypeScript interface as well as a reference used to access the implementations. The implementations of Utility APIs are defined by extensions that are provided and can be overridden the same as any other extension. -... +### Routes -const routes = ( - - ... - } /> - ... - -); -``` +The [Backstage routing system](../frontend-system/architecture/36-routes.md) adds a layer of indirection that makes it possible for plugins to route to each other's extensions without explicit knowledge of what URL paths the extensions are rendered at or if they even exist at all. It makes it possible for plugins to share routes with each other and dynamically generate concrete links at runtime. It is the responsibility of the app to resolve these links to actual URLs, but it is also possible for integrators to define their own route bindings that decide how the links should be resolved. The routing system also lets plugins define internal routes, aiding in the linking to different content in the same plugin. -Note that we use `"/catalog"` as our path to this plugin page, but we can choose -any route we want for the page, as long as it doesn't collide with the routes -that we choose for the other plugins in the app. +## Backend building blocks -These components that are exported from plugins are referred to as "Plugin -Extension Components", or "Extension Components". They are regular React -components, but in addition to being able to be rendered by React, they also -contain various pieces of metadata that is used to wire together the entire app. -Extension components are created using `create*Extension` methods, which you can -read more about in the -[composability documentation](../plugins/composability.md). +The architectural diagram provides an overview of the different building blocks, and the other blocks that each of them interact with. -As of this moment, there is no config based install procedure for plugins. Some -code changes are required. +### Backend -### Plugin architecture +This is the [backend instance](../backend-system/architecture/02-backends.md) itself, which you can think of as the unit of deployment. It does not have any functionality in and of itself, but is simply responsible for wiring things together. + +It is up to you to decide how many different backends you want to deploy. You can have all features in a single one, or split things out into multiple smaller deployments, depending on your need to scale and isolate individual features. + +### Backend plugins + +[Plugins](../backend-system/architecture/04-plugins.md) provide the actual features. They operate completely independently of each other. If plugins want to communicate with each other, they must do so over the wire. There can be no direct communication between plugins through code. Because of this constraint, each plugin can be considered to be its own microservice. + +[Plugin architecture](#plugin-architecture) provides greater detail about the architecture of the plugins themselves. + +### Services + +[Services](../backend-system/architecture/03-services.md) provide utilities to help make it simpler to implement plugins, so that each plugin doesn't need to implement everything from scratch. There are many built-in core services, such as the ones for logging, database access, and reading configuration, but you can also import third-party services, or create your own. + +Services are also a customization point for individual backend installations. You can override services with your own implementations, as well as make smaller customizations to existing services. + +### Extension Points + +Many plugins have ways in which you can extend them, for example entity providers for the Catalog, or custom actions for the Scaffolder. These extension patterns are now encoded into Extension Points. + +[Extension Points](../backend-system/architecture/05-extension-points.md) look a little bit like services, since you depend on them just like you would a service. A key difference is that extension points are registered and provided by plugins or modules themselves, based on what customizations each of them want to expose. + +Extension Points are exported separately from the plugin or module instance itself, and it is possible to expose multiple different extension points at once. This makes it easier to evolve and deprecate individual Extension Points over time, rather than dealing with a single large API surface. + +### Modules + +[Modules](../backend-system/architecture/06-modules.md) use Extension Points to add new features to other plugins or modules. They might for example add an individual Catalog Entity Provider, or one or more Scaffolder Actions. + +Each module may only use Extension Points that belong to a single plugin, and the module must be deployed together with that plugin in the same backend instance. Modules may only communicate with their plugin or other modules through the registered extension points. + +Just like plugins, modules also have access to services and can depend on their own service implementations. They will however share services with the plugin that they extend - there are no module-specific service implementations. + +## Databases + +The databases host your Backstage data. The Backstage backend and its built-in plugins are based on the [Knex](http://knexjs.org/) library, and set up a separate logical database per plugin. This gives great isolation and lets them perform migrations and evolve separately from each other. + +The Knex library supports a multitude of databases, but Backstage at this time of writing is tested primarily against two of them: + +- SQLite, which is mainly used as an in-memory mock/test database +- PostgreSQL, which is the preferred production database. + +Other databases such as the MySQL variants are reported to work but [aren't fully tested](https://github.com/backstage/backstage/issues/2460) yet. + +You can find instructions on setting up a PostgreSQL for your Backstage instance in [Database](../getting-started/config/database.md). [Configuring Plugin Databases](../tutorials/configuring-plugin-databases.md) provides information on how to configure a database for a plugin. + +## Plugin architecture Architecturally, plugins can take three forms: -1. Standalone -2. Service backed -3. Third-party backed +- [Standalone](#standalone-plugins) +- [Service backend](#service-backend-plugins) +- [Third-party backend](#third-party-backend-plugins) -#### Standalone plugins +### Standalone plugins -Standalone plugins run entirely in the browser. -[The Tech Radar plugin](https://demo.backstage.io/tech-radar), for example, -simply renders hard-coded information. It doesn't make any API requests to other -services. +Standalone plugins run entirely in the browser. [The Tech Radar plugin](https://demo.backstage.io/tech-radar), for example, simply renders hard-coded information. It doesn't make any API requests to other services. + +The architecture of the Tech Radar installed into a Backstage app is very simple. You just need to add Tech Radar as a frontend plugin into your app, as shown in the following diagram. + +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend containers in order to highlight the changes that pertain to the addition of the specified plugin. + +![ui and tech radar plugin connected together](../assets/architecture-overview/simplified-standalone-plugin-architecture.jpeg) + +Once the plugin has been added, then you can view the Tech Radar information in the Backstage UI. ![tech radar plugin ui](../assets/architecture-overview/tech-radar-plugin.png) -The architecture of the Tech Radar installed into a Backstage app is very -simple. +### Service backend plugins -![ui and tech radar plugin connected together](../assets/architecture-overview/tech-radar-plugin-architecture.png) +Service backend plugins make API requests to a service which is within the purview of the organization running Backstage. -#### Service backed plugins +The Lighthouse plugin, for example, makes requests to the [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service). The `lighthouse-audit-service` is a microservice which runs a copy of Google's [Lighthouse library](https://github.com/GoogleChrome/lighthouse/) and stores the results in a PostgreSQL database. -Service backed plugins make API requests to a service which is within the -purview of the organisation running Backstage. +The Lighthouse plugin is added to the frontend. The lighthouse-audit-service container is already publicly available in Docker Hub and can be downloaded and run with -The Lighthouse plugin, for example, makes requests to the -[lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service). -The `lighthouse-audit-service` is a microservice which runs a copy of Google's -[Lighthouse library](https://github.com/GoogleChrome/lighthouse/) and stores the -results in a PostgreSQL database. +```bash +docker run spotify/lighthouse-audit-service:latest +``` -Its architecture looks like this: +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend, in order to highlight the changes that pertain to the addition of the specified plugin. -![lighthouse plugin backed to microservice and database](../assets/architecture-overview/lighthouse-plugin-architecture.png) +![lighthouse plugin backend to microservice and database](../assets/architecture-overview/simplified-service-based-plugin-architecture.jpeg) -The software catalog in Backstage is another example of a service backed plugin. -It retrieves a list of services, or "entities", from the Backstage Backend -service and renders them in a table for the user. +The software catalog in Backstage is another example of a service backend plugin. It retrieves a list of services, or "entities", from the Backstage Backend service and renders them in a table for the user. -### Third-party backed plugins +### Third-party backend plugins -Third-party backed plugins are similar to service backed plugins. The main -difference is that the service which backs the plugin is hosted outside of the -ecosystem of the company hosting Backstage. +Third-party backend plugins are similar to service backend plugins. The main difference is that the service which backs the plugin is hosted outside of the ecosystem of the company hosting Backstage. -The CircleCI plugin is an example of a third-party backed plugin. CircleCI is a -SaaS service which can be used without any knowledge of Backstage. It has an API -which a Backstage plugin consumes to display content. +The CircleCI plugin is an example of a third-party backend plugin. CircleCI is a SaaS service which can be used without any knowledge of Backstage. It has an API which a Backstage plugin consumes to display content. -Requests going to CircleCI from the user's browser are passed through a proxy -service that Backstage provides. Without this, the requests would be blocked by -Cross Origin Resource Sharing policies which prevent a browser page served at -[https://example.com](https://example.com) from serving resources hosted at -https://circleci.com. +Requests going to CircleCI from the user's browser are passed through a proxy service that Backstage provides. Without this, the requests would be blocked by Cross Origin Resource Sharing policies which prevent a browser page served at [https://example.com](https://example.com) from serving resources hosted at https://circleci.com. -![CircleCI plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/circle-ci-plugin-architecture.png) +> **NOTE:** +> The following diagram does not show the detailed contents of the frontend and backend, in order to highlight the changes that pertain to the addition of the specified plugin. + +![CircleCI plugin talking to proxy talking to SaaS Circle CI](../assets/architecture-overview/simplified-third-party-plugin-architecture.jpeg) ## Package Architecture -Backstage relies heavily on NPM packages, both for distribution of libraries, -and structuring of code within projects. While the way you structure your -Backstage project is up to you, there is a set of established patterns that we -encourage you to follow. These patterns can help set up a sound project -structure as well as provide familiarity between different Backstage projects. +Backstage relies heavily on NPM packages, both for distribution of libraries, and structuring of code within projects. While the way you structure your Backstage project is up to you, there is a set of established patterns that we encourage you to follow. These patterns can help set up a sound project structure as well as provide familiarity between different Backstage projects. -The following diagram shows an overview of the package architecture of -Backstage. It takes the point of view of an individual plugin and all of the -packages that it may contain, indicated by the thicker border and italic text. -Surrounding the plugin are different package groups which are the different -possible interface points of the plugin. Note that not all library package lists -are complete as packages have been omitted for brevity. +The following diagram shows an overview of the package architecture of Backstage. It takes the point of view of an individual plugin and all of the packages that it may contain, indicated by the thicker border and italic text. Surrounding the plugin are different package groups which are the different possible interface points of the plugin. Note that not all library package lists are complete as packages have been omitted for brevity. ![Package architecture](../assets/architecture-overview/package-architecture.drawio.svg) ### Overview -The arrows in the diagram above indicate a runtime dependency on the code of the -target package. This strict dependency graph only applies to runtime -`dependencies`, and there may be `devDependencies` that break the rules of this -table for the purpose of testing. While there are some arrows that show a -dependency on a collection of frontend, backend and isomorphic packages, those -still have to abide by important compatibility rules shown in the bottom left. +The arrows in the diagram above indicate a runtime dependency on the code of the target package. This strict dependency graph only applies to runtime +`dependencies`, and there may be `devDependencies` that break the rules of this table for the purpose of testing. While there are some arrows that show a dependency on a collection of frontend, backend and isomorphic packages, those still have to abide by important compatibility rules shown in the bottom left. -The `app` and `backend` packages are the entry points of a Backstage project. -The `app` package is the frontend application that brings together a collection -of frontend plugins and customizes them to fit an organization, while the -`backend` package is the backend service that powers the Backstage application. -Worth noting is that there can be more than one instance of each of these -packages within a project. Particularly the `backend` packages can benefit from -being split up into smaller deployment units that each serve their own purpose -with a smaller collection of plugins. +The `app` and `backend` packages are the entry points of a Backstage project. The `app` package is the frontend application that brings together a collection of frontend plugins and customizes them to fit an organization, while the `backend` package is the backend service that powers the Backstage application. Worth noting is that there can be more than one instance of each of these packages within a project. Particularly the `backend` packages can benefit from being split up into smaller deployment units that each serve their own purpose with a smaller collection of plugins. ### Plugin Packages -A typical plugin consists of up to five packages, two frontend ones, two -backend, and one isomorphic package. All packages within the plugin must share a -common prefix, typically of the form `@/plugin-`, but -alternatives like `backstage-plugin-` or -`@/backstage-plugin-` are also valid. Along with this prefix, -each of the packages have their own unique suffix that denotes their role. In -addition to these five plugin packages it's also possible for a plugin to have -additional frontend and backend modules that can be installed to enable optional -features. For a full list of suffixes and their roles, see the -[Plugin Package Structure ADR](../architecture-decisions/adr011-plugin-package-structure.md). +A typical plugin consists of up to five packages, two frontend ones, two backend, and one isomorphic package. All packages within the plugin must share a common prefix, typically of the form `@/plugin-`, but alternatives like `backstage-plugin-` or `@/backstage-plugin-` are also valid. Along with this prefix, each of the packages have their own unique suffix that denotes their role. In addition to these five plugin packages it's also possible for a plugin to have +additional frontend and backend modules that can be installed to enable optional features. For a full list of suffixes and their roles, see the [Plugin Package Structure ADR](../architecture-decisions/adr011-plugin-package-structure.md). -The `-react`, `-common`, and `-node` plugin packages together form the external -library of a plugin. The plugin library enables other plugins to build on top of -and extend a plugin, and likewise allows the plugin to depend on and extend -other plugins. Because of this, it is preferable that plugin library packages -allow duplicate installations of themselves, as you may end up with a mix of -versions being installed as dependencies of various plugins. It is also -forbidden for plugins to directly import non-library packages from other -plugins, all communication between plugins must be handled through libraries and -the application itself. +The `-react`, `-common`, and `-node` plugin packages together form the external library of a plugin. The plugin library enables other plugins to build on top of and extend a plugin, and likewise allows the plugin to depend on and extend other plugins. Because of this, it is preferable that plugin library packages allow duplicate installations of themselves, as you may end up with a mix of versions being installed as dependencies of various plugins. It is also forbidden for plugins to directly import non-library packages from other plugins, all communication between plugins must be handled through libraries and the application itself. ### Frontend Packages -The frontend packages are grouped into two main groups. The first one is -"Frontend App Core", which is the set of packages that are only used by the -`app` package itself. These packages help build up the core structure of the app -as well as provide a foundation for the plugin libraries to rely upon. +The frontend packages are grouped into two main groups. The first one is "Frontend App Core", which is the set of packages that are only used by the `app` package itself. These packages help build up the core structure of the app as well as provide a foundation for the plugin libraries to rely upon. -The second group is the rest of the shared packages, further divided into -"Frontend Plugin Core" and "Frontend Libraries". The core packages are -considered particularly stable and form the core of the frontend framework. -Their most important role is to form the boundary around each plugin and provide -a set of tools that helps you combine a collection of plugins into a running -application. The rest of the frontend packages are more traditional libraries -that serve as building blocks to create plugins. +The second group is the rest of the shared packages, further divided into "Frontend Plugin Core" and "Frontend Libraries". The core packages are considered particularly stable and form the core of the frontend framework. Their most important role is to form the boundary around each plugin and provide a set of tools that helps you combine a collection of plugins into a running application. The rest of the frontend packages are more traditional libraries that serve as building blocks to create plugins. ### Backend Packages -The backend library packages do not currently share a similar plugin -architecture as the frontend packages. They are instead simply a collection of -building blocks and patterns that help you build backend services. This is -however likely to change in the future. +The backend library packages do not currently share a similar plugin architecture as the frontend packages. They are instead simply a collection of building blocks and patterns that help you build backend services. This is however likely to change in the future. ### Common Packages -The common packages are the packages effectively depended on by all other pages. -This is a much smaller set of packages but they are also very pervasive. Because -the common packages are isomorphic and must execute both in the frontend and -backend, they are never allowed to depend on any of the frontend or backend -packages. +The common packages are the packages effectively depended on by all other pages. This is a much smaller set of packages but they are also very pervasive. Because the common packages are isomorphic and must execute both in the frontend and backend, they are never allowed to depend on any of the frontend or backend packages. -The Backstage CLI is in a category of its own and is depended on by virtually -all other packages. It's not a library in itself though, and must always be a -development dependency only. +The Backstage CLI is in a category of its own and is depended on by virtually all other packages. It's not a library in itself though, and must always be a development dependency only. ### Deciding where you place your code -It can sometimes be difficult to decide where to place your plugin code. For example -should it go directly in the `-backend` plugin package or in the `-node` package? -As a general guideline you should try to keep the exposure of your code as low -as possible. If it doesn't need to be public API, it's best to avoid. If you don't -need it to be used by other plugins, then keep it directly in the plugin packages. +It can sometimes be difficult to decide where to place your plugin code. For example, should it go directly in the `-backend` plugin package or in the `-node` package? As a general guideline you should try to keep the exposure of your code as low as possible. If it doesn't need to be public API, it's best to avoid. If you don't need it to be used by other plugins, then keep it directly in the plugin packages. Below is a chart to help you decide where to place your code. ![Package decision](../assets/architecture-overview/package-decision.drawio.svg) -## Databases - -As we have seen, both the `lighthouse-audit-service` and `catalog-backend` -require a database to work with. - -The Backstage backend and its built-in plugins are based on the -[Knex](http://knexjs.org/) library, and set up a separate logical database per -plugin. This gives great isolation and lets them perform migrations and evolve -separately from each other. - -The Knex library supports a multitude of databases, but Backstage at this time -of writing is tested primarily against two of them: SQLite, which is mainly used as -an in-memory mock/test database, and PostgreSQL, which is the preferred -production database. Other databases such as the MySQL variants are reported to -work but -[aren't fully tested](https://github.com/backstage/backstage/issues/2460) -yet. - ## Cache -The Backstage backend and its built-in plugins are also able to leverage cache -stores as a means of improving performance or reliability. Similar to how -databases are supported, plugins receive logically separated cache connections, -which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. +The Backstage backend and its built-in plugins are also able to leverage cache stores as a means of improving performance or reliability. Similar to how databases are supported, plugins receive logically separated cache connections, which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood. -At this time of writing, Backstage can be configured to use one of five cache -stores: memory, which is mainly used for local testing, memcache, redis, valkey or infinispan, -which are cache stores better suited for production deployment. The right cache -store for your Backstage instance will depend on your own run-time constraints -and those required of the plugins you're running. +At this time of writing, Backstage can be configured to use one of five cache stores: + +- `memory` +- `memcache` +- `redis` +- `valkey` +- `infinispan` + +`memory` is primarily used for local development, for production deployments, we recommend using one of the other cache stores. The right cache store for your Backstage instance will depend on your own run-time constraints and those required of the plugins you're running. ### Use memory for cache @@ -351,30 +298,3 @@ backend: ``` Contributions supporting other cache stores are welcome! - -## Containerization - -The example Backstage architecture shown above would Dockerize into three -separate Docker images. - -1. The frontend container -2. The backend container -3. The Lighthouse audit service container - -![Boxes around the architecture to indicate how it is containerised](../assets/architecture-overview/containerised.png) - -The backend container can be built by running the following command: - -```bash -yarn run build -yarn run build-image -``` - -This will create a container called `example-backend`. - -The lighthouse-audit-service container is already publicly available in Docker -Hub and can be downloaded and run with - -```bash -docker run spotify/lighthouse-audit-service:latest -``` diff --git a/docs/overview/technical-overview.md b/docs/overview/technical-overview.md new file mode 100644 index 0000000000..63c20dbf2f --- /dev/null +++ b/docs/overview/technical-overview.md @@ -0,0 +1,88 @@ +--- +id: technical-overview +title: Technical overview +description: Technical overview of Backstage +--- + +## Purpose + +Backstage is an open source framework for building developer portals that was created at Spotify to simplify end-to-end software development. As Spotify grew, their infrastructure became more fragmented and teams couldn't find the APIs they were supposed to use, or who owned a service, or documentation on anything. + +Backstage is powered by a centralized [software catalog](#software-catalog-system-model) and utilizes an abstraction layer that sits on top of all of your infrastructure and developer tooling, allowing you to manage all of your software, services, tooling, and testing in one place. + +Backstage uses a [plugin-architecture](#plugin-architecture-overview) which allows you customize the functionality of your Backstage application using a wide variety of available plugins or you can write your own. It also includes automated templates that your teams can use to create new microservices, helping to ensure consistency and adherence to your best practices. Backstage also provides the ability to create, maintain, and find the documentation for all of your software. + +Backstage is now a [CNCF incubation project](https://backstage.io/blog/2022/03/16/backstage-turns-two#out-of-the-sandbox-and-into-incubation). + +## Benefits + +- For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. +- For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. +- For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. +- For _everyone_, it is a single, consistent experience that ties all of your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. + +If you have question or want support, please join our [Discord server](https://discord.gg/backstage-687207715902193673). + +## Core Features + +Backstage includes the following set of core features: + +- [Authentication and Identity](../auth/index.md) - Sign-in and identification of users, and delegating access to third-party resources, using built-in authentication providers. +- [Kubernetes](../features/kubernetes/index.md) - A tool that allows developers to check the health of their services whether it is on a local host or in production. +- [Notifications](../notifications/index.md) - Provides a means for plugins and external services to send messages to either individual users or groups. +- [Permissions](../permissions/overview.md) - Ability to enforce rules concerning the type of access a user is given to specific data, APIs, or interface actions. +- [Search](https://backstage.io/docs/features/search/) - Search for information in the Backstage ecosystem. You can customize the look and feel of each search result and use your own search engine. +- [Software Catalog](../features/software-catalog/index.md) - A centralized system that contains metadata for all your software, such as services, websites, libraries, ML models, data pipelines, and so on. It can also contain metadata for the physical or virtual infrastructure needed to operate a piece of software. The software catalog can be viewed and searched through a UI. +- [Software Templates](../features/software-templates/index.md) - A tool to help you create components inside Backstage. A template can load skeletons of code, include some variables, and then publish the template to a location, such as GitHub. +- [TechDocs](https://backstage.io/docs/features/techdocs/) - A docs-like-code solution built into Backstage. Documentation is written in Markdown files which lives together with the code. + +## Plugin Architecture Overview + +Plugins are client side applications which mount themselves on the Backstage UI. They allow you to incorporate a wide variety of infrastructure and software development tools into your Backstage application. Backstage uses a [plugin-architecture](../overview/architecture-overview.md#plugin-architecture) to provide a consistent user experience, in a single UI, around all of your plugins. + +The Backstage architecture supports three types of plugins: + +- Standalone - runs entirely in a browser and it does not make any API requests to other services. +- Service backed - makes API requests to a service within the ecosystem of the organization running Backstage. +- Third-party backed - similar to service-backed, but the service backing the plugin is hosted outside of the ecosystem of the company hosting Backstage. + +Many of the features available in Backstage are provided by plugins. For example, the Software Catalog is a service backed plugin. When you view the catalog, it retrieves a set of services ("[entities](#software-catalog-system-model)") from the Backstage Backend service and renders them in a table in the UI for you. + +## Software Catalog System Model + +The system model behind the software catalog is based on [_entities_](../references/glossary.md#entity) and it models two main types: + +- Core Entities +- Organizational Entities + +`Core Entities` include: + +- `Components` - Individual pieces of software that can be tracked in source control and can implement APIs for other components to consume. +- `APIs` - Implemented by components and form the boundaries between different components. The API can be either public, restricted, or private. +- `Resources` - The physical or virtual infrastructure needed to operate a component. + +![core entity relationships in the software model](../assets/software-catalog/software-model-core-entities.drawio.svg) + +`Organizational Entities` include: + +- `User` - A person, such as an employee, contractor, or similar. +- `Group` - An organizational entity, such as a team, business unit, and so on. + +When you have a large catalogue of components, APIs, and resources, it can be difficult to understand how they work together. Ecosystem modeling allows you to organize a large catalog of core entities into: + +- Systems - A collection of resources and components that cooperate to perform a function by exposing one or several public APIs. It hides the resources and private APIs between the components from the consumer. +- Domains - A collection of systems that share terminology, domain models, metrics, KPIs, business purpose, or documentation. + +There are three additional items that can be part of the system model: + +- `Location` - A marker that references other places to look for catalog data. +- `Type` - It has no set meaning. You can assign your own types and use them as desired. +- `Template` - Describes both the parameters that are rendered in the frontend part of the scaffolding wizard, and the steps that are executed when scaffolding that component. + +The following diagram illustrates an example of ecosystem modeling, and provides sample relationships between a domain, system, core entities, and organization entities. + +![ecosystem modeling example](../assets/software-catalog/software-model-entities.drawio.svg) + +The following shows an example of viewing all of the components, APIs, and resources that are managed by your group after setting up the relationships to create a group organizational entity. + +![sample ui showing group ownership](../assets/technical-overview/backstage-ui-group-ownership.png) diff --git a/docs/releases/v1.44.0-next.2-changelog.md b/docs/releases/v1.44.0-next.2-changelog.md new file mode 100644 index 0000000000..74b467ebba --- /dev/null +++ b/docs/releases/v1.44.0-next.2-changelog.md @@ -0,0 +1,2298 @@ +# Release v1.44.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.44.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.44.0-next.2) + +## @backstage/config@1.3.5-next.0 + +# @backstage/config + +## 1.3.4-next.0 + +### Patch Changes + +- b45b094: Allow colon to be used as config key. + +## 1.3.3 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- 3507fcd: Just some more circular dep cleanup + +## 1.3.3-next.0 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- 3507fcd: Just some more circular dep cleanup + +## 1.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1 + - @backstage/errors@1.2.7 + +## 1.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1-next.0 + - @backstage/errors@1.2.7-next.0 + +## 1.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6 + - @backstage/types@1.2.0 + +## 1.3.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6-next.0 + - @backstage/types@1.2.0 + +## 1.3.0 + +### Minor Changes + +- d52d7f9: Make `readDurationFromConfig` support both ISO and ms formats as well, to make it easier to enter time as an end user + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.0 + - @backstage/errors@1.2.5 + +## 1.2.0 + +### Minor Changes + +- 50cf9df: The `ConfigReader` now treats `null` values as present but explicitly undefined, meaning it will not fall back to the next level of configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.2.0-next.1 + +### Minor Changes + +- 50cf9df: The `ConfigReader` now treats `null` values as present but explicitly undefined, meaning it will not fall back to the next level of configuration. + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.3-next.0 + - @backstage/types@1.1.1 + +## 1.1.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + +## 1.1.0-next.2 + +### Patch Changes + +- 406b786a2a2c: Mark package as being free of side effects, allowing more optimized Webpack builds. +- Updated dependencies + - @backstage/errors@1.2.2-next.0 + - @backstage/types@1.1.1-next.0 + +## 1.1.0-next.1 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.1.0-next.0 + +### Minor Changes + +- 62f448edb0b5: Added a `readDurationFromConfig` function + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.1.0 + +## 1.0.7 + +### Patch Changes + +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.7-next.0 + +### Patch Changes + +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.6 + +### Patch Changes + +- ba2d69ee17: Adds the ability to coerce values to their boolean representatives. + Values such as `"true"` `1` `on` and `y` will become `true` when using `getBoolean` and the opposites `false`. + This happens particularly when such parameters are used with environmental substitution as environment variables are always strings. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.6-next.0 + +### Patch Changes + +- ba2d69ee17: Adds the ability to coerce values to their boolean representatives. + Values such as `"true"` `1` `on` and `y` will become `true` when using `getBoolean` and the opposites `false`. + This happens particularly when such parameters are used with environmental substitution as environment variables are always strings. +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2 + +## 1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## 1.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + +## 1.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1-next.0 + +## 1.0.3 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 1.0.2 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. + +## 1.0.2-next.0 + +### Patch Changes + +- 7d47def9c4: Removed dependency on `@types/jest`. + +## 1.0.1 + +### Patch Changes + +- 6e830352d4: Updated dependency `@types/node` to `^16.0.0`. + +## 1.0.1-next.0 + +### Patch Changes + +- 6e830352d4: Updated dependency `@types/node` to `^16.0.0`. + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.0 + +## 0.1.15 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + +## 0.1.14 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/types@0.1.2 + +## 0.1.13 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +## 0.1.13-next.0 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +## 0.1.12 + +### Patch Changes + +- f5343e7c1a: The `ConfigReader#get` method now always returns a deep clone of the configuration data. + +## 0.1.11 + +### Patch Changes + +- 10d267a1b7: Minor exports cleanup +- 41c49884d2: Start using the new `@backstage/types` package. Initially, this means using the `Observable` and `Json*` types from there. The types also remain in their old places but deprecated, and will be removed in a future release. +- 925a967f36: Replace usage of test-utils-core with test-utils + +## 0.1.10 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability + +## 0.1.9 + +### Patch Changes + +- f88b2c7db: Documented `Config` interface and mark types as public. + +## 0.1.8 + +### Patch Changes + +- 47113f1f1: Only warn once per key when trying to read visibility-filtered values + +## 0.1.7 + +### Patch Changes + +- 90f25476a: Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. + +## 0.1.6 + +### Patch Changes + +- e9d3983ee: Add warning when trying to access configuration values that have been filtered out by visibility. + +## 0.1.5 + +### Patch Changes + +- d8b81fd28: Bump `json-schema` dependency from `0.2.5` to `0.3.0`. + +## 0.1.4 + +### Patch Changes + +- 0434853a5: Reformulate the json types to break type recursion + +## 0.1.3 + +### Patch Changes + +- a1f5e6545: Adds an optional type to `config.get` & `config.getOptional`. This avoids the need for casting. For example: + + ```ts + const config = useApi(configApiRef); + + const myConfig = config.get('myPlugin.complexConfig'); + // vs + const myConfig config.get('myPlugin.complexConfig') as SomeTypeDefinition; + ``` + +## 0.1.2 + +### Patch Changes + +- e3bd9fc2f: Fix unneeded defensive code +- e3bd9fc2f: Fix useless conditional + +## @backstage/config-loader@1.10.5-next.0 + +# @backstage/config-loader + +## 1.10.4-next.0 + +### Patch Changes + +- b45b094: Allow colon to be used as config key. +- Updated dependencies + - @backstage/config@1.3.4-next.0 + +## 1.10.3 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files +- Updated dependencies + - @backstage/types@1.2.2 + +## 1.10.3-next.0 + +### Patch Changes + +- a73f495: Allow using `BACKSTAGE_ENV` for loading environment specific config files + +## 1.10.2 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- Updated dependencies + - @backstage/config@1.3.3 + +## 1.10.2-next.0 + +### Patch Changes + +- ff23618: Loosen the requirements for a key to be considered valid config. +- Updated dependencies + - @backstage/config@1.3.3-next.0 + +## 1.10.1 + +### Patch Changes + +- 72d019d: Removed various typos +- Updated dependencies + - @backstage/config@1.3.2 + - @backstage/cli-common@0.1.15 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.1-next.0 + +### Patch Changes + +- 72d019d: Removed various typos +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.0 + +### Minor Changes + +- 2fd73aa: The include transforms applied during config loading will now only apply to the known keys `$file`, `$env`, and `$include`. Any other key that begins with a \`# @backstage/config-loader will now be passed through as is. + +### Patch Changes + +- f422984: Added `@types/minimist` to `devDependencies` +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.10.0-next.0 + +### Minor Changes + +- 2fd73aa: The include transforms applied during config loading will now only apply to the known keys `$file`, `$env`, and `$include`. Any other key that begins with a \`# @backstage/config-loader will now be passed through as is. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.6 + +### Patch Changes + +- f866b86: Internal refactor to use explicit `require` for lazy-loading dependency. +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.6-next.0 + +### Patch Changes + +- f866b86: Internal refactor to use explicit `require` for lazy-loading dependency. +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + - @backstage/types@1.2.1 + +## 1.9.5 + +### Patch Changes + +- 8ecf8cb: Exclude `@backstage/backend-common` from schema collection if `@backstage/backend-defaults` is present +- Updated dependencies + - @backstage/types@1.2.1 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.2 + - @backstage/errors@1.2.7 + +## 1.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.2.1-next.0 + - @backstage/config@1.3.2-next.0 + - @backstage/errors@1.2.7-next.0 + - @backstage/cli-common@0.1.15 + +## 1.9.5-next.0 + +### Patch Changes + +- 8ecf8cb: Exclude `@backstage/backend-common` from schema collection if `@backstage/backend-defaults` is present +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + - @backstage/errors@1.2.6 + - @backstage/types@1.2.0 + +## 1.9.3 + +### Patch Changes + +- 5c9cc05: Use native fetch instead of node-fetch +- Updated dependencies + - @backstage/errors@1.2.6 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1 + - @backstage/types@1.2.0 + +## 1.9.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.6-next.0 + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.1-next.0 + - @backstage/types@1.2.0 + +## 1.9.3-next.0 + +### Patch Changes + +- 5c9cc05: Use native fetch instead of node-fetch +- Updated dependencies + - @backstage/cli-common@0.1.15 + - @backstage/config@1.3.0 + - @backstage/errors@1.2.5 + - @backstage/types@1.2.0 + +## 1.9.2 + +### Patch Changes + +- c5e39e7: Internal refactor to use the deferred from the types package +- Updated dependencies + - @backstage/config@1.3.0 + - @backstage/types@1.2.0 + - @backstage/cli-common@0.1.15 + - @backstage/errors@1.2.5 + +## 1.9.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.15-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.1 + +### Patch Changes + +- ef3c507: Updated dependency `typescript-json-schema` to `^0.65.0`. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.1-next.0 + +### Patch Changes + +- ef3c507: Updated dependency `typescript-json-schema` to `^0.65.0`. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0-next.1 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.2-next.0 + +### Patch Changes + +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.0 + +### Minor Changes + +- 2ce31b3: The default environment variable substitution function will now trim whitespace characters from the substituted value. This alleviates bugs where whitespace characters are mistakenly included in environment variables. + + If you depend on the old behavior, you can override the default substitution function with your own, for example: + + ```ts + ConfigSources.default({ + substitutionFunc: async name => process.env[name], + }); + ``` + +- 99bab65: Support parameter substitution for environment variables + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.0-next.0 + +### Minor Changes + +- 99bab65: Support parameter substitution for environment variables + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.7.0 + +### Minor Changes + +- db8358d: Forward `null` values read from configuration files in configuration data, rather than treating them as an absence of config. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## 1.7.0-next.1 + +### Minor Changes + +- db8358d: Forward `null` values read from configuration files in configuration data, rather than treating them as an absence of config. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0-next.1 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.4-next.0 + - @backstage/types@1.1.1 + +## 1.6.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.4-next.0 + - @backstage/config@1.1.2-next.0 + - @backstage/cli-common@0.1.13 + - @backstage/types@1.1.1 + +## 1.6.2 + +### Patch Changes + +- 0a9a03c: Make schema processing gracefully handle an empty config. +- 6bb6f3e: Updated dependency `fs-extra` to `^11.2.0`. + Updated dependency `@types/fs-extra` to `^11.0.0`. +- bf3da16: Updated dependency `typescript-json-schema` to `^0.63.0`. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.2-next.0 + +### Patch Changes + +- 0a9a03c: Make schema processing gracefully handle an empty config. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.1 + +### Patch Changes + +- 7acbb5a: Removed `mock-fs` dev dependency. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.1-next.0 + +### Patch Changes + +- 7acbb5a: Removed `mock-fs` dev dependency. +- Updated dependencies + - @backstage/config@1.1.1 + - @backstage/cli-common@0.1.13 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.0 + +### Minor Changes + +- 24f5a85: Add "path" to `TransformFunc` context + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.6.0-next.0 + +### Minor Changes + +- 24f5a85: Add "path" to `TransformFunc` context + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.3 + +### Patch Changes + +- 22ca64f117: Correctly resolve config targets into absolute paths +- 087bab5b42: Updated dependency `typescript-json-schema` to `^0.62.0`. +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.2-next.0 + +### Patch Changes + +- 22ca64f117: Correctly resolve config targets into absolute paths +- Updated dependencies + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/errors@1.2.3 + - @backstage/types@1.1.1 + +## 1.5.1 + +### Patch Changes + +- 0b55f773a7: Removed some unused dependencies +- 30c553c1d2: Updated dependency `typescript-json-schema` to `^0.61.0`. +- 773ea341d2: The `FileConfigSource` will now retry file reading after a short delay if it reads an empty file. This is to avoid flakiness during watch mode where change events can trigger before the file content has been written. +- a4617c422a: Added `watch` option to configuration loaders that can be used to disable file watching by setting it to `false`. +- Updated dependencies + - @backstage/errors@1.2.3 + - @backstage/cli-common@0.1.13 + - @backstage/config@1.1.1 + - @backstage/types@1.1.1 + +## 1.5.1-next.1 + +### Patch Changes + +- 0b55f773a7: Removed some unused dependencies +- 30c553c1d2: Updated dependency `typescript-json-schema` to `^0.61.0`. +- a4617c422a: Added `watch` option to configuration loaders that can be used to disable file watching by setting it to `false`. +- Updated dependencies + - @backstage/errors@1.2.3-next.0 + - @backstage/cli-common@0.1.13-next.0 + - @backstage/config@1.1.1-next.0 + - @backstage/types@1.1.1 + +## 1.5.1-next.0 + +### Patch Changes + +- 773ea341d2: The `FileConfigSource` will now retry file reading after a short delay if it reads an empty file. This is to avoid flakiness during watch mode where change events can trigger before the file content has been written. +- Updated dependencies + - @backstage/cli-common@0.1.13-next.0 + - @backstage/config@1.1.0 + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + +## 1.5.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/config@1.1.0 + - @backstage/errors@1.2.2 + - @backstage/types@1.1.1 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.2 + - @backstage/errors@1.2.2-next.0 + - @backstage/types@1.1.1-next.0 + - @backstage/cli-common@0.1.12 + +## 1.5.0-next.2 + +### Patch Changes + +- 8cec7664e146: Removed `@types/node` dependency +- Updated dependencies + - @backstage/config@1.1.0-next.1 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.1.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.5.0-next.0 + +### Minor Changes + +- 9606ba0939e6: Deep visibility now also applies to values that are not covered by the configuration schema. + + For example, given the following configuration schema: + + ```ts + // plugins/a/config.schema.ts + export interface Config { + /** @deepVisibility frontend */ + a?: unknown; + } + + // plugins/a/config.schema.ts + export interface Config { + a?: { + b?: string; + }; + } + ``` + + All values under `a` are now visible to the frontend, while previously only `a` and `a/b` would've been visible. + +### Patch Changes + +- f9657b891b00: Do not unnecessarily notify subscribers when no-op updates to config happen +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0-next.1 + +### Minor Changes + +- 2f1859585998: Loading invalid TypeScript configuration schemas will now throw an error rather than silently being ignored. + + In particular this includes defining any additional types other than `Config` in the schema file, or use of unsupported types such as `Record` or `Partial`. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.4.0-next.0 + +### Minor Changes + +- cd514545d1d0: Adds a new `deepVisibility` schema keyword that sets child visibility recursively to the defined value, respecting preexisting values or child `deepVisibility`. + + Example usage: + + ```ts + export interface Config { + /** + * Enforces a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + secretValue: { + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + + Example of a schema that would not be allowed: + + ```ts + export interface Config { + /** + * Set the top level property to secret, enforcing a default of `secret` instead of `backend` for this object. + * @deepVisibility secret + */ + mySecretProperty: { + type: 'object'; + properties: { + frontendUrl: { + /** + * We can NOT override the visibility to reveal a property to the front end. + * @visibility frontend + */ + type: 'string'; + }; + + verySecretProperty: { + type: 'string'; + }; + }; + }; + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/errors@1.2.1 + - @backstage/types@1.1.0 + +## 1.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 1.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.1-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + - @backstage/types@1.1.0 + +## 1.3.1 + +### Patch Changes + +- f25427f665f7: Fix a bug where config items with `/` in the key were incorrectly handled. +- a5c5491ff50c: Use `durationToMilliseconds` from `@backstage/types` instead of our own +- Updated dependencies + - @backstage/types@1.1.0 + - @backstage/errors@1.2.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.8 + +## 1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.2.0-next.0 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/types@1.0.2 + +## 1.3.1-next.0 + +### Patch Changes + +- f25427f665f7: Fix a bug where config items with `/` in the key were incorrectly handled. +- Updated dependencies + - @backstage/config@1.0.7 + - @backstage/cli-common@0.1.12 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.3.0 + +### Minor Changes + +- 201206132da: Introduced a new config source system to replace `loadConfig`. There is a new `ConfigSource` interface along with utilities provided by `ConfigSources`, as well as a number of built-in configuration source implementations. The new system is more flexible and makes it easier to create new and reusable sources of configuration, such as loading configuration from secret providers. + + The following is an example of how to load configuration using the default behavior: + + ```ts + const source = ConfigSources.default({ + argv: options?.argv, + remote: options?.remote, + }); + const config = await ConfigSources.toConfig(source); + ``` + + The `ConfigSource` interface looks like this: + + ```ts + export interface ConfigSource { + readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator; + } + ``` + + It is best implemented using an async iterator: + + ```ts + class MyConfigSource implements ConfigSource { + async *readConfigData() { + yield { + config: [ + { + context: 'example', + data: { backend: { baseUrl: 'http://localhost' } }, + }, + ], + }; + } + } + ``` + +### Patch Changes + +- 7c116bcac7f: Fixed the way that some request errors are thrown +- 473db605a4f: Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config. +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.3.0-next.0 + +### Minor Changes + +- 201206132da: Introduced a new config source system to replace `loadConfig`. There is a new `ConfigSource` interface along with utilities provided by `ConfigSources`, as well as a number of built-in configuration source implementations. The new system is more flexible and makes it easier to create new and reusable sources of configuration, such as loading configuration from secret providers. + + The following is an example of how to load configuration using the default behavior: + + ```ts + const source = ConfigSources.default({ + argv: options?.argv, + remote: options?.remote, + }); + const config = await ConfigSources.toConfig(source); + ``` + + The `ConfigSource` interface looks like this: + + ```ts + export interface ConfigSource { + readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator; + } + ``` + + It is best implemented using an async iterator: + + ```ts + class MyConfigSource implements ConfigSource { + async *readConfigData() { + yield { + config: [ + { + context: 'example', + data: { backend: { baseUrl: 'http://localhost' } }, + }, + ], + }; + } + } + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.2.0 + +### Minor Changes + +- c791fcd96b9: Configuration validation is now more permissive when it comes to config whose values are `string` but whose schemas declare them to be `boolean` or `number`. + + For example, configuration was previously marked invalid when a string `'true'` was set on a property expecting type `boolean` or a string `'146'` was set on a property expecting type `number` (as when providing configuration via variable substitution sourced from environment variables). Now, such configurations will be considered valid and their values will be coerced to the right type at read-time. + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/errors@1.1.5 + - @backstage/types@1.0.2 + +## 1.1.9 + +### Patch Changes + +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/errors@1.1.5 + - @backstage/cli-common@0.1.12 + - @backstage/config@1.0.7 + - @backstage/types@1.0.2 + +## 1.1.9-next.0 + +### Patch Changes + +- 52b0022dab7: Updated dependency `msw` to `^1.0.0`. +- 482dae5de1c: Updated link to docs. +- Updated dependencies + - @backstage/errors@1.1.5-next.0 + - @backstage/cli-common@0.1.12-next.0 + - @backstage/config@1.0.7-next.0 + - @backstage/types@1.0.2 + +## 1.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## 1.1.7 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 40e7e6e1a2: Updated dependency `typescript-json-schema` to `^0.55.0`. +- Updated dependencies + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.5 + +## 1.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.11-next.0 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/types@1.0.2-next.1 + +## 1.1.7-next.1 + +### Patch Changes + +- 40e7e6e1a2: Updated dependency `typescript-json-schema` to `^0.55.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/cli-common@0.1.10 + - @backstage/errors@1.1.4-next.1 + +## 1.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 1.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 1.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## 1.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3 + - @backstage/errors@1.1.2 + - @backstage/types@1.0.0 + +## 1.1.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.2 + - @backstage/errors@1.1.2-next.2 + - @backstage/types@1.0.0 + +## 1.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.1 + - @backstage/errors@1.1.2-next.1 + - @backstage/types@1.0.0 + +## 1.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.3-next.0 + - @backstage/errors@1.1.2-next.0 + - @backstage/types@1.0.0 + +## 1.1.4 + +### Patch Changes + +- 5ecca7e44b: No longer log when reloading remote config. +- 7d47def9c4: Removed dependency on `@types/jest`. +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.2 + - @backstage/errors@1.1.1 + +## 1.1.4-next.2 + +### Patch Changes + +- 5ecca7e44b: No longer log when reloading remote config. +- 7d47def9c4: Removed dependency on `@types/jest`. +- Updated dependencies + - @backstage/cli-common@0.1.10-next.0 + - @backstage/config@1.0.2-next.0 + - @backstage/errors@1.1.1-next.0 + +## 1.1.4-next.1 + +### Patch Changes + +- 667d917488: Updated dependency `msw` to `^0.47.0`. +- 87ec2ba4d6: Updated dependency `msw` to `^0.46.0`. + +## 1.1.4-next.0 + +### Patch Changes + +- bf5e9030eb: Updated dependency `msw` to `^0.45.0`. + +## 1.1.3 + +### Patch Changes + +- bcada7cd9f: From now on the `$file` placeholder will trim the whitespaces and newline characters from the end of the file it reads. +- a70869e775: Updated dependency `msw` to `^0.43.0`. +- 72622d9143: Updated dependency `yaml` to `^2.0.0`. +- 8006d0f9bf: Updated dependency `msw` to `^0.44.0`. +- a3acec8819: Updated dependency `typescript-json-schema` to `^0.54.0`. +- Updated dependencies + - @backstage/errors@1.1.0 + +## 1.1.3-next.1 + +### Patch Changes + +- a70869e775: Updated dependency `msw` to `^0.43.0`. +- 72622d9143: Updated dependency `yaml` to `^2.0.0`. +- a3acec8819: Updated dependency `typescript-json-schema` to `^0.54.0`. + +## 1.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.1.0-next.0 + +## 1.1.2 + +### Patch Changes + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. + +## 1.1.2-next.0 + +### Patch Changes + +- 8f7b1835df: Updated dependency `msw` to `^0.41.0`. + +## 1.1.1 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully +- Updated dependencies + - @backstage/cli-common@0.1.9 + - @backstage/config@1.0.1 + +## 1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.9-next.0 + - @backstage/config@1.0.1-next.0 + +## 1.1.1-next.0 + +### Patch Changes + +- cfc0f19699: Updated dependency `fs-extra` to `10.1.0`. +- 9e8ef53498: Handle empty config files gracefully + +## 1.1.0 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- 230ad0826f: Bump to using `@types/node` v16 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## 1.1.0-next.1 + +### Minor Changes + +- 19f6c6c32a: Added `ignoreSchemaErrors` to `schema.process`. + +### Patch Changes + +- 230ad0826f: Bump to using `@types/node` v16 + +## 1.0.1-next.0 + +### Patch Changes + +- e0a51384ac: build(deps): bump `ajv` from 7.0.3 to 8.10.0 +- c47509e1a0: Implemented changes suggested by Deepsource.io including multiple double non-null assertion operators and unexpected awaits for non-promise values. + +## 1.0.0 + +### Major Changes + +- b58c70c223: This package has been promoted to v1.0! To understand how this change affects the package, please check out our [versioning policy](https://backstage.io/docs/overview/versioning-policy). + +### Patch Changes + +- 664821371e: The `typescript-json-schema` dependency that is used during schema collection is now lazy loaded, as it eagerly loads in the TypeScript compiler. +- f910c2a3f8: build(deps): bump `typescript-json-schema` from 0.52.0 to 0.53.0 +- Updated dependencies + - @backstage/config@1.0.0 + - @backstage/errors@1.0.0 + - @backstage/types@1.0.0 + +## 0.9.7 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 + +## 0.9.7-next.0 + +### Patch Changes + +- e0a69ba49f: build(deps): bump `fs-extra` from 9.1.0 to 10.0.1 + +## 0.9.6 + +### Patch Changes + +- c3a1300f79: Include any files included in configuration via $include or $file directives when watching for configuration changes. + +## 0.9.5 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/cli-common@0.1.8 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/types@0.1.3 + +## 0.9.4 + +### Patch Changes + +- 1ed305728b: Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/errors@0.2.1 + - @backstage/cli-common@0.1.7 + - @backstage/config@0.1.14 + - @backstage/types@0.1.2 + +## 0.9.3 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +- Updated dependencies + - @backstage/config@0.1.13 + +## 0.9.3-next.0 + +### Patch Changes + +- f685e1398f: Loading of app configurations now reference the `@deprecated` construct from + JSDoc to determine if a property in-use has been deprecated. Users are notified + of deprecated keys in the format: + + ```txt + The configuration key 'catalog.processors.githubOrg' of app-config.yaml is deprecated and may be removed soon. Configure a GitHub integration instead. + ``` + + When the `withDeprecatedKeys` option is set to `true` in the `process` method + of `loadConfigSchema`, the user will be notified that deprecated keys have been + identified in their app configuration. + + The `backend-common` and `plugin-app-backend` packages have been updated to set + `withDeprecatedKeys` to true so that users are notified of deprecated settings + by default. + +- Updated dependencies + - @backstage/config@0.1.13-next.0 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@0.1.12 + - @backstage/errors@0.2.0 + +## 0.9.1 + +### Patch Changes + +- 84663d59a3: Bump `typescript-json-schema` from `^0.51.0` to `^0.52.0`. + +## 0.9.0 + +### Minor Changes + +- f6722d2458: Removed deprecated option `env` from `LoadConfigOptions` and associated tests +- 67d6cb3c7e: Removed deprecated option `configPaths` as it has been superseded by `configTargets` + +### Patch Changes + +- 1e7070443d: In case remote.reloadIntervalSeconds is passed, it must be a valid positive value + +## 0.8.1 + +### Patch Changes + +- b055a6addc: Align on usage of `cross-fetch` vs `node-fetch` in frontend vs backend packages, and remove some unnecessary imports of either one of them +- 4bea7b81d3: Uses key visibility as fallback in non-object arrays + +## 0.8.0 + +### Minor Changes + +- 1e99c73c75: Update `loadConfig` to return `LoadConfigResult` instead of an array of `AppConfig`. + + This function is primarily used internally by other config loaders like `loadBackendConfig` which means no changes are required for most users. + + If you use `loadConfig` directly you will need to update your usage from: + + ```diff + - const appConfigs = await loadConfig(options) + + const { appConfigs } = await loadConfig(options) + ``` + +### Patch Changes + +- 8809b6c0dd: Update the json-schema dependency version. +- Updated dependencies + - @backstage/cli-common@0.1.6 + +## 0.7.2 + +### Patch Changes + +- 0611f3b3e2: Reading app config from a remote server +- 26c5659c97: Bump msw to the same version as the rest + +## 0.7.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` +- ea21f7f567: bump `typescript-json-schema` from 0.50.1 to 0.51.0 +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/cli-common@0.1.5 + - @backstage/errors@0.1.4 + +## 0.7.0 + +### Minor Changes + +- 7e97d0b8c1: Removed the `EnvFunc` public export. Its only usage was to be passed in to `LoadConfigOptions.experimentalEnvFunc`. If you were using this type, add a definition in your own project instead with the signature `(name: string) => Promise`. + +### Patch Changes + +- 223e8de6b4: Configuration schema errors are now filtered using the provided visibility option. This means that schema errors due to missing backend configuration will no longer break frontend builds. +- 7e97d0b8c1: Add public tags and documentation +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/errors@0.1.3 + +## 0.6.10 + +### Patch Changes + +- 957e4b3351: Updated dependencies +- Updated dependencies + - @backstage/cli-common@0.1.4 + +## 0.6.9 + +### Patch Changes + +- ee7a1a4b64: Add option to collect configuration schemas from explicit package paths in addition to by package name. +- e68bd978e2: Allow collection of configuration schemas from multiple versions of the same package. + +## 0.6.8 + +### Patch Changes + +- d1da88a19: Properly export all used types. +- Updated dependencies + - @backstage/cli-common@0.1.3 + - @backstage/config@0.1.9 + +## 0.6.7 + +### Patch Changes + +- 0ade9d02b: Include `devDependencies` and `optionalDependencies` in the detection of Backstage packages when collecting configuration schema. +- 9b8cec063: Add support for config file watching through a new group of `watch` options to `loadConfig`. +- Updated dependencies + - @backstage/config@0.1.7 + +## 0.6.6 + +### Patch Changes + +- e9d3983ee: Add option to populate the `filteredKeys` property when processing configuration with a schema. +- Updated dependencies + - @backstage/config@0.1.6 + +## 0.6.5 + +### Patch Changes + +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. + +## 0.6.4 + +### Patch Changes + +- f00493739: Removed workaround for breaking change in typescript 4.3 and bump `typescript-json-schema` instead. This should again allow the usage of `@items.visibility ` to set the visibility of array items. + +## 0.6.3 + +### Patch Changes + +- 2cf98d279: Resolve the path to app-config.yaml from the current working directory. This will allow use of `yarn link` or running the CLI in other directories and improve the experience for local backstage development. +- 438a512eb: Fixed configuration schema parsing when using TypeScript `4.3`. + +## 0.6.2 + +### Patch Changes + +- 290405276: Updated dependencies + +## 0.6.1 + +### Patch Changes + +- d8b81fd28: Bump `json-schema` dependency from `0.2.5` to `0.3.0`. +- Updated dependencies [d8b81fd28] + - @backstage/config@0.1.5 + +## 0.6.0 + +### Minor Changes + +- 82c66b8cd: Fix bug where `${...}` was not being escaped to `${...}` + + Add support for environment variable substitution in `$include`, `$file` and + `$env` transform values. + + - This change allows for including dynamic paths, such as environment specific + secrets by using the same environment variable substitution (`${..}`) already + supported outside of the various include transforms. + - If you are currently using the syntax `${...}` in your include transform values, + you will need to escape the substitution by using `${...}` instead to maintain + the same behavior. + +## 0.5.1 + +### Patch Changes + +- 062df71db: Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere +- e9aab60c7: Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. + +## 0.5.0 + +### Minor Changes + +- ef7957be4: Removed support for the deprecated `$data` placeholder. +- ef7957be4: Enable further processing of configuration files included using the `$include` placeholder. Meaning that for example for example `$env` includes will be processed as usual in included files. + +### Patch Changes + +- ef7957be4: Added support for environment variable substitutions in string configuration values using a `${VAR}` placeholder. All environment variables must be available, or the entire expression will be evaluated to `undefined`. To escape a substitution, use `${...}`, which will end up as `${...}`. + + For example: + + ```yaml + app: + baseUrl: https://${BASE_HOST} + ``` + +## 0.4.1 + +### Patch Changes + +- ad5c56fd9: Deprecate `$data` and replace it with `$include` which allows for any type of json value to be read from external files. In addition, `$include` can be used without a path, which causes the value at the root of the file to be loaded. + + Most usages of `$data` can be directly replaced with `$include`, except if the referenced value is not a string, in which case the value needs to be changed. For example: + + ```yaml + # app-config.yaml + foo: + $data: foo.yaml#myValue # replacing with $include will turn the value into a number + $data: bar.yaml#myValue # replacing with $include is safe + + # foo.yaml + myValue: 0xf00 + + # bar.yaml + myValue: bar + ``` + +## 0.4.0 + +### Minor Changes + +- 4e7091759: Fix typo of "visibility" in config schema reference + + If you have defined a config element named `visiblity`, you + will need to fix the spelling to `visibility`. For more info, + see . + +### Patch Changes + +- b4488ddb0: Added a type alias for PositionError = GeolocationPositionError + +## 0.3.0 + +### Minor Changes + +- 1722cb53c: Added support for loading and validating configuration schemas, as well as declaring config visibility through schemas. + + The new `loadConfigSchema` function exported by `@backstage/config-loader` allows for the collection and merging of configuration schemas from all nearby dependencies of the project. + + A configuration schema is declared using the `https://backstage.io/schema/config-v1` JSON Schema meta schema, which is based on draft07. The only difference to the draft07 schema is the custom `visibility` keyword, which is used to indicate whether the given config value should be visible in the frontend or not. The possible values are `frontend`, `backend`, and `secret`, where `backend` is the default. A visibility of `secret` has the same scope at runtime, but it will be treated with more care in certain contexts, and defining both `frontend` and `secret` for the same value in two different schemas will result in an error during schema merging. + + Packages that wish to contribute configuration schema should declare it in a root `"configSchema"` field in `package.json`. The field can either contain an inlined JSON schema, or a relative path to a schema file. Schema files can be in either `.json` or `.d.ts` format. + + TypeScript configuration schema files should export a single `Config` type, for example: + + ```ts + export interface Config { + app: { + /** + * Frontend root URL + * @visibility frontend + */ + baseUrl: string; + }; + } + ``` + +## 0.2.0 + +### Minor Changes + +- 8c2b76e45: **BREAKING CHANGE** + + The existing loading of additional config files like `app-config.development.yaml` using APP_ENV or NODE_ENV has been removed. + Instead, the CLI and backend process now accept one or more `--config` flags to load config files. + + Without passing any flags, `app-config.yaml` and, if it exists, `app-config.local.yaml` will be loaded. + If passing any `--config ` flags, only those files will be loaded, **NOT** the default `app-config.yaml` one. + + The old behaviour of for example `APP_ENV=development` can be replicated using the following flags: + + ```bash + --config ../../app-config.yaml --config ../../app-config.development.yaml + ``` + +- ce5512bc0: Added support for new shorthand when defining secrets, where `$env: ENV` can be used instead of `$secret: { env: ENV }` etc. + +## @backstage/backend-defaults@0.13.0-next.2 + +### Minor Changes + +- 8495b18: Add a new `externalTokenHandlersServiceRef` to allow custom external token validations + +## @backstage/plugin-mui-to-bui@0.2.0-next.0 + +### Minor Changes + +- d5cbdba: This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.2-next.1 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-notifications-backend-module-slack@0.2.0-next.1 + +### Minor Changes + +- 3d09bb2: Adds username as optional config in order to send Slack notifications with a specific username in the case when using one Slack App for more than just Backstage. + +## @backstage/app-defaults@1.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/cli@0.34.4-next.2 + +### Patch Changes + +- ab96bb7: Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + + **Example usage:** + + Consider the following plugin dev folder structure: + + dev/ + index.tsx + alpha/ + index.ts + + - The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; + - Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. + +## @backstage/core-components@0.18.2-next.2 + +### Patch Changes + +- 95935fb: Fixed dependency graph automatically scrolling forever +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + +## @backstage/create-app@0.7.5-next.2 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/dev-utils@1.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + +## @backstage/test-utils@1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + +## @backstage/theme@0.6.9-next.0 + +### Patch Changes + +- d5cbdba: The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered. + +## @backstage/ui@0.7.2-next.1 + +### Patch Changes + +- a9b88be: Enable tooltips on disabled buttons with automatic wrapper +- 4adbb03: Avoid overriding onChange when spreading props + +## @backstage/plugin-app@0.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-auth@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-catalog-backend@3.1.2-next.2 + +### Patch Changes + +- 6493c98: Log before provider-orphaning eviction happens +- 77516c5: Added new `catalog:validate-entity` action to actions registry. + + This action can be used to validate entities against the software catalog. + This is useful for validating `catalog-info.yaml` file changes locally using the + Backstage MCP server. + +## @backstage/plugin-home@0.8.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-kubernetes-react@0.5.12-next.2 + +### Patch Changes + +- ac405f2: The configmaps added to be rendered +- f7a4144: Fixes calculation of CPU utilization in the PodTable +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + +## @backstage/plugin-notifications@0.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-notifications-backend@0.5.11-next.1 + +### Patch Changes + +- 3b8e156: Fixed exclude entity reference not working in notification sending + +## @backstage/plugin-org@0.6.45-next.2 + +### Patch Changes + +- 8b7351f: Add `initialRelationAggregation` and `showAggregateMembersToggle` options to `EntityMembersListCard` as well to `EntityOwnershipCard` +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + +## @backstage/plugin-scaffolder@1.34.2-next.2 + +### Patch Changes + +- 075e064: Added missing form fields for the new frontend system. +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + +## @backstage/plugin-scaffolder-react@1.19.2-next.2 + +### Patch Changes + +- e61f89e: Don't change loading to false until we've actually got some log state +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-search-react@1.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-signals@0.0.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## @backstage/plugin-techdocs@1.15.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + +## @backstage/plugin-user-settings@0.8.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + +## example-app@0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mui-to-bui@0.2.0-next.0 + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + +## example-app-next@0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-app@0.3.1-next.2 + - @backstage/plugin-auth@0.1.1-next.1 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + +## techdocs-cli-embedded-app@0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.4-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/test-utils@1.7.12-next.1 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 diff --git a/docs/releases/v1.44.0-next.3-changelog.md b/docs/releases/v1.44.0-next.3-changelog.md new file mode 100644 index 0000000000..8a65d6d6a6 --- /dev/null +++ b/docs/releases/v1.44.0-next.3-changelog.md @@ -0,0 +1,219 @@ +# Release v1.44.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.44.0-next.3) + +## @backstage/backend-app-api@1.2.8-next.1 + +### Patch Changes + +- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. + +## @backstage/cli@0.34.4-next.3 + +### Patch Changes + +- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. + +- 024645e: Remove unused @octokit modules from cli package + + - @octokit/graphql + - @octokit/graphql-schema + - @octokit/oauth-app + +- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. + +## @backstage/core-components@0.18.2-next.3 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. + +## @backstage/create-app@0.7.5-next.3 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/frontend-plugin-api@0.12.1-next.2 + +### Patch Changes + +- 8ed53eb: Added `coreExtensionData.title`, especially useful for creating extensible layout with tabbed pages, but available for use for other cases too. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + +## @backstage/ui@0.7.2-next.2 + +### Patch Changes + +- 3c921c5: Making href mandatory in tabs that are part of a Header component +- 5c21e45: Add react router for internal routing for ButtonLinks +- 9781815: Remove auto selection of tabs for tabs that all have href defined +- f6dff5b: Using react router for internal links in the Menu component + +## @backstage/plugin-app-visualizer@0.1.24-next.2 + +### Patch Changes + +- 4406144: Ensure that the text rendering has react keys for all elements +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-catalog-backend-module-aws@0.4.16-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-azure@0.3.10-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.5.4-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-gerrit@0.3.7-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-github@0.11.1-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-github-org@0.3.15-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.11.1-next.2 + +## @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2 + +### Patch Changes + +- 0443119: Fixed an issue in `GitlabDiscoveryEntityProvider` where entity fetching could fail for projects with special characters or that had been renamed or moved. +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.2.14-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.7.4-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.8.1-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-backend-module-puppetdb@0.2.15-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + +## @backstage/plugin-catalog-graph@0.5.2-next.2 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-home@0.8.13-next.3 + +### Patch Changes + +- e7d59d3: fix(home): correct `clearAll` logic to properly handle `deletable` flag +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-kubernetes@0.12.12-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-kubernetes-cluster@0.0.30-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + +## @backstage/plugin-notifications@0.5.10-next.3 + +### Patch Changes + +- f5e0963: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-notifications-backend@0.5.11-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-notifications-backend-module-email@0.3.14-next.1 + +### Patch Changes + +- b8cf31a: chore(deps): bump `nodemailer` from 6.9.16 to 7.0.7 +- f5e0963: Removed unused dependencies + +## @backstage/plugin-scaffolder@1.34.2-next.3 + +### Patch Changes + +- d9aed74: Forward `ui:disabled` in `OwnedEntityPicker` to allow disabling it +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.2.14-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.9.6-next.2 + +### Patch Changes + +- f5e0963: Removed unused dependencies + +## @backstage/plugin-signals@0.0.24-next.3 + +### Patch Changes + +- 7b41d9d: Remove `app-root-element:signals/signals-display` which was not doing anything useful +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index d176278655..ce8f862c98 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -177,11 +177,12 @@ Usage: backstage-cli package start [options] Start a package for local development Options: - --config Config files to load instead of app-config.yaml (default: []) - --role Run the command with an explicit package role - --check Enable type checking and linting if available - --inspect Enable debugger in Node.js environments - --inspect-brk Enable debugger in Node.js environments, breaking before code starts + --config Config files to load instead of app-config.yaml (default: []) + --role Run the command with an explicit package role + --check Enable type checking and linting if available + --inspect Enable debugger in Node.js environments + --inspect-brk Enable debugger in Node.js environments, breaking before code starts + --entrypoint Entry directory path (uses index file) or entry file path (without extension). Defaults to "dev" ``` ## package build diff --git a/docs/tooling/cli/04-templates.md b/docs/tooling/cli/04-templates.md index c3bfb57d11..f634894c4f 100644 --- a/docs/tooling/cli/04-templates.md +++ b/docs/tooling/cli/04-templates.md @@ -147,3 +147,13 @@ The `role` property in the template yaml file is used to determine what input wi | `plugin-web-library` | `pluginId` | `plugins` | none | | `plugin-node-library` | `pluginId` | `plugins` | none | | `plugin-common-library` | `pluginId` | `plugins` | none | + +## Dependency Versioning + +The `yarn new` command automatically detects if the [Backstage Yarn plugin](https://github.com/backstage/backstage/tree/master/packages/yarn-plugin) is installed in your repository and adjusts dependency versioning accordingly. + +When the Backstage Yarn plugin is installed (detected via `.yarnrc.yml`), `yarn new` will generate `backstage:^` ranges for all `@backstage/*` dependencies. This ensures that new packages use the same Backstage version as defined in your `backstage.json` file. + +When the plugin is not installed, `yarn new` uses the standard npm version ranges (e.g., `^1.0.0`) for all dependencies, maintaining backward compatibility. + +Regardless of plugin installation, `workspace:` ranges found in your `yarn.lock` file will always take precedence over both `backstage:^` and npm ranges. This ensures that packages within monorepos continue to use workspace linking when available. diff --git a/docs/tooling/package-metadata.md b/docs/tooling/package-metadata.md index ad9d5ec5a0..3911ada413 100644 --- a/docs/tooling/package-metadata.md +++ b/docs/tooling/package-metadata.md @@ -96,6 +96,32 @@ This field can be generated by the `backstage-cli repo fix --publish` command. T The presence of this field is checked by the `backstage-cli package prepack` command, which is used to prepare a package for publishing. You can read more about this requirement in the section on [metadata for published packages](#metadata-for-published-packages). +### `backstage.features` + +This field declares where to find Backstage features exported from the package. It is a map of features export paths to their feature type, similar to the `exports` field. + +```js title="Example usage of the backstage.features field" +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "features": { + "./alpha": "@backstage/FrontendPlugin" + } + } +} +``` + +This field is automatically generated by the `backstage-cli package prepack` command when publishing a package and therefore does not need to be committed to the repository. + +If you are using your own tooling to publish a package, you need to populate this field manually, using the following feature types: + +- `@backstage/BackendFeature` +- `@backstage/FrontendPlugin` +- `@backstage/FrontendModule` +- `@backstage/FrontendFeatureLoader` + +You can read more about the feature types in the [backend system](../backend-system/architecture/01-index.md) and [frontend system](../frontend-system/architecture/00-index.md) documentation. + ### `backstage.pluginPackages` For any package that is part of a plugin, this field should be set to a list of all packages that are directly part of the same plugin. This includes frontend and backend plugin packages as well as related libraries, but not modules. diff --git a/docs/tutorials/manual-knex-rollback.md b/docs/tutorials/manual-knex-rollback.md index b0f67ba3e0..442a48e2f5 100644 --- a/docs/tutorials/manual-knex-rollback.md +++ b/docs/tutorials/manual-knex-rollback.md @@ -23,7 +23,7 @@ You can interact with Knex running the commands below in the project root: We want to check the migration status: ```sh -$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Found 2 Completed Migration file/files. 20211229105307_init.js @@ -34,7 +34,7 @@ No Pending Migration files Found. Now lets rollback a specific migration called `20240113144027_assets-namespace.js`: ```sh -$ node_modules/.bin/knex migrate:down 20240113144027_assets-namespace.js --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:down 20240113144027_assets-namespace.js --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Batch 2 rolled back the following migrations: 20240113144027_assets-namespace.js @@ -43,7 +43,7 @@ Batch 2 rolled back the following migrations: Now we can check the migration status again to confirm the rollback: ```sh -$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ +$ node_modules/.bin/knex migrate:status --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg --migrations-directory node_modules/@backstage/plugin-catalog-backend/migrations/ Using environment: production Found 1 Completed Migration file/files. 20211229105307_init.js @@ -54,7 +54,7 @@ Found 1 Pending Migration file/files. Now lets use `migrate:currentVersion` which retrieves the current migration version. If there aren't any migrations run yet, it will return "none". ```sh -$ node_modules/.bin/knex migrate:currentVersion --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_app" --client pg +$ node_modules/.bin/knex migrate:currentVersion --connection "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST/backstage_plugin_catalog" --client pg Using environment: production Current Version: 20240113144027 ``` diff --git a/microsite/data/plugins/datadog-entity-sync.yaml b/microsite/data/plugins/datadog-entity-sync.yaml new file mode 100644 index 0000000000..2f0cd06cd5 --- /dev/null +++ b/microsite/data/plugins/datadog-entity-sync.yaml @@ -0,0 +1,10 @@ +--- +title: Datadog Entity Sync +author: datadog +authorUrl: https://www.datadoghq.com/ +category: Development +description: Sync entities from the Backstage catalog to the Datadog Software Catalog. +documentation: https://github.com/DataDog/datadog-backstage-plugins/tree/main/plugins/datadog-entity-sync-backend#installation +iconUrl: https://imgix.datadoghq.com/img/about/presskit/logo-v/dd_vertical_purple.png?auto=format&fit=max&w=847&dpr=2 +npmPackageName: '@datadog/backstage-plugin-datadog-entity-sync-backend' +addedDate: '2025-09-19' diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index d801d41ca3..850e6058af 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -269,6 +269,10 @@ const config: Config = { from: '/docs/plugins/url-reader/', to: '/docs/backend-system/core-services/url-reader', }, + { + from: '/docs/getting-started/app-custom-theme', + to: '/docs/conf/user-interface', + }, ], }), [ diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts index 72608f4bac..a5c735d2f4 100644 --- a/microsite/sidebars.ts +++ b/microsite/sidebars.ts @@ -22,9 +22,8 @@ export default { docs: { Overview: [ 'overview/what-is-backstage', + 'overview/technical-overview', 'overview/architecture-overview', - 'overview/background', - 'overview/vision', 'overview/roadmap', 'overview/threat-model', 'overview/versioning-policy', @@ -38,7 +37,6 @@ export default { 'getting-started/config/database', 'getting-started/config/authentication', 'getting-started/configure-app-with-plugins', - 'getting-started/app-custom-theme', 'getting-started/homepage', ], }, @@ -561,6 +559,16 @@ export default { 'tooling/package-metadata', ], }, + { + type: 'category', + label: 'User Interface', + items: [ + 'conf/user-interface/index', + 'conf/user-interface/logo', + 'conf/user-interface/icons', + 'conf/user-interface/sidebar', + ], + }, ], Tutorials: [ { 'Non-technical': ['overview/adopting'] }, diff --git a/microsite/src/theme/customTheme.scss b/microsite/src/theme/customTheme.scss index 707acb4073..823327b28f 100644 --- a/microsite/src/theme/customTheme.scss +++ b/microsite/src/theme/customTheme.scss @@ -171,6 +171,11 @@ html[data-theme='light'] { border-left: 3px solid rgb(255, 255, 255, 0.5); } +table { + width: 100%; + display: table; +} + /* #endregion */ /* For the docusaurus-pushfeedback plugin */ diff --git a/mkdocs.yml b/mkdocs.yml index eea5fc9c40..45fdd2c9d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,7 @@ nav: - Database: 'getting-started/config/database.md' - Authentication: 'getting-started/config/authentication.md' - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md' - - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md' + - Customize the look-and-feel of your App: 'conf/user-interface/index.md' - Customizing your Homepage: 'getting-started/homepage.md' - Deployment: - Deploying Backstage: 'deployment/index.md' diff --git a/package.json b/package.json index b510a5a864..5ef45ce170 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.44.0-next.1", + "version": "1.44.0-next.3", "backstage": { "cli": { "new": { @@ -114,6 +114,7 @@ "csstype@npm:^3.1.2": "3.0.9", "csstype@npm:^3.1.3": "3.0.9", "jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch", + "GendocuPublicApis": "npm:gendocu-public-apis@^1.0.0", "recast@npm:0.23.9>ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-43c4ac4b0d.patch" }, "dependencies": { diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 7e0fe1b55d..2253e0e2cd 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/app-defaults +## 1.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 1.7.1-next.1 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 1a2cbecc12..3361b7e7b3 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.7.1-next.1", + "version": "1.7.1-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 87c84cf26c..95149a2a8a 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,28 @@ # example-app-next +## 0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-app@0.3.1-next.2 + - @backstage/plugin-auth@0.1.1-next.1 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + ## 0.0.28-next.1 ### Patch Changes diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 35380c961f..dcaddfb9bf 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -21,6 +21,11 @@ app: ownerEntityRefs: [cubic-belugas] extensions: + - entity-card:org/members-list: + config: + showAggregateMembersToggle: true + initialRelationAggregation: aggregated + # - apis.plugin.graphiql.browse.gitlab: true # - graphiql-endpoint:graphiql/gitlab: true diff --git a/packages/app-next/package.json b/packages/app-next/package.json index ebfb59c96a..67218d0cf6 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.28-next.1", + "version": "0.0.28-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 675001c296..c7f8d48b37 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,27 @@ # example-app +## 0.2.114-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-mui-to-bui@0.2.0-next.0 + - @backstage/plugin-scaffolder-react@1.19.2-next.2 + - @backstage/plugin-org@0.6.45-next.2 + - @backstage/cli@0.34.4-next.2 + - @backstage/ui@0.7.2-next.1 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/plugin-scaffolder@1.34.2-next.2 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/plugin-home@0.8.13-next.2 + - @backstage/plugin-notifications@0.5.10-next.2 + - @backstage/plugin-search-react@1.9.5-next.2 + - @backstage/plugin-signals@0.0.24-next.2 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + - @backstage/plugin-user-settings@0.8.27-next.2 + ## 0.2.114-next.1 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 56b5178692..b1cd05ac7a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.114-next.1", + "version": "0.2.114-next.2", "backstage": { "role": "frontend" }, @@ -56,6 +56,7 @@ "@backstage/plugin-home": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^", + "@backstage/plugin-mui-to-bui": "workspace:^", "@backstage/plugin-notifications": "workspace:^", "@backstage/plugin-org": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ad525420a7..45f91ce8a9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -73,6 +73,7 @@ import { } from '@backstage/plugin-notifications'; import { CustomizableHomePage } from './components/home/CustomizableHomePage'; import { HomePage } from './components/home/HomePage'; +import { BuiThemerPage } from '@backstage/plugin-mui-to-bui'; const app = createApp({ apis, @@ -208,6 +209,7 @@ const routes = ( {customDevToolsPage} } /> + } /> ); diff --git a/packages/app/src/components/home/CustomizableHomePage.tsx b/packages/app/src/components/home/CustomizableHomePage.tsx index b592490f2e..1d3cab646b 100644 --- a/packages/app/src/components/home/CustomizableHomePage.tsx +++ b/packages/app/src/components/home/CustomizableHomePage.tsx @@ -37,6 +37,7 @@ const defaultConfig = [ y: 0, width: 24, height: 2, + deletable: false, }, { component: 'HomePageRecentlyVisited', diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index a0176a008f..e36a8a781b 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-app-api +## 1.2.8-next.1 + +### Patch Changes + +- dd69cf6: Moved up registration of unhandled rejections and errors listeners to be done as early as possible, avoiding flakiness in backend startups and instead always logging these failures rather than sometimes crashing the process. + ## 1.2.8-next.0 ### Patch Changes diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index b082de0cfa..1613e05cc0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "1.2.8-next.0", + "version": "1.2.8-next.1", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 9b925adb30..09b22e3a7a 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -258,6 +258,30 @@ export class BackendInitializer { createInstanceMetadataServiceFactory(this.#registrations), ); + // This makes sure that any uncaught errors or unhandled rejections are + // caught and logged, rather than terminating the process. We register these + // as early as possible while still using the root logger service, the + // tradeoff that if there are any unhandled errors as part of the that + // instationation, it will cause the process to crash. If there are multiple + // backend instances, each instance will log the error, because we can't + // determine which instance the error came from. + if (process.env.NODE_ENV !== 'test') { + const rootLogger = await this.#serviceRegistry.get( + coreServices.rootLogger, + 'root', + ); + process.on('unhandledRejection', (reason: Error) => { + rootLogger + ?.child({ type: 'unhandledRejection' }) + ?.error('Unhandled rejection', reason); + }); + process.on('uncaughtException', error => { + rootLogger + ?.child({ type: 'uncaughtException' }) + ?.error('Uncaught exception', error); + }); + } + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); @@ -449,25 +473,6 @@ export class BackendInitializer { await lifecycleService.startup(); initLogger.onAllStarted(); - - // Once the backend is started, any uncaught errors or unhandled rejections are caught - // and logged, in order to avoid crashing the entire backend on local failures. - if (process.env.NODE_ENV !== 'test') { - const rootLogger = await this.#serviceRegistry.get( - coreServices.rootLogger, - 'root', - ); - process.on('unhandledRejection', (reason: Error) => { - rootLogger - ?.child({ type: 'unhandledRejection' }) - ?.error('Unhandled rejection', reason); - }); - process.on('uncaughtException', error => { - rootLogger - ?.child({ type: 'uncaughtException' }) - ?.error('Uncaught exception', error); - }); - } } // It's fine to call .stop() multiple times, which for example can happen with manual stop + process exit diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 1201918fb5..5cf7aa3414 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-defaults +## 0.13.0-next.2 + +### Minor Changes + +- 8495b18: Add a new `externalTokenHandlersServiceRef` to allow custom external token validations + ## 0.13.0-next.1 ### Patch Changes diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index f85d34db9e..b5a4acd229 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.13.0-next.1", + "version": "0.13.0-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" diff --git a/packages/backend-defaults/report-auth.api.md b/packages/backend-defaults/report-auth.api.md index 1883e65a6a..2fc0f16452 100644 --- a/packages/backend-defaults/report-auth.api.md +++ b/packages/backend-defaults/report-auth.api.md @@ -4,6 +4,7 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; +import type { Config } from '@backstage/config'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -14,6 +15,36 @@ export const authServiceFactory: ServiceFactory< 'singleton' >; +// @public +export function createExternalTokenHandler( + handler: ExternalTokenHandler, +): ExternalTokenHandler; + +// @public +export interface ExternalTokenHandler { + // (undocumented) + initialize(ctx: { options: Config }): TContext; + // (undocumented) + type: string; + // (undocumented) + verifyToken( + token: string, + ctx: TContext, + ): Promise< + | { + subject: string; + } + | undefined + >; +} + +// @public +export const externalTokenHandlersServiceRef: ServiceRef< + ExternalTokenHandler, + 'plugin', + 'multiton' +>; + // @public export interface PluginTokenHandler { // (undocumented) diff --git a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts index d8cb60e8b5..dba4f70dec 100644 --- a/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts +++ b/packages/backend-defaults/src/entrypoints/auth/DefaultAuthService.ts @@ -25,7 +25,7 @@ import { import { AuthenticationError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import { decodeJwt } from 'jose'; -import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +import { ExternalAuthTokenHandler } from './external/ExternalAuthTokenHandler'; import { createCredentialsWithNonePrincipal, createCredentialsWithServicePrincipal, @@ -41,7 +41,7 @@ export class DefaultAuthService implements AuthService { constructor( private readonly userTokenHandler: UserTokenHandler, private readonly pluginTokenHandler: PluginTokenHandler, - private readonly externalTokenHandler: ExternalTokenHandler, + private readonly externalTokenHandler: ExternalAuthTokenHandler, private readonly pluginId: string, private readonly disableDefaultAuthPolicy: boolean, private readonly pluginKeySource: PluginKeySource, diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index a244614135..b70aedfb9a 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -31,6 +31,9 @@ import { toInternalBackstageCredentials } from './helpers'; import { PluginTokenHandler } from './plugin/PluginTokenHandler'; import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { externalTokenHandlersServiceRef } from './external/ExternalAuthTokenHandler'; +import { createExternalTokenHandler } from './external/helpers'; + const server = setupServer(); // TODO: Ship discovery mock service in the service factory tester @@ -450,8 +453,88 @@ describe('authServiceFactory', () => { dependencies: [...mockDeps, customPluginTokenHandler], }); const searchAuth = await tester.getSubject('search'); - searchAuth.authenticate('unlimited-static-token'); + await searchAuth.authenticate('unlimited-static-token'); expect(customLogic).toHaveBeenCalledWith('unlimited-static-token'); }); }); + describe('add custom ExternalTokenHandler', () => { + it('should allow custom logic to be injected into the plugin token handler', async () => { + const customLogic = jest.fn(); + const customConfig = jest.fn(); + const deps = [ + discoveryServiceFactory, + mockServices.rootConfig.factory({ + data: { + backend: { + baseUrl: 'http://localhost', + auth: { + keys: [{ secret: 'abc' }], + externalAccess: [ + { + type: 'static', + options: { + token: 'limited-static-token', + subject: 'limited-static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'do.it' }, + ], + }, + { + type: 'static', + options: { + token: 'unlimited-static-token', + subject: 'unlimited-static-subject', + }, + }, + { + type: 'custom', + options: { + [`custom-config`]: 'custom-config', + foo: 'bar', + }, + }, + ], + }, + }, + }, + }), + ]; + + const customExternalTokenHandler = createExternalTokenHandler<{ + [`custom-config`]: string; + foo: string; + }>({ + type: 'custom', + initialize({ options }) { + const customConfigValue = options.getString('custom-config'); + const foo = options.getString('foo'); + customConfig(customConfigValue); + return { [`custom-config`]: customConfigValue, foo }; + }, + async verifyToken(token, ctx) { + customLogic(token); + return { + subject: ctx.foo, + }; + }, + }); + + const customPluginTokenHandler = createServiceFactory({ + service: externalTokenHandlersServiceRef, + deps: {}, + async factory() { + return customExternalTokenHandler; + }, + }); + const tester = ServiceFactoryTester.from(authServiceFactory, { + dependencies: [...deps, customPluginTokenHandler], + }); + const searchAuth = await tester.getSubject('search'); + await searchAuth.authenticate('custom-token'); + + expect(customLogic).toHaveBeenCalledWith('custom-token'); + expect(customConfig).toHaveBeenCalledWith('custom-config'); + }); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts index 91910292e6..ee171aef25 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.ts @@ -20,7 +20,10 @@ import { createServiceRef, } from '@backstage/backend-plugin-api'; import { DefaultAuthService } from './DefaultAuthService'; -import { ExternalTokenHandler } from './external/ExternalTokenHandler'; +import { + ExternalAuthTokenHandler, + externalTokenHandlersServiceRef, +} from './external/ExternalAuthTokenHandler'; import { DefaultPluginTokenHandler, PluginTokenHandler, @@ -64,6 +67,7 @@ export const authServiceFactory = createServiceFactory({ plugin: coreServices.pluginMetadata, database: coreServices.database, pluginTokenHandlerDecorator: pluginTokenHandlerDecoratorServiceRef, + externalTokenHandlers: externalTokenHandlersServiceRef, }, async factory({ config, @@ -72,6 +76,7 @@ export const authServiceFactory = createServiceFactory({ logger, database, pluginTokenHandlerDecorator, + externalTokenHandlers, }) { const disableDefaultAuthPolicy = config.getOptionalBoolean( @@ -102,10 +107,11 @@ export const authServiceFactory = createServiceFactory({ }), ); - const externalTokens = ExternalTokenHandler.create({ + const externalTokens = ExternalAuthTokenHandler.create({ ownPluginId: plugin.getId(), config, logger, + externalTokenHandlers, }); return new DefaultAuthService( diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts new file mode 100644 index 0000000000..e3f1acf270 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.test.ts @@ -0,0 +1,501 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; +import { ExternalAuthTokenHandler } from './ExternalAuthTokenHandler'; +import { createExternalTokenHandler } from './helpers'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; +import { + mockServices, + registerMswTestHooks, +} from '@backstage/backend-test-utils'; +import { randomBytes } from 'crypto'; +import { SignJWT, exportJWK, generateKeyPair } from 'jose'; +import { DateTime } from 'luxon'; +import { v4 as uuid } from 'uuid'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +// Simplified copy of TokenFactory in @backstage/plugin-auth-backend +interface AnyJWK extends Record { + use: 'sig'; + alg: string; + kid: string; + kty: string; +} +class FakeTokenFactory { + private readonly keys = new Array(); + + constructor( + private readonly options: { + issuer: string; + keyDurationSeconds: number; + }, + ) {} + + async issueToken(params: { + claims: { + sub: string; + ent?: string[]; + }; + }): Promise { + const pair = await generateKeyPair('RS256'); + const publicKey = await exportJWK(pair.publicKey); + const kid = uuid(); + publicKey.kid = kid; + this.keys.push(publicKey as AnyJWK); + + const iss = this.options.issuer; + const sub = params.claims.sub; + const ent = params.claims.ent; + const aud = 'backstage'; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.options.keyDurationSeconds; + + return new SignJWT({ iss, sub, aud, iat, exp, ent, kid }) + .setProtectedHeader({ alg: 'RS256', ent: ent, kid: kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(pair.privateKey); + } + + async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { + return { keys: this.keys }; + } +} + +describe('ExternalTokenHandler', () => { + const server = setupServer(); + registerMswTestHooks(server); + + it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { + const handler1: ExternalTokenHandler = createExternalTokenHandler({ + type: 'type1', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue(undefined), + }); + + const handler2: ExternalTokenHandler = + createExternalTokenHandler({ + type: 'type2', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }); + + const accessRestrictions: AccessRestrictionsMap = new Map( + Object.entries({ + plugin1: { + permissionNames: ['do.it'], + } satisfies BackstagePrincipalAccessRestrictions, + }), + ); + + const plugin1 = new ExternalAuthTokenHandler('plugin1', [ + { + context: undefined, + handler: handler1, + }, + { + context: undefined, + handler: handler2, + allAccessRestrictions: accessRestrictions, + }, + ]); + const plugin2 = new ExternalAuthTokenHandler('plugin2', [ + { + context: undefined, + handler: handler1, + }, + { + context: undefined, + handler: handler2, + allAccessRestrictions: accessRestrictions, + }, + ]); + + await expect(plugin1.verifyToken('token')).resolves.toEqual({ + subject: 'sub', + accessRestrictions: { permissionNames: ['do.it'] }, + }); + await expect( + plugin2.verifyToken('token'), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This token's access is restricted to plugin(s) 'plugin1'"`, + ); + }); + + it('successfully parses known methods', async () => { + const legacyKey = randomBytes(24); + + const factory = new FakeTokenFactory({ + issuer: 'blah', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const handler = ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'legacy', + options: { + secret: legacyKey.toString('base64'), + subject: 'legacy-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + { + type: 'static', + options: { + token: 'defdefdef', + subject: 'static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + { + type: 'jwks', + options: { + url: 'https://example.com/.well-known/jwks.json', + algorithm: 'RS256', + issuer: 'blah', + audience: 'backstage', + subjectPrefix: 'custom-prefix', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + }); + + const legacyToken = await new SignJWT({ + sub: 'backstage-server', + exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(legacyKey); + + await expect(handler.verifyToken(legacyToken)).resolves.toEqual({ + subject: 'legacy-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + + await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ + subject: 'static-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + + const jwksToken = await factory.issueToken({ + claims: { sub: 'jwks-subject' }, + }); + await expect(handler.verifyToken(jwksToken)).resolves.toEqual({ + subject: 'external:custom-prefix:jwks-subject', + accessRestrictions: { permissionNames: ['catalog.entity.read'] }, + }); + }); + it('successfully uses legacy configs', async () => { + const legacyKey = randomBytes(24); + const factory = new FakeTokenFactory({ + issuer: 'my-company', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const logger = mockServices.logger.mock(); + const handler = ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger, + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + keys: [ + { + secret: legacyKey.toString('base64'), + }, + ], + }, + }, + }, + }), + }); + + expect(logger.warn).toHaveBeenCalledWith( + `DEPRECATION WARNING: The backend.auth.keys config has been replaced by backend.auth.externalAccess, see https://backstage.io/docs/auth/service-to-service-auth`, + ); + + const legacyToken = await new SignJWT({ + sub: 'backstage-server', + exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), + }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(legacyKey); + + await expect(handler.verifyToken(legacyToken)).resolves.toEqual({ + subject: 'external:backstage-plugin', + }); + }); + it('successfully uses custom token handlers', async () => { + const factory = new FakeTokenFactory({ + issuer: 'my-company', + keyDurationSeconds: 100, + }); + + server.use( + rest.get( + 'https://example.com/.well-known/jwks.json', + async (_, res, ctx) => { + const keys = await factory.listPublicKeys(); + return res(ctx.json(keys)); + }, + ), + ); + + const verifyMock = jest.fn().mockResolvedValue({}); + const initializeMock = jest.fn().mockReturnValue({ context: 'a' }); + + const handler = ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [ + createExternalTokenHandler({ + type: 'internal-custom', + initialize: initializeMock, + verifyToken: verifyMock, + }), + ], + }); + + expect(initializeMock).toHaveBeenCalledWith({ + options: expect.objectContaining({ + data: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + }), + }); + + const customToken = await factory.issueToken({ + claims: { sub: 'internal-subject' }, + }); + + await handler.verifyToken(customToken); + + expect(verifyMock).toHaveBeenCalledWith(customToken, { context: 'a' }); + }); + it('should fail if custom handler has same type as builtin handlers', async () => { + const logger = mockServices.logger.mock(); + const customStaticHandler: ExternalTokenHandler = + createExternalTokenHandler({ + type: 'static', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'custom-static-subject', + }), + }); + + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger, + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'static', + options: { + token: 'mytoken', + subject: 'static-subject', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [customStaticHandler], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Duplicate external token handler type 'static', each handler must have a unique type"`, + ); + }); + it('should fail if 2 custom handlers have the same type', async () => { + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), + externalTokenHandlers: [ + createExternalTokenHandler({ + type: 'internal-custom', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }), + createExternalTokenHandler({ + type: 'internal-custom', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }), + ], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Duplicate external token handler type 'internal-custom', each handler must have a unique type"`, + ); + }); + it('should fail if config contains types not declared', async () => { + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Unknown type 'internal-custom' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks'"`, + ); + }); + + it('should show valid custom types in errors', async () => { + const createHandler = () => + ExternalAuthTokenHandler.create({ + ownPluginId: 'catalog', + logger: mockServices.logger.mock(), + config: mockServices.rootConfig({ + data: { + backend: { + auth: { + externalAccess: [ + { + type: 'internal-custom-invalid', + options: { + issuer: 'my-company', + subject: 'internal-subject', + audience: 'backstage', + }, + accessRestrictions: [ + { plugin: 'catalog', permission: 'catalog.entity.read' }, + ], + }, + ], + }, + }, + }, + }), + externalTokenHandlers: [ + createExternalTokenHandler({ + type: 'internal-custom', + initialize: jest.fn().mockResolvedValue(undefined), + verifyToken: jest.fn().mockResolvedValue({ + subject: 'sub', + }), + }), + ], + }); + + expect(createHandler).toThrowErrorMatchingInlineSnapshot( + `"Unknown type 'internal-custom-invalid' in backend.auth.externalAccess, expected one of 'static', 'legacy', 'jwks', 'internal-custom'"`, + ); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts new file mode 100644 index 0000000000..21b3aed99d --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalAuthTokenHandler.ts @@ -0,0 +1,187 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BackstagePrincipalAccessRestrictions, + createServiceRef, + LoggerService, + RootConfigService, +} from '@backstage/backend-plugin-api'; +import { NotAllowedError } from '@backstage/errors'; +import { legacyTokenHandler } from './legacy'; +import { staticTokenHandler } from './static'; +import { jwksTokenHandler } from './jwks'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; +import { readAccessRestrictionsFromConfig } from './helpers'; + +const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; +const OLD_CONFIG_KEY = 'backend.auth.keys'; +let loggedDeprecationWarning = false; + +/** + * @public + * This service is used to add custom handlers for external token. + */ +export const externalTokenHandlersServiceRef = createServiceRef< + ExternalTokenHandler +>({ + id: 'core.auth.externalTokenHandlers', + multiton: true, +}); + +const defaultHandlers: Record> = { + static: staticTokenHandler, + legacy: legacyTokenHandler, + jwks: jwksTokenHandler, +}; + +type ContextMapEntry = { + context: T; + handler: ExternalTokenHandler; + allAccessRestrictions?: AccessRestrictionsMap; +}; + +/** + * Handles all types of external caller token types (i.e. not Backstage user + * tokens, nor Backstage backend plugin tokens). + * + * @internal + */ +export class ExternalAuthTokenHandler { + static create(options: { + ownPluginId: string; + config: RootConfigService; + logger: LoggerService; + externalTokenHandlers?: ExternalTokenHandler[]; + }): ExternalAuthTokenHandler { + const { + ownPluginId, + config, + externalTokenHandlers: customHandlers = [], + logger, + } = options; + + const handlersTypes = customHandlers.reduce< + Record> + >( + (acc, handler) => { + if (acc[handler.type]) { + throw new Error( + `Duplicate external token handler type '${handler.type}', each handler must have a unique type`, + ); + } + acc[handler.type] = handler; + return acc; + }, + { ...defaultHandlers }, + ); + + const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; + const contexts: ContextMapEntry[] = handlerConfigs.map( + handlerConfig => { + const type = handlerConfig.getString('type'); + + const handler = handlersTypes[type]; + if (!handler) { + const valid = Object.keys(handlersTypes) + .map(h => `'${h}'`) + .join(', '); + throw new Error( + `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, + ); + } + return { + context: handler.initialize({ + options: handlerConfig.getConfig('options'), + }), + handler, + allAccessRestrictions: handlerConfig + ? readAccessRestrictionsFromConfig(handlerConfig) + : undefined, + }; + }, + ); + + // Load the old keys too + const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? []; + + if (legacyConfigs.length && !loggedDeprecationWarning) { + loggedDeprecationWarning = true; + + logger.warn( + `DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, + ); + } + + for (const legacyConfig of legacyConfigs) { + contexts.push({ + context: legacyTokenHandler.initialize({ + legacy: true, + options: legacyConfig, + }), + handler: legacyTokenHandler, + allAccessRestrictions: readAccessRestrictionsFromConfig(legacyConfig), + }); + } + + return new ExternalAuthTokenHandler(ownPluginId, contexts); + } + + constructor( + private readonly ownPluginId: string, + private readonly contexts: { + context: unknown; + handler: ExternalTokenHandler; + allAccessRestrictions?: AccessRestrictionsMap; + }[], + ) {} + + async verifyToken(token: string): Promise< + | { + subject: string; + accessRestrictions?: BackstagePrincipalAccessRestrictions; + } + | undefined + > { + for (const { handler, allAccessRestrictions, context } of this.contexts) { + const result = await handler.verifyToken(token, context); + if (result) { + if (allAccessRestrictions) { + const accessRestrictions = allAccessRestrictions.get( + this.ownPluginId, + ); + if (!accessRestrictions) { + const valid = [...allAccessRestrictions.keys()] + .map(k => `'${k}'`) + .join(', '); + throw new NotAllowedError( + `This token's access is restricted to plugin(s) ${valid}`, + ); + } + + return { + ...result, + accessRestrictions, + }; + } + + return result; + } + } + + return undefined; + } +} diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts deleted file mode 100644 index 39b01c46b6..0000000000 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { ExternalTokenHandler } from './ExternalTokenHandler'; -import { TokenHandler } from './types'; -import { - mockServices, - registerMswTestHooks, -} from '@backstage/backend-test-utils'; -import { randomBytes } from 'crypto'; -import { SignJWT, exportJWK, generateKeyPair } from 'jose'; -import { DateTime } from 'luxon'; -import { v4 as uuid } from 'uuid'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; - -// Simplified copy of TokenFactory in @backstage/plugin-auth-backend -interface AnyJWK extends Record { - use: 'sig'; - alg: string; - kid: string; - kty: string; -} -class FakeTokenFactory { - private readonly keys = new Array(); - - constructor( - private readonly options: { - issuer: string; - keyDurationSeconds: number; - }, - ) {} - - async issueToken(params: { - claims: { - sub: string; - ent?: string[]; - }; - }): Promise { - const pair = await generateKeyPair('RS256'); - const publicKey = await exportJWK(pair.publicKey); - const kid = uuid(); - publicKey.kid = kid; - this.keys.push(publicKey as AnyJWK); - - const iss = this.options.issuer; - const sub = params.claims.sub; - const ent = params.claims.ent; - const aud = 'backstage'; - const iat = Math.floor(Date.now() / 1000); - const exp = iat + this.options.keyDurationSeconds; - - return new SignJWT({ iss, sub, aud, iat, exp, ent, kid }) - .setProtectedHeader({ alg: 'RS256', ent: ent, kid: kid }) - .setIssuer(iss) - .setAudience(aud) - .setSubject(sub) - .setIssuedAt(iat) - .setExpirationTime(exp) - .sign(pair.privateKey); - } - - async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { - return { keys: this.keys }; - } -} - -describe('ExternalTokenHandler', () => { - const server = setupServer(); - registerMswTestHooks(server); - - it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { - const handler1: TokenHandler = { - add: jest.fn(), - verifyToken: jest.fn().mockResolvedValue(undefined), - }; - - const handler2: TokenHandler = { - add: jest.fn(), - verifyToken: jest.fn().mockResolvedValue({ - subject: 'sub', - allAccessRestrictions: new Map( - Object.entries({ - plugin1: { - permissionNames: ['do.it'], - } satisfies BackstagePrincipalAccessRestrictions, - }), - ), - }), - }; - - const plugin1 = new ExternalTokenHandler('plugin1', [handler1, handler2]); - const plugin2 = new ExternalTokenHandler('plugin2', [handler1, handler2]); - - await expect(plugin1.verifyToken('token')).resolves.toEqual({ - subject: 'sub', - accessRestrictions: { permissionNames: ['do.it'] }, - }); - await expect( - plugin2.verifyToken('token'), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"This token's access is restricted to plugin(s) 'plugin1'"`, - ); - }); - - it('successfully parses known methods', async () => { - const legacyKey = randomBytes(24); - - const factory = new FakeTokenFactory({ - issuer: 'blah', - keyDurationSeconds: 100, - }); - - server.use( - rest.get( - 'https://example.com/.well-known/jwks.json', - async (_, res, ctx) => { - const keys = await factory.listPublicKeys(); - return res(ctx.json(keys)); - }, - ), - ); - - const handler = ExternalTokenHandler.create({ - ownPluginId: 'catalog', - logger: mockServices.logger.mock(), - config: mockServices.rootConfig({ - data: { - backend: { - auth: { - externalAccess: [ - { - type: 'legacy', - options: { - secret: legacyKey.toString('base64'), - subject: 'legacy-subject', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - { - type: 'static', - options: { - token: 'defdefdef', - subject: 'static-subject', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - { - type: 'jwks', - options: { - url: 'https://example.com/.well-known/jwks.json', - algorithm: 'RS256', - issuer: 'blah', - audience: 'backstage', - subjectPrefix: 'custom-prefix', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], - }, - ], - }, - }, - }, - }), - }); - - const legacyToken = await new SignJWT({ - sub: 'backstage-server', - exp: DateTime.now().plus({ minutes: 1 }).toUnixInteger(), - }) - .setProtectedHeader({ alg: 'HS256' }) - .sign(legacyKey); - - await expect(handler.verifyToken(legacyToken)).resolves.toEqual({ - subject: 'legacy-subject', - accessRestrictions: { permissionNames: ['catalog.entity.read'] }, - }); - - await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ - subject: 'static-subject', - accessRestrictions: { permissionNames: ['catalog.entity.read'] }, - }); - - const jwksToken = await factory.issueToken({ - claims: { sub: 'jwks-subject' }, - }); - await expect(handler.verifyToken(jwksToken)).resolves.toEqual({ - subject: 'external:custom-prefix:jwks-subject', - accessRestrictions: { permissionNames: ['catalog.entity.read'] }, - }); - }); -}); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts deleted file mode 100644 index 72eeae6fcf..0000000000 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - BackstagePrincipalAccessRestrictions, - LoggerService, - RootConfigService, -} from '@backstage/backend-plugin-api'; -import { NotAllowedError } from '@backstage/errors'; -import { LegacyTokenHandler } from './legacy'; -import { StaticTokenHandler } from './static'; -import { JWKSHandler } from './jwks'; -import { TokenHandler } from './types'; - -const NEW_CONFIG_KEY = 'backend.auth.externalAccess'; -const OLD_CONFIG_KEY = 'backend.auth.keys'; -let loggedDeprecationWarning = false; - -/** - * Handles all types of external caller token types (i.e. not Backstage user - * tokens, nor Backstage backend plugin tokens). - * - * @internal - */ -export class ExternalTokenHandler { - static create(options: { - ownPluginId: string; - config: RootConfigService; - logger: LoggerService; - }): ExternalTokenHandler { - const { ownPluginId, config, logger } = options; - - const staticHandler = new StaticTokenHandler(); - const legacyHandler = new LegacyTokenHandler(); - const jwksHandler = new JWKSHandler(); - const handlers: Record = { - static: staticHandler, - legacy: legacyHandler, - jwks: jwksHandler, - }; - - // Load the new-style handlers - const handlerConfigs = config.getOptionalConfigArray(NEW_CONFIG_KEY) ?? []; - for (const handlerConfig of handlerConfigs) { - const type = handlerConfig.getString('type'); - const handler = handlers[type]; - if (!handler) { - const valid = Object.keys(handlers) - .map(k => `'${k}'`) - .join(', '); - throw new Error( - `Unknown type '${type}' in ${NEW_CONFIG_KEY}, expected one of ${valid}`, - ); - } - handler.add(handlerConfig); - } - - // Load the old keys too - const legacyConfigs = config.getOptionalConfigArray(OLD_CONFIG_KEY) ?? []; - if (legacyConfigs.length && !loggedDeprecationWarning) { - loggedDeprecationWarning = true; - logger.warn( - `DEPRECATION WARNING: The ${OLD_CONFIG_KEY} config has been replaced by ${NEW_CONFIG_KEY}, see https://backstage.io/docs/auth/service-to-service-auth`, - ); - } - for (const handlerConfig of legacyConfigs) { - legacyHandler.addOld(handlerConfig); - } - - return new ExternalTokenHandler(ownPluginId, Object.values(handlers)); - } - - constructor( - private readonly ownPluginId: string, - private readonly handlers: TokenHandler[], - ) {} - - async verifyToken(token: string): Promise< - | { - subject: string; - accessRestrictions?: BackstagePrincipalAccessRestrictions; - } - | undefined - > { - for (const handler of this.handlers) { - const result = await handler.verifyToken(token); - if (result) { - const { allAccessRestrictions, ...rest } = result; - if (allAccessRestrictions) { - const accessRestrictions = allAccessRestrictions.get( - this.ownPluginId, - ); - if (!accessRestrictions) { - const valid = [...allAccessRestrictions.keys()] - .map(k => `'${k}'`) - .join(', '); - throw new NotAllowedError( - `This token's access is restricted to plugin(s) ${valid}`, - ); - } - - return { - ...rest, - accessRestrictions, - }; - } - - return rest; - } - } - - return undefined; - } -} diff --git a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts index d6aa0a01ff..94d289bd7c 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { AccessRestriptionsMap } from './types'; +import { AccessRestrictionsMap, ExternalTokenHandler } from './types'; /** * Parses and returns the `accessRestrictions` configuration from an @@ -25,12 +25,12 @@ import { AccessRestriptionsMap } from './types'; */ export function readAccessRestrictionsFromConfig( externalAccessEntryConfig: Config, -): AccessRestriptionsMap | undefined { +): AccessRestrictionsMap | undefined { const configs = externalAccessEntryConfig.getOptionalConfigArray('accessRestrictions') ?? []; - const result: AccessRestriptionsMap = new Map(); + const result: AccessRestrictionsMap = new Map(); for (const config of configs) { const validKeys = ['plugin', 'permission', 'permissionAttribute']; for (const key of config.keys()) { @@ -147,3 +147,37 @@ function readPermissionAttributes(externalAccessEntryConfig: Config) { return Object.keys(result).length ? result : undefined; } + +/** + * Creates an external token handler with the provided implementation. + * + * This helper function simplifies the creation of external token handlers by + * providing type safety and a consistent API. External token handlers are used + * to validate tokens from external systems that need to authenticate with Backstage. + * + * See {@link https://backstage.io/docs/auth/service-to-service-auth#adding-custom-externaltokenhandler | the service-to-service auth docs} + * for more information about implementing custom external token handlers. + * + * @public + * @param handler - The external token handler implementation with type, initialize, and verifyToken methods + * @returns The same handler instance, typed as ExternalTokenHandler + * + * @example + * ```ts + * const customHandler = createExternalTokenHandler({ + * type: 'custom', + * initialize({ options }) { + * return { apiKey: options.getString('apiKey') }; + * }, + * async verifyToken(token, context) { + * // Custom validation logic here + * return { subject: 'custom:user' }; + * }, + * }); + * ``` + */ +export function createExternalTokenHandler( + handler: ExternalTokenHandler, +): ExternalTokenHandler { + return handler; +} diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index 321e1823aa..778e5090bf 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -20,7 +20,7 @@ import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { v4 as uuid } from 'uuid'; -import { JWKSHandler } from './jwks'; +import { jwksTokenHandler } from './jwks'; // Simplified copy of TokenFactory in @backstage/plugin-auth-backend interface AnyJWK extends Record { @@ -101,114 +101,45 @@ describe('JWKSHandler', () => { it('verifies token with valid entry', async () => { const validEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: mockBaseUrl, - audience: 'backstage', - }, + url: `${mockBaseUrl}/.well-known/jwks.json`, + algorithm: 'RS256', + issuer: mockBaseUrl, + audience: 'backstage', }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(validEntry)); + const context = jwksTokenHandler.initialize({ + options: new ConfigReader(validEntry), + }); const token = await factory.issueToken({ claims: { sub: mockSubject }, }); - const result = await jwksHandler.verifyToken(token); + const result = await jwksTokenHandler.verifyToken(token, context); expect(result).toEqual({ subject: `external:${mockSubject}` }); }); - it('skips invalid entry and continues verification', async () => { - const invalidEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: ['fakeIssuer'], - audience: ['fakeAud'], - }, - }; - - const validEntry = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: ['multiple-issuers', mockBaseUrl], - audience: ['multiple-audiences', 'backstage'], - }, - }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(invalidEntry)); - jwksHandler.add(new ConfigReader(validEntry)); - - const token = await factory.issueToken({ - claims: { sub: mockSubject }, - }); - - const result = await jwksHandler.verifyToken(token); - - expect(result).toEqual({ subject: `external:${mockSubject}` }); - }); - - it('returns undefined if no valid entry found', async () => { - const invalidEntry1 = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: 'RS256', - issuer: 'wrong', - }, - }; - - const invalidEntry2 = { - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - algorithm: ['HS256'], - audience: 'wrong', - }, - }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(invalidEntry1)); - jwksHandler.add(new ConfigReader(invalidEntry2)); - - const token = await factory.issueToken({ - claims: { sub: mockSubject }, - }); - - const result = await jwksHandler.verifyToken(token); - - expect(result).toBeUndefined(); - }); - it('rejects bad config', () => { - const jwksHandler = new JWKSHandler(); - expect(() => { - jwksHandler.add( - new ConfigReader({ - options: { - url: 'https://exampl e.com/jwks', - }, + return jwksTokenHandler.initialize({ + options: new ConfigReader({ + url: 'https://exampl e.com/jwks', }), - ); + }); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); expect(() => { - jwksHandler.add( - new ConfigReader({ - options: { - url: 'https://example.com/jwks\n', - }, + return jwksTokenHandler.initialize({ + options: new ConfigReader({ + url: 'https://example.com/jwks\n', }), - ); + }); }).toThrow('Illegal JWKS URL, must be a set of non-space characters'); }); it('gracefully handles no added tokens', async () => { - const handler = new JWKSHandler(); - await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); + await expect( + jwksTokenHandler.verifyToken('ghi', {} as any), + ).resolves.toBeUndefined(); }); it('uses custom subject prefix if provided', async () => { @@ -221,41 +152,18 @@ describe('JWKSHandler', () => { subjectPrefix: 'custom-prefix', }, }; - const jwksHandler = new JWKSHandler(); - - jwksHandler.add(new ConfigReader(validEntry)); + const context = jwksTokenHandler.initialize({ + options: new ConfigReader(validEntry.options), + }); const token = await factory.issueToken({ claims: { sub: mockSubject }, }); - const result = await jwksHandler.verifyToken(token); + const result = await jwksTokenHandler.verifyToken(token, context); expect(result).toEqual({ subject: `external:${validEntry.options.subjectPrefix}:${mockSubject}`, }); }); - - it('carries over access restrictions', async () => { - const jwksHandler = new JWKSHandler(); - jwksHandler.add( - new ConfigReader({ - options: { - url: `${mockBaseUrl}/.well-known/jwks.json`, - }, - accessRestrictions: [{ plugin: 'scaffolder', permission: 'do.it' }], - }), - ); - - const token = await factory.issueToken({ claims: { sub: mockSubject } }); - - await expect(jwksHandler.verifyToken(token)).resolves.toEqual({ - subject: `external:${mockSubject}`, - allAccessRestrictions: new Map( - Object.entries({ - scaffolder: { permissionNames: ['do.it'] }, - }), - ), - }); - }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts index d88dc62a47..33b55df775 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.ts @@ -15,51 +15,40 @@ */ import { jwtVerify, createRemoteJWKSet, JWTVerifyGetKey } from 'jose'; -import { Config } from '@backstage/config'; import { + createExternalTokenHandler, readAccessRestrictionsFromConfig, readStringOrStringArrayFromConfig, } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { AccessRestrictionsMap } from './types'; -/** - * Handles `type: jwks` access. - * - * @internal - */ -export class JWKSHandler implements TokenHandler { - #entries: Array<{ - algorithms?: string[]; - audiences?: string[]; - issuers?: string[]; - subjectPrefix?: string; - url: URL; - jwks: JWTVerifyGetKey; - allAccessRestrictions?: AccessRestriptionsMap; - }> = []; +type JWKSTokenContext = { + algorithms?: string[]; + audiences?: string[]; + issuers?: string[]; + subjectPrefix?: string; + url: URL; + jwks: JWTVerifyGetKey; + allAccessRestrictions?: AccessRestrictionsMap; +}; - add(config: Config) { - if (!config.getString('options.url').match(/^\S+$/)) { +export const jwksTokenHandler = createExternalTokenHandler({ + type: 'jwks', + initialize({ options }): JWKSTokenContext { + if (!options.getString('url').match(/^\S+$/)) { throw new Error( 'Illegal JWKS URL, must be a set of non-space characters', ); } - const algorithms = readStringOrStringArrayFromConfig( - config, - 'options.algorithm', - ); - const issuers = readStringOrStringArrayFromConfig(config, 'options.issuer'); - const audiences = readStringOrStringArrayFromConfig( - config, - 'options.audience', - ); - const subjectPrefix = config.getOptionalString('options.subjectPrefix'); - const url = new URL(config.getString('options.url')); + const algorithms = readStringOrStringArrayFromConfig(options, 'algorithm'); + const issuers = readStringOrStringArrayFromConfig(options, 'issuer'); + const audiences = readStringOrStringArrayFromConfig(options, 'audience'); + const subjectPrefix = options.getOptionalString('subjectPrefix'); + const url = new URL(options.getString('url')); const jwks = createRemoteJWKSet(url); - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - - this.#entries.push({ + const allAccessRestrictions = readAccessRestrictionsFromConfig(options); + return { algorithms, audiences, issuers, @@ -67,33 +56,30 @@ export class JWKSHandler implements TokenHandler { subjectPrefix, url, allAccessRestrictions, - }); - } + }; + }, - async verifyToken(token: string) { - for (const entry of this.#entries) { - try { - const { - payload: { sub }, - } = await jwtVerify(token, entry.jwks, { - algorithms: entry.algorithms, - issuer: entry.issuers, - audience: entry.audiences, - }); + async verifyToken(token: string, context: JWKSTokenContext) { + try { + const { + payload: { sub }, + } = await jwtVerify(token, context.jwks, { + algorithms: context.algorithms, + issuer: context.issuers, + audience: context.audiences, + }); - if (sub) { - const prefix = entry.subjectPrefix - ? `external:${entry.subjectPrefix}:` - : 'external:'; - return { - subject: `${prefix}${sub}`, - allAccessRestrictions: entry.allAccessRestrictions, - }; - } - } catch { - continue; + if (sub) { + const prefix = context.subjectPrefix + ? `external:${context.subjectPrefix}:` + : 'external:'; + return { + subject: `${prefix}${sub}`, + }; } + } catch { + return undefined; } return undefined; - } -} + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts index c674e46ec9..48dbcc2418 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.test.ts @@ -18,49 +18,35 @@ import { ConfigReader } from '@backstage/config'; import { randomBytes } from 'crypto'; import { SignJWT, importJWK } from 'jose'; import { DateTime } from 'luxon'; -import { LegacyTokenHandler } from './legacy'; +import { legacyTokenHandler } from './legacy'; describe('LegacyTokenHandler', () => { - const tokenHandler = new LegacyTokenHandler(); const key1 = randomBytes(24); const key2 = randomBytes(24); const key3 = randomBytes(24); - const accessRestrictions1 = new Map( - Object.entries({ - scaffolder: {}, - }), - ); - const accessRestrictions2 = new Map( - Object.entries({ - catalog: { permissionNames: ['catalog.entity.read'] }, - }), - ); - tokenHandler.add( - new ConfigReader({ - options: { - secret: key1.toString('base64'), - subject: 'key1', - }, - accessRestrictions: [{ plugin: 'scaffolder' }], + const tokenHandler = legacyTokenHandler; + + const context1 = tokenHandler.initialize({ + options: new ConfigReader({ + secret: key1.toString('base64'), + subject: 'key1', }), - ); - tokenHandler.add( - new ConfigReader({ - options: { - secret: key2.toString('base64'), - subject: 'key2', - }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], + }); + + const context2 = tokenHandler.initialize({ + options: new ConfigReader({ + secret: key2.toString('base64'), + subject: 'key2', }), - ); - tokenHandler.addOld( - new ConfigReader({ + }); + + const contextWithLegacy = tokenHandler.initialize({ + options: new ConfigReader({ secret: key3.toString('base64'), }), - ); + legacy: true, + }); it('should verify valid tokens', async () => { const token1 = await new SignJWT({ @@ -70,9 +56,8 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(token1)).resolves.toEqual({ + await expect(tokenHandler.verifyToken(token1, context1)).resolves.toEqual({ subject: 'key1', - allAccessRestrictions: accessRestrictions1, }); const token2 = await new SignJWT({ @@ -82,9 +67,8 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key2); - await expect(tokenHandler.verifyToken(token2)).resolves.toEqual({ + await expect(tokenHandler.verifyToken(token2, context2)).resolves.toEqual({ subject: 'key2', - allAccessRestrictions: accessRestrictions2, }); const token3 = await new SignJWT({ @@ -94,7 +78,9 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key3); - await expect(tokenHandler.verifyToken(token3)).resolves.toEqual({ + await expect( + tokenHandler.verifyToken(token3, contextWithLegacy), + ).resolves.toEqual({ subject: 'external:backstage-plugin', }); }); @@ -107,10 +93,18 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(validToken)).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(validToken, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(validToken, context2), + ).resolves.toBeUndefined(); await expect( - tokenHandler.verifyToken('statickeyblaaa'), + tokenHandler.verifyToken('statickeyblaaa', context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken('statickeyblaaa', context2), ).resolves.toBeUndefined(); const randomToken = await new SignJWT({ @@ -120,7 +114,10 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(randomBytes(24)); await expect( - tokenHandler.verifyToken(randomToken), + tokenHandler.verifyToken(randomToken, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(randomToken, context2), ).resolves.toBeUndefined(); const mockPublicKey = { @@ -144,7 +141,10 @@ describe('LegacyTokenHandler', () => { .sign(await importJWK(mockPrivateKey)); await expect( - tokenHandler.verifyToken(keyWithWrongAlg), + tokenHandler.verifyToken(keyWithWrongAlg, context1), + ).resolves.toBeUndefined(); + await expect( + tokenHandler.verifyToken(keyWithWrongAlg, context2), ).resolves.toBeUndefined(); }); @@ -157,130 +157,151 @@ describe('LegacyTokenHandler', () => { .setProtectedHeader({ alg: 'HS256' }) .sign(key1); - await expect(tokenHandler.verifyToken(keyWithWrongExp)).rejects.toThrow( - /\"exp\" claim must be a number/, - ); + await expect( + tokenHandler.verifyToken(keyWithWrongExp, context1), + ).rejects.toThrow(/\"exp\" claim must be a number/); }); it('rejects bad config', () => { - const handler = new LegacyTokenHandler(); + const handler = legacyTokenHandler; // new style add, bad secrets expect(() => - handler.add( - new ConfigReader({ options: { _missingsecret: true, subject: 'ok' } }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.secret' in 'mock-config'"`, - ); - expect(() => - handler.add(new ConfigReader({ options: { secret: '', subject: 'ok' } })), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.secret' in 'mock-config', got empty-string, wanted string"`, - ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'has spaces', subject: 'ok' } }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal secret, must be a valid base64 string"`, - ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'hasnewline\n', subject: 'ok' }, + handler.initialize({ + options: new ConfigReader({ + _missingsecret: true, + subject: 'ok', }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal secret, must be a valid base64 string"`, - ); - expect(() => - handler.add(new ConfigReader({ options: { secret: 3, subject: 'ok' } })), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.secret' in 'mock-config', got number, wanted string"`, - ); - - // new style add, bad subjects - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', _missingsubject: true }, - }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.subject' in 'mock-config'"`, - ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'b2s=', subject: '' } }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, - ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'has spaces' }, - }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal subject, must be a set of non-space characters"`, - ); - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'hasnewline\n' }, - }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal subject, must be a set of non-space characters"`, - ); - expect(() => - handler.add( - new ConfigReader({ options: { secret: 'b2s=', subject: 3 } }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, - ); - - // new style add, bad access restrictions - expect(() => - handler.add( - new ConfigReader({ - options: { secret: 'b2s=', subject: 'subject' }, - accessRestrictions: [{ plugin: ['a'] }], - }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'accessRestrictions[0].plugin' in 'mock-config', got array, wanted string"`, - ); - - // old style add - expect(() => - handler.addOld(new ConfigReader({ secret: 'b2s=' })), - ).not.toThrow(); - expect(() => - handler.addOld(new ConfigReader({ _missingsecret: true })), + }), ).toThrowErrorMatchingInlineSnapshot( `"Missing required config value at 'secret' in 'mock-config'"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: '' })), + handler.initialize({ + options: new ConfigReader({ secret: '', subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 'has spaces' })), + handler.initialize({ + options: new ConfigReader({ + secret: 'has spaces', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 'hasnewline\n' })), + handler.initialize({ + options: new ConfigReader({ + secret: 'hasnewline\n', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal secret, must be a valid base64 string"`, ); expect(() => - handler.addOld(new ConfigReader({ secret: 3 })), + handler.initialize({ + options: new ConfigReader({ secret: 3, subject: 'ok' }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, + ); + + // new style add, bad subjects + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + _missingsubject: true, + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'subject' in 'mock-config'"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=', subject: '' }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + subject: 'has spaces', + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ + secret: 'b2s=', + subject: 'hasnewline\n', + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal subject, must be a set of non-space characters"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=', subject: 3 }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, + ); + + // old style add + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'b2s=' }), + legacy: true, + }), + ).not.toThrow(); + + expect(() => + handler.initialize({ + options: new ConfigReader({ _missingsecret: true }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required config value at 'secret' in 'mock-config'"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: '' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid type in config for key 'secret' in 'mock-config', got empty-string, wanted string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'has spaces' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 'hasnewline\n' }), + legacy: true, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal secret, must be a valid base64 string"`, + ); + expect(() => + handler.initialize({ + options: new ConfigReader({ secret: 3 }), + legacy: true, + }), ).toThrowErrorMatchingInlineSnapshot( `"Invalid type in config for key 'secret' in 'mock-config', got number, wanted string"`, ); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts index 9c60e70707..2ab8c3a5ea 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/legacy.ts @@ -16,72 +16,56 @@ import { Config } from '@backstage/config'; import { base64url, decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose'; -import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; -/** - * Handles `type: legacy` access. - * - * @internal - */ -export class LegacyTokenHandler implements TokenHandler { - #entries = new Array<{ - key: Uint8Array; - result: { - subject: string; - allAccessRestrictions?: AccessRestriptionsMap; - }; - }>(); +import { ExternalTokenHandler } from './types'; - add(config: Config) { - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); - this.#doAdd( - config.getString('options.secret'), - config.getString('options.subject'), - allAccessRestrictions, - ); - } +export type LegacyConfigWrapper = { + legacy: boolean; + config: Config; +}; - // used only for the old backend.auth.keys array - addOld(config: Config) { - // This choice of subject is for compatibility reasons - this.#doAdd(config.getString('secret'), 'external:backstage-plugin'); - } +type LegacyTokenHandlerContext = { + key: Uint8Array; + + subject: string; +}; + +type LegacyTokenHandlerOverloaded = + ExternalTokenHandler & { + initialize(ctx: { + options: Config; + legacy: true; + }): LegacyTokenHandlerContext; + }; + +export const legacyTokenHandler: LegacyTokenHandlerOverloaded = { + type: 'legacy', + initialize(ctx: { + options: Config; + legacy?: true; + }): LegacyTokenHandlerContext { + const secret = ctx.options.getString('secret'); + const subject = ctx.legacy + ? 'external:backstage-plugin' + : ctx.options.getString('subject'); - #doAdd( - secret: string, - subject: string, - allAccessRestrictions?: AccessRestriptionsMap, - ) { if (!secret.match(/^\S+$/)) { throw new Error('Illegal secret, must be a valid base64 string'); } else if (!subject.match(/^\S+$/)) { throw new Error('Illegal subject, must be a set of non-space characters'); } - let key: Uint8Array; try { - key = base64url.decode(secret); + return { + key: base64url.decode(secret), + subject, + }; } catch { throw new Error('Illegal secret, must be a valid base64 string'); } + }, - if (this.#entries.some(e => e.key === key)) { - throw new Error( - 'Legacy externalAccess token was declared more than once', - ); - } - - this.#entries.push({ - key, - result: { - subject, - allAccessRestrictions, - }, - }); - } - - async verifyToken(token: string) { + async verifyToken(token: string, context: LegacyTokenHandlerContext) { // First do a duck typing check to see if it remotely looks like a legacy token try { // We do a fair amount of checking upfront here. Since we aren't certain @@ -102,19 +86,18 @@ export class LegacyTokenHandler implements TokenHandler { return undefined; } - for (const { key, result } of this.#entries) { - try { - await jwtVerify(token, key); - return result; - } catch (e) { - if (e.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { - throw e; - } - // Otherwise continue to try the next key + try { + await jwtVerify(token, context.key); + return { + subject: context.subject, + }; + } catch (error) { + if (error.code !== 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') { + throw error; } } // None of the signing keys matched return undefined; - } -} + }, +}; diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts index a5945daba9..10e78d2dbf 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.test.ts @@ -15,137 +15,146 @@ */ import { ConfigReader } from '@backstage/config'; -import { StaticTokenHandler } from './static'; +import { staticTokenHandler } from './static'; describe('StaticTokenHandler', () => { it('accepts any of the added list of tokens', async () => { - const handler = new StaticTokenHandler(); - handler.add( - new ConfigReader({ - options: { token: 'abcabcabc', subject: 'one' }, - accessRestrictions: [{ plugin: 'scaffolder' }], + const context1 = staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'abcabcabc', + subject: 'one', }), - ); - handler.add( - new ConfigReader({ - options: { token: 'defdefdef', subject: 'two' }, - accessRestrictions: [ - { plugin: 'catalog', permission: 'catalog.entity.read' }, - ], + }); + const context2 = staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'defdefdef', + subject: 'two', }), - ); - const accessRestrictionsOne = new Map(Object.entries({ scaffolder: {} })); - const accessRestrictionsTwo = new Map( - Object.entries({ - catalog: { - permissionNames: ['catalog.entity.read'], - }, - }), - ); + }); - await expect(handler.verifyToken('abcabcabc')).resolves.toEqual({ + await expect( + staticTokenHandler.verifyToken('abcabcabc', context1), + ).resolves.toEqual({ subject: 'one', - allAccessRestrictions: accessRestrictionsOne, }); - await expect(handler.verifyToken('defdefdef')).resolves.toEqual({ + await expect( + staticTokenHandler.verifyToken('defdefdef', context2), + ).resolves.toEqual({ subject: 'two', - allAccessRestrictions: accessRestrictionsTwo, }); - await expect(handler.verifyToken('ghighighi')).resolves.toBeUndefined(); + await expect( + staticTokenHandler.verifyToken('ghighighi', context1), + ).resolves.toBeUndefined(); }); it('gracefully handles no added tokens', async () => { - const handler = new StaticTokenHandler(); - await expect(handler.verifyToken('ghi')).resolves.toBeUndefined(); + await expect( + staticTokenHandler.verifyToken('ghi', {} as any), + ).resolves.toBeUndefined(); }); it('rejects bad config', () => { - const handler = new StaticTokenHandler(); - expect(() => - handler.add( - new ConfigReader({ options: { _missingtoken: true, subject: 'ok' } }), - ), + staticTokenHandler.initialize({ + options: new ConfigReader({ _missingtoken: true, subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.token' in 'mock-config'"`, + `"Missing required config value at 'token' in 'mock-config'"`, ); expect(() => - handler.add(new ConfigReader({ options: { token: '', subject: 'ok' } })), + staticTokenHandler.initialize({ + options: new ConfigReader({ token: '', subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.token' in 'mock-config', got empty-string, wanted string"`, + `"Invalid type in config for key 'token' in 'mock-config', got empty-string, wanted string"`, ); expect(() => - handler.add( - new ConfigReader({ options: { token: 'has spaces', subject: 'ok' } }), - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Illegal token, must be a set of non-space characters"`, - ); - expect(() => - handler.add( - new ConfigReader({ - options: { - token: 'hasnewlinebutislongenough\n', - subject: 'ok', - }, + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'has spaces', + subject: 'ok', }), - ), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be a set of non-space characters"`, ); expect(() => - handler.add( - new ConfigReader({ options: { token: 'short', subject: 'ok' } }), - ), + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'hasnewlinebutislongenough\n', + subject: 'ok', + }), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Illegal token, must be a set of non-space characters"`, + ); + expect(() => + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'short', + subject: 'ok', + }), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal token, must be at least 8 characters length"`, ); expect(() => - handler.add(new ConfigReader({ options: { token: 3, subject: 'ok' } })), + staticTokenHandler.initialize({ + options: new ConfigReader({ token: 3, subject: 'ok' }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.token' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'token' in 'mock-config', got number, wanted string"`, ); expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', _missingsubject: true }, + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + _missingsubject: true, }), - ), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required config value at 'options.subject' in 'mock-config'"`, + `"Missing required config value at 'subject' in 'mock-config'"`, ); expect(() => - handler.add( - new ConfigReader({ options: { token: 'validtoken', subject: '' } }), - ), + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: '', + }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got empty-string, wanted string"`, + `"Invalid type in config for key 'subject' in 'mock-config', got empty-string, wanted string"`, ); expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', subject: 'has spaces' }, + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 'has spaces', }), - ), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); expect(() => - handler.add( - new ConfigReader({ - options: { token: 'validtoken', subject: 'hasnewline\n' }, + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 'hasnewline\n', }), - ), + }), ).toThrowErrorMatchingInlineSnapshot( `"Illegal subject, must be a set of non-space characters"`, ); expect(() => - handler.add( - new ConfigReader({ options: { token: 'validtoken', subject: 3 } }), - ), + staticTokenHandler.initialize({ + options: new ConfigReader({ + token: 'validtoken', + subject: 3, + }), + }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid type in config for key 'options.subject' in 'mock-config', got number, wanted string"`, + `"Invalid type in config for key 'subject' in 'mock-config', got number, wanted string"`, ); }); }); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/static.ts b/packages/backend-defaults/src/entrypoints/auth/external/static.ts index 1e89d8c6a1..502150d5a8 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/static.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/static.ts @@ -15,29 +15,18 @@ */ import { Config } from '@backstage/config'; -import { readAccessRestrictionsFromConfig } from './helpers'; -import { AccessRestriptionsMap, TokenHandler } from './types'; +import { createExternalTokenHandler } from './helpers'; const MIN_TOKEN_LENGTH = 8; -/** - * Handles `type: static` access. - * - * @internal - */ -export class StaticTokenHandler implements TokenHandler { - #entries = new Map< - string, - { - subject: string; - allAccessRestrictions?: AccessRestriptionsMap; - } - >(); - - add(config: Config) { - const token = config.getString('options.token'); - const subject = config.getString('options.subject'); - const allAccessRestrictions = readAccessRestrictionsFromConfig(config); +export const staticTokenHandler = createExternalTokenHandler<{ + token: string; + subject: string; +}>({ + type: 'static', + initialize(ctx: { options: Config }): { token: string; subject: string } { + const token = ctx.options.getString('token'); + const subject = ctx.options.getString('subject'); if (!token.match(/^\S+$/)) { throw new Error('Illegal token, must be a set of non-space characters'); @@ -47,16 +36,17 @@ export class StaticTokenHandler implements TokenHandler { ); } else if (!subject.match(/^\S+$/)) { throw new Error('Illegal subject, must be a set of non-space characters'); - } else if (this.#entries.has(token)) { - throw new Error( - 'Static externalAccess token was declared more than once', - ); } - this.#entries.set(token, { subject, allAccessRestrictions }); - } - - async verifyToken(token: string) { - return this.#entries.get(token); - } -} + return { token, subject }; + }, + async verifyToken( + token: string, + context: { token: string; subject: string }, + ) { + if (token === context.token) { + return { subject: context.subject }; + } + return undefined; + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/external/types.ts b/packages/backend-defaults/src/entrypoints/auth/external/types.ts index 6a1fd084ed..01d5a26149 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/types.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/types.ts @@ -15,20 +15,23 @@ */ import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; +import type { Config } from '@backstage/config'; -export type AccessRestriptionsMap = Map< +export type AccessRestrictionsMap = Map< string, // plugin ID BackstagePrincipalAccessRestrictions >; -export interface TokenHandler { - add(options: Config): void; - verifyToken(token: string): Promise< - | { - subject: string; - allAccessRestrictions?: AccessRestriptionsMap; - } - | undefined - >; +/** + * @public + * This interface is used to handle external tokens. + * It is used by the auth service to verify tokens and extract the subject. + */ +export interface ExternalTokenHandler { + type: string; + initialize(ctx: { options: Config }): TContext; + verifyToken( + token: string, + ctx: TContext, + ): Promise<{ subject: string } | undefined>; } diff --git a/packages/backend-defaults/src/entrypoints/auth/index.ts b/packages/backend-defaults/src/entrypoints/auth/index.ts index e48926f0ce..69d41b0310 100644 --- a/packages/backend-defaults/src/entrypoints/auth/index.ts +++ b/packages/backend-defaults/src/entrypoints/auth/index.ts @@ -19,4 +19,10 @@ export { pluginTokenHandlerDecoratorServiceRef, } from './authServiceFactory'; +export { createExternalTokenHandler } from './external/helpers'; + +export { externalTokenHandlersServiceRef } from './external/ExternalAuthTokenHandler'; + +export type { ExternalTokenHandler } from './external/types'; + export type { PluginTokenHandler } from './plugin/PluginTokenHandler'; diff --git a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts index 9aec52bc08..b692cd2b87 100644 --- a/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts +++ b/packages/backend-defaults/src/entrypoints/auth/plugin/PluginTokenHandler.ts @@ -46,7 +46,7 @@ type Options = { /** * @public - * Issues and verifies {@link https://backstage.iceio/docs/auth/service-to-service-auth | service-to-service tokens}. + * Issues and verifies {@link https://backstage.io/docs/auth/service-to-service-auth | service-to-service tokens}. */ export interface PluginTokenHandler { verifyToken( diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 28b2293982..900401a8fa 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/cli +## 0.34.4-next.3 + +### Patch Changes + +- f2cf564: Removed the script transform cache from the default Jest configuration. The script cache provided a moderate performance boost, but it is incompatible with Jest 30. +- 024645e: Remove unused @octokit modules from cli package + + - @octokit/graphql + - @octokit/graphql-schema + - @octokit/oauth-app + +- d14ef24: Added automatic detection and support for the Backstage Yarn plugin when generating new packages with `yarn new`. When the plugin is installed, new packages will automatically use `backstage:^` ranges for `@backstage/*` dependencies. + +## 0.34.4-next.2 + +### Patch Changes + +- ab96bb7: Added a new `--entrypoint` option to the `package start` command, which allows you to specify a custom entry directory/file for development applications. This is particularly useful when maintaining separate dev apps for different versions of your plugin (e.g., stable and alpha). + + **Example usage:** + + Consider the following plugin dev folder structure: + + ``` + dev/ + index.tsx + alpha/ + index.ts + ``` + + - The default `yarn package start` command uses the `dev/` folder as the entry point and executes `dev/index.tsx` file; + - Running `yarn package start --entrypoint dev/alpha` will instead use `dev/alpha/` as the entry point and execute `dev/alpha/index.ts` file. + ## 0.34.4-next.1 ### Patch Changes diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index e2f5bb9cc6..a8df5242ad 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -312,6 +312,7 @@ Usage: program [options] Options: --check --config + --entrypoint --inspect [host] --inspect-brk [host] --link diff --git a/packages/cli/config/jestCachingModuleLoader.js b/packages/cli/config/jestCachingModuleLoader.js index b2b6a4f8ae..a3fdf106e9 100644 --- a/packages/cli/config/jestCachingModuleLoader.js +++ b/packages/cli/config/jestCachingModuleLoader.js @@ -16,29 +16,12 @@ const { default: JestRuntime } = require('jest-runtime'); -const scriptTransformCache = new Map(); - module.exports = class CachingJestRuntime extends JestRuntime { constructor(config, ...restArgs) { super(config, ...restArgs); this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts'); } - // This may or may not be a good idea. Theoretically I don't know why this would impact - // test correctness and flakiness, but it seems like it may introduce flakiness and strange failures. - // It does seem to speed up test execution by a fair amount though. - createScriptFromCode(scriptSource, filename) { - let script = scriptTransformCache.get(scriptSource); - if (!script) { - script = super.createScriptFromCode(scriptSource, filename); - // Tried to store the script object in a WeakRef here. It starts out at - // about 90% hit rate, but eventually drops all the way to 20%, and overall - // it seemed to increase memory usage by 20% or so. - scriptTransformCache.set(scriptSource, script); - } - return script; - } - // Unfortunately we need to use this unstable API to make sure that .js files // are only loaded as modules where ESM is supported, i.e. Node.js packages. unstable_shouldLoadAsEsm(path, ...restArgs) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 3c12b383ad..19d9428979 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.34.4-next.1", + "version": "0.34.4-next.3", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -59,9 +59,6 @@ "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", "@module-federation/enhanced": "^0.9.0", - "@octokit/graphql": "^5.0.0", - "@octokit/graphql-schema": "^13.7.0", - "@octokit/oauth-app": "^4.2.0", "@octokit/request": "^8.0.0", "@rollup/plugin-commonjs": "^26.0.0", "@rollup/plugin-json": "^6.0.0", diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index 744874d7e9..4cb7591faf 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -79,4 +79,110 @@ describe('createPackageVersionProvider', () => { `^${corePluginApiPkg.version}`, ); }); + + describe('with backstage protocol options', () => { + it('should return backstage:^ for @backstage packages when preferBackstageProtocol is true', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('@backstage/core-plugin-api')).toBe('backstage:^'); + expect(provider('@backstage/cli')).toBe('backstage:^'); + }); + + it('should not return backstage:^ for non-@backstage packages even when preferBackstageProtocol is true', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"react@^18.0.0": + version "18.0.0" + +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" + +"@internal/library@workspace:packages/internal": + version "0.0.0-use.local" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('react', '18.0.0')).toBe('^18.0.0'); + expect(provider('@backstage/core-plugin-api')).toBe('backstage:^'); + expect(provider('@internal/library')).toBe('workspace:^'); + }); + + it('should prefer workspace ranges over backstage protocol', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"react@workspace:packages/internal": + version "0.0.0-use.local" + +"@backstage/core-plugin-api@workspace:packages/internal": + version "0.0.0-use.local" + +"@internal/library@workspace:packages/internal": + version "0.0.0-use.local" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: true, + }); + + expect(provider('react')).toBe('workspace:^'); + expect(provider('@backstage/core-plugin-api')).toBe('workspace:^'); + expect(provider('@internal/library')).toBe('workspace:^'); + }); + + it('should not use backstage protocol when preferBackstageProtocol is false', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: false, + }); + + expect(provider('@backstage/core-plugin-api')).toBe( + `^${corePluginApiPkg.version}`, + ); + }); + + it('should not use backstage protocol when options are not provided', async () => { + mockDir.setContent({ + 'yarn.lock': `${HEADER} +"@backstage/core-plugin-api@^1.0.0": + version "1.0.0" +`, + }); + + const lockfilePath = mockDir.resolve('yarn.lock'); + const lockfile = await Lockfile.load(lockfilePath); + const provider = createPackageVersionProvider(lockfile); + + expect(provider('@backstage/core-plugin-api')).toBe( + `^${corePluginApiPkg.version}`, + ); + }); + }); }); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5a2a8c7838..a7a7ca1bdb 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -89,23 +89,35 @@ export function findVersion() { export const version = findVersion(); export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); -export function createPackageVersionProvider(lockfile?: Lockfile) { +export function createPackageVersionProvider( + lockfile?: Lockfile, + options?: { + preferBackstageProtocol?: boolean; + }, +) { return (name: string, versionHint?: string): string => { const packageVersion = packageVersions[name]; + + // 1) workspace precedence (existing logic) - check this first + const lockfileEntries = lockfile?.get(name); + const lockfileEntry = lockfileEntries?.find(entry => + entry.range.startsWith('workspace:'), + ); + if (lockfileEntry) { + return 'workspace:^'; + } + + // 2) backstage:^ when plugin is present and allowed + if (options?.preferBackstageProtocol && name.startsWith('@backstage/')) { + return 'backstage:^'; + } + + // 3) fallback to current npm resolution const targetVersion = versionHint || packageVersion; if (!targetVersion) { throw new Error(`No version available for package ${name}`); } - const lockfileEntries = lockfile?.get(name); - - for (const specifier of ['^', '~', '*']) { - const range = `workspace:${specifier}`; - if (lockfileEntries?.some(entry => entry.range === range)) { - return range; - } - } - const validRanges = lockfileEntries?.filter(entry => semver.satisfies(targetVersion, entry.range), ); diff --git a/packages/cli/src/lib/yarnPlugin.test.ts b/packages/cli/src/lib/yarnPlugin.test.ts new file mode 100644 index 0000000000..d5c865e1c4 --- /dev/null +++ b/packages/cli/src/lib/yarnPlugin.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { getHasYarnPlugin } from './yarnPlugin'; + +const mockDir = createMockDirectory(); + +jest.mock('./paths', () => ({ + paths: { + resolveTargetRoot(filename: string) { + return mockDir.resolve(filename); + }, + }, +})); + +describe('getHasYarnPlugin', () => { + beforeEach(() => { + mockDir.clear(); + }); + + it('should return false when .yarnrc.yml does not exist', async () => { + mockDir.setContent({}); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when .yarnrc.yml is empty', async () => { + mockDir.setContent({ + '.yarnrc.yml': '', + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when plugins array is empty', async () => { + mockDir.setContent({ + '.yarnrc.yml': 'plugins: []', + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return false when plugins array does not contain backstage plugin', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(false); + }); + + it('should return true when backstage plugin is present', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); + + it('should return true when backstage plugin is the only plugin', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); + + it('should throw error when .yarnrc.yml has invalid content', async () => { + mockDir.setContent({ + '.yarnrc.yml': 'invalid: yaml: content: [', + }); + + await expect(getHasYarnPlugin()).rejects.toThrow(); + }); + + it('should throw error when .yarnrc.yml has unexpected structure', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: "not an array" +`, + }); + + await expect(getHasYarnPlugin()).rejects.toThrow( + 'Unexpected content in .yarnrc.yml', + ); + }); + + it('should handle plugins with different structure', async () => { + mockDir.setContent({ + '.yarnrc.yml': ` +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-backstage.cjs + - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs +`, + }); + + const result = await getHasYarnPlugin(); + expect(result).toBe(true); + }); +}); diff --git a/packages/cli/src/lib/yarnPlugin.ts b/packages/cli/src/lib/yarnPlugin.ts new file mode 100644 index 0000000000..5ad49b2524 --- /dev/null +++ b/packages/cli/src/lib/yarnPlugin.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import yaml from 'yaml'; +import z from 'zod'; +import { paths } from './paths'; + +const yarnRcSchema = z.object({ + plugins: z + .array( + z.object({ + path: z.string(), + }), + ) + .optional(), +}); + +/** + * Detects whether the Backstage Yarn plugin is installed in the target repository. + * + * @returns Promise - true if the plugin is installed, false otherwise + */ +export async function getHasYarnPlugin(): Promise { + const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); + const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { + if (e.code === 'ENOENT') { + // gracefully continue in case the file doesn't exist + return ''; + } + throw e; + }); + + if (!yarnRcContent) { + return false; + } + + const parseResult = yarnRcSchema.safeParse(yaml.parse(yarnRcContent)); + + if (!parseResult.success) { + throw new Error( + `Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`, + ); + } + + const yarnRc = parseResult.data; + + const backstagePlugin = yarnRc.plugins?.some( + plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs', + ); + + return Boolean(backstagePlugin); +} diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 05fc668356..b38957f488 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -23,6 +23,7 @@ import { paths } from '../../../../../lib/paths'; export async function command(opts: OptionValues): Promise { await startPackage({ role: await findRoleFromCommand(opts), + entrypoint: opts.entrypoint, targetDir: paths.targetDir, configPaths: opts.config as string[], checksEnabled: Boolean(opts.check), diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts new file mode 100644 index 0000000000..ca153adb22 --- /dev/null +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { resolveEntryPath } from './startPackage'; + +describe('resolveEntryPath', () => { + const mockDir = createMockDirectory(); + + afterEach(() => { + mockDir.clear(); + }); + + it('should remove file extensions', () => { + mockDir.setContent({ + 'dev/custom.tsx': '// dev app code', + }); + + const result = resolveEntryPath('dev/custom.tsx', mockDir.path); + + expect(result).toBe('dev/custom'); + }); + + it('should remove trailing slashes', () => { + mockDir.setContent({ + 'dev/alpha.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev/alpha/', mockDir.path); + + expect(result).toBe('dev/alpha'); + }); + + it('should handle multiple dots in filename', () => { + mockDir.setContent({ + 'index.alpha.ts': 'export const data = {};', + }); + + const result = resolveEntryPath('index.alpha.ts', mockDir.path); + + expect(result).toBe('index.alpha'); + }); + + it('should handle simple directory names', () => { + mockDir.setContent({ + 'dev/index.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev', mockDir.path); + + expect(result).toBe('dev/index'); + }); + + it('should handle nested directory paths', () => { + mockDir.setContent({ + 'dev/alpha/index.ts': '// dev app code', + }); + + const result = resolveEntryPath('dev/alpha', mockDir.path); + + expect(result).toBe('dev/alpha/index'); + }); + + it('should return the file when there is a directory with the same name', () => { + mockDir.setContent({ + 'dev/alpha.ts': '// dev app code', + 'dev/app-config.yaml': '// dev app config', + 'dev/alpha/index.ts': '// dev app code', + 'dev/alpha/app-config.yaml': '// dev app config', + }); + + const result = resolveEntryPath('dev/alpha', mockDir.path); + + expect(result).toBe('dev/alpha'); + }); +}); diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 10f705ba4d..b49fd482e5 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -17,9 +17,24 @@ import { PackageRole } from '@backstage/cli-node'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; +import { parse, resolve, join } from 'path'; +import { glob } from 'glob'; + +export function resolveEntryPath( + entrypoint: string = 'dev', + targetDir: string, +): string { + const { dir: entryDir, name: entryName } = parse(entrypoint); + const [entryFile] = glob.sync(`${resolve(targetDir, entryDir, entryName)}.*`); + if (entryFile) { + return join(entryDir, entryName); + } + return join(entryDir, entryName, 'index'); +} export async function startPackage(options: { role: PackageRole; + entrypoint?: string; targetDir: string; configPaths: string[]; checksEnabled: boolean; @@ -45,8 +60,8 @@ export async function startPackage(options: { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ - entry: 'dev/index', ...options, + entry: resolveEntryPath(options.entrypoint, options.targetDir), }); case 'frontend-dynamic-container' as PackageRole: // experimental return startFrontend({ diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index fd31a6c782..5382adfec9 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -137,6 +137,10 @@ export const buildPlugin = createCliPlugin({ '--link ', 'Link an external workspace for module resolution', ) + .option( + '--entrypoint ', + 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', + ) .action(lazy(() => import('./commands/package/start'), 'command')); await defaultCommand.parseAsync(args, { from: 'user' }); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 391ccf8f22..39cfeddbd5 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -21,11 +21,10 @@ import chalk from 'chalk'; import { minimatch } from 'minimatch'; import semver from 'semver'; import { OptionValues } from 'commander'; -import yaml from 'yaml'; -import z from 'zod'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { paths } from '../../../../lib/paths'; +import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; import { mapDependencies, fetchPackageInfo, @@ -497,42 +496,3 @@ async function asLockfileVersion(version: string) { return version; } - -const yarnRcSchema = z.object({ - plugins: z - .array( - z.object({ - path: z.string(), - }), - ) - .optional(), -}); - -async function getHasYarnPlugin() { - const yarnRcPath = paths.resolveTargetRoot('.yarnrc.yml'); - const yarnRcContent = await fs.readFile(yarnRcPath, 'utf-8').catch(e => { - if (e.code === 'ENOENT') { - // gracefully continue in case the file doesn't exist - return ''; - } - throw e; - }); - - if (!yarnRcContent) { - return false; - } - - const parseResult = yarnRcSchema.safeParse(yaml.parse(yarnRcContent)); - - if (!parseResult.success) { - throw new Error( - `Unexpected content in .yarnrc.yml: ${parseResult.error.toString()}`, - ); - } - - const yarnRc = parseResult.data; - - return yarnRc.plugins?.some( - plugin => plugin.path === '.yarn/plugins/@yarnpkg/plugin-backstage.cjs', - ); -} diff --git a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts index 519f7c6dbc..9dfd60baba 100644 --- a/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts +++ b/packages/cli/src/modules/new/lib/execution/PortableTemplater.ts @@ -27,6 +27,7 @@ import lowerFirst from 'lodash/lowerFirst'; import { Lockfile } from '../../../../lib/versioning'; import { paths } from '../../../../lib/paths'; import { createPackageVersionProvider } from '../../../../lib/version'; +import { getHasYarnPlugin } from '../../../../lib/yarnPlugin'; const builtInHelpers = { camelCase, @@ -53,7 +54,10 @@ export class PortableTemplater { /* ignored */ } - const versionProvider = createPackageVersionProvider(lockfile); + const hasYarnPlugin = await getHasYarnPlugin(); + const versionProvider = createPackageVersionProvider(lockfile, { + preferBackstageProtocol: hasYarnPlugin, + }); const templater = new PortableTemplater( { diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index dda4d6e222..7c8e46f3f8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.10.4-next.0", + "version": "1.10.5-next.0", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" diff --git a/packages/config/package.json b/packages/config/package.json index ec6cd78422..87e69d0157 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config", - "version": "1.3.4-next.0", + "version": "1.3.5-next.0", "description": "Config API used by Backstage core, backend, and CLI", "backstage": { "role": "common-library" diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index b6109caf2e..90b0695ab9 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-components +## 0.18.2-next.3 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. + +## 0.18.2-next.2 + +### Patch Changes + +- 95935fb: Fixed dependency graph automatically scrolling forever +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + ## 0.18.2-next.1 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index c2d09762b3..fdde2c7e79 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-components", - "version": "0.18.2-next.1", + "version": "0.18.2-next.3", "description": "Core components used by Backstage plugins and apps", "backstage": { "role": "web-library" diff --git a/packages/core-components/report.api.md b/packages/core-components/report.api.md index b89b61ac77..9c9992deff 100644 --- a/packages/core-components/report.api.md +++ b/packages/core-components/report.api.md @@ -274,6 +274,7 @@ export interface DependencyGraphProps paddingY?: number; ranker?: DependencyGraphTypes.Ranker; rankMargin?: number; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; renderNode?: DependencyGraphTypes.RenderNodeFunction; showArrowHeads?: boolean; @@ -316,6 +317,33 @@ export namespace DependencyGraphTypes { NETWORK_SIMPLEX = 'network-simplex', TIGHT_TREE = 'tight-tree', } + export type RenderEdgeFunction = ( + props: RenderEdgeProps, + ) => ReactNode; + export type RenderEdgeProps = { + edge: T & { + points: { + x: number; + y: number; + }[]; + label?: string; + labeloffset?: number; + labelpos?: string; + width?: number; + height?: number; + weight?: number; + minlen?: number; + showArrowHeads?: boolean; + from?: string; + to?: string; + relations?: string[]; + }; + id: { + v: string; + w: string; + name?: string | undefined; + }; + }; export type RenderLabelFunction = ( props: RenderLabelProps, ) => ReactNode; diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index df26bb86fd..4b99402c3a 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -54,7 +54,7 @@ const useStyles = makeStyles((theme: Theme) => ({ minWidth: '100%', }, fixedHeight: { - height: '100%', + maxHeight: '100%', }, fullscreen: { backgroundColor: theme.palette.background.paper, @@ -167,6 +167,10 @@ export interface DependencyGraphProps * Weight applied to edges in graph */ edgeWeight?: number; + /** + * Custom edge rendering component + */ + renderEdge?: Types.RenderEdgeFunction; /** * Custom node rendering component */ @@ -248,6 +252,7 @@ export function DependencyGraph( labelOffset = 10, edgeRanks = 1, edgeWeight = 1, + renderEdge, renderLabel, defs, zoom = 'enabled', @@ -282,7 +287,8 @@ export function DependencyGraph( const [_measureRef] = useMeasure(); const measureRef = once(_measureRef); - const scalableHeight = fit === 'grow' ? maxHeight : containerHeight; + const scalableHeight = + fit === 'grow' && !fullScreenHandle.active ? maxHeight : '100%'; const containerRef = useMemo( () => @@ -335,10 +341,16 @@ export function DependencyGraph( const { width: newContainerWidth, height: newContainerHeight } = root.getBoundingClientRect(); - if (containerWidth !== newContainerWidth) { + if ( + containerWidth !== newContainerWidth && + newContainerWidth <= maxWidth + ) { setContainerWidth(newContainerWidth); } - if (containerHeight !== newContainerHeight) { + if ( + containerHeight !== newContainerHeight && + newContainerHeight <= maxHeight + ) { setContainerHeight(newContainerHeight); } }, 100), @@ -464,39 +476,32 @@ export function DependencyGraph( ); return ( -
- - {allowFullscreen && ( - - - {fullScreenHandle.active ? ( - - ) : ( - - )} - - - )} + {allowFullscreen && ( + + + {fullScreenHandle.active ? ( + + ) : ( + + )} + + + )} +
( height={graphHeight} y={maxHeight / 2 - graphHeight / 2} x={maxWidth / 2 - graphWidth / 2} - viewBox={`0 0 ${graphWidth} ${graphHeight}`} + viewBox={`-25 -25 ${graphWidth + 50} ${graphHeight + 50}`} > {graphEdges.map(e => { const edge = graph.current.edge(e) as GraphEdge; if (!edge) return null; + if (renderEdge) return renderEdge({ edge, id: e }); + return ( ( - -
+
+ ); } diff --git a/packages/core-components/src/components/DependencyGraph/types.ts b/packages/core-components/src/components/DependencyGraph/types.ts index a449637e49..4436f8d1aa 100644 --- a/packages/core-components/src/components/DependencyGraph/types.ts +++ b/packages/core-components/src/components/DependencyGraph/types.ts @@ -93,6 +93,42 @@ export namespace DependencyGraphTypes { props: RenderNodeProps, ) => ReactNode; + /** + * Properties of {@link DependencyGraphTypes.RenderEdgeFunction} for {@link DependencyGraphTypes.DependencyEdge} + * + * @public + */ + export type RenderEdgeProps = { + edge: T & { + points: { x: number; y: number }[]; + label?: string; + labeloffset?: number; + labelpos?: string; + width?: number; + height?: number; + weight?: number; + minlen?: number; + showArrowHeads?: boolean; + from?: string; + to?: string; + relations?: string[]; + }; + id: { + v: string; + w: string; + name?: string | undefined; + }; + }; + + /** + * Custom React component for graph {@link DependencyGraphTypes.DependencyEdge} + * + * @public + */ + export type RenderEdgeFunction = ( + props: RenderEdgeProps, + ) => ReactNode; + /** * Graph direction * diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index b2960b4c83..dfff2af4f3 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/create-app +## 0.7.5-next.3 + +### Patch Changes + +- Bumped create-app version. + +## 0.7.5-next.2 + +### Patch Changes + +- Bumped create-app version. + ## 0.7.5-next.1 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index cd37f6eef6..9fa8af4c1b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/create-app", - "version": "0.7.5-next.1", + "version": "0.7.5-next.3", "description": "A CLI that helps you create your own Backstage app", "backstage": { "role": "cli" diff --git a/packages/create-app/templates/next-app/packages/backend/package.json.hbs b/packages/create-app/templates/next-app/packages/backend/package.json.hbs index 483c0b7a4e..518683ffd7 100644 --- a/packages/create-app/templates/next-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/next-app/packages/backend/package.json.hbs @@ -44,7 +44,7 @@ "@backstage/plugin-signals-backend": "^{{version '@backstage/plugin-signals-backend'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", "app": "link:../app", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^12.0.0", "node-gyp": "^10.0.0", "pg": "^8.11.3" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 7f7338afeb..4a0af6926c 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/dev-utils +## 1.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + ## 1.1.15-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 707717f6a4..9540f79c9e 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.1.15-next.1", + "version": "1.1.15-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index 7e12499a1c..e80d058140 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -96,6 +96,7 @@ describe('createPublicSignInApp', () => {
; reactElement: ConfigurableExtensionDataRef< JSX_3.Element, 'core.reactElement', diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 18466d8196..b778539101 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,6 +20,7 @@ import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { + title: createExtensionDataRef().with({ id: 'core.title' }), reactElement: createExtensionDataRef().with({ id: 'core.reactElement', }), diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 54f526a1f2..121bf3b763 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,18 @@ # techdocs-cli-embedded-app +## 0.2.113-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.34.4-next.2 + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + - @backstage/app-defaults@1.7.1-next.2 + - @backstage/test-utils@1.7.12-next.1 + - @backstage/plugin-techdocs@1.15.1-next.2 + - @backstage/plugin-techdocs-react@1.3.4-next.1 + ## 0.2.113-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/app-config.yaml b/packages/techdocs-cli-embedded-app/app-config.yaml index 6ede05b587..c8dfa1683c 100644 --- a/packages/techdocs-cli-embedded-app/app-config.yaml +++ b/packages/techdocs-cli-embedded-app/app-config.yaml @@ -7,3 +7,7 @@ backend: techdocs: builder: 'external' + sanitizer: + # Allow live reload locally. Added in this config to avoid updating the main techdocs plugin. + allowedCustomElementTagNameRegExp: '^live-reload$' + allowedCustomElementAttributeNameRegExp: '^live-reload-(epoch|request-id)$' diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 951a3df06e..a4019eb918 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.113-next.1", + "version": "0.2.113-next.2", "backstage": { "role": "frontend" }, diff --git a/packages/techdocs-cli-embedded-app/src/App.tsx b/packages/techdocs-cli-embedded-app/src/App.tsx index 273a828b4a..13d5b925f0 100644 --- a/packages/techdocs-cli-embedded-app/src/App.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.tsx @@ -36,6 +36,7 @@ import * as plugins from './plugins'; import { configLoader } from './config'; import { Root } from './components/Root'; import { techDocsPage, TechDocsThemeToggle } from './components/TechDocsPage'; +import { TechDocsLiveReload } from './LiveReloadAddon'; const app = createApp({ apis, @@ -54,6 +55,14 @@ const ThemeToggleAddon = techdocsPlugin.provide( }), ); +const LiveReloadAddon = techdocsPlugin.provide( + createTechDocsAddonExtension({ + name: 'LiveReloadAddon', + component: TechDocsLiveReload, + location: TechDocsAddonLocations.Content, + }), +); + const routes = ( @@ -71,6 +80,7 @@ const routes = ( > {techDocsPage} + diff --git a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx new file mode 100644 index 0000000000..1a80def9e9 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; +import { TechDocsLiveReload } from './LiveReloadAddon'; + +jest.mock('@backstage/plugin-techdocs-react', () => ({ + useShadowRootElements: jest.fn(() => [ + { + querySelector: jest.fn((selector: string) => { + if (selector === 'live-reload') { + return { + getAttribute: (name: string) => { + if (name === 'live-reload-epoch') return '10'; + if (name === 'live-reload-request-id') return '1'; + return null; + }, + }; + } + return null; + }), + }, + ]), +})); + +describe('TechDocsLiveReload', () => { + const originalXHR = global.XMLHttpRequest; + let originalLocation: Location; + let openSpy: jest.Mock; + let sendSpy: jest.Mock; + + beforeEach(() => { + originalLocation = window.location; + openSpy = jest.fn(); + sendSpy = jest.fn(function (this: any) { + // simulate long-poll response that does NOT trigger reload (epoch unchanged) + setTimeout(() => { + (this as any).status = 200; + (this as any).responseText = '10'; + (this as any).onloadend?.call(this); + }, 0); + }); + + class MockXHR { + onloadend: ((this: any) => void) | null = null; + status = 0; + responseText = ''; + open = openSpy; + send = sendSpy as any; + abort = jest.fn(); + } + + global.XMLHttpRequest = MockXHR as any; + + // Replace window.location with a mutable object for tests + delete (window as any).location; + (window as any).location = { ...originalLocation, reload: jest.fn() }; + jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); + jest.spyOn(window, 'removeEventListener').mockImplementation(() => {}); + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + configurable: true, + }); + }); + + afterEach(() => { + global.XMLHttpRequest = originalXHR; + jest.restoreAllMocks(); + // restore original window.location + delete (window as any).location; + (window as any).location = originalLocation; + }); + + it('polls livereload endpoint and does not reload when epoch unchanged', async () => { + const reloadSpy = window.location.reload as unknown as jest.Mock; + render(); + expect(openSpy).toHaveBeenCalledWith('GET', '/.livereload/10/1'); + // give microtask queue a tick + await new Promise(res => setTimeout(res, 0)); + expect(reloadSpy).not.toHaveBeenCalled(); + }); + + it('reloads when server epoch increases', async () => { + const reloadSpy = window.location.reload as unknown as jest.Mock; + + sendSpy.mockImplementation(function (this: any) { + setTimeout(() => { + (this as any).status = 200; + (this as any).responseText = '11'; + (this as any).onloadend?.call(this); + }, 0); + }); + + render(); + await new Promise(res => setTimeout(res, 0)); + expect(reloadSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx new file mode 100644 index 0000000000..905a339c72 --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/LiveReloadAddon.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; +import { useEffect, useRef } from 'react'; + +interface TechDocsLiveReloadProps { + /** Whether to enable livereload (default: true in development) */ + enabled?: boolean; +} + +/** + * LiveReload addon for Techdocs CLI. + * + * Support mkdocs built-in livereload, in a TechDocs CLI preview environment. + * See https://github.com/backstage/backstage/issues/30514 for more details. + */ +export const TechDocsLiveReload = ({ + enabled = true, +}: TechDocsLiveReloadProps) => { + const body = useShadowRootElements(['body']); + const reqRef = useRef(null); + const timeoutRef = useRef(null); + const LIVE_RELOAD_ELEMENT = 'live-reload'; + const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch'; + const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id'; + const CLI_LIVERELOAD_PATH = '/.livereload'; + + useEffect(() => { + if (!enabled || !body[0]) { + return undefined; + } + + const liveReloadElement = body[0].querySelector(LIVE_RELOAD_ELEMENT); + + if (!liveReloadElement) { + return undefined; + } + + const epoch = parseInt( + liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_EPOCH) || '0', + 10, + ); + const requestId = parseInt( + liveReloadElement.getAttribute(LIVE_RELOAD_ATTR_REQUEST_ID) || '0', + 10, + ); + + if (!epoch || !requestId) { + return undefined; + } + + const livereloadUrl = CLI_LIVERELOAD_PATH; + + const poll = () => { + reqRef.current = new XMLHttpRequest(); + reqRef.current.onloadend = function handleLoadEnd(this: XMLHttpRequest) { + if (parseFloat(this.responseText) > epoch) { + window.location.reload(); + } else { + timeoutRef.current = setTimeout(poll, this.status === 200 ? 0 : 3000); + } + }; + reqRef.current.open('GET', `${livereloadUrl}/${epoch}/${requestId}`); + reqRef.current.send(); + }; + + const stop = () => { + if (reqRef.current) { + reqRef.current.abort(); + reqRef.current = null; + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + // Stop when tab is inactive + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + poll(); + } else { + stop(); + } + }; + + const handleBeforeUnload = () => { + stop(); + }; + + // Start polling if page is visible + if (document.visibilityState === 'visible') { + poll(); + } + + document.addEventListener('visibilitychange', handleVisibilityChange); + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + stop(); + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [body, enabled]); + + return null; +}; diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index c38bc86d7a..d7e8f62ea0 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -18,6 +18,10 @@ import serveHandler from 'serve-handler'; import http from 'http'; import httpProxy from 'http-proxy'; import { createLogger } from './utility'; +import { + proxyHtmlWithLivereloadInjection, + proxyMkdocsLivereload, +} from './livereload'; export default class HTTPServer { private readonly proxyEndpoint: string; @@ -59,8 +63,8 @@ export default class HTTPServer { const proxyHandler = this.createProxy(); const server = http.createServer( (request: http.IncomingMessage, response: http.ServerResponse) => { - // This endpoind is used by the frontend to issue a cookie for the user. - // But the MkDocs server doesn't expose it as a the Backestage backend does. + // This endpoint is used by the frontend to issue a cookie for the user. + // But the MkDocs server doesn't expose it as a the Backstage backend does. // So we need to fake it here to prevent 404 errors. if (request.url === '/api/techdocs/.backstage/auth/v1/cookie') { const oneHourInMilliseconds = 60 * 60 * 1000; @@ -72,6 +76,19 @@ export default class HTTPServer { } if (request.url?.startsWith(this.proxyEndpoint)) { + // Handle HTML files with livereload parameter injection + if (request.url?.endsWith('.html')) { + proxyHtmlWithLivereloadInjection({ + request, + response, + mkdocsTargetAddress: this.mkdocsTargetAddress, + proxyEndpoint: this.proxyEndpoint, + onError: (error: Error) => reject(error), + }); + return; + } + + // Handle non-HTML files with regular proxy const [proxy, forwardPath] = proxyHandler(request); proxy.on('error', (error: Error) => { @@ -93,6 +110,17 @@ export default class HTTPServer { return; } + // This endpoint is used by the frontend to pass livereload requests to the mkdocs server. + if (request.url?.startsWith('/.livereload')) { + proxyMkdocsLivereload({ + request, + response, + mkdocsTargetAddress: this.mkdocsTargetAddress, + onError: (error: Error) => reject(error), + }); + return; + } + serveHandler(request, response, { public: this.backstageBundleDir, trailingSlash: true, diff --git a/packages/techdocs-cli/src/lib/livereload.test.ts b/packages/techdocs-cli/src/lib/livereload.test.ts new file mode 100644 index 0000000000..b21251c45a --- /dev/null +++ b/packages/techdocs-cli/src/lib/livereload.test.ts @@ -0,0 +1,180 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import http from 'http'; +import { + injectLivereloadParameters, + proxyHtmlWithLivereloadInjection, + proxyMkdocsLivereload, +} from './livereload'; + +// Note: This mock returns a singleton proxy object so tests can access the +// registered event handlers (e.g. `proxyRes`) from code under test. +jest.mock('http-proxy', () => { + const handlers: Record = {}; + const fakeProxy = { + on: jest.fn((event: string, cb: Function) => { + handlers[event] = cb; + }), + web: jest.fn((_req: unknown, _res: unknown) => { + // no-op; tests will manually trigger handlers['proxyRes'] when needed + }), + __handlers: handlers, + }; + const create = jest.fn(() => fakeProxy); + return { + __esModule: true, + default: { createProxyServer: create }, + createProxyServer: create, + }; +}); + +describe('livereload helpers', () => { + describe('injectLivereloadParameters', () => { + it('injects live-reload element when mkdocs script is present', () => { + const html = + ''; + const result = injectLivereloadParameters(html); + expect(result).toContain(''); + }); + + it('returns original html when mkdocs script is absent', () => { + const html = '

No livereload

'; + const result = injectLivereloadParameters(html); + expect(result).toBe(html); + }); + }); + + describe('proxyHtmlWithLivereloadInjection', () => { + it('injects parameters into HTML responses and sets CORS headers', () => { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/api/techdocs/some/path/index.html', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + end: jest.fn(), + } as unknown as http.ServerResponse; + + proxyHtmlWithLivereloadInjection({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + proxyEndpoint: '/api/techdocs/', + onError: () => {}, + }); + + // Simulate mkdocs proxy response with HTML + const proxyRes: any = { + headers: { 'content-type': 'text/html; charset=utf-8' }, + on: (event: string, cb: Function) => { + if (event === 'data') { + cb(''); + } + if (event === 'end') { + cb(); + } + }, + pipe: jest.fn(), + }; + + (proxy as any).__handlers.proxyRes(proxyRes, {} as any, res); + + expect(res.end).toHaveBeenCalled(); + const injectedHtml = (res.end as jest.Mock).mock.calls[0][0] as string; + expect(injectedHtml).toContain(' { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/api/techdocs/some/path/asset.css', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + end: jest.fn(), + } as unknown as http.ServerResponse; + + proxyHtmlWithLivereloadInjection({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + proxyEndpoint: '/api/techdocs/', + onError: () => {}, + }); + + const proxyRes: any = { + headers: { 'content-type': 'text/css' }, + on: jest.fn(), + pipe: jest.fn(), + }; + + (proxy as any).__handlers.proxyRes(proxyRes, {} as any, res); + + expect(res.end).not.toHaveBeenCalled(); + expect(proxyRes.pipe).toHaveBeenCalledWith(res); + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS'); + }); + }); + + describe('proxyMkdocsLivereload', () => { + it('rewrites path and sets CORS headers', () => { + const { createProxyServer } = jest.requireMock('http-proxy') as any; + const proxy = createProxyServer(); + + const req = { + url: '/.livereload/1/2', + } as unknown as http.IncomingMessage; + const headers: Record = {}; + const res = { + setHeader: (k: string, v: any) => { + headers[k] = String(v); + }, + } as unknown as http.ServerResponse; + + proxyMkdocsLivereload({ + request: req, + response: res, + mkdocsTargetAddress: 'http://localhost:8000', + onError: () => {}, + }); + + expect(req.url).toBe('/livereload/1/2'); + expect(headers['Access-Control-Allow-Origin']).toBe('*'); + expect(headers['Access-Control-Allow-Methods']).toBe('GET, OPTIONS'); + expect(headers['Access-Control-Allow-Headers']).toBe('Content-Type'); + expect((proxy as any).web).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/techdocs-cli/src/lib/livereload.ts b/packages/techdocs-cli/src/lib/livereload.ts new file mode 100644 index 0000000000..c108971b82 --- /dev/null +++ b/packages/techdocs-cli/src/lib/livereload.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import http from 'http'; +import httpProxy from 'http-proxy'; + +/** + * Livereload support for techdocs-cli. + * + * Context: + * - MkDocs implements autoreload using a long-poll endpoint `/livereload` and a script that injects + * a call like: `livereload(epoch, request_id)`, where `epoch` is derived from Python's + * `time.monotonic()`. + * - Node.js monotonic clocks (`process.hrtime`/`performance.now`) are not compatible with Python's + * value across processes and platforms. We therefore CANNOT reliably re-create the same epoch on + * the frontend or in this CLI, and must read the values produced by MkDocs itself. + * - The MkDocs script tag is removed by DOM sanitization (DomPurify) in TechDocs, so we can't rely + * on the script being present in the embedded app. To bridge this, we extract the parameters on + * the server side while proxying HTML and inject them as a safe custom element that survives + * sanitization: ``. + * - The frontend addon reads that element and polls `/.livereload` (served by techdocs-cli), which + * this module maps to MkDocs `/livereload` with permissive CORS headers. + * - Quality-of-life: if extraction fails or the endpoint is unavailable, normal docs still work. + * + * See issue for background and rationale: https://github.com/backstage/backstage/issues/30514 + */ + +const LIVE_RELOAD_ELEMENT = 'live-reload'; +const LIVE_RELOAD_ATTR_EPOCH = 'live-reload-epoch'; +const LIVE_RELOAD_ATTR_REQUEST_ID = 'live-reload-request-id'; +const CLI_LIVERELOAD_PATH = '/.livereload'; +const MKDOCS_LIVERELOAD_PATH = '/livereload'; +const CONTENT_TYPE_HTML = 'text/html'; +const HEADER_CONTENT_LENGTH = 'content-length'; + +const BODY_START_RE = /]*>/; +// Matches mkdocs injected call livereload(epoch, requestId) +const MKDOCS_LIVERELOAD_CALL_RE = /livereload\(\s*(\d+)\s*,\s*(\d+)\s*\)\s*;?/; + +/** + * Extract livereload parameters from mkdocs HTML and inject them as a custom element. + * The injected element will later be read by the frontend addon even after DOM sanitization. + * + * Note: + * - we don't add to because of DomPurify sanitization. + * - we add close to the body opening to avoid reading too far into the body. + * - we should use streamed injection to improve performance. + */ +export function injectLivereloadParameters(html: string): string { + const livereloadMatch = html.match(MKDOCS_LIVERELOAD_CALL_RE); + + // If we couldn't find livereload parameters, return original HTML untouched. + if (!livereloadMatch) { + return html; + } + + const [, epoch, requestId] = livereloadMatch; + // Insert a minimal custom element that the frontend addon can discover post-sanitization. + // Note: embedded app needs a custom config to allow the element to survive sanitization. + const liveReloadTag = `<${LIVE_RELOAD_ELEMENT} ${LIVE_RELOAD_ATTR_EPOCH}="${epoch}" ${LIVE_RELOAD_ATTR_REQUEST_ID}="${requestId}">`; + + // Naively find where to insert the livereload tag. + const bodyStart = html.match(BODY_START_RE); + const bodyStartIndex = bodyStart?.index ?? 0; + const bodyStartLength = bodyStart?.[0]?.length ?? 0; + if (bodyStartIndex === 0 || bodyStartLength === 0) { + return html; + } + const bodyEndIndex = bodyStartIndex + bodyStartLength; + + return html.slice(0, bodyEndIndex) + liveReloadTag + html.slice(bodyEndIndex); +} + +function setCorsHeaders(response: http.ServerResponse) { + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); +} + +/** + * Proxies a mkdocs HTML response, injecting livereload parameters into the HTML body. + */ +export function proxyHtmlWithLivereloadInjection(options: { + request: http.IncomingMessage; + response: http.ServerResponse; + mkdocsTargetAddress: string; + proxyEndpoint: string; + onError: (error: Error) => void; +}): void { + const { request, response, mkdocsTargetAddress, proxyEndpoint, onError } = + options; + + const htmlProxy = httpProxy.createProxyServer({ + target: mkdocsTargetAddress, + selfHandleResponse: true, + }); + + htmlProxy.on('error', onError); + + // Intercept HTML responses to inject `` + htmlProxy.on('proxyRes', (proxyRes, _req, res) => { + const contentType = proxyRes.headers['content-type']; + const contentEncoding = proxyRes.headers['content-encoding']; + const isHtml = + contentType && + typeof contentType === 'string' && + contentType.startsWith(CONTENT_TYPE_HTML); + if (isHtml && !contentEncoding) { + const chunks: Buffer[] = []; + proxyRes.on('data', (chunk: Buffer | string) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + proxyRes.on('end', () => { + const body = Buffer.concat(chunks).toString('utf8'); + const modifiedHtml = injectLivereloadParameters(body); + res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200; + Object.keys(proxyRes.headers).forEach(key => { + if (key.toLowerCase() !== HEADER_CONTENT_LENGTH) { + res.setHeader(key, proxyRes.headers[key]!); + } + }); + setCorsHeaders(res); + res.end(modifiedHtml); + }); + } else { + res.statusCode = (proxyRes.statusCode as number | undefined) ?? 200; + Object.keys(proxyRes.headers).forEach(key => { + res.setHeader(key, proxyRes.headers[key]!); + }); + setCorsHeaders(res); + proxyRes.pipe(res); + } + }); + + const forwardPath = + request.url?.replace(new RegExp(`^${proxyEndpoint}`, 'i'), '') || ''; + request.url = forwardPath; + htmlProxy.web(request, response); +} + +/** + * Proxies mkdocs livereload long-polling requests, mapping the CLI path to mkdocs path. + */ +export function proxyMkdocsLivereload(options: { + request: http.IncomingMessage; + response: http.ServerResponse; + mkdocsTargetAddress: string; + onError: (error: Error) => void; +}): void { + const { request, response, mkdocsTargetAddress, onError } = options; + + const proxy = httpProxy.createProxyServer({ target: mkdocsTargetAddress }); + proxy.on('error', onError); + + setCorsHeaders(response); + response.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + request.url = request.url?.replace( + CLI_LIVERELOAD_PATH, + MKDOCS_LIVERELOAD_PATH, + ); + proxy.web(request, response); +} diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 8425798421..a7005cc96d 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/test-utils +## 1.7.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/theme@0.6.9-next.0 + ## 1.7.12-next.0 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 6e914f3b50..1eb1849d1f 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.7.12-next.0", + "version": "1.7.12-next.1", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 9cb3a6527b..5d531e0a9b 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.6.9-next.0 + +### Patch Changes + +- d5cbdba: The `UnifiedThemeProvider` now coordinates theme attributes on the document `body` in case multiple theme providers are rendered. + ## 0.6.8 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 3699d858a4..1ac8aeb9bb 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/theme", - "version": "0.6.8", + "version": "0.6.9-next.0", "description": "material-ui theme for use with Backstage.", "backstage": { "role": "web-library" diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx index a80e36cf35..64694aff37 100644 --- a/packages/theme/src/unified/UnifiedThemeProvider.tsx +++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ReactNode, useEffect } from 'react'; +import { ReactNode } from 'react'; import CssBaseline from '@material-ui/core/CssBaseline'; import { ThemeProvider, @@ -58,6 +58,8 @@ const generateV4ClassName = createGenerateClassName({ productionPrefix: 'jss4-', }); +import { useApplyThemeAttributes } from './useApplyThemeAttributes'; + /** * Provides themes for all Material UI versions supported by the provided unified theme. * @@ -70,18 +72,11 @@ export function UnifiedThemeProvider( const v4Theme = theme.getTheme('v4') as Mui4Theme; const v5Theme = theme.getTheme('v5') as Mui5Theme; - const themeMode = v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode; - const themeName = 'backstage'; - useEffect(() => { - document.body.setAttribute('data-theme-mode', themeMode); - document.body.setAttribute('data-theme-name', themeName); - - return () => { - document.body.removeAttribute('data-theme-mode'); - document.body.removeAttribute('data-theme-name'); - }; - }, [themeMode, themeName]); + useApplyThemeAttributes( + v4Theme ? v4Theme.palette.type : v5Theme?.palette.mode, + 'backstage', + ); let cssBaseline: JSX.Element | undefined = undefined; if (!noCssBaseline) { diff --git a/packages/theme/src/unified/useApplyThemeAttributes.test.tsx b/packages/theme/src/unified/useApplyThemeAttributes.test.tsx new file mode 100644 index 0000000000..b6c1ec1852 --- /dev/null +++ b/packages/theme/src/unified/useApplyThemeAttributes.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; +import { renderHook } from '@testing-library/react'; +import { useApplyThemeAttributes } from './useApplyThemeAttributes'; + +describe('useApplyThemeAttributes', () => { + beforeEach(() => { + document.body.removeAttribute('data-unified-theme-stack'); + document.body.removeAttribute('data-theme-mode'); + document.body.removeAttribute('data-theme-name'); + }); + + it('pushes attributes on mount and pops on unmount', () => { + const { unmount } = renderHook(() => + useApplyThemeAttributes('light', 'one'), + ); + expect(document.body.getAttribute('data-theme-mode')).toBe('light'); + expect(document.body.getAttribute('data-theme-name')).toBe('one'); + expect( + JSON.parse(document.body.getAttribute('data-unified-theme-stack') || '[]') + .length, + ).toBe(1); + + unmount(); + expect(document.body.getAttribute('data-theme-mode')).toBeNull(); + expect(document.body.getAttribute('data-theme-name')).toBeNull(); + expect(document.body.getAttribute('data-unified-theme-stack')).toBeNull(); + }); + + it('stacks multiple mounts and applies top-most', () => { + const r1 = renderHook(() => useApplyThemeAttributes('light', 'one')); + const r2 = renderHook(() => useApplyThemeAttributes('dark', 'two')); + + expect(document.body.getAttribute('data-theme-mode')).toBe('dark'); + expect(document.body.getAttribute('data-theme-name')).toBe('two'); + + r2.unmount(); + expect(document.body.getAttribute('data-theme-mode')).toBe('light'); + expect(document.body.getAttribute('data-theme-name')).toBe('one'); + + r1.unmount(); + expect(document.body.getAttribute('data-theme-mode')).toBeNull(); + expect(document.body.getAttribute('data-theme-name')).toBeNull(); + }); +}); diff --git a/packages/theme/src/unified/useApplyThemeAttributes.ts b/packages/theme/src/unified/useApplyThemeAttributes.ts new file mode 100644 index 0000000000..8295d69ec6 --- /dev/null +++ b/packages/theme/src/unified/useApplyThemeAttributes.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useEffect } from 'react'; + +type ThemeFrame = { mode?: string; name?: string }; + +const STACK_ATTR = 'data-unified-theme-stack'; +const MODE_ATTR = 'data-theme-mode'; +const NAME_ATTR = 'data-theme-name'; + +function mutateAttrStack(mutator: (stack: ThemeFrame[]) => void) { + const stack = (() => { + const raw = document.body.getAttribute(STACK_ATTR); + if (!raw) { + return [] as ThemeFrame[]; + } + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? (parsed as ThemeFrame[]) : []; + } catch { + return [] as ThemeFrame[]; + } + })(); + + mutator(stack); + + if (stack.length === 0) { + document.body.removeAttribute(STACK_ATTR); + } else { + document.body.setAttribute(STACK_ATTR, JSON.stringify(stack)); + } + + const top = stack[stack.length - 1]; + if (top?.mode) { + document.body.setAttribute(MODE_ATTR, String(top.mode)); + } else { + document.body.removeAttribute(MODE_ATTR); + } + if (top?.name) { + document.body.setAttribute(NAME_ATTR, String(top.name)); + } else { + document.body.removeAttribute(NAME_ATTR); + } +} + +export function useApplyThemeAttributes(themeMode: string, themeName: string) { + useEffect(() => { + mutateAttrStack(stack => { + stack.push({ mode: themeMode, name: themeName }); + }); + + return () => { + mutateAttrStack(stack => { + stack.pop(); + }); + }; + }, [themeMode, themeName]); +} diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 4c9d6f833b..95314bf768 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/ui +## 0.7.2-next.2 + +### Patch Changes + +- 3c921c5: Making href mandatory in tabs that are part of a Header component +- 5c21e45: Add react router for internal routing for ButtonLinks +- 9781815: Remove auto selection of tabs for tabs that all have href defined +- f6dff5b: Using react router for internal links in the Menu component + +## 0.7.2-next.1 + +### Patch Changes + +- a9b88be: Enable tooltips on disabled buttons with automatic wrapper +- 4adbb03: Avoid overriding onChange when spreading props + ## 0.7.2-next.0 ### Patch Changes diff --git a/packages/ui/css/styles.css b/packages/ui/css/styles.css index 8aa99175b3..d93ba87829 100644 --- a/packages/ui/css/styles.css +++ b/packages/ui/css/styles.css @@ -10522,6 +10522,59 @@ } } +.bui-PasswordField { + font-family: var(--bui-font-regular); + --bui-passwordmanager-icon-width: var(--bui-space-1); + flex-direction: column; + flex-shrink: 0; + width: 100%; + display: flex; +} + +.bui-InputVisibility { + right: var(--bui-passwordmanager-icon-width); + cursor: pointer; + color: var(--bui-fg-primary); + background-color: #0000; + border: none; + justify-content: center; + align-items: center; + margin: 0; + padding: 0; + display: flex; + position: absolute; + top: 0; + bottom: 0; + + &[data-size="small"] { + width: 2rem; + height: 2rem; + } + + &[data-size="small"] svg { + width: 1rem; + height: 1rem; + } + + &[data-size="medium"] { + width: 2.5rem; + height: 2.5rem; + } + + &[data-size="medium"] svg { + width: 1.25rem; + height: 1.25rem; + } +} + +.bui-PasswordField .bui-InputWrapper[data-size="small"] .bui-Input { + padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); +} + +.bui-PasswordField .bui-InputWrapper[data-size="medium"] .bui-Input { + padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); +} + .bui-SearchField { flex: 1 0; diff --git a/packages/ui/package.json b/packages/ui/package.json index b4432c8b6b..fd6ecd169c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/ui", - "version": "0.7.2-next.0", + "version": "0.7.2-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 5d04bf8626..1f1664b9e9 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -525,6 +525,12 @@ export const componentDefinitions: { readonly emptyState: 'bui-MenuEmptyState'; }; }; + readonly PasswordField: { + readonly classNames: { + readonly root: 'bui-PasswordField'; + readonly inputVisibility: 'bui-InputVisibility'; + }; + }; readonly Popover: { readonly classNames: { readonly root: 'bui-Popover'; @@ -643,6 +649,7 @@ export const componentDefinitions: { readonly inputWrapper: 'bui-InputWrapper'; readonly input: 'bui-Input'; readonly inputIcon: 'bui-InputIcon'; + readonly inputAction: 'bui-InputAction'; }; readonly dataAttributes: { readonly invalid: readonly [true, false]; @@ -1038,7 +1045,7 @@ export interface HeaderProps { // @public export interface HeaderTab { // (undocumented) - href?: string; + href: string; // (undocumented) id: string; // (undocumented) @@ -1760,6 +1767,7 @@ export interface TextFieldProps icon?: ReactNode; placeholder?: string; size?: 'small' | 'medium' | Partial>; + type?: 'text' | 'email' | 'tel' | 'url'; } // @public (undocumented) diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx index b4667f017b..f032af318c 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.stories.tsx @@ -14,15 +14,23 @@ * limitations under the License. */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react-vite'; import { ButtonLink } from './ButtonLink'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { Icon } from '../Icon'; +import { MemoryRouter } from 'react-router-dom'; const meta = { title: 'Backstage UI/ButtonLink', component: ButtonLink, + decorators: [ + (Story: StoryFn) => ( + + + + ), + ], argTypes: { size: { control: 'select', diff --git a/packages/ui/src/components/ButtonLink/ButtonLink.tsx b/packages/ui/src/components/ButtonLink/ButtonLink.tsx index 153643a530..b5b5ab208d 100644 --- a/packages/ui/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/ui/src/components/ButtonLink/ButtonLink.tsx @@ -16,13 +16,16 @@ import clsx from 'clsx'; import { forwardRef, Ref } from 'react'; -import { Link as RALink } from 'react-aria-components'; +import { Link as RALink, RouterProvider } from 'react-aria-components'; +import { useNavigate, useHref } from 'react-router-dom'; import type { ButtonLinkProps } from './types'; import { useStyles } from '../../hooks/useStyles'; +import { isExternalLink } from '../../utils/isExternalLink'; /** @public */ export const ButtonLink = forwardRef( (props: ButtonLinkProps, ref: Ref) => { + const navigate = useNavigate(); const { size = 'small', variant = 'primary', @@ -30,6 +33,7 @@ export const ButtonLink = forwardRef( iconEnd, children, className, + href, ...rest } = props; @@ -40,17 +44,48 @@ export const ButtonLink = forwardRef( const { classNames: classNamesButtonLink } = useStyles('ButtonLink'); + const isExternal = isExternalLink(href); + + // If it's an external link, render RALink without RouterProvider + if (isExternal) { + return ( + + {iconStart} + {children} + {iconEnd} + + ); + } + + // For internal links, use RouterProvider return ( - - {iconStart} - {children} - {iconEnd} - + + + {iconStart} + {children} + {iconEnd} + + ); }, ); diff --git a/packages/ui/src/components/Header/types.ts b/packages/ui/src/components/Header/types.ts index 41bc437a18..c7c602abfc 100644 --- a/packages/ui/src/components/Header/types.ts +++ b/packages/ui/src/components/Header/types.ts @@ -39,7 +39,7 @@ export interface HeaderProps { export interface HeaderTab { id: string; label: string; - href?: string; + href: string; /** * Strategy for matching the current route to determine if this tab should be active. * - 'exact': Tab href must exactly match the current pathname (default) diff --git a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx index c4849ddc20..ab527bcda4 100644 --- a/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx +++ b/packages/ui/src/components/HeaderPage/HeaderPage.stories.tsx @@ -44,22 +44,27 @@ const tabs: HeaderTab[] = [ { id: 'overview', label: 'Overview', + href: '/overview', }, { id: 'checks', label: 'Checks', + href: '/checks', }, { id: 'tracks', label: 'Tracks', + href: '/tracks', }, { id: 'campaigns', label: 'Campaigns', + href: '/campaigns', }, { id: 'integrations', label: 'Integrations', + href: '/integrations', }, ]; @@ -148,7 +153,11 @@ export const WithCustomActions: Story = { <> - } /> + } + aria-label="More options" + /> {menuItems.map(option => ( { const { classNames } = useStyles('Menu'); @@ -74,19 +74,22 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => { export const Menu = (props: MenuProps) => { const { placement = 'bottom start', ...rest } = props; const { classNames } = useStyles('Menu'); + const navigate = useNavigate(); return ( - - - - {props.children} - - - - - - + + + + + {props.children} + + + + + + + ); }; @@ -201,7 +204,6 @@ export const MenuAutocompleteListbox = ( export const MenuItem = (props: MenuItemProps) => { const { iconStart, color = 'primary', children, href, ...rest } = props; const { classNames } = useStyles('Menu'); - const navigate = useNavigate(); const isLink = href !== undefined; const isExternal = isExternalLink(href); @@ -224,11 +226,23 @@ export const MenuItem = (props: MenuItemProps) => { ); - if (isLink && !isExternal) { + if (isLink && isExternal) { return ( - - {content} - + window.open(href, '_blank', 'noopener,noreferrer')} + {...rest} + > +
+ {iconStart} + {children} +
+
+ +
+
); } diff --git a/packages/ui/src/components/PasswordField/PasswordField.stories.tsx b/packages/ui/src/components/PasswordField/PasswordField.stories.tsx new file mode 100644 index 0000000000..b89f891430 --- /dev/null +++ b/packages/ui/src/components/PasswordField/PasswordField.stories.tsx @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { PasswordField } from './PasswordField'; +import { Form } from 'react-aria-components'; +import { Icon } from '../Icon'; +import { Flex } from '../Flex'; +import { FieldLabel } from '../FieldLabel'; + +const meta = { + title: 'Backstage UI/PasswordField', + component: PasswordField, + argTypes: { + isRequired: { + control: 'boolean', + }, + icon: { + control: 'object', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + name: 'secret', + placeholder: 'Enter a secret', + style: { + maxWidth: '300px', + }, + }, +}; + +export const Sizes: Story = { + args: { + ...Default.args, + }, + render: args => ( + + } /> + } /> + + ), +}; + +export const DefaultValue: Story = { + args: { + ...Default.args, + defaultValue: 'https://example.com', + }, +}; + +export const WithLabel: Story = { + args: { + ...Default.args, + label: 'Label', + }, +}; + +export const WithDescription: Story = { + args: { + ...WithLabel.args, + description: 'Description', + }, +}; + +export const Required: Story = { + args: { + ...WithLabel.args, + isRequired: true, + }, +}; + +export const Disabled: Story = { + args: { + ...Default.args, + isDisabled: true, + }, +}; + +export const WithIcon: Story = { + args: { + ...Default.args, + }, + render: args => ( + } /> + ), +}; + +export const DisabledWithIcon: Story = { + args: { + ...WithIcon.args, + isDisabled: true, + }, + render: WithIcon.render, +}; + +export const ShowError: Story = { + args: { + ...WithLabel.args, + }, + render: args => ( + + + + ), +}; + +export const Validation: Story = { + args: { + ...WithLabel.args, + validate: value => (value === 'admin' ? 'Nice try!' : null), + }, +}; + +export const CustomField: Story = { + render: () => ( + <> + + + + ), +}; diff --git a/packages/ui/src/components/PasswordField/PasswordField.styles.css b/packages/ui/src/components/PasswordField/PasswordField.styles.css new file mode 100644 index 0000000000..371d769ea6 --- /dev/null +++ b/packages/ui/src/components/PasswordField/PasswordField.styles.css @@ -0,0 +1,70 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.bui-PasswordField { + display: flex; + flex-direction: column; + font-family: var(--bui-font-regular); + width: 100%; + flex-shrink: 0; + /* Reserve space for browser/password manager icon (e.g. 1Password) */ + --bui-passwordmanager-icon-width: var(--bui-space-1); +} + +.bui-InputVisibility { + position: absolute; + right: var(--bui-passwordmanager-icon-width); + top: 0; + bottom: 0; + background-color: transparent; + cursor: pointer; + border: none; + padding: 0; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--bui-fg-primary); + + /* Size: small */ + &[data-size='small'] { + width: 2rem; + height: 2rem; + } + &[data-size='small'] svg { + width: 1rem; + height: 1rem; + } + + /* Size: medium */ + &[data-size='medium'] { + width: 2.5rem; + height: 2.5rem; + } + &[data-size='medium'] svg { + width: 1.25rem; + height: 1.25rem; + } +} + +/* Ensure input has enough right padding for our toggle + PM icon */ +.bui-PasswordField .bui-InputWrapper[data-size='small'] .bui-Input { + padding-right: calc(2rem + var(--bui-passwordmanager-icon-width)); +} + +.bui-PasswordField .bui-InputWrapper[data-size='medium'] .bui-Input { + padding-right: calc(2.5rem + var(--bui-passwordmanager-icon-width)); +} diff --git a/packages/ui/src/components/PasswordField/PasswordField.tsx b/packages/ui/src/components/PasswordField/PasswordField.tsx new file mode 100644 index 0000000000..fc5dae1088 --- /dev/null +++ b/packages/ui/src/components/PasswordField/PasswordField.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef, useEffect, useState } from 'react'; +import { + Input, + TextField as AriaTextField, + Button as RAButton, +} from 'react-aria-components'; +import clsx from 'clsx'; +import { FieldLabel } from '../FieldLabel'; +import { FieldError } from '../FieldError'; + +import type { PasswordFieldProps } from './types'; +import { useStyles } from '../../hooks/useStyles'; +import { RiEyeLine, RiEyeOffLine } from '@remixicon/react'; + +/** @public */ +export const PasswordField = forwardRef( + (props, ref) => { + const { + className, + icon, + size = 'small', + label, + secondaryLabel, + description, + isRequired, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + placeholder, + ...rest + } = props; + + useEffect(() => { + if (!label && !ariaLabel && !ariaLabelledBy) { + console.warn( + 'PasswordField requires either a visible label, aria-label, or aria-labelledby for accessibility', + ); + } + }, [label, ariaLabel, ariaLabelledBy]); + + const { classNames: passwordFieldClassNames, dataAttributes } = useStyles( + 'PasswordField', + { + size, + }, + ); + + const { classNames: textFieldClassNames } = useStyles('TextField', {}); + + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. + const secondaryLabelText = + secondaryLabel || (isRequired ? 'Required' : null); + + // Manage secret visibility toggle + const [isVisible, setIsVisible] = useState(false); + + return ( + + +
+ {icon && ( + + )} +
+ setIsVisible(v => !v)} + className={passwordFieldClassNames.inputVisibility} + > + {isVisible ? : } + +
+ +
+ +
+ ); + }, +); + +PasswordField.displayName = 'PasswordField'; diff --git a/packages/ui/src/components/PasswordField/index.ts b/packages/ui/src/components/PasswordField/index.ts new file mode 100644 index 0000000000..df7874e1c2 --- /dev/null +++ b/packages/ui/src/components/PasswordField/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './PasswordField'; +export * from './types'; diff --git a/packages/ui/src/components/PasswordField/types.ts b/packages/ui/src/components/PasswordField/types.ts new file mode 100644 index 0000000000..8af9a6134c --- /dev/null +++ b/packages/ui/src/components/PasswordField/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { TextFieldProps as AriaTextFieldProps } from 'react-aria-components'; +import { ReactNode } from 'react'; +import type { Breakpoint } from '../../types'; +import type { FieldLabelProps } from '../FieldLabel/types'; + +/** @public */ +export interface PasswordFieldProps + extends AriaTextFieldProps, + Omit { + /** + * An icon to render before the input + */ + icon?: ReactNode; + + /** + * The size of the password field + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | Partial>; + + /** + * Text to display in the input when it has no value + */ + placeholder?: string; +} diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index a811d2a451..7cb89dfbfb 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -51,6 +51,7 @@ export const Default: Story = { style: { maxWidth: '300px', }, + 'aria-label': 'Search', }, }; @@ -192,7 +193,7 @@ export const InHeader: Story = { size="small" variant="secondary" /> - + } @@ -272,3 +273,24 @@ export const StartCollapsedWithButtons: Story = { ), }; + +export const StartCollapsedWithOnChange: Story = { + args: { + ...StartCollapsed.args, + }, + render: args => { + const handleChange = (value: string) => { + console.log('Search value:', value); + }; + + return ( + + + + ); + }, +}; diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index e497cecd49..1c4bb7fa18 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -39,6 +39,7 @@ export const SearchField = forwardRef( secondaryLabel, description, isRequired, + onChange, placeholder = 'Search', startCollapsed = false, 'aria-label': ariaLabel, diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index cbfe3d8562..c818d0fe4b 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -455,3 +455,60 @@ export const RootPathMatching: Story = { ), }; + +export const AutoSelectionOfTabs: Story = { + args: { + children: '', + }, + render: () => ( + +
+ + Current URL: /random-page + + + {/* Without hrefs */} + + {' '} + Case 1: Without hrefs + + + + Settings + Preferences + Advanced + + + Settings content - React Aria manages this selection + + + Preferences content - works normally + + + Advanced content - local state only + + + + {/* With hrefs */} + + {' '} + Case 2: With hrefs By default no selection is shown + because the URL doesn't match any tab's href.{' '} + + + + + Catalog + + + Create + + + Docs + + + +
+
+ ), +}; diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index dacb355fc9..371de4ef41 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -115,9 +115,22 @@ export const Tabs = (props: TabsProps) => { } } } + + //No route matches - check if all tabs have hrefs (pure navigation) + const allTabsHaveHref = tabListChildren.every( + child => isValidElement(child) && child.props.href, + ); + + if (allTabsHaveHref) { + // Pure navigation tabs, no route match + return null; + } else { + // Mixed tabs or pure local state + return undefined; + } } } - return null; + return undefined; })(); if (!children) return null; diff --git a/packages/ui/src/components/TextField/types.ts b/packages/ui/src/components/TextField/types.ts index 75a5c68aaa..b971ef8d26 100644 --- a/packages/ui/src/components/TextField/types.ts +++ b/packages/ui/src/components/TextField/types.ts @@ -23,6 +23,14 @@ import type { FieldLabelProps } from '../FieldLabel/types'; export interface TextFieldProps extends AriaTextFieldProps, Omit { + /** + * The HTML input type for the text field + * + * @remarks + * Use `SearchField` for + * search inputs and `PasswordField` for password inputs. + */ + type?: 'text' | 'email' | 'tel' | 'url'; /** * An icon to render before the input */ diff --git a/packages/ui/src/css/components.css b/packages/ui/src/css/components.css index 8aa81a8217..9696425431 100644 --- a/packages/ui/src/css/components.css +++ b/packages/ui/src/css/components.css @@ -39,6 +39,7 @@ @import '../components/TagGroup/TagGroup.styles.css'; @import '../components/Text/styles.css'; @import '../components/TextField/TextField.styles.css'; +@import '../components/PasswordField/PasswordField.styles.css'; @import '../components/SearchField/SearchField.styles.css'; @import '../components/Skeleton/Skeleton.styles.css'; @import '../components/Tooltip/Tooltip.styles.css'; diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index 5a0e8bdc1a..931bfacca7 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -174,6 +174,12 @@ export const componentDefinitions = { emptyState: 'bui-MenuEmptyState', }, }, + PasswordField: { + classNames: { + root: 'bui-PasswordField', + inputVisibility: 'bui-InputVisibility', + }, + }, Popover: { classNames: { root: 'bui-Popover', @@ -286,6 +292,7 @@ export const componentDefinitions = { inputWrapper: 'bui-InputWrapper', input: 'bui-Input', inputIcon: 'bui-InputIcon', + inputAction: 'bui-InputAction', }, dataAttributes: { invalid: [true, false] as const, diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index ea0b147aff..cdd5db4986 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-app-visualizer +## 0.1.24-next.2 + +### Patch Changes + +- 4406144: Ensure that the text rendering has react keys for all elements +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.1.24-next.1 ### Patch Changes diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index fa30acefe2..f55c085ed0 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.24-next.1", + "version": "0.1.24-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx index b388da80b4..4385ba0938 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -63,15 +63,15 @@ function nodeToText( .filter(e => options?.showDisabled || e.instance) .sort((a, b) => a.spec.id.localeCompare(b.spec.id)); if (children.length === 0) { - return mkDiv(`${key} []`, { indent: true }); + return mkDiv(`${key} []`, { key, indent: true }); } return mkDiv( [ - mkDiv(`${key} [`), + mkDiv(`${key} [`, { key: 'start' }), ...children.map(e => - mkDiv(nodeToText(e, options), { indent: true }), + mkDiv(nodeToText(e, options), { indent: true, key: e.spec.id }), ), - mkDiv(']'), + mkDiv(']', { key: 'end' }), ], { key, indent: true }, ); diff --git a/plugins/app/CHANGELOG.md b/plugins/app/CHANGELOG.md index f4bf0f1255..8e3730b128 100644 --- a/plugins/app/CHANGELOG.md +++ b/plugins/app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-app +## 0.3.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.3.1-next.1 ### Patch Changes diff --git a/plugins/app/package.json b/plugins/app/package.json index 5a31bcc847..d2e405e4b9 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app", - "version": "0.3.1-next.1", + "version": "0.3.1-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "app", diff --git a/plugins/app/src/alpha/appModulePublicSignIn.test.tsx b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx index a40bf19dd6..787b8949e5 100644 --- a/plugins/app/src/alpha/appModulePublicSignIn.test.tsx +++ b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx @@ -99,6 +99,7 @@ describe('appModulePublicSignIn', () => {
{ if (req.headers.get('authorization') !== 'Bearer test-token') { return res( @@ -119,7 +120,11 @@ function setupFakeServer( return res(ctx.status(200)); } - if (EXISTING_PROJECT_PATH === req.params.project_path) { + if ( + // Gitlab GET project endpoint can use either numeric project.id or string project.path_with_namespace + EXISTING_PROJECT_ID.toString() === req.params.projectIdentifier || + EXISTING_PROJECT_PATH === req.params.projectIdentifier + ) { return res(ctx.status(200)); } @@ -385,7 +390,7 @@ describe('GitlabDiscoveryProcessor', () => { path_with_namespace: '1', }, { - id: 1, + id: EXISTING_PROJECT_ID, archived: false, default_branch: 'main', last_activity_at: '2021-08-05T11:03:05.774Z', diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index 126bb07f03..5ae71dda06 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -139,7 +139,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { const project_branch = branch === '*' ? project.default_branch : branch; const projectHasFile: boolean = await client.hasFile( - project.path_with_namespace, + project.id, project_branch, catalogPath, ); diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 9af00d2564..2b28381ceb 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -266,13 +266,13 @@ const httpProjectFindByIdDynamic = all_projects_response.map(project => { return res(ctx.json(all_projects_response.find(p => p.id === project.id))); }); }); -const httpProjectCatalogDynamic = all_projects_response.map(project => { - const path: string = project.path_with_namespace - ? project.path_with_namespace!.replace(/\//g, '%2F') - : `${project.path_with_namespace}%2F${project.name}`; +/** + * See https://docs.gitlab.com/api/repository_files/#get-file-from-repository + */ +const httpProjectCatalogDynamic = all_projects_response.flatMap(project => { return rest.head( - `${apiBaseUrl}/projects/${path}/repository/files/catalog-info.yaml`, + `${apiBaseUrl}/projects/${project.id.toString()}/repository/files/catalog-info.yaml`, (req, res, ctx) => { const branch = req.url.searchParams.get('ref'); if ( diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 9b3bac6399..60974d2d0f 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -581,20 +581,12 @@ describe('hasFile', () => { }); it('should find catalog file', async () => { - const hasFile = await client.hasFile( - 'group1/test-repo1', - 'main', - 'catalog-info.yaml', - ); + const hasFile = await client.hasFile(1, 'main', 'catalog-info.yaml'); expect(hasFile).toBe(true); }); it('should not find catalog file', async () => { - const hasFile = await client.hasFile( - 'group1/test-repo1', - 'unknown', - 'catalog-info.yaml', - ); + const hasFile = await client.hasFile(1, 'unknown', 'catalog-info.yaml'); expect(hasFile).toBe(false); }); }); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 3178e2c0d1..5c373dcf4a 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -342,18 +342,19 @@ export class GitLabClient { /** * General existence check. + * @see {@link https://docs.gitlab.com/api/repository_files/#get-file-from-repository | GitLab Repository Files API} * - * @param projectPath - The path to the project + * @param projectIdentifier - The identifier of the project, either the numeric ID or the namespaced path. * @param branch - The branch used to search * @param filePath - The path to the file */ async hasFile( - projectPath: string, + projectIdentifier: string | number, branch: string, filePath: string, ): Promise { const endpoint: string = `/projects/${encodeURIComponent( - projectPath, + projectIdentifier, )}/repository/files/${encodeURIComponent(filePath)}`; const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); request.searchParams.append('ref', branch); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index 11b6a0ce04..b3371dcc9e 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -590,7 +590,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { this.config.fallbackBranch; const hasFile = await client.hasFile( - project.path_with_namespace ?? '', + project.id, project_branch, this.config.catalogFile, ); diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 3db73151fe..9bd2ff31d0 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.8.1-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.8.1-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/knip-report.md b/plugins/catalog-backend-module-msgraph/knip-report.md index 1b4a6feaab..97d5b385fd 100644 --- a/plugins/catalog-backend-module-msgraph/knip-report.md +++ b/plugins/catalog-backend-module-msgraph/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused devDependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-msgraph/package.json | error | diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 580fcc7962..bcf0ae17a3 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.8.1-next.0", + "version": "0.8.1-next.1", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", @@ -67,7 +67,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0", "msw": "^1.0.0" }, "configSchema": "config.d.ts" diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index cdbee9bfb8..7645b8d390 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.2.15-next.1 + +### Patch Changes + +- 99fcf98: Removed unused dependencies + ## 0.2.15-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/knip-report.md b/plugins/catalog-backend-module-puppetdb/knip-report.md index ac2ab908f0..97d5b385fd 100644 --- a/plugins/catalog-backend-module-puppetdb/knip-report.md +++ b/plugins/catalog-backend-module-puppetdb/knip-report.md @@ -1,8 +1,3 @@ # Knip report -## Unused dependencies (1) - -| Name | Location | Severity | -| :---- | :----------- | :------- | -| luxon | plugins/catalog-backend-module-puppetdb/package.json | error | diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 2501290bed..4a01305a1a 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.2.15-next.0", + "version": "0.2.15-next.1", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", @@ -56,7 +56,6 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "luxon": "^3.0.0", "uuid": "^11.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 0f2c5cb2ae..7051f03873 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend +## 3.1.2-next.2 + +### Patch Changes + +- 6493c98: Log before provider-orphaning eviction happens +- 77516c5: Added new `catalog:validate-entity` action to actions registry. + + This action can be used to validate entities against the software catalog. + This is useful for validating `catalog-info.yaml` file changes locally using the + Backstage MCP server. + ## 3.1.1-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index fbb6789146..d3ca09ead2 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "3.1.1-next.1", + "version": "3.1.2-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", diff --git a/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts b/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts new file mode 100644 index 0000000000..19ec699cb7 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createValidateEntityAction.test.ts @@ -0,0 +1,284 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createValidateEntityAction } from './createValidateEntityAction'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { actionsRegistryServiceMock } from '@backstage/backend-test-utils/alpha'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; + +describe('createValidateEntityAction', () => { + const validEntityYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + namespace: default +spec: + type: service + lifecycle: production +`; + + const invalidYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + namespace: default +spec: + type: service + lifecycle: production + invalid: yaml: syntax: error +`; + + const validEntityObject = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-component', + namespace: 'default', + }, + spec: { + type: 'service', + lifecycle: 'production', + }, + }; + + it('should validate a valid entity YAML and return success', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + // Mock validateEntity to return valid response + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: true, + isValidYaml: true, + errors: [], + entity: validEntityObject, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + 'url:https://localhost/entity-validator', + { credentials: expect.any(Object) }, + ); + }); + + it('should validate entity with custom location', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const customLocation = 'https://github.com/example/repo/catalog-info.yaml'; + + await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { + entity: validEntityYaml, + location: customLocation, + }, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + customLocation, + { credentials: expect.any(Object) }, + ); + }); + + it('should return validation errors when entity is invalid', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + const validationErrors = [ + { name: 'ValidationError', message: 'Missing required field: spec.type' }, + { name: 'ValidationError', message: 'Invalid lifecycle value' }, + ]; + + mockCatalog.validateEntity.mockResolvedValue({ + valid: false, + errors: validationErrors, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: true, + errors: ['Missing required field: spec.type', 'Invalid lifecycle value'], + entity: undefined, + }); + }); + + it('should handle invalid YAML syntax', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: invalidYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('YAML parsing error')], + }); + + // validateEntity should not be called if YAML parsing fails + expect(mockCatalog.validateEntity).not.toHaveBeenCalled(); + }); + + it('should handle catalog service errors gracefully', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + const catalogError = new Error('Catalog service unavailable'); + mockCatalog.validateEntity.mockRejectedValue(catalogError); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: ['Validation error: Catalog service unavailable'], + }); + }); + + it('should handle empty YAML input', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: '' }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('Validation error')], + }); + }); + + it('should handle malformed YAML with special characters', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const malformedYaml = ` +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test-component + annotations: + invalid: "unclosed quote +spec: + type: service +`; + + const result = await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: malformedYaml }, + }); + + expect(result.output).toEqual({ + isValid: false, + isValidYaml: false, + errors: [expect.stringContaining('YAML parsing error')], + }); + }); + + it('should pass credentials to catalog service', async () => { + const mockActionsRegistry = actionsRegistryServiceMock(); + const mockCatalog = catalogServiceMock.mock(); + + mockCatalog.validateEntity.mockResolvedValue({ + valid: true, + }); + + createValidateEntityAction({ + catalog: mockCatalog, + actionsRegistry: mockActionsRegistry, + }); + + const mockCredentials: BackstageCredentials = { + $$type: '@backstage/BackstageCredentials', + principal: { type: 'user', userEntityRef: 'user:default/test' }, + }; + + await mockActionsRegistry.invoke({ + id: 'test:validate-entity', + input: { entity: validEntityYaml }, + credentials: mockCredentials, + }); + + expect(mockCatalog.validateEntity).toHaveBeenCalledWith( + validEntityObject, + 'url:https://localhost/entity-validator', + { credentials: mockCredentials }, + ); + }); +}); diff --git a/plugins/catalog-backend/src/actions/createValidateEntityAction.ts b/plugins/catalog-backend/src/actions/createValidateEntityAction.ts new file mode 100644 index 0000000000..a77a932e25 --- /dev/null +++ b/plugins/catalog-backend/src/actions/createValidateEntityAction.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import yaml from 'yaml'; +import { CatalogService } from '@backstage/plugin-catalog-node'; + +export const createValidateEntityAction = (options: { + actionsRegistry: ActionsRegistryService; + catalog: CatalogService; +}) => { + const { actionsRegistry, catalog } = options; + actionsRegistry.register({ + name: 'validate-entity', + title: 'Validate Catalog Entity', + description: ` + This action can be used to validate catalog-info.yaml file contents meant to be used with the software catalog. + It checks both that the YAML syntax is correct, and that the entity content is valid according to the catalog's rules. + `, + attributes: { + readOnly: true, + idempotent: true, + destructive: false, + }, + schema: { + input: z => + z.object({ + entity: z.string().describe('Entity YAML content to validate'), + location: z + .string() + .url() + .optional() + .describe('Location to validate'), + }), + output: z => + z.object({ + isValid: z.boolean().describe('Whether the entity is valid'), + isValidYaml: z.boolean().describe('Whether the YAML syntax is valid'), + errors: z.array(z.string()).describe('Array of validation errors'), + entity: z + .record(z.any()) + .optional() + .describe('Parsed entity object if valid'), + }), + }, + action: async ({ input, credentials }) => { + const { entity: content, location } = input; + try { + let entity; + try { + entity = yaml.parse(content); + } catch (yamlError) { + return { + output: { + isValid: false, + isValidYaml: false, + errors: [`YAML parsing error: ${yamlError.message}`], + }, + }; + } + + const resp = await catalog.validateEntity( + entity, + location ?? 'url:https://localhost/entity-validator', + { credentials }, + ); + + return { + output: { + isValid: resp.valid, + isValidYaml: true, + errors: resp.valid ? [] : resp.errors.map(e => e.message), + entity: resp.valid ? entity : undefined, + }, + }; + } catch (error) { + return { + output: { + isValid: false, + isValidYaml: false, + errors: [`Validation error: ${error.message}`], + }, + }; + } + }, + }); +}; diff --git a/plugins/catalog-backend/src/actions/index.ts b/plugins/catalog-backend/src/actions/index.ts new file mode 100644 index 0000000000..c53f956f62 --- /dev/null +++ b/plugins/catalog-backend/src/actions/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; +import { CatalogService } from '@backstage/plugin-catalog-node'; +import { createGetCatalogEntityAction } from './createGetCatalogEntityAction.ts'; +import { createValidateEntityAction } from './createValidateEntityAction.ts'; + +export const createCatalogActions = (options: { + actionsRegistry: ActionsRegistryService; + catalog: CatalogService; +}) => { + createGetCatalogEntityAction(options); + createValidateEntityAction(options); +}; diff --git a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts index 1580194639..d28f39f5e1 100644 --- a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts +++ b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts @@ -13,62 +13,166 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityProvider } from '@backstage/plugin-catalog-node'; -import { mockServices } from '@backstage/backend-test-utils'; + +import { mockServices, TestDatabases } from '@backstage/backend-test-utils'; import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase'; -import { evictEntitiesFromOrphanedProviders } from './evictEntitiesFromOrphanedProviders'; +import { + evictEntitiesFromOrphanedProviders, + getOrphanedEntityProviderNames, +} from './evictEntitiesFromOrphanedProviders'; +import { applyDatabaseMigrations } from '../database/migrations'; +import { Knex } from 'knex'; -describe('evictEntitiesFromOrphanedProviders', () => { - const db = { - transaction: jest.fn().mockImplementation(cb => cb((() => {}) as any)), - replaceUnprocessedEntities: jest.fn(), - listReferenceSourceKeys: jest.fn(), - } as unknown as jest.Mocked; +jest.setTimeout(60_000); - const providers = [ - { getProviderName: () => 'provider1' }, - { getProviderName: () => 'provider2' }, - ] as unknown as EntityProvider[]; - const logger = mockServices.logger.mock(); +const databases = TestDatabases.create(); +const logger = mockServices.logger.mock(); - it('replaces unprocessed entities for orphaned providers with empty items', async () => { - db.listReferenceSourceKeys.mockResolvedValue(['foo', 'bar']); +afterEach(() => { + jest.resetAllMocks(); +}); - await evictEntitiesFromOrphanedProviders({ db, providers, logger }); +describe.each(databases.eachSupportedId())('%p', databaseId => { + let knex: Knex; + let db: DefaultProviderDatabase; - expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2); - expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith( - 1, - expect.anything(), - { - sourceKey: 'foo', - type: 'full', - items: [], - }, - ); - expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith( - 2, - expect.anything(), - { - sourceKey: 'bar', - type: 'full', - items: [], - }, - ); + beforeEach(async () => { + knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + db = new DefaultProviderDatabase({ database: knex, logger }); }); - it('does not replace unprocessed entities for providers that are not orphaned', async () => { - db.listReferenceSourceKeys.mockResolvedValue(['foo', 'provider1']); + afterEach(async () => { + await knex.destroy(); + }); - await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + describe('getOrphanedEntityProviderNames', () => { + it('correctly locates and logs orphaned providers', async () => { + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; - expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith( - expect.anything(), - { - sourceKey: 'provider1', - type: 'full', - items: [], - }, - ); + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'provider2', target_entity_ref: 'x' }, + { source_key: 'provider3', target_entity_ref: 'x' }, + ]); + + await expect( + getOrphanedEntityProviderNames({ + db, + providers, + logger, + }), + ).resolves.toEqual(['provider3']); + + expect(logger.warn).toHaveBeenCalledTimes(4); + expect(logger.warn).toHaveBeenCalledWith( + `Found 1 orphaned entity provider(s)`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Database contained providers: 'provider2', 'provider3'`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Installed providers were: 'provider1', 'provider2'`, + ); + expect(logger.warn).toHaveBeenCalledWith( + `Orphaned providers were thus: 'provider3'`, + ); + }); + }); + + describe('evictEntitiesFromOrphanedProviders', () => { + it('replaces unprocessed entities for orphaned providers with empty items', async () => { + jest.spyOn(db, 'replaceUnprocessedEntities'); + + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; + + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'foo', target_entity_ref: 'x' }, + { source_key: 'bar', target_entity_ref: 'x' }, + ]); + + await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + + expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2); + expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'foo', + type: 'full', + items: [], + }, + ); + expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'bar', + type: 'full', + items: [], + }, + ); + }); + + it('does not replace unprocessed entities for providers that are not orphaned', async () => { + jest.spyOn(db, 'replaceUnprocessedEntities'); + + const providers = [ + { getProviderName: () => 'provider1', connect: jest.fn() }, + { getProviderName: () => 'provider2', connect: jest.fn() }, + ]; + + await knex('refresh_state').insert([ + { + entity_id: 'x', + entity_ref: 'x', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: knex.fn.now(), + last_discovery_at: knex.fn.now(), + }, + ]); + await knex('refresh_state_references').insert([ + { source_key: 'foo', target_entity_ref: 'x' }, + { source_key: 'provider1', target_entity_ref: 'x' }, + ]); + + await evictEntitiesFromOrphanedProviders({ db, providers, logger }); + + expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith( + expect.anything(), + { + sourceKey: 'provider1', + type: 'full', + items: [], + }, + ); + }); }); }); diff --git a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts index a6bda5e8c5..753a266250 100644 --- a/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts +++ b/plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts @@ -18,12 +18,14 @@ import { EntityProvider } from '@backstage/plugin-catalog-node'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ProviderDatabase } from '../database/types'; -async function getOrphanedEntityProviderNames({ +export async function getOrphanedEntityProviderNames({ db, providers, + logger, }: { db: ProviderDatabase; providers: EntityProvider[]; + logger: LoggerService; }): Promise { const dbProviderNames = await db.transaction(async tx => db.listReferenceSourceKeys(tx), @@ -31,9 +33,21 @@ async function getOrphanedEntityProviderNames({ const providerNames = providers.map(p => p.getProviderName()); - return dbProviderNames.filter( + const orphaned = dbProviderNames.filter( dbProviderName => !providerNames.includes(dbProviderName), ); + + if (orphaned.length) { + const dbProviderNamesString = dbProviderNames.map(p => `'${p}'`).join(', '); + const providerNamesString = providerNames.map(p => `'${p}'`).join(', '); + const orphanedString = orphaned.map(p => `'${p}'`).join(', '); + logger.warn(`Found ${orphaned.length} orphaned entity provider(s)`); + logger.warn(`Database contained providers: ${dbProviderNamesString}`); + logger.warn(`Installed providers were: ${providerNamesString}`); + logger.warn(`Orphaned providers were thus: ${orphanedString}`); + } + + return orphaned; } async function removeEntitiesForProvider({ diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 24147e3cc5..cfe05712d9 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -45,7 +45,7 @@ import { Permission } from '@backstage/plugin-permission-common'; import { merge } from 'lodash'; import { CatalogBuilder } from './CatalogBuilder'; import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha'; -import { createGetCatalogEntityAction } from '../actions/createGetCatalogEntityAction'; +import { createCatalogActions } from '../actions'; class CatalogLocationsExtensionPointImpl implements CatalogLocationsExtensionPoint @@ -320,7 +320,7 @@ export const catalogPlugin = createBackendPlugin({ httpRouter.use(router); - createGetCatalogEntityAction({ + createCatalogActions({ catalog, actionsRegistry, }); diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index b492c41a37..41c3c1c519 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-graph +## 0.5.2-next.2 + +### Patch Changes + +- 431130c: Added `renderEdge` prop to `` component in `@backstage/core-components` to allow custom rendering of graph edges. +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.5.2-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index f455dbce31..7531a926a4 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.5.2-next.1", + "version": "0.5.2-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-graph/report.api.md b/plugins/catalog-graph/report.api.md index 8194209cf5..b7621d05ef 100644 --- a/plugins/catalog-graph/report.api.md +++ b/plugins/catalog-graph/report.api.md @@ -179,6 +179,7 @@ export type EntityRelationsGraphProps = { zoom?: 'enabled' | 'disabled' | 'enable-on-click'; renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; }; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 17b5861173..e71f928c15 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -89,6 +89,7 @@ export type EntityRelationsGraphProps = { zoom?: 'enabled' | 'disabled' | 'enable-on-click'; renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; + renderEdge?: DependencyGraphTypes.RenderEdgeFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; showArrowHeads?: boolean; }; @@ -114,6 +115,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { zoom = 'enabled', renderNode, renderLabel, + renderEdge, curve, showArrowHeads, } = props; @@ -156,6 +158,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { edges={edges} renderNode={renderNode || DefaultRenderNode} renderLabel={renderLabel || DefaultRenderLabel} + renderEdge={renderEdge} direction={direction} className={classes.graph} fit="contain" diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 54dbb31ae0..85e2a009f4 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-home +## 0.8.13-next.3 + +### Patch Changes + +- e7d59d3: fix(home): correct `clearAll` logic to properly handle `deletable` flag +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + +## 0.8.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + - @backstage/theme@0.6.9-next.0 + ## 0.8.13-next.1 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 52a4fdd395..060f090974 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.8.13-next.1", + "version": "0.8.13-next.3", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx index fec7895170..a265a47290 100644 --- a/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx +++ b/plugins/home/src/components/CustomHomepage/CustomHomepageGrid.tsx @@ -282,7 +282,7 @@ export const CustomHomepageGrid = (props: CustomHomepageGridProps) => { }; const clearLayout = () => { - setWidgets(widgets.filter(w => !w.deletable)); + setWidgets(widgets.filter(w => w.deletable === false)); }; const changeEditMode = (mode: boolean) => { diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index 527c13c88d..be3fed6035 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.30-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + ## 0.0.30-next.1 ### Patch Changes diff --git a/plugins/kubernetes-cluster/knip-report.md b/plugins/kubernetes-cluster/knip-report.md index 55eba7a829..97d5b385fd 100644 --- a/plugins/kubernetes-cluster/knip-report.md +++ b/plugins/kubernetes-cluster/knip-report.md @@ -1,18 +1,3 @@ # Knip report -## Unused dependencies (5) - -| Name | Location | Severity | -| :---------------------- | :----------- | :------- | -| @kubernetes-models/base | plugins/kubernetes-cluster/package.json | error | -| cronstrue | plugins/kubernetes-cluster/package.json | error | -| js-yaml | plugins/kubernetes-cluster/package.json | error | -| lodash | plugins/kubernetes-cluster/package.json | error | -| luxon | plugins/kubernetes-cluster/package.json | error | - -## Unused devDependencies (1) - -| Name | Location | Severity | -| :------------------- | :----------- | :------- | -| @testing-library/dom | plugins/kubernetes-cluster/package.json | error | diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 3e0544ced9..033e1138f4 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.30-next.1", + "version": "0.0.30-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", @@ -62,20 +62,14 @@ "@backstage/plugin-kubernetes-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@kubernetes-models/apimachinery": "^2.0.0", - "@kubernetes-models/base": "^5.0.0", "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.61", - "cronstrue": "^2.2.0", - "js-yaml": "^4.0.0", "kubernetes-models": "^4.1.0", - "lodash": "^4.17.21", - "luxon": "^3.0.0", "react-use": "^17.2.4" }, "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", - "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@types/node": "^20.16.0", diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index ee05844a0b..7704c954b2 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-react +## 0.5.12-next.2 + +### Patch Changes + +- ac405f2: The configmaps added to be rendered +- f7a4144: Fixes calculation of CPU utilization in the PodTable +- Updated dependencies + - @backstage/core-components@0.18.2-next.2 + ## 0.5.12-next.1 ### Patch Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 583bb5ef0d..76d1a08cdf 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.5.12-next.1", + "version": "0.5.12-next.2", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json b/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json new file mode 100644 index 0000000000..ab0518e87d --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/1-configmaps.json @@ -0,0 +1,26 @@ +{ + "configMaps": [ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "app-config", + "namespace": "default", + "uid": "1ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "dice-roller" + }, + "annotations": {} + }, + "data": { + "database.host": "localhost", + "database.port": "5432", + "app.name": "dice-roller", + "app.version": "1.0.0" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json b/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json new file mode 100644 index 0000000000..2c0c7f8121 --- /dev/null +++ b/plugins/kubernetes-react/src/__fixtures__/2-configmaps.json @@ -0,0 +1,47 @@ +{ + "configMaps": [ + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "app-config", + "namespace": "default", + "uid": "1ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732552", + "creationTimestamp": "2021-07-16T22:39:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "dice-roller" + }, + "annotations": {} + }, + "data": { + "database.host": "localhost", + "database.port": "5432", + "app.name": "dice-roller", + "app.version": "1.0.0" + } + }, + { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "redis-config", + "namespace": "default", + "uid": "2ea073bc-7a4b-4b99-8321-0305bce85568", + "resourceVersion": "1362732553", + "creationTimestamp": "2021-07-16T22:40:58Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller", + "app": "redis" + }, + "annotations": {} + }, + "data": { + "redis.conf": "# Redis configuration\nmaxmemory 256mb\nmaxmemory-policy allkeys-lru", + "redis.host": "redis-service", + "redis.port": "6379" + } + } + ] +} diff --git a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx index 642ed642a4..4e8c4a1947 100644 --- a/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes-react/src/components/Cluster/Cluster.tsx @@ -30,6 +30,7 @@ import { DeploymentsAccordions } from '../DeploymentsAccordions'; import { StatefulSetsAccordions } from '../StatefulSetsAccordions'; import { IngressesAccordions } from '../IngressesAccordions'; import { ServicesAccordions } from '../ServicesAccordions'; +import { ConfigmapsAccordions } from '../ConfigmapsAccordions'; import { CronJobsAccordions } from '../CronJobsAccordions'; import { CustomResources } from '../CustomResources'; import { DaemonSetsAccordions } from '../DaemonSetsAccordions'; @@ -170,6 +171,11 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { ) : undefined} + {groupedResponses.configMaps.length > 0 ? ( + + + + ) : undefined} {groupedResponses.cronJobs.length > 0 ? ( diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx new file mode 100644 index 0000000000..d819a01cd9 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { screen } from '@testing-library/react'; +import { ConfigmapsAccordions } from './ConfigmapsAccordions'; +import * as oneConfigmapsFixture from '../../__fixtures__/1-configmaps.json'; +import * as twoConfigmapsFixture from '../../__fixtures__/2-configmaps.json'; +import { renderInTestApp } from '@backstage/test-utils'; +import { kubernetesProviders } from '../../hooks/test-utils'; + +describe('ConfigmapsAccordions', () => { + it('should render 1 configmap', async () => { + const wrapper = kubernetesProviders( + oneConfigmapsFixture, + new Set(), + ); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-config')).toBeInTheDocument(); + expect(screen.getByText('ConfigMap')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + }); + + it('should render 2 configmaps', async () => { + const wrapper = kubernetesProviders( + twoConfigmapsFixture, + new Set(), + ); + + await renderInTestApp(wrapper()); + + expect(screen.getByText('app-config')).toBeInTheDocument(); + expect(screen.getByText('redis-config')).toBeInTheDocument(); + expect(screen.getAllByText('ConfigMap')).toHaveLength(2); + expect(screen.getAllByText('namespace: default')).toHaveLength(2); + expect(screen.getByText('Data Count: 4')).toBeInTheDocument(); + expect(screen.getByText('Data Count: 3')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx new file mode 100644 index 0000000000..bdb5b10d35 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsAccordions.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useContext } from 'react'; +import Accordion from '@material-ui/core/Accordion'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import type { V1ConfigMap } from '@kubernetes/client-node'; +import { ConfigmapsDrawer } from './ConfigmapsDrawer.tsx'; +import { GroupedResponsesContext } from '../../hooks'; +import { StructuredMetadataTable } from '@backstage/core-components'; + +type ConfigmapSummaryProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapSummary = ({ configmap }: ConfigmapSummaryProps) => { + return ( + + + + + + + + Data Count: {configmap.data ? Object.keys(configmap.data).length : 0} + + + + ); +}; + +type ConfigmapsCardProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapCard = ({ configmap }: ConfigmapsCardProps) => { + const metadata: any = {}; + + metadata.data = configmap.data; + + return ( + + ); +}; + +export type ConfigmapsAccordionsProps = {}; + +type ConfigmapsAccordionProps = { + configmap: V1ConfigMap; +}; + +const ConfigmapsAccordion = ({ configmap }: ConfigmapsAccordionProps) => { + return ( + + }> + + + + + + + ); +}; + +export const ConfigmapsAccordions = ({}: ConfigmapsAccordionsProps) => { + const groupedResponses = useContext(GroupedResponsesContext); + return ( + + {groupedResponses.configMaps.map((configmap, i) => ( + + + + ))} + + ); +}; diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx new file mode 100644 index 0000000000..cde7265cab --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as oneConfigmapsFixture from '../../__fixtures__/1-configmaps.json'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { ConfigmapsDrawer } from './ConfigmapsDrawer'; +import { kubernetesClusterLinkFormatterApiRef } from '../../api'; + +describe('ConfigmapsDrawer', () => { + it('should render configmap drawer', async () => { + const { getByText, getAllByText } = await renderInTestApp( + + + , + ); + + expect(getAllByText('app-config')).toHaveLength(3); + expect(getAllByText('ConfigMap')).toHaveLength(3); + expect(getByText('YAML')).toBeInTheDocument(); + expect(getByText('namespace: default')).toBeInTheDocument(); + }); +}); diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx new file mode 100644 index 0000000000..97e8aab723 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/ConfigmapsDrawer.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import Chip from '@material-ui/core/Chip'; +import type { V1ConfigMap } from '@kubernetes/client-node'; + +export const ConfigmapsDrawer = ({ + configmap, + expanded, +}: { + configmap: V1ConfigMap; + expanded?: boolean; +}) => { + const namespace = configmap.metadata?.namespace; + return ( + { + return configmapObject || {}; + }} + > + + + + {configmap.metadata?.name ?? 'unknown object'} + + + + + ConfigMap + + + {namespace && ( + + + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts new file mode 100644 index 0000000000..25a1578a79 --- /dev/null +++ b/plugins/kubernetes-react/src/components/ConfigmapsAccordions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './ConfigmapsAccordions.tsx'; diff --git a/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx index fb90d17e2b..76136641af 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodsTable.test.tsx @@ -74,7 +74,7 @@ describe('PodsTable', () => { limitTotal: '134217728', }, cpu: { - currentUsage: 0.4966115, + currentUsage: 0.0496, requestTotal: 0.05, limitTotal: 0.05, }, diff --git a/plugins/kubernetes-react/src/utils/pod.test.tsx b/plugins/kubernetes-react/src/utils/pod.test.tsx index 3f82c14f86..a75039ceea 100644 --- a/plugins/kubernetes-react/src/utils/pod.test.tsx +++ b/plugins/kubernetes-react/src/utils/pod.test.tsx @@ -42,7 +42,7 @@ describe('pod', () => { const result = podStatusToCpuUtil({ cpu: { // ~50m - currentUsage: 0.4966115, + currentUsage: 0.0496, // 50m requestTotal: 0.05, // 100m diff --git a/plugins/kubernetes-react/src/utils/pod.tsx b/plugins/kubernetes-react/src/utils/pod.tsx index 97b44d4d24..e033b7c3b8 100644 --- a/plugins/kubernetes-react/src/utils/pod.tsx +++ b/plugins/kubernetes-react/src/utils/pod.tsx @@ -139,22 +139,14 @@ export const currentToDeclaredResourceToPerc = ( export const podStatusToCpuUtil = (podStatus: ClientPodStatus): ReactNode => { const cpuUtil = podStatus.cpu; - let currentUsage: number | string = cpuUtil.currentUsage; - - // current usage number for CPU is a different unit than request/limit total - // this might be a bug in the k8s library - if (typeof cpuUtil.currentUsage === 'number') { - currentUsage = cpuUtil.currentUsage / 10; - } - return ( diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 62ffb354bf..670ff085f6 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes +## 0.12.12-next.2 + +### Patch Changes + +- 99fcf98: Removed unused dependencies +- Updated dependencies + - @backstage/core-components@0.18.2-next.3 + - @backstage/frontend-plugin-api@0.12.1-next.2 + ## 0.12.12-next.1 ### Patch Changes diff --git a/plugins/kubernetes/knip-report.md b/plugins/kubernetes/knip-report.md index 11b66acd1b..97d5b385fd 100644 --- a/plugins/kubernetes/knip-report.md +++ b/plugins/kubernetes/knip-report.md @@ -1,18 +1,3 @@ # Knip report -## Unused dependencies (11) - -| Name | Location | Severity | -| :------------------------------ | :----------- | :------- | -| @kubernetes-models/apimachinery | plugins/kubernetes/package.json | error | -| @kubernetes-models/base | plugins/kubernetes/package.json | error | -| @kubernetes/client-node | plugins/kubernetes/package.json | error | -| @xterm/addon-attach | plugins/kubernetes/package.json | error | -| kubernetes-models | plugins/kubernetes/package.json | error | -| @xterm/addon-fit | plugins/kubernetes/package.json | error | -| @xterm/xterm | plugins/kubernetes/package.json | error | -| cronstrue | plugins/kubernetes/package.json | error | -| js-yaml | plugins/kubernetes/package.json | error | -| lodash | plugins/kubernetes/package.json | error | -| luxon | plugins/kubernetes/package.json | error | diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 1ae796c1c0..113deee19f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.12.12-next.1", + "version": "0.12.12-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", @@ -67,18 +67,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@kubernetes-models/apimachinery": "^2.0.0", - "@kubernetes-models/base": "^5.0.0", - "@kubernetes/client-node": "1.1.2", - "@material-ui/core": "^4.12.2", - "@xterm/addon-attach": "^0.11.0", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "cronstrue": "^2.2.0", - "js-yaml": "^4.0.0", - "kubernetes-models": "^4.1.0", - "lodash": "^4.17.21", - "luxon": "^3.0.0" + "@material-ui/core": "^4.12.2" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/mui-to-bui/.eslintrc.js b/plugins/mui-to-bui/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/mui-to-bui/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/mui-to-bui/CHANGELOG.md b/plugins/mui-to-bui/CHANGELOG.md new file mode 100644 index 0000000000..14720ff0e7 --- /dev/null +++ b/plugins/mui-to-bui/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-mui-to-bui + +## 0.2.0-next.0 + +### Minor Changes + +- d5cbdba: This is the first release of the Material UI to Backstage UI migration helper plugin. It adds a new page at `/mui-to-bui` that converts an existing MUI v5 theme into Backstage UI (BUI) CSS variables, with live preview and copy/download. + +### Patch Changes + +- Updated dependencies + - @backstage/ui@0.7.2-next.1 + - @backstage/theme@0.6.9-next.0 diff --git a/plugins/mui-to-bui/README.md b/plugins/mui-to-bui/README.md new file mode 100644 index 0000000000..4e11db20b6 --- /dev/null +++ b/plugins/mui-to-bui/README.md @@ -0,0 +1,63 @@ +# mui-to-bui + +## Description + +The Backstage UI Themer helps you convert an existing MUI v5 theme into Backstage UI (BUI) CSS custom properties. It detects installed app themes, generates a complete set of BUI CSS variables for each theme (light and dark), and lets you preview how common BUI components look with your colors and typography. You can copy the generated CSS to the clipboard or download it as a `.css` file, and it works with both the old and new Backstage frontend systems without requiring any backend setup. + +## Installation + +### 1) Add the dependency to your app + +Run this from your Backstage repo root: + +```bash +yarn add --cwd packages/app @backstage/plugin-mui-to-bui +``` + +### 2) Wire it up depending on your frontend system + +#### Old frontend system (legacy `App.tsx` with ``) + +Add a route for the page in your app: + +```tsx +// packages/app/src/App.tsx +import React from 'react'; +import { Route } from 'react-router-dom'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { BuiThemerPage } from '@backstage/plugin-mui-to-bui'; + +export const App = () => ( + + {/* ...your other routes */} + } /> + +); +``` + +#### New frontend system + +If package discovery is enabled in your app, this plugin is picked up automatically after installation — no code changes required. Just navigate to `/mui-to-bui`. + +If you prefer explicit registration (or don't use discovery), register the plugin as a feature. The page route (`/mui-to-bui`) is provided by the plugin. + +```tsx +// packages/app/src/App.tsx (or your app entry where you call createApp) +import React from 'react'; +import { createApp } from '@backstage/frontend-defaults'; +import buiThemerPlugin from '@backstage/plugin-mui-to-bui'; + +const app = createApp({ + features: [ + // ...other features + buiThemerPlugin, + ], +}); + +export default app.createRoot(); +``` + +## Accessing the Themer page + +- Navigate to `/mui-to-bui` in your Backstage app (for example `http://localhost:3000/mui-to-bui`). +- Optional: Add a sidebar/link in your app that points to `/mui-to-bui` if you want a permanent navigation entry. diff --git a/plugins/mui-to-bui/catalog-info.yaml b/plugins/mui-to-bui/catalog-info.yaml new file mode 100644 index 0000000000..9e3605267d --- /dev/null +++ b/plugins/mui-to-bui/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-mui-to-bui + title: '@backstage/plugin-mui-to-bui' +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/mui-to-bui/dev/index.tsx b/plugins/mui-to-bui/dev/index.tsx new file mode 100644 index 0000000000..8837d540c5 --- /dev/null +++ b/plugins/mui-to-bui/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createDevApp } from '@backstage/dev-utils'; +import { buiThemerPlugin, BuiThemerPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(buiThemerPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/mui-to-bui', + }) + .render(); diff --git a/plugins/mui-to-bui/knip-report.md b/plugins/mui-to-bui/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/plugins/mui-to-bui/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/plugins/mui-to-bui/package.json b/plugins/mui-to-bui/package.json new file mode 100644 index 0000000000..b8cb51b1c8 --- /dev/null +++ b/plugins/mui-to-bui/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-mui-to-bui", + "version": "0.2.0-next.0", + "backstage": { + "role": "frontend-plugin", + "pluginId": "mui-to-bui", + "pluginPackages": [ + "@backstage/plugin-mui-to-bui" + ] + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/mui-to-bui" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/core-compat-api": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@backstage/ui": "workspace:^", + "@mui/material": "^5.12.2", + "@mui/system": "^5.16.14" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } +} diff --git a/plugins/mui-to-bui/report.api.md b/plugins/mui-to-bui/report.api.md new file mode 100644 index 0000000000..593130ea86 --- /dev/null +++ b/plugins/mui-to-bui/report.api.md @@ -0,0 +1,66 @@ +## API Report File for "@backstage/plugin-mui-to-bui" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { JSX as JSX_3 } from 'react/jsx-runtime'; +import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/frontend-plugin-api'; +import { RouteRef as RouteRef_2 } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const BuiThemerPage: () => JSX_3.Element; + +// @public (undocumented) +export const buiThemerPlugin: BackstagePlugin< + { + root: RouteRef_2; + }, + {} +>; + +// @public (undocumented) +const _default: OverridableFrontendPlugin< + { + root: RouteRef; + }, + {}, + { + 'page:mui-to-bui': ExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + }; + configInput: { + path?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + RouteRef, + 'core.routing.ref', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + loader: () => Promise; + routeRef?: RouteRef; + }; + }>; + } +>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx new file mode 100644 index 0000000000..375f9411a7 --- /dev/null +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.test.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { renderInTestApp } from '@backstage/test-utils'; +import { BuiThemePreview } from './BuiThemePreview'; + +describe('BuiThemePreview', () => { + it('renders headings and sample components', async () => { + const { container } = await renderInTestApp( + , + ); + + expect(container.firstChild).toHaveAttribute('data-theme-mode', 'light'); + expect(await screen.findByText('Button Variants')).toBeInTheDocument(); + expect(screen.getByText('Button Variants')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Primary' })).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Secondary' }), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Tertiary' }), + ).toBeInTheDocument(); + expect(screen.getByText('Form Inputs')).toBeInTheDocument(); + expect(screen.getByText('Tag Variants')).toBeInTheDocument(); + expect(screen.getByText('Text Variants')).toBeInTheDocument(); + }); +}); diff --git a/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx new file mode 100644 index 0000000000..e59f4e78dd --- /dev/null +++ b/plugins/mui-to-bui/src/components/BuiThemerPage/BuiThemePreview.tsx @@ -0,0 +1,167 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Box, + Button, + Card, + CardBody, + CardHeader, + Checkbox, + Flex, + Radio, + RadioGroup, + Select, + Text, + TextField, + TagGroup, + Tag, +} from '@backstage/ui'; + +interface IsolatedPreviewProps { + mode: 'light' | 'dark'; + styleObject: Record; +} + +export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) { + return ( +
+ + + Button Variants + + + + + + + + + + + + + + + + + + + + Form Inputs + + + +