diff --git a/.changeset/README.md b/.changeset/README.md
index 4f3b76b096..6c68425e59 100644
--- a/.changeset/README.md
+++ b/.changeset/README.md
@@ -6,3 +6,58 @@ find the full documentation for it [in our repository](https://github.com/change
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md)
+
+---
+
+## Backstage UI Changesets
+
+For `@backstage/ui` changesets, use this format:
+
+```markdown
+---
+'@backstage/ui': patch
+---
+
+Brief summary
+
+Optional description with code examples.
+
+**Migration:**
+
+Migration instructions (breaking changes only).
+
+**Affected components:** button, card
+```
+
+**Required:**
+
+- End with `**Affected components:**` + comma-separated component names
+- For breaking changes: Add `**Migration:**` section
+- No headings (`##`, `###`) inside - use bold markers
+
+**Examples:**
+
+```markdown
+Fixed button hover state
+
+**Affected components:** button
+```
+
+````markdown
+**BREAKING**: New Table API
+
+**Migration:**
+
+Update imports:
+
+```diff
+- import { Table } from '@backstage/ui';
++ import { Table, type ColumnConfig } from '@backstage/ui';
+```
+````
+
+**Affected components:** table
+
+```
+
+```
diff --git a/.changeset/add-masked-and-hidden-option.md b/.changeset/add-masked-and-hidden-option.md
new file mode 100644
index 0000000000..ad5c810a8e
--- /dev/null
+++ b/.changeset/add-masked-and-hidden-option.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder-backend-module-gitlab': patch
+---
+
+Added `maskedAndHidden` option to `gitlab:projectVariable:create` and `publish:gitlab` action to support creating GitLab project variables that are both masked and hidden. Updated gitbeaker to version 43.8.0 for proper type support.
diff --git a/.changeset/afraid-items-drum.md b/.changeset/afraid-items-drum.md
deleted file mode 100644
index 94ddd608cd..0000000000
--- a/.changeset/afraid-items-drum.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli-node': patch
----
-
-Updated to use new utilities from `@backstage/cli-common`.
diff --git a/.changeset/all-socks-taste.md b/.changeset/all-socks-taste.md
deleted file mode 100644
index 377782c2f9..0000000000
--- a/.changeset/all-socks-taste.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend-module-gitlab': minor
----
-
-In the `gitlabRepoPush` action, add 'auto' possibility for `commitAction` input.
diff --git a/.changeset/blue-moons-crash.md b/.changeset/blue-moons-crash.md
new file mode 100644
index 0000000000..bb5ae15c2e
--- /dev/null
+++ b/.changeset/blue-moons-crash.md
@@ -0,0 +1,5 @@
+---
+'@backstage/ui': patch
+---
+
+Made Accordion a `bg` provider so nested components like Button auto-increment their background level. Updated `useDefinition` to resolve `bg` `propDef` defaults for provider components.
diff --git a/.changeset/bright-moons-open.md b/.changeset/bright-moons-open.md
new file mode 100644
index 0000000000..2668dfb2e6
--- /dev/null
+++ b/.changeset/bright-moons-open.md
@@ -0,0 +1,6 @@
+---
+'@backstage/plugin-search-react': patch
+'@backstage/plugin-search': patch
+---
+
+Fixes the search component not registering the first search on navigate to the search page.
diff --git a/.changeset/brown-towns-find.md b/.changeset/brown-towns-find.md
new file mode 100644
index 0000000000..c3244d9427
--- /dev/null
+++ b/.changeset/brown-towns-find.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-api-docs': patch
+---
+
+Fixed invisible text in parameter input fields when using dark mode in OpenAPI definition pages
diff --git a/.changeset/bump-bfj-v9.md b/.changeset/bump-bfj-v9.md
new file mode 100644
index 0000000000..63309e4657
--- /dev/null
+++ b/.changeset/bump-bfj-v9.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Updated dependency `bfj` to `^9.0.2`.
diff --git a/.changeset/bumpy-planets-go.md b/.changeset/bumpy-planets-go.md
deleted file mode 100644
index 5477711c1a..0000000000
--- a/.changeset/bumpy-planets-go.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Fixed Checkbox indicator showing checkmark color when unchecked.
-
-Affected components: Checkbox
diff --git a/.changeset/chatty-sides-wear.md b/.changeset/chatty-sides-wear.md
deleted file mode 100644
index 33937a1286..0000000000
--- a/.changeset/chatty-sides-wear.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-graph': patch
----
-
-Fixed zoom out description (replace `&` with &)
diff --git a/.changeset/chilly-bikes-rule.md b/.changeset/chilly-bikes-rule.md
deleted file mode 100644
index 894999cf56..0000000000
--- a/.changeset/chilly-bikes-rule.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@techdocs/cli': patch
----
-
-Updated to use new utilities from `@backstage/cli-common`.
diff --git a/.changeset/chilly-hotels-walk.md b/.changeset/chilly-hotels-walk.md
deleted file mode 100644
index e9b1ff66ba..0000000000
--- a/.changeset/chilly-hotels-walk.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-search-backend-node': minor
----
-
-Improving method that search tokenizer breaks apart entity names
diff --git a/.changeset/chilly-waves-relate.md b/.changeset/chilly-waves-relate.md
deleted file mode 100644
index 2e78f9245e..0000000000
--- a/.changeset/chilly-waves-relate.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@backstage/cli': minor
----
-
-Switched the default module resolution to `bundler` and the `module` setting to `ES2020`.
-
-You may need to bump some dependencies as part of this change and fix imports in code. The most common source of this is that type checking will now consider the `exports` field in `package.json` when resolving imports. This in turn can break older versions of packages that had incompatible `exports` fields. Generally these issues will have already been fixed in the upstream packages.
-
-You might be tempted to use `--skipLibCheck` to hide issues due to this change, but it will weaken the type safety of your project. If you run into a large number of issues and want to keep the old behavior, you can reset the `moduleResolution` and `module` settings your own `tsconfig.json` file to `node` and `ESNext` respectively. But keep in mind that the `node` option will be removed in future versions of TypeScript.
-
-A future version of Backstage will make these new settings mandatory, as we move to rely on the `exports` field for type resolution in packages, rather than the `typesVersions` field.
diff --git a/.changeset/clean-toys-reply.md b/.changeset/clean-toys-reply.md
deleted file mode 100644
index 66002903ab..0000000000
--- a/.changeset/clean-toys-reply.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-auth-node': patch
----
-
-remove leading dot in auth cookie cleanup call
diff --git a/.changeset/cli-common-cached-paths.md b/.changeset/cli-common-cached-paths.md
new file mode 100644
index 0000000000..6d9ec14297
--- /dev/null
+++ b/.changeset/cli-common-cached-paths.md
@@ -0,0 +1,28 @@
+---
+'@backstage/cli-common': minor
+---
+
+Added `targetPaths` and `findOwnPaths` as replacements for `findPaths`, with a cleaner separation between target project paths and package-relative paths.
+
+To migrate existing `findPaths` usage:
+
+```ts
+// Before
+import { findPaths } from '@backstage/cli-common';
+const paths = findPaths(__dirname);
+
+// After — for target project paths (cwd-based):
+import { targetPaths } from '@backstage/cli-common';
+// paths.targetDir → targetPaths.dir
+// paths.targetRoot → targetPaths.rootDir
+// paths.resolveTarget('src') → targetPaths.resolve('src')
+// paths.resolveTargetRoot('yarn.lock') → targetPaths.resolveRoot('yarn.lock')
+
+// After — for package-relative paths:
+import { findOwnPaths } from '@backstage/cli-common';
+const own = findOwnPaths(__dirname);
+// paths.ownDir → own.dir
+// paths.ownRoot → own.rootDir
+// paths.resolveOwn('config/jest.js') → own.resolve('config/jest.js')
+// paths.resolveOwnRoot('tsconfig.json') → own.resolveRoot('tsconfig.json')
+```
diff --git a/.changeset/cli-internal-refactor.md b/.changeset/cli-internal-refactor.md
new file mode 100644
index 0000000000..c3f8f38540
--- /dev/null
+++ b/.changeset/cli-internal-refactor.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Internal refactor to use new concurrency utilities from `@backstage/cli-node`.
diff --git a/.changeset/cli-node-parallel-helpers.md b/.changeset/cli-node-parallel-helpers.md
new file mode 100644
index 0000000000..5ad7ad7f4d
--- /dev/null
+++ b/.changeset/cli-node-parallel-helpers.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli-node': patch
+---
+
+Added `runConcurrentTasks` and `runWorkerQueueThreads` utilities, moved from the `@backstage/cli` internal code.
diff --git a/.changeset/cli-translations-export-import.md b/.changeset/cli-translations-export-import.md
new file mode 100644
index 0000000000..6631eebdfd
--- /dev/null
+++ b/.changeset/cli-translations-export-import.md
@@ -0,0 +1,9 @@
+---
+'@backstage/cli': patch
+---
+
+Added `translations export` and `translations import` commands for managing translation files.
+
+The `translations export` command discovers all `TranslationRef` definitions across frontend plugin dependencies and exports their default messages as JSON files. The `translations import` command generates `TranslationResource` wiring code from translated JSON files, ready to be plugged into the app.
+
+Both commands support a `--pattern` option for controlling the message file layout, for example `--pattern '{lang}/{id}.json'` for language-based directory grouping.
diff --git a/.changeset/common-coins-stare.md b/.changeset/common-coins-stare.md
deleted file mode 100644
index 0bd0078cf9..0000000000
--- a/.changeset/common-coins-stare.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Fixed `ButtonIcon` incorrectly applying `className` to inner elements instead of only the root element.
-
-Affected components: ButtonIcon
diff --git a/.changeset/crazy-hornets-spend.md b/.changeset/crazy-hornets-spend.md
deleted file mode 100644
index 4db99675ac..0000000000
--- a/.changeset/crazy-hornets-spend.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-'@backstage/plugin-catalog-graph': patch
-'@backstage/plugin-api-docs': patch
-'@backstage/plugin-techdocs': patch
-'@backstage/plugin-catalog': patch
-'@backstage/plugin-search': patch
-'@backstage/plugin-app': patch
-'@backstage/plugin-org': patch
----
-
-Minor extension type updates after frontend API bump
diff --git a/.changeset/create-app-1764689798.md b/.changeset/create-app-1764689798.md
deleted file mode 100644
index b50d431d4b..0000000000
--- a/.changeset/create-app-1764689798.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Bumped create-app version.
diff --git a/.changeset/dependabot-0d8f6de.md b/.changeset/dependabot-0d8f6de.md
deleted file mode 100644
index b0a3955f2d..0000000000
--- a/.changeset/dependabot-0d8f6de.md
+++ /dev/null
@@ -1,47 +0,0 @@
----
-'@backstage/backend-defaults': patch
-'@backstage/backend-dynamic-feature-service': patch
-'@backstage/backend-openapi-utils': patch
-'@backstage/backend-test-utils': patch
-'@backstage/cli': patch
-'@backstage/plugin-app-backend': patch
-'@backstage/plugin-app-node': patch
-'@backstage/plugin-auth-backend-module-atlassian-provider': patch
-'@backstage/plugin-auth-backend-module-auth0-provider': patch
-'@backstage/plugin-auth-backend-module-aws-alb-provider': patch
-'@backstage/plugin-auth-backend-module-azure-easyauth-provider': patch
-'@backstage/plugin-auth-backend-module-bitbucket-provider': patch
-'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch
-'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch
-'@backstage/plugin-auth-backend-module-gitlab-provider': patch
-'@backstage/plugin-auth-backend-module-guest-provider': patch
-'@backstage/plugin-auth-backend-module-microsoft-provider': patch
-'@backstage/plugin-auth-backend-module-oidc-provider': patch
-'@backstage/plugin-auth-backend-module-okta-provider': patch
-'@backstage/plugin-auth-backend-module-onelogin-provider': patch
-'@backstage/plugin-auth-backend-module-openshift-provider': patch
-'@backstage/plugin-auth-backend-module-pinniped-provider': patch
-'@backstage/plugin-auth-backend': patch
-'@backstage/plugin-auth-node': patch
-'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
-'@backstage/plugin-catalog-backend': patch
-'@backstage/plugin-devtools-backend': patch
-'@backstage/plugin-events-backend': patch
-'@backstage/plugin-events-node': patch
-'@backstage/plugin-gateway-backend': patch
-'@backstage/plugin-kubernetes-backend': patch
-'@backstage/plugin-mcp-actions-backend': patch
-'@backstage/plugin-notifications-backend': patch
-'@backstage/plugin-permission-backend': patch
-'@backstage/plugin-permission-node': patch
-'@backstage/plugin-proxy-backend': patch
-'@backstage/plugin-scaffolder-backend': patch
-'@backstage/plugin-search-backend': patch
-'@backstage/plugin-signals-backend': patch
-'@backstage/plugin-signals-node': patch
-'@backstage/plugin-techdocs-backend': patch
-'@backstage/plugin-techdocs-node': patch
-'@backstage/plugin-user-settings-backend': patch
----
-
-chore(deps): bump `express` from 4.21.2 to 4.22.0
diff --git a/.changeset/dependabot-5e654fe.md b/.changeset/dependabot-5e654fe.md
deleted file mode 100644
index 93563cffc6..0000000000
--- a/.changeset/dependabot-5e654fe.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-defaults': patch
----
-
-build(deps): bump `node-forge` from 1.3.1 to 1.3.2
diff --git a/.changeset/dependabot-d2ec7e9.md b/.changeset/dependabot-d2ec7e9.md
new file mode 100644
index 0000000000..efa4ec5510
--- /dev/null
+++ b/.changeset/dependabot-d2ec7e9.md
@@ -0,0 +1,11 @@
+---
+'@backstage/backend-defaults': patch
+'@backstage/cli': patch
+'@backstage/eslint-plugin': patch
+'@backstage/repo-tools': patch
+'@backstage/plugin-auth-backend': patch
+'@backstage/plugin-catalog-backend-module-github': patch
+'@backstage/plugin-catalog-backend': patch
+---
+
+build(deps): bump `minimatch` from 9.0.5 to 10.2.1
diff --git a/.changeset/famous-jars-lose.md b/.changeset/famous-jars-lose.md
deleted file mode 100644
index 4070701e4e..0000000000
--- a/.changeset/famous-jars-lose.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Fixed Table Row component to correctly handle cases where no `href` is provided, preventing unnecessary router provider wrapping and fixing the cursor incorrectly showing as a pointer despite the element not being a link.
-
-Affected components: Row
diff --git a/.changeset/fancy-ends-turn.md b/.changeset/fancy-ends-turn.md
new file mode 100644
index 0000000000..7787b47971
--- /dev/null
+++ b/.changeset/fancy-ends-turn.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-notifications-backend-module-slack': minor
+---
+
+Add an extension for custom Slack message layouts
diff --git a/.changeset/fifty-coats-feel.md b/.changeset/fifty-coats-feel.md
deleted file mode 100644
index 0e3425a505..0000000000
--- a/.changeset/fifty-coats-feel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-kubernetes-react': patch
----
-
-Enabled a pod terminal at GKE
diff --git a/.changeset/fifty-lights-marry.md b/.changeset/fifty-lights-marry.md
deleted file mode 100644
index 423ef89721..0000000000
--- a/.changeset/fifty-lights-marry.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch
-'@backstage/plugin-kubernetes-backend': patch
-'@backstage/backend-defaults': patch
-'@backstage/plugin-scaffolder-react': patch
-'@backstage/integration': patch
----
-
-Updated luxon types
diff --git a/.changeset/fine-eagles-sleep.md b/.changeset/fine-eagles-sleep.md
deleted file mode 100644
index 0af6d0dfc0..0000000000
--- a/.changeset/fine-eagles-sleep.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/backend-app-api': minor
----
-
-Introduced backend startup result tracking and error handling. The `Backend.start()` method now returns a `BackendStartupResult` with detailed success/failure status and timing information for all plugins and modules. When startup fails, a `BackendStartupError` is thrown that includes the complete startup results, making it easier to diagnose which plugins or modules failed.
-
-This also improves the default error message when backend startup fails, and of course makes it possible to craft your own custom error reporting based on the startup results.
diff --git a/.changeset/fix-frontend-feature-compat.md b/.changeset/fix-frontend-feature-compat.md
new file mode 100644
index 0000000000..ced9237285
--- /dev/null
+++ b/.changeset/fix-frontend-feature-compat.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-plugin-api': patch
+---
+
+Made the `pluginId` property optional in the `FrontendFeature` type, allowing plugins published against older versions of the framework to be used without type errors.
diff --git a/.changeset/fix-prettier-existence-check.md b/.changeset/fix-prettier-existence-check.md
new file mode 100644
index 0000000000..96e8b79082
--- /dev/null
+++ b/.changeset/fix-prettier-existence-check.md
@@ -0,0 +1,5 @@
+---
+'@backstage/repo-tools': patch
+---
+
+Fixed prettier existence checks in OpenAPI commands to use `fs.pathExists` instead of checking the resolved path string, which was always truthy.
diff --git a/.changeset/flat-pillows-rush.md b/.changeset/flat-pillows-rush.md
deleted file mode 100644
index bb64013f62..0000000000
--- a/.changeset/flat-pillows-rush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/frontend-plugin-api': patch
----
-
-Fixed a versioning conflict that could result in a `.withContext` is not a function error.
diff --git a/.changeset/floppy-bobcats-serve.md b/.changeset/floppy-bobcats-serve.md
deleted file mode 100644
index d698e1482c..0000000000
--- a/.changeset/floppy-bobcats-serve.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-'@backstage/core-components': patch
----
-
-Fixed bug in the `LogViewer` component where shift + click always opened a new window instead of just changing the selection.
-
-In addition, improved the `LogViewer` component by a few usability enhancements:
-
-- Added support for multiple selections using cmd/ctrl + click
-- Improved the generated hash that is added to the URL to also support ranges & multiple selections
-- Added an hover effect & info tooltip to the "Copy to clipboard" button to indicate its functionality
-- Added some color and a separator to the line numbers to improve readability
diff --git a/.changeset/fluffy-owls-act.md b/.changeset/fluffy-owls-act.md
new file mode 100644
index 0000000000..03ae42fd99
--- /dev/null
+++ b/.changeset/fluffy-owls-act.md
@@ -0,0 +1,7 @@
+---
+'@backstage/backend-defaults': patch
+'@backstage/integration': minor
+'@backstage/plugin-catalog-backend-module-gitlab': patch
+---
+
+Add configurable throttling and retry mechanism for GitLab integration.
diff --git a/.changeset/four-peaches-train.md b/.changeset/four-peaches-train.md
deleted file mode 100644
index 81b35589c1..0000000000
--- a/.changeset/four-peaches-train.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/backend-defaults': patch
-'@backstage/backend-plugin-api': patch
----
-
-**BREAKING ALPHA**: The old `instanceMetadataService` has been removed from alpha. Please switch over to using the stable `coreServices.rootInstanceMetadata` and related types instead, available from `@backstage/backend-plugin-api`.
diff --git a/.changeset/fruity-rivers-arrive.md b/.changeset/fruity-rivers-arrive.md
deleted file mode 100644
index a816946bcc..0000000000
--- a/.changeset/fruity-rivers-arrive.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend': minor
----
-
-Add `defaultEnvironment` config to scaffolder to enable more flexible and custom templates. Now it's possible enable access to default parameters and secrets in templates, improving security and reducing complexity.
diff --git a/.changeset/fruity-words-melt.md b/.changeset/fruity-words-melt.md
deleted file mode 100644
index 0f02acee2b..0000000000
--- a/.changeset/fruity-words-melt.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs-module-addons-contrib': patch
----
-
-Updated tests to match test-utils change
diff --git a/.changeset/funny-hornets-peel.md b/.changeset/funny-hornets-peel.md
deleted file mode 100644
index 2af6702295..0000000000
--- a/.changeset/funny-hornets-peel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend': patch
----
-
-Updated `isolated-vm` to `6.0.1`
diff --git a/.changeset/funny-papayas-rest.md b/.changeset/funny-papayas-rest.md
deleted file mode 100644
index 95f52966ec..0000000000
--- a/.changeset/funny-papayas-rest.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/repo-tools': patch
-'@backstage/codemods': patch
----
-
-Updated to use new utilities from `@backstage/cli-common`.
diff --git a/.changeset/fuzzy-phones-own.md b/.changeset/fuzzy-phones-own.md
deleted file mode 100644
index 61725d1f1d..0000000000
--- a/.changeset/fuzzy-phones-own.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-'@backstage/plugin-catalog-backend-module-github-org': patch
-'@backstage/plugin-catalog-backend-module-github': patch
----
-
-Introduce new configuration option to exclude suspended users from GitHub Enterprise instances.
-
-When it’s set to true, suspended users won’t be returned when querying the organization users for GitHub Enterprise instances.
-Note that this option should be used only against GitHub Enterprise instances, the property does not exist in the github.com GraphQL schema, setting it will cause a schema validation error and the syncing of users will fail.
diff --git a/.changeset/fuzzy-rivers-travel.md b/.changeset/fuzzy-rivers-travel.md
deleted file mode 100644
index fa2c08a671..0000000000
--- a/.changeset/fuzzy-rivers-travel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/frontend-plugin-api': patch
----
-
-Made the return type of `.withOverrides` to be simplified.
diff --git a/.changeset/fuzzy-trees-live.md b/.changeset/fuzzy-trees-live.md
deleted file mode 100644
index 84cec56fab..0000000000
--- a/.changeset/fuzzy-trees-live.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core-components': patch
----
-
-long words like urls now breaks to new line on warning panels instead of overflowing the container
diff --git a/.changeset/gentle-singers-love.md b/.changeset/gentle-singers-love.md
deleted file mode 100644
index ed4372ee06..0000000000
--- a/.changeset/gentle-singers-love.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-app': patch
----
-
-Support to set `defaultLanguage` and `availableLanguages` for the app language API in the new frontend system
diff --git a/.changeset/gentle-trains-juggle.md b/.changeset/gentle-trains-juggle.md
deleted file mode 100644
index e8b1397180..0000000000
--- a/.changeset/gentle-trains-juggle.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/plugin-kubernetes-backend': patch
----
-
-Replace `@aws-sdk/signature-v4` with `@smithy/signature-v4`,
-as stated in the [package documentation](https://www.npmjs.com/package/@aws-sdk/signature-v4?activeTab=readme)
diff --git a/.changeset/great-files-shave.md b/.changeset/great-files-shave.md
deleted file mode 100644
index 271a8e8955..0000000000
--- a/.changeset/great-files-shave.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-react': patch
----
-
-Fixed an issue where `EntityOwnerPicker` failed to filter options when the input text contained uppercase characters.
diff --git a/.changeset/happy-bottles-invite.md b/.changeset/happy-bottles-invite.md
deleted file mode 100644
index 07fc222cad..0000000000
--- a/.changeset/happy-bottles-invite.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-defaults': minor
----
-
-Move `better-sqlite3` from dependencies to peer dependencies
diff --git a/.changeset/happy-streets-dress.md b/.changeset/happy-streets-dress.md
deleted file mode 100644
index 46f4d798ed..0000000000
--- a/.changeset/happy-streets-dress.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/create-app': patch
----
-
-Updated engines to support Node 22 or 24
diff --git a/.changeset/honest-bears-itch.md b/.changeset/honest-bears-itch.md
deleted file mode 100644
index 4c0cd5f47e..0000000000
--- a/.changeset/honest-bears-itch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-test-utils': patch
----
-
-Switched `textextensions` dependency for `text-extensions`.
diff --git a/.changeset/kind-hoops-double.md b/.changeset/kind-hoops-double.md
deleted file mode 100644
index fd2f9f023f..0000000000
--- a/.changeset/kind-hoops-double.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/integration': patch
----
-
-Made the github urls case insensitive.
diff --git a/.changeset/large-planes-punch.md b/.changeset/large-planes-punch.md
deleted file mode 100644
index 3216653eb9..0000000000
--- a/.changeset/large-planes-punch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-defaults': patch
----
-
-Fix for `jose` types
diff --git a/.changeset/legal-cloths-spend.md b/.changeset/legal-cloths-spend.md
deleted file mode 100644
index 50d965307a..0000000000
--- a/.changeset/legal-cloths-spend.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-mcp-actions-backend': patch
----
-
-Clarify error handling in readme and update handleError.ts to include all backstage/errors
diff --git a/.changeset/legal-otters-punch.md b/.changeset/legal-otters-punch.md
deleted file mode 100644
index 2003492e29..0000000000
--- a/.changeset/legal-otters-punch.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/plugin-kubernetes-backend': patch
-'@backstage/plugin-kubernetes-common': patch
-'@backstage/plugin-kubernetes-react': patch
----
-
-Add Kubernetes Plugin Secrets Accordion with masked secret datas
diff --git a/.changeset/lemon-corners-hug.md b/.changeset/lemon-corners-hug.md
deleted file mode 100644
index b41c09c928..0000000000
--- a/.changeset/lemon-corners-hug.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs-node': patch
----
-
-There was an issue in the uploading of large size files to the AWS S3. We have modified the logic by adding retry along with multipart uploading functionality.
diff --git a/.changeset/long-hairs-throw.md b/.changeset/long-hairs-throw.md
new file mode 100644
index 0000000000..a00b24fda3
--- /dev/null
+++ b/.changeset/long-hairs-throw.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Added a new `ui:autoSelect` option to the EntityPicker field that controls whether an entity is automatically selected when the field loses focus. When set to `false`, the field will remain empty if the user closes it without explicitly selecting an entity, preventing unintentional selections. Defaults to `true` for backward compatibility.
diff --git a/.changeset/loose-pets-slide.md b/.changeset/loose-pets-slide.md
deleted file mode 100644
index b8ec201f1c..0000000000
--- a/.changeset/loose-pets-slide.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-'@backstage/repo-tools': patch
----
-
-Updates OpenAPI generator templates to preserve original property names (like 'group-name', 'user-id') from OpenAPI specs when propertyNaming=original is specified. Previously, these were always converted to camelCase regardless of the propertyNaming setting.
-
-- Updates modelGeneric.mustache templates in both client and server generators
-- Updates modelTaggedUnion.mustache templates in both client and server generators
-- Uses {{baseName}} when available, falls back to {{name}} for backward compatibility
-- Maintains backward compatibility - no changes when propertyNaming=original is not used
diff --git a/.changeset/loud-yaks-watch.md b/.changeset/loud-yaks-watch.md
deleted file mode 100644
index b7ad65b9eb..0000000000
--- a/.changeset/loud-yaks-watch.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/repo-tools': patch
----
-
-Bump `@microsoft/api-documenter` and `@microsoft/api-extractor` to latest versions.
diff --git a/.changeset/lucky-days-hug.md b/.changeset/lucky-days-hug.md
deleted file mode 100644
index 4d76caa098..0000000000
--- a/.changeset/lucky-days-hug.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/plugin-kubernetes-react': patch
-'@backstage/plugin-kubernetes': patch
----
-
-add missing i18n support for kubernetes and kubernetes-react
diff --git a/.changeset/many-planes-join.md b/.changeset/many-planes-join.md
deleted file mode 100644
index 66107cd370..0000000000
--- a/.changeset/many-planes-join.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/core-app-api': patch
-'@backstage/test-utils': patch
----
-
-Internal update of translation imports.
diff --git a/.changeset/metal-boxes-laugh.md b/.changeset/metal-boxes-laugh.md
deleted file mode 100644
index 9a5ab24e0e..0000000000
--- a/.changeset/metal-boxes-laugh.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core-plugin-api': patch
----
-
-The `useApp` and `useRouteRef` functions are now forwards compatible with the new frontend system. Along with the previous route reference changes this means that there is no longer a need to use `compatWrapper` from `@backstage/core-compat-api` to make code based on `@backstage/core-plugin-api` compatible with `@backstage/frontend-plugin-api` APIs.
diff --git a/.changeset/metal-humans-lose.md b/.changeset/metal-humans-lose.md
deleted file mode 100644
index f96d33a779..0000000000
--- a/.changeset/metal-humans-lose.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-'@backstage/plugin-catalog-unprocessed-entities': patch
-'@backstage/plugin-app-visualizer': patch
-'@backstage/plugin-catalog-import': patch
-'@backstage/plugin-catalog-graph': patch
-'@backstage/plugin-catalog-react': patch
-'@backstage/plugin-notifications': patch
-'@backstage/plugin-user-settings': patch
-'@backstage/plugin-kubernetes': patch
-'@backstage/plugin-scaffolder': patch
-'@backstage/plugin-api-docs': patch
-'@backstage/plugin-devtools': patch
-'@backstage/plugin-techdocs': patch
-'@backstage/plugin-catalog': patch
-'@backstage/plugin-search': patch
-'@backstage/plugin-home': patch
-'@backstage/plugin-app': patch
-'@backstage/plugin-org': patch
----
-
-Remove unnecessary use of `compatWrapper` and `convertLegacyRouteRef`(s) for the new frontend system.
diff --git a/.changeset/metal-humans-move.md b/.changeset/metal-humans-move.md
new file mode 100644
index 0000000000..21881246fc
--- /dev/null
+++ b/.changeset/metal-humans-move.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Minor internal optimisation
diff --git a/.changeset/migrate-to-target-paths.md b/.changeset/migrate-to-target-paths.md
new file mode 100644
index 0000000000..efc03ba289
--- /dev/null
+++ b/.changeset/migrate-to-target-paths.md
@@ -0,0 +1,12 @@
+---
+'@backstage/cli': patch
+'@backstage/cli-node': patch
+'@backstage/backend-dynamic-feature-service': patch
+'@backstage/codemods': patch
+'@backstage/config-loader': patch
+'@backstage/create-app': patch
+'@backstage/repo-tools': patch
+'@techdocs/cli': patch
+---
+
+Migrated from deprecated `findPaths` to `targetPaths` and `findOwnPaths` from `@backstage/cli-common`.
diff --git a/.changeset/modern-taxes-start.md b/.changeset/modern-taxes-start.md
deleted file mode 100644
index 9fd7645570..0000000000
--- a/.changeset/modern-taxes-start.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli-common': patch
----
-
-Added new `run`, `runOutput`, and `runCheck` utilities to help run child processes in a safe and portable way.
diff --git a/.changeset/neat-pens-clean.md b/.changeset/neat-pens-clean.md
deleted file mode 100644
index aff67a2a10..0000000000
--- a/.changeset/neat-pens-clean.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/repo-tools': patch
----
-
-Properly create workspace in OS temporary directory for `generate-patch` command
diff --git a/.changeset/nice-humans-cry.md b/.changeset/nice-humans-cry.md
deleted file mode 100644
index c6119702b4..0000000000
--- a/.changeset/nice-humans-cry.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend': patch
-'@backstage/backend-defaults': patch
----
-
-Internal change to support new versions of the `logform` library
diff --git a/.changeset/nice-trams-shake.md b/.changeset/nice-trams-shake.md
deleted file mode 100644
index b58c03200f..0000000000
--- a/.changeset/nice-trams-shake.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-react': patch
----
-
-Fixed bug where custom `review.name` values were incorrectly formatted by `startCase`, preserving them exactly as written.
diff --git a/.changeset/old-parks-smell.md b/.changeset/old-parks-smell.md
deleted file mode 100644
index f089c7da21..0000000000
--- a/.changeset/old-parks-smell.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-'@backstage/plugin-techdocs-addons-test-utils': major
----
-
-**BREAKING**: `TechDocsAddonTester.renderWithEffects()` no longer returns a screen; this means that you can no longer grab assertions such as `getByText` from its return value.
-
-Newer versions of `@testing-library` recommends using the `screen` export for assertions - and removing this from the addon tester contract allows us to more freely iterate on which underlying version of the testing library is being used.
-
-One notable effect of this, however, is that the `@testing-library` `screen` does NOT support assertions on the shadow DOM, which techdocs relies on. You will therefore want to add a dependency on [the `shadow-dom-testing-library` package](https://github.com/konnorrogers/shadow-dom-testing-library/) in your tests, and using its `screen` and its dedicated `*Shadow*` methods. As an example, if you keep doing `getByText` you will not get matches inside the shadow DOM - switch to `getByShadowText` instead.
-
-```ts
-import { screen } from 'shadow-dom-testing-library';
-
-// ... render the addon ...
-await TechDocsAddonTester.buildAddonsInTechDocs([ ])
- .withDom(
TEST_CONTENT)
- .renderWithEffects();
-
-expect(screen.getByShadowText('TEST_CONTENT')).toBeInTheDocument();
-```
diff --git a/.changeset/open-points-beam.md b/.changeset/open-points-beam.md
deleted file mode 100644
index b58bf2b3d5..0000000000
--- a/.changeset/open-points-beam.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-'@backstage/ui': minor
----
-
-**Breaking change** The `Cell` component has been refactored to be a generic wrapper component that accepts `children` for custom cell content. The text-specific functionality (previously part of `Cell`) has been moved to a new `CellText` component.
-
-### Migration Guide
-
-If you were using `Cell` with text-specific props (`title`, `description`, `leadingIcon`, `href`), you need to update your code to use `CellText` instead:
-
-**Before:**
-
-```tsx
- | }
- href="/path"
-/>
-```
-
-**After:**
-
-```tsx
- }
- href="/path"
-/>
-```
-
-For custom cell content, use the new generic `Cell` component:
-
-```tsx
-{/* Your custom content */} |
-```
diff --git a/.changeset/orange-mugs-post-1.md b/.changeset/orange-mugs-post-1.md
new file mode 100644
index 0000000000..bbf2b51082
--- /dev/null
+++ b/.changeset/orange-mugs-post-1.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-backend': minor
+---
+
+Added opentelemetry metrics for SCM events:
+
+- `catalog.events.scm.messages` with attribute `eventType`: Counter for the number of SCM events actually received by the catalog backend. The `eventType` is currently either `location` or `repository`.
diff --git a/.changeset/orange-mugs-post-2.md b/.changeset/orange-mugs-post-2.md
new file mode 100644
index 0000000000..ed78bbc175
--- /dev/null
+++ b/.changeset/orange-mugs-post-2.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-catalog-node': minor
+---
+
+Added the ability for SCM events subscribers to mark the fact that they have taken actions based on events, which produces output metrics:
+
+- `catalog.events.scm.actions` with attribute `action`: Counter for the number of actions actually taken by catalog internals or other subscribers, based on SCM events. The `action` is currently either `create`, `delete`, `refresh`, or `move`.
diff --git a/.changeset/pink-terms-know.md b/.changeset/pink-terms-know.md
new file mode 100644
index 0000000000..ca64a76f39
--- /dev/null
+++ b/.changeset/pink-terms-know.md
@@ -0,0 +1,9 @@
+---
+'@backstage/cli': patch
+'@backstage/codemods': patch
+'@backstage/create-app': patch
+'@backstage/repo-tools': patch
+'@techdocs/cli': patch
+---
+
+Upgraded `commander` dependency from `^12.0.0` to `^14.0.3` across all CLI packages.
diff --git a/.changeset/quiet-hats-sleep.md b/.changeset/polite-singers-lead.md
similarity index 66%
rename from .changeset/quiet-hats-sleep.md
rename to .changeset/polite-singers-lead.md
index 5f75f22f6c..0aa775e4e2 100644
--- a/.changeset/quiet-hats-sleep.md
+++ b/.changeset/polite-singers-lead.md
@@ -2,4 +2,4 @@
'@backstage/plugin-scaffolder-backend': patch
---
-Fixing OpenAPI definition
+Removed unused dependencies
diff --git a/.changeset/pre.json b/.changeset/pre.json
index f44b869497..00dbb0f757 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -2,256 +2,213 @@
"mode": "pre",
"tag": "next",
"initialVersions": {
- "example-app": "0.2.115",
- "@backstage/app-defaults": "1.7.2",
- "example-app-next": "0.0.29",
- "app-next-example-plugin": "0.0.29",
- "example-backend": "0.0.44",
- "@backstage/backend-app-api": "1.3.0",
- "@backstage/backend-defaults": "0.13.1",
- "@backstage/backend-dev-utils": "0.1.5",
- "@backstage/backend-dynamic-feature-service": "0.7.6",
- "@backstage/backend-openapi-utils": "0.6.3",
- "@backstage/backend-plugin-api": "1.5.0",
- "@backstage/backend-test-utils": "1.10.0",
- "@backstage/catalog-client": "1.12.1",
+ "example-app": "0.0.32",
+ "@backstage/app-defaults": "1.7.5",
+ "app-example-plugin": "0.0.32",
+ "example-app-legacy": "0.2.118",
+ "example-backend": "0.0.47",
+ "@backstage/backend-app-api": "1.5.0",
+ "@backstage/backend-defaults": "0.15.2",
+ "@backstage/backend-dev-utils": "0.1.7",
+ "@backstage/backend-dynamic-feature-service": "0.7.9",
+ "@backstage/backend-openapi-utils": "0.6.6",
+ "@backstage/backend-plugin-api": "1.7.0",
+ "@backstage/backend-test-utils": "1.11.0",
+ "@backstage/catalog-client": "1.13.0",
"@backstage/catalog-model": "1.7.6",
- "@backstage/cli": "0.34.5",
- "@backstage/cli-common": "0.1.15",
- "@backstage/cli-node": "0.2.15",
- "@backstage/codemods": "0.1.52",
+ "@backstage/cli": "0.35.4",
+ "@backstage/cli-common": "0.1.18",
+ "@backstage/cli-node": "0.2.18",
+ "@backstage/codemods": "0.1.54",
"@backstage/config": "1.3.6",
- "@backstage/config-loader": "1.10.6",
- "@backstage/core-app-api": "1.19.2",
- "@backstage/core-compat-api": "0.5.4",
- "@backstage/core-components": "0.18.3",
- "@backstage/core-plugin-api": "1.12.0",
- "@backstage/create-app": "0.7.6",
- "@backstage/dev-utils": "1.1.17",
- "e2e-test": "0.2.34",
- "@backstage/e2e-test-utils": "0.1.1",
+ "@backstage/config-loader": "1.10.8",
+ "@backstage/core-app-api": "1.19.5",
+ "@backstage/core-compat-api": "0.5.8",
+ "@backstage/core-components": "0.18.7",
+ "@backstage/core-plugin-api": "1.12.3",
+ "@backstage/create-app": "0.7.9",
+ "@backstage/dev-utils": "1.1.20",
+ "e2e-test": "0.2.37",
+ "@backstage/e2e-test-utils": "0.1.2",
"@backstage/errors": "1.2.7",
- "@backstage/eslint-plugin": "0.2.0",
- "@backstage/frontend-app-api": "0.13.2",
- "@backstage/frontend-defaults": "0.3.3",
- "@backstage/frontend-dynamic-feature-loader": "0.1.7",
- "@internal/frontend": "0.0.15",
- "@backstage/frontend-plugin-api": "0.13.0",
- "@backstage/frontend-test-utils": "0.4.1",
- "@backstage/integration": "1.18.2",
- "@backstage/integration-aws-node": "0.1.19",
- "@backstage/integration-react": "1.2.12",
+ "@backstage/eslint-plugin": "0.2.1",
+ "@backstage/filter-predicates": "0.1.0",
+ "@backstage/frontend-app-api": "0.15.0",
+ "@backstage/frontend-defaults": "0.4.0",
+ "@backstage/frontend-dynamic-feature-loader": "0.1.9",
+ "@internal/frontend": "0.0.17",
+ "@backstage/frontend-plugin-api": "0.14.0",
+ "@backstage/frontend-test-utils": "0.5.0",
+ "@backstage/integration": "1.20.0",
+ "@backstage/integration-aws-node": "0.1.20",
+ "@backstage/integration-react": "1.2.15",
+ "@backstage/module-federation-common": "0.1.0",
"@internal/opaque": "0.0.1",
"@backstage/release-manifests": "0.0.13",
- "@backstage/repo-tools": "0.16.0",
- "@internal/scaffolder": "0.0.15",
- "@techdocs/cli": "1.10.2",
- "techdocs-cli-embedded-app": "0.2.114",
- "@backstage/test-utils": "1.7.13",
- "@backstage/theme": "0.7.0",
+ "@backstage/repo-tools": "0.16.4",
+ "@internal/scaffolder": "0.0.18",
+ "@techdocs/cli": "1.10.5",
+ "techdocs-cli-embedded-app": "0.2.117",
+ "@backstage/test-utils": "1.7.15",
+ "@backstage/theme": "0.7.2",
"@backstage/types": "1.2.2",
- "@backstage/ui": "0.9.0",
- "@backstage/version-bridge": "1.0.11",
- "yarn-plugin-backstage": "0.0.7",
- "@backstage/plugin-api-docs": "0.13.1",
- "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10",
- "@backstage/plugin-app": "0.3.2",
- "@backstage/plugin-app-backend": "0.5.8",
- "@backstage/plugin-app-node": "0.1.39",
- "@backstage/plugin-app-visualizer": "0.1.25",
- "@backstage/plugin-auth": "0.1.2",
- "@backstage/plugin-auth-backend": "0.25.6",
- "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.9",
- "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.14",
- "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.9",
- "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-github-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-google-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-guest-provider": "0.2.14",
- "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.14",
- "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.9",
- "@backstage/plugin-auth-backend-module-okta-provider": "0.2.9",
- "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.2",
- "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.9",
- "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.9",
- "@backstage/plugin-auth-node": "0.6.9",
- "@backstage/plugin-auth-react": "0.1.21",
- "@backstage/plugin-bitbucket-cloud-common": "0.3.4",
- "@backstage/plugin-catalog": "1.32.0",
- "@backstage/plugin-catalog-backend": "3.2.0",
- "@backstage/plugin-catalog-backend-module-aws": "0.4.17",
- "@backstage/plugin-catalog-backend-module-azure": "0.3.11",
- "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.8",
- "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.5",
- "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.5",
- "@backstage/plugin-catalog-backend-module-gcp": "0.3.14",
- "@backstage/plugin-catalog-backend-module-gerrit": "0.3.8",
- "@backstage/plugin-catalog-backend-module-gitea": "0.1.6",
- "@backstage/plugin-catalog-backend-module-github": "0.11.2",
- "@backstage/plugin-catalog-backend-module-github-org": "0.3.16",
- "@backstage/plugin-catalog-backend-module-gitlab": "0.7.5",
- "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.15",
- "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.6",
- "@backstage/plugin-catalog-backend-module-ldap": "0.12.0",
- "@backstage/plugin-catalog-backend-module-logs": "0.1.16",
- "@backstage/plugin-catalog-backend-module-msgraph": "0.8.2",
- "@backstage/plugin-catalog-backend-module-openapi": "0.2.16",
- "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.16",
- "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.14",
- "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.6",
- "@backstage/plugin-catalog-common": "1.1.7",
- "@backstage/plugin-catalog-graph": "0.5.3",
- "@backstage/plugin-catalog-import": "0.13.7",
- "@backstage/plugin-catalog-node": "1.20.0",
- "@backstage/plugin-catalog-react": "1.21.3",
- "@backstage/plugin-catalog-unprocessed-entities": "0.2.23",
- "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.11",
- "@backstage/plugin-config-schema": "0.1.74",
- "@backstage/plugin-devtools": "0.1.33",
- "@backstage/plugin-devtools-backend": "0.5.11",
- "@backstage/plugin-devtools-common": "0.1.19",
- "@backstage/plugin-events-backend": "0.5.8",
- "@backstage/plugin-events-backend-module-aws-sqs": "0.4.17",
- "@backstage/plugin-events-backend-module-azure": "0.2.26",
- "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.26",
- "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.7",
- "@backstage/plugin-events-backend-module-gerrit": "0.2.26",
- "@backstage/plugin-events-backend-module-github": "0.4.6",
- "@backstage/plugin-events-backend-module-gitlab": "0.3.7",
- "@backstage/plugin-events-backend-module-google-pubsub": "0.1.6",
- "@backstage/plugin-events-backend-module-kafka": "0.1.5",
- "@backstage/plugin-events-backend-test-utils": "0.1.50",
- "@backstage/plugin-events-node": "0.4.17",
- "@internal/plugin-todo-list": "1.0.45",
- "@internal/plugin-todo-list-backend": "1.0.45",
- "@internal/plugin-todo-list-common": "1.0.28",
- "@backstage/plugin-gateway-backend": "1.1.0",
- "@backstage/plugin-home": "0.8.14",
- "@backstage/plugin-home-react": "0.1.32",
- "@backstage/plugin-kubernetes": "0.12.13",
- "@backstage/plugin-kubernetes-backend": "0.20.4",
- "@backstage/plugin-kubernetes-cluster": "0.0.31",
- "@backstage/plugin-kubernetes-common": "0.9.8",
- "@backstage/plugin-kubernetes-node": "0.3.6",
- "@backstage/plugin-kubernetes-react": "0.5.13",
- "@backstage/plugin-mcp-actions-backend": "0.1.5",
- "@backstage/plugin-mui-to-bui": "0.2.1",
- "@backstage/plugin-notifications": "0.5.11",
- "@backstage/plugin-notifications-backend": "0.6.0",
- "@backstage/plugin-notifications-backend-module-email": "0.3.16",
- "@backstage/plugin-notifications-backend-module-slack": "0.2.1",
- "@backstage/plugin-notifications-common": "0.2.0",
- "@backstage/plugin-notifications-node": "0.2.21",
- "@backstage/plugin-org": "0.6.46",
- "@backstage/plugin-org-react": "0.1.44",
- "@backstage/plugin-permission-backend": "0.7.6",
- "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.14",
- "@backstage/plugin-permission-common": "0.9.3",
- "@backstage/plugin-permission-node": "0.10.6",
- "@backstage/plugin-permission-react": "0.4.38",
- "@backstage/plugin-proxy-backend": "0.6.8",
- "@backstage/plugin-proxy-node": "0.1.10",
- "@backstage/plugin-scaffolder": "1.34.3",
- "@backstage/plugin-scaffolder-backend": "3.0.1",
- "@backstage/plugin-scaffolder-backend-module-azure": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.16",
- "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.15",
- "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.17",
- "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-github": "0.9.2",
- "@backstage/plugin-scaffolder-backend-module-gitlab": "0.10.0",
- "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.16",
- "@backstage/plugin-scaffolder-backend-module-rails": "0.5.15",
- "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.15",
- "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.16",
- "@backstage/plugin-scaffolder-common": "1.7.3",
- "@backstage/plugin-scaffolder-node": "0.12.1",
- "@backstage/plugin-scaffolder-node-test-utils": "0.3.5",
- "@backstage/plugin-scaffolder-react": "1.19.3",
- "@backstage/plugin-search": "1.5.0",
- "@backstage/plugin-search-backend": "2.0.8",
- "@backstage/plugin-search-backend-module-catalog": "0.3.10",
- "@backstage/plugin-search-backend-module-elasticsearch": "1.7.8",
- "@backstage/plugin-search-backend-module-explore": "0.3.9",
- "@backstage/plugin-search-backend-module-pg": "0.5.50",
- "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.15",
- "@backstage/plugin-search-backend-module-techdocs": "0.4.8",
- "@backstage/plugin-search-backend-node": "1.3.17",
- "@backstage/plugin-search-common": "1.2.21",
- "@backstage/plugin-search-react": "1.10.0",
- "@backstage/plugin-signals": "0.0.25",
- "@backstage/plugin-signals-backend": "0.3.10",
- "@backstage/plugin-signals-node": "0.1.26",
- "@backstage/plugin-signals-react": "0.0.17",
- "@backstage/plugin-techdocs": "1.16.0",
- "@backstage/plugin-techdocs-addons-test-utils": "1.1.2",
- "@backstage/plugin-techdocs-backend": "2.1.2",
+ "@backstage/ui": "0.12.0",
+ "@backstage/version-bridge": "1.0.12",
+ "yarn-plugin-backstage": "0.0.9",
+ "@backstage/plugin-api-docs": "0.13.4",
+ "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.11",
+ "@backstage/plugin-app": "0.4.0",
+ "@backstage/plugin-app-backend": "0.5.11",
+ "@backstage/plugin-app-node": "0.1.42",
+ "@backstage/plugin-app-react": "0.2.0",
+ "@backstage/plugin-app-visualizer": "0.2.0",
+ "@backstage/plugin-auth": "0.1.5",
+ "@backstage/plugin-auth-backend": "0.27.0",
+ "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.12",
+ "@backstage/plugin-auth-backend-module-auth0-provider": "0.3.0",
+ "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.13",
+ "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.17",
+ "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.12",
+ "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.12",
+ "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.12",
+ "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.12",
+ "@backstage/plugin-auth-backend-module-github-provider": "0.5.0",
+ "@backstage/plugin-auth-backend-module-gitlab-provider": "0.4.0",
+ "@backstage/plugin-auth-backend-module-google-provider": "0.3.12",
+ "@backstage/plugin-auth-backend-module-guest-provider": "0.2.16",
+ "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.12",
+ "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.12",
+ "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.17",
+ "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.13",
+ "@backstage/plugin-auth-backend-module-okta-provider": "0.2.12",
+ "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.12",
+ "@backstage/plugin-auth-backend-module-openshift-provider": "0.1.4",
+ "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.11",
+ "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.11",
+ "@backstage/plugin-auth-node": "0.6.13",
+ "@backstage/plugin-auth-react": "0.1.24",
+ "@backstage/plugin-bitbucket-cloud-common": "0.3.7",
+ "@backstage/plugin-catalog": "1.33.0",
+ "@backstage/plugin-catalog-backend": "3.4.0",
+ "@backstage/plugin-catalog-backend-module-aws": "0.4.20",
+ "@backstage/plugin-catalog-backend-module-azure": "0.3.14",
+ "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.11",
+ "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.8",
+ "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.8",
+ "@backstage/plugin-catalog-backend-module-gcp": "0.3.16",
+ "@backstage/plugin-catalog-backend-module-gerrit": "0.3.11",
+ "@backstage/plugin-catalog-backend-module-gitea": "0.1.9",
+ "@backstage/plugin-catalog-backend-module-github": "0.12.2",
+ "@backstage/plugin-catalog-backend-module-github-org": "0.3.19",
+ "@backstage/plugin-catalog-backend-module-gitlab": "0.8.0",
+ "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.18",
+ "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.9",
+ "@backstage/plugin-catalog-backend-module-ldap": "0.12.2",
+ "@backstage/plugin-catalog-backend-module-logs": "0.1.19",
+ "@backstage/plugin-catalog-backend-module-msgraph": "0.9.0",
+ "@backstage/plugin-catalog-backend-module-openapi": "0.2.19",
+ "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.19",
+ "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.17",
+ "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.8",
+ "@backstage/plugin-catalog-common": "1.1.8",
+ "@backstage/plugin-catalog-graph": "0.5.7",
+ "@backstage/plugin-catalog-import": "0.13.10",
+ "@backstage/plugin-catalog-node": "2.0.0",
+ "@backstage/plugin-catalog-react": "2.0.0",
+ "@backstage/plugin-catalog-unprocessed-entities": "0.2.26",
+ "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.13",
+ "@backstage/plugin-config-schema": "0.1.77",
+ "@backstage/plugin-devtools": "0.1.36",
+ "@backstage/plugin-devtools-backend": "0.5.14",
+ "@backstage/plugin-devtools-common": "0.1.22",
+ "@backstage/plugin-devtools-react": "0.1.1",
+ "@backstage/plugin-events-backend": "0.5.11",
+ "@backstage/plugin-events-backend-module-aws-sqs": "0.4.19",
+ "@backstage/plugin-events-backend-module-azure": "0.2.28",
+ "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.28",
+ "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.9",
+ "@backstage/plugin-events-backend-module-gerrit": "0.2.28",
+ "@backstage/plugin-events-backend-module-github": "0.4.9",
+ "@backstage/plugin-events-backend-module-gitlab": "0.3.9",
+ "@backstage/plugin-events-backend-module-google-pubsub": "0.2.0",
+ "@backstage/plugin-events-backend-module-kafka": "0.3.1",
+ "@backstage/plugin-events-backend-test-utils": "0.1.52",
+ "@backstage/plugin-events-node": "0.4.19",
+ "@internal/plugin-todo-list": "1.0.48",
+ "@internal/plugin-todo-list-backend": "1.0.47",
+ "@internal/plugin-todo-list-common": "1.0.29",
+ "@backstage/plugin-gateway-backend": "1.1.2",
+ "@backstage/plugin-home": "0.9.2",
+ "@backstage/plugin-home-react": "0.1.35",
+ "@backstage/plugin-kubernetes": "0.12.16",
+ "@backstage/plugin-kubernetes-backend": "0.21.1",
+ "@backstage/plugin-kubernetes-cluster": "0.0.34",
+ "@backstage/plugin-kubernetes-common": "0.9.10",
+ "@backstage/plugin-kubernetes-node": "0.4.1",
+ "@backstage/plugin-kubernetes-react": "0.5.16",
+ "@backstage/plugin-mcp-actions-backend": "0.1.9",
+ "@backstage/plugin-mui-to-bui": "0.2.4",
+ "@backstage/plugin-notifications": "0.5.14",
+ "@backstage/plugin-notifications-backend": "0.6.2",
+ "@backstage/plugin-notifications-backend-module-email": "0.3.18",
+ "@backstage/plugin-notifications-backend-module-slack": "0.3.1",
+ "@backstage/plugin-notifications-common": "0.2.1",
+ "@backstage/plugin-notifications-node": "0.2.23",
+ "@backstage/plugin-org": "0.6.49",
+ "@backstage/plugin-org-react": "0.1.47",
+ "@backstage/plugin-permission-backend": "0.7.9",
+ "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.16",
+ "@backstage/plugin-permission-common": "0.9.6",
+ "@backstage/plugin-permission-node": "0.10.10",
+ "@backstage/plugin-permission-react": "0.4.40",
+ "@backstage/plugin-proxy-backend": "0.6.10",
+ "@backstage/plugin-proxy-node": "0.1.12",
+ "@backstage/plugin-scaffolder": "1.35.3",
+ "@backstage/plugin-scaffolder-backend": "3.1.3",
+ "@backstage/plugin-scaffolder-backend-module-azure": "0.2.18",
+ "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.19",
+ "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.3.3",
+ "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.18",
+ "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.18",
+ "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.20",
+ "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.18",
+ "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.18",
+ "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.18",
+ "@backstage/plugin-scaffolder-backend-module-github": "0.9.6",
+ "@backstage/plugin-scaffolder-backend-module-gitlab": "0.11.3",
+ "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.19",
+ "@backstage/plugin-scaffolder-backend-module-rails": "0.5.18",
+ "@backstage/plugin-scaffolder-backend-module-sentry": "0.3.1",
+ "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.19",
+ "@backstage/plugin-scaffolder-common": "1.7.6",
+ "@backstage/plugin-scaffolder-node": "0.12.5",
+ "@backstage/plugin-scaffolder-node-test-utils": "0.3.8",
+ "@backstage/plugin-scaffolder-react": "1.19.7",
+ "@backstage/plugin-search": "1.6.0",
+ "@backstage/plugin-search-backend": "2.0.12",
+ "@backstage/plugin-search-backend-module-catalog": "0.3.12",
+ "@backstage/plugin-search-backend-module-elasticsearch": "1.8.0",
+ "@backstage/plugin-search-backend-module-explore": "0.3.11",
+ "@backstage/plugin-search-backend-module-pg": "0.5.52",
+ "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.17",
+ "@backstage/plugin-search-backend-module-techdocs": "0.4.11",
+ "@backstage/plugin-search-backend-node": "1.4.1",
+ "@backstage/plugin-search-common": "1.2.22",
+ "@backstage/plugin-search-react": "1.10.3",
+ "@backstage/plugin-signals": "0.0.28",
+ "@backstage/plugin-signals-backend": "0.3.12",
+ "@backstage/plugin-signals-node": "0.1.28",
+ "@backstage/plugin-signals-react": "0.0.19",
+ "@backstage/plugin-techdocs": "1.17.0",
+ "@backstage/plugin-techdocs-addons-test-utils": "2.0.2",
+ "@backstage/plugin-techdocs-backend": "2.1.5",
"@backstage/plugin-techdocs-common": "0.1.1",
- "@backstage/plugin-techdocs-module-addons-contrib": "1.1.30",
- "@backstage/plugin-techdocs-node": "1.13.9",
- "@backstage/plugin-techdocs-react": "1.3.5",
- "@backstage/plugin-user-settings": "0.8.29",
- "@backstage/plugin-user-settings-backend": "0.3.8",
- "@backstage/plugin-user-settings-common": "0.0.1"
+ "@backstage/plugin-techdocs-module-addons-contrib": "1.1.33",
+ "@backstage/plugin-techdocs-node": "1.14.2",
+ "@backstage/plugin-techdocs-react": "1.3.8",
+ "@backstage/plugin-user-settings": "0.9.0",
+ "@backstage/plugin-user-settings-backend": "0.4.0",
+ "@backstage/plugin-user-settings-common": "0.1.0"
},
- "changesets": [
- "afraid-items-drum",
- "bumpy-planets-go",
- "chatty-sides-wear",
- "chilly-bikes-rule",
- "chilly-hotels-walk",
- "clean-toys-reply",
- "common-coins-stare",
- "create-app-1764689798",
- "famous-jars-lose",
- "fifty-coats-feel",
- "fine-eagles-sleep",
- "flat-pillows-rush",
- "floppy-bobcats-serve",
- "four-peaches-train",
- "fruity-rivers-arrive",
- "fruity-words-melt",
- "funny-papayas-rest",
- "fuzzy-phones-own",
- "fuzzy-trees-live",
- "gentle-trains-juggle",
- "great-files-shave",
- "happy-bottles-invite",
- "kind-hoops-double",
- "legal-cloths-spend",
- "legal-otters-punch",
- "loose-pets-slide",
- "lucky-days-hug",
- "many-planes-join",
- "metal-boxes-laugh",
- "metal-humans-lose",
- "modern-taxes-start",
- "neat-pens-clean",
- "nice-trams-shake",
- "old-parks-smell",
- "open-points-beam",
- "quiet-hats-sleep",
- "rotten-melons-sleep",
- "short-groups-knock",
- "slick-books-sleep",
- "slick-onions-wash",
- "slimy-islands-play",
- "spicy-teeth-study",
- "stale-eagles-rush",
- "tender-dancers-hunt",
- "tough-lies-grow",
- "twenty-ducks-relate"
- ]
+ "changesets": []
}
diff --git a/.changeset/pretty-days-taste.md b/.changeset/pretty-days-taste.md
new file mode 100644
index 0000000000..51c97e42ee
--- /dev/null
+++ b/.changeset/pretty-days-taste.md
@@ -0,0 +1,7 @@
+---
+'@backstage/backend-defaults': patch
+---
+
+Fixed `yarn backstage-cli config:check --strict --config app-config.yaml` config validation error by adding
+an optional `default` type discriminator to PostgreSQL connection configuration,
+allowing `config:check` to properly validate `default` connection configurations.
diff --git a/.changeset/renovate-7b76d6d.md b/.changeset/renovate-7b76d6d.md
deleted file mode 100644
index 9ccb2dd94b..0000000000
--- a/.changeset/renovate-7b76d6d.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/cli': patch
-'@backstage/plugin-scaffolder-backend': patch
----
-
-Updated dependency `esbuild` to `^0.27.0`.
diff --git a/.changeset/renovate-8b1c21e.md b/.changeset/renovate-8b1c21e.md
new file mode 100644
index 0000000000..52b1ed546a
--- /dev/null
+++ b/.changeset/renovate-8b1c21e.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Updated dependency `webpack` to `~5.105.0`.
diff --git a/.changeset/renovate-97eef4b.md b/.changeset/renovate-97eef4b.md
deleted file mode 100644
index 9b3692bc1c..0000000000
--- a/.changeset/renovate-97eef4b.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/config-loader': patch
----
-
-Updated dependency `typescript-json-schema` to `^0.67.0`.
diff --git a/.changeset/renovate-a3c2cbd.md b/.changeset/renovate-a3c2cbd.md
deleted file mode 100644
index 2fde85d39e..0000000000
--- a/.changeset/renovate-a3c2cbd.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-react': patch
----
-
-Updated dependency `use-immer` to `^0.11.0`.
diff --git a/.changeset/renovate-d7ceb06.md b/.changeset/renovate-d7ceb06.md
deleted file mode 100644
index d22f266301..0000000000
--- a/.changeset/renovate-d7ceb06.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Updated dependency `@pmmmwh/react-refresh-webpack-plugin` to `^0.6.0`.
diff --git a/.changeset/renovate-ea25c93.md b/.changeset/renovate-ea25c93.md
deleted file mode 100644
index 958e032b64..0000000000
--- a/.changeset/renovate-ea25c93.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Updated dependency `webpack` to `~5.103.0`.
diff --git a/.changeset/rotten-melons-sleep.md b/.changeset/rotten-melons-sleep.md
deleted file mode 100644
index c12e0a9669..0000000000
--- a/.changeset/rotten-melons-sleep.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'@backstage/plugin-catalog-unprocessed-entities-common': patch
-'@backstage/plugin-catalog-unprocessed-entities': patch
----
-
-Moved types, API and client to the common package, allowing both frontend and
-backend plugins to use the `CatalogUnprocessedEntitiesClient`.
-
-The following types, clients and interfaces have been deprecated and should be
-imported from the `@backstage/plugin-catalog-unprocessed-entities-common` instead:
-`CatalogUnprocessedEntitiesApi`, `CatalogUnprocessedEntitiesApiResponse`, `UnprocessedEntity`,
-`UnprocessedEntityCache`, `UnprocessedEntityError`, `CatalogUnprocessedEntitiesClient`.
-
-All those types, clients and interfaces are re-exported temporarily in the
-`@backstage/plugin-catalog-unprocessed-entities` package until cleaned up.
diff --git a/.changeset/rude-groups-shout.md b/.changeset/rude-groups-shout.md
new file mode 100644
index 0000000000..59e4a22aa9
--- /dev/null
+++ b/.changeset/rude-groups-shout.md
@@ -0,0 +1,5 @@
+---
+'@backstage/ui': patch
+---
+
+Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type.
diff --git a/.changeset/salty-camels-wash.md b/.changeset/salty-camels-wash.md
deleted file mode 100644
index 56600db540..0000000000
--- a/.changeset/salty-camels-wash.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Fixed internal usage of `yargs`.
diff --git a/.changeset/scaffolder-export-form-fields-api.md b/.changeset/scaffolder-export-form-fields-api.md
new file mode 100644
index 0000000000..02ead2d2b2
--- /dev/null
+++ b/.changeset/scaffolder-export-form-fields-api.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Added back the `formFieldsApiRef` and `ScaffolderFormFieldsApi` alpha exports that were unintentionally removed.
diff --git a/.changeset/seven-games-rest.md b/.changeset/seven-games-rest.md
deleted file mode 100644
index b498c3b138..0000000000
--- a/.changeset/seven-games-rest.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs-backend': patch
----
-
-Corrected `ErrorCallback` type to work with Node 22 types
diff --git a/.changeset/short-groups-knock.md b/.changeset/short-groups-knock.md
deleted file mode 100644
index 0b622a71ed..0000000000
--- a/.changeset/short-groups-knock.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/cli-common': patch
-'@backstage/create-app': patch
-'@backstage/cli': patch
----
-
-Add proxy configuration from env-vars to create-app tasks
diff --git a/.changeset/sixty-pianos-begin.md b/.changeset/sixty-pianos-begin.md
new file mode 100644
index 0000000000..98db19c21d
--- /dev/null
+++ b/.changeset/sixty-pianos-begin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/repo-tools': patch
+---
+
+Updated `@microsoft/api-extractor` to `7.57.3` and added tests for `getTsDocConfig`
diff --git a/.changeset/slick-books-sleep.md b/.changeset/slick-books-sleep.md
deleted file mode 100644
index 7aa3be5b03..0000000000
--- a/.changeset/slick-books-sleep.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/theme': patch
----
-
-Added a `themeName` prop to `UnifiedThemeProvider`, enabling Backstage UI `data-theme-name` CSS attribute to be set based on active theme.
diff --git a/.changeset/slick-onions-wash.md b/.changeset/slick-onions-wash.md
deleted file mode 100644
index 4627d02e81..0000000000
--- a/.changeset/slick-onions-wash.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/core-components': patch
----
-
-Add i18n support for LogViewer search control
diff --git a/.changeset/slimy-islands-play.md b/.changeset/slimy-islands-play.md
deleted file mode 100644
index 497d86761b..0000000000
--- a/.changeset/slimy-islands-play.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode.
-
-Affected components: Table, TableHeader, Row, Column
diff --git a/.changeset/slimy-mugs-taste.md b/.changeset/slimy-mugs-taste.md
deleted file mode 100644
index 5773db6853..0000000000
--- a/.changeset/slimy-mugs-taste.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-test-utils': patch
----
-
-Fix PostgreSQL 18 `TestDatabases` by pinning the data directory
diff --git a/.changeset/sour-bats-press.md b/.changeset/sour-bats-press.md
deleted file mode 100644
index bfcf3985e6..0000000000
--- a/.changeset/sour-bats-press.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-auth-node': patch
----
-
-fix flawed cookie removal logic with chunked tokens
diff --git a/.changeset/spicy-teeth-study.md b/.changeset/spicy-teeth-study.md
deleted file mode 100644
index 03cc8a5440..0000000000
--- a/.changeset/spicy-teeth-study.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Fixed `useTable` hook to prioritize `providedRowCount` over data length for accurate row count in server-side pagination scenarios.
diff --git a/.changeset/stable-translation-plugin-app.md b/.changeset/stable-translation-plugin-app.md
new file mode 100644
index 0000000000..6e57cfeeb8
--- /dev/null
+++ b/.changeset/stable-translation-plugin-app.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app': patch
+---
+
+Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior.
diff --git a/.changeset/stable-translation-test-utils.md b/.changeset/stable-translation-test-utils.md
new file mode 100644
index 0000000000..3d624a5dc8
--- /dev/null
+++ b/.changeset/stable-translation-test-utils.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-test-utils': patch
+---
+
+Switched `MockTranslationApi` and related test utility imports from `@backstage/core-plugin-api/alpha` to the stable `@backstage/frontend-plugin-api` export. The `TranslationApi` type in the API report is now sourced from a single package. This has no effect on runtime behavior.
diff --git a/.changeset/stale-eagles-rush.md b/.changeset/stale-eagles-rush.md
deleted file mode 100644
index fe640ccd44..0000000000
--- a/.changeset/stale-eagles-rush.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/frontend-plugin-api': patch
-'@backstage/core-plugin-api': patch
----
-
-Reversed the relationship between the old `@backstage/core-plugin-api` and the new `@backstage/frontend-plugin-api`. Previously, the a lot of API definitions and utilities where defined in the old and re-exported from the old, but this change flips that around so that they now reside in the new package and are re-exported from the old. The external API of both packages remain the same, but this is a step towards being able to add further compatibility with the new frontend system built into the old.
diff --git a/.changeset/stupid-cases-fold.md b/.changeset/stupid-cases-fold.md
deleted file mode 100644
index 5cf0f0f72a..0000000000
--- a/.changeset/stupid-cases-fold.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Switched ECMAScript version to ES2023.
diff --git a/.changeset/stupid-pans-hope.md b/.changeset/stupid-pans-hope.md
new file mode 100644
index 0000000000..4fd8c5914a
--- /dev/null
+++ b/.changeset/stupid-pans-hope.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-client': patch
+---
+
+Minor update to catalog client docs
diff --git a/.changeset/swift-flowers-grin.md b/.changeset/swift-flowers-grin.md
new file mode 100644
index 0000000000..33466615cd
--- /dev/null
+++ b/.changeset/swift-flowers-grin.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-backend': patch
+---
+
+Changed the `search` table foreign key to point to `final_entities` instead of `refresh_state`
diff --git a/.changeset/tall-ideas-lead.md b/.changeset/tall-ideas-lead.md
deleted file mode 100644
index 60bef903d8..0000000000
--- a/.changeset/tall-ideas-lead.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch
----
-
-Improve error message when provided target branch is missing
diff --git a/.changeset/tame-mirrors-sit.md b/.changeset/tame-mirrors-sit.md
deleted file mode 100644
index f317185ea2..0000000000
--- a/.changeset/tame-mirrors-sit.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/backend-dev-utils': patch
----
-
-Internal update for Node.js v24 support.
diff --git a/.changeset/tender-dancers-hunt.md b/.changeset/tender-dancers-hunt.md
deleted file mode 100644
index e20ec43ae0..0000000000
--- a/.changeset/tender-dancers-hunt.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/cli': patch
----
-
-Updated to use new utilities from `@backstage/cli-common`.
diff --git a/.changeset/tired-bushes-write.md b/.changeset/tired-bushes-write.md
new file mode 100644
index 0000000000..af49530ef4
--- /dev/null
+++ b/.changeset/tired-bushes-write.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Update migrations to be reversible
diff --git a/.changeset/tired-dogs-remain.md b/.changeset/tired-dogs-remain.md
deleted file mode 100644
index 06c193068b..0000000000
--- a/.changeset/tired-dogs-remain.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-'@backstage/plugin-scaffolder-backend-module-rails': patch
-'@backstage/plugin-kubernetes-cluster': patch
-'@techdocs/cli': patch
-'@backstage/cli-common': patch
-'@backstage/create-app': patch
-'@backstage/repo-tools': patch
-'@backstage/codemods': patch
-'@backstage/cli': patch
----
-
-Bumped dev dependencies `@types/node`
diff --git a/.changeset/tough-lies-grow.md b/.changeset/tough-lies-grow.md
deleted file mode 100644
index dab952df65..0000000000
--- a/.changeset/tough-lies-grow.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@backstage/plugin-techdocs-backend': patch
-'@backstage/plugin-techdocs': patch
----
-
-The `techdocs` config is now marked as optional.
diff --git a/.changeset/twenty-ducks-relate.md b/.changeset/twenty-ducks-relate.md
deleted file mode 100644
index 359eda66f3..0000000000
--- a/.changeset/twenty-ducks-relate.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@backstage/ui': patch
----
-
-Fixed Table column sorting indicator to show up arrow when no sort is active, correctly indicating that clicking will sort ascending.
-
-Affected components: Column
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
new file mode 100644
index 0000000000..170a4eb868
--- /dev/null
+++ b/.claude/CLAUDE.md
@@ -0,0 +1 @@
+Follow the instructions at /.github/copilot-instructions.md
diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc
new file mode 100644
index 0000000000..3a81a0cb5f
--- /dev/null
+++ b/.cursor/rules/general.mdc
@@ -0,0 +1,7 @@
+---
+description: General project guidelines for Backstage development
+globs:
+alwaysApply: true
+---
+
+Follow the instructions at /.github/copilot-instructions.md
diff --git a/contrib/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
similarity index 78%
rename from contrib/.devcontainer/Dockerfile
rename to .devcontainer/Dockerfile
index 6407aa26dd..9fcd4ba572 100644
--- a/contrib/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/devcontainers/typescript-node:20
+FROM mcr.microsoft.com/devcontainers/typescript-node:22
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install chromium \
diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env
new file mode 100644
index 0000000000..65350b61d4
--- /dev/null
+++ b/.devcontainer/devcontainer.env
@@ -0,0 +1 @@
+NODE_OPTIONS=--no-node-snapshot
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..f4369920a5
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,61 @@
+{
+ "name": "Backstage",
+ "forwardPorts": [3000, 7007],
+ "build": { "dockerfile": "Dockerfile" },
+ "features": {
+ "ghcr.io/devcontainers/features/common-utils:2.5.6": {},
+ "ghcr.io/devcontainers-extra/features/mkdocs:2": {}
+ },
+ "postCreateCommand": "bash .devcontainer/setup.sh",
+ "hostRequirements": {
+ "cpus": 2,
+ "memory": "4gb",
+ "storage": "32gb"
+ },
+ "runArgs": [
+ "--env-file",
+ ".devcontainer/devcontainer.env",
+ "--sysctl",
+ "net.ipv6.conf.all.disable_ipv6=1"
+ ],
+ "portsAttributes": {
+ "3000": {
+ "label": "Frontend port",
+ "onAutoForward": "silent",
+ "requireLocalPort": true
+ },
+ "7007": {
+ "label": "Backend port",
+ "onAutoForward": "silent",
+ "requireLocalPort": true
+ },
+ "9464": {
+ "onAutoForward": "silent"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "esbenp.prettier-vscode",
+ "dbaeumer.vscode-eslint",
+ "Intility.vscode-backstage"
+ ],
+ "settings": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "eslint.validate": [
+ "javascript",
+ "javascriptreact",
+ "typescript",
+ "typescriptreact"
+ ],
+ "files.eol": "\n",
+ "prettier.endOfLine": "lf",
+ "editor.codeActionsOnSave": {
+ "source.fixAll": "explicit",
+ "source.sortMembers": "explicit"
+ }
+ }
+ }
+ }
+}
diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh
new file mode 100755
index 0000000000..7615e2b3cc
--- /dev/null
+++ b/.devcontainer/setup.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+echo "Installing Backstage dependencies..."
+
+yarn install
+export VIRTUAL_ENV=$HOME/venv
+python3 -m venv $VIRTUAL_ENV
+export PATH="$VIRTUAL_ENV/bin:$PATH"
+python3 -m pip install mkdocs-techdocs-core
+
+echo ""
+echo "╔════════════════════════════════════════════════════════╗ "
+echo "║ 🚀 Setup Complete! Ready to launch Backstage! ║ "
+echo "╠════════════════════════════════════════════════════════╣ "
+echo "║ ║ "
+echo "║ Open a new terminal and run: ║ "
+echo "║ ║ "
+echo "║ yarn start ║ "
+echo "║ ║ "
+echo "║ Then access Backstage at: ║ "
+echo "║ ║ "
+echo "║ http://localhost:3000 ║ "
+echo "║ ║ "
+echo "║ You might need to refresh the page once backend ║ "
+echo "║ is ready. ║ "
+echo "║ ║ "
+echo "║ Happy coding! 🎉 ║ "
+echo "╚════════════════════════════════════════════════════════╝ "
+echo ""
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index 54f9e69576..4f7c5afa5e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-var path = require('path');
+var path = require('node:path');
module.exports = {
root: true,
- plugins: ['@spotify', 'notice', 'react', 'testing-library', '@backstage'],
+ plugins: ['@spotify', 'notice', 'react', 'testing-library', '@backstage', 'node-import'],
rules: {
+ 'node-import/prefer-node-protocol': 1,
'@backstage/no-mixed-plugin-imports': [
'error',
{
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 40c09a9362..725c6c676b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,13 +4,13 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
-* @backstage/maintainers @backstage/reviewers
+* @backstage/maintainers
yarn.lock @backstage/maintainers @backstage-service
*/yarn.lock @backstage/maintainers @backstage-service
/.changeset @backstage/operations-maintainers
/.changeset/*.md
/.github @backstage/operations-maintainers
-/.github/vale @backstage/documentation-maintainers
+/.github/vale @backstage/maintainers @backstage/documentation-maintainers
/.patches @backstage/operations-maintainers
/beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers
/docs @backstage/maintainers @backstage/documentation-maintainers
@@ -25,11 +25,11 @@ yarn.lock @backstage/maintainers @backst
/docs/plugins/integrating-search-into-plugins.md @backstage/search-maintainers
/docs/releases @backstage/operations-maintainers
/docs/tooling @backstage/tooling-maintainers
-/microsite @backstage/documentation-maintainers
+/microsite @backstage/maintainers @backstage/documentation-maintainers
/microsite/data/plugins @backstage/maintainers
/microsite/static @backstage/maintainers @backstage/documentation-maintainers
/packages @backstage/framework-maintainers
-/packages/backend-openapi-utils @backstage/maintainers @backstage/reviewers @backstage/openapi-tooling-maintainers
+/packages/backend-openapi-utils @backstage/maintainers @backstage/openapi-tooling-maintainers
/packages/catalog-client @backstage/catalog-maintainers
/packages/catalog-model @backstage/catalog-maintainers
/packages/cli @backstage/tooling-maintainers
@@ -47,21 +47,21 @@ yarn.lock @backstage/maintainers @backst
/plugins/app-* @backstage/framework-maintainers
/plugins/auth @backstage/auth-maintainers
/plugins/auth-* @backstage/auth-maintainers
-/plugins/api-docs @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers
-/plugins/bitbucket-cloud-common @backstage/maintainers @backstage/reviewers @pjungermann
-/plugins/catalog @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers
-/plugins/catalog-* @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers
-/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann
-/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann
-/plugins/catalog-backend-module-backstage-openapi @backstage/maintainers @backstage/reviewers @backstage/openapi-tooling-maintainers
-/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers
-/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @pjungermann
-/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers
-/plugins/catalog-graph @backstage/maintainers @backstage/reviewers @backstage/catalog-maintainers @backstage/sda-se-reviewers
-/plugins/devtools @backstage/maintainers @backstage/reviewers @awanlin
-/plugins/devtools-backend @backstage/maintainers @backstage/reviewers @awanlin
-/plugins/devtools-common @backstage/maintainers @backstage/reviewers @awanlin
-/plugins/events-* @backstage/maintainers @backstage/reviewers @backstage/events-maintainers
+/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers
+/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann
+/plugins/catalog @backstage/maintainers @backstage/catalog-maintainers
+/plugins/catalog-* @backstage/maintainers @backstage/catalog-maintainers
+/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-maintainers @pjungermann
+/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-maintainers @pjungermann
+/plugins/catalog-backend-module-backstage-openapi @backstage/maintainers @backstage/openapi-tooling-maintainers
+/plugins/catalog-backend-module-gitea @backstage/maintainers @backstage/catalog-maintainers
+/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-maintainers @pjungermann
+/plugins/catalog-backend-module-puppetdb @backstage/maintainers @backstage/catalog-maintainers
+/plugins/catalog-graph @backstage/maintainers @backstage/catalog-maintainers @backstage/sda-se-reviewers
+/plugins/devtools @backstage/maintainers @awanlin
+/plugins/devtools-backend @backstage/maintainers @awanlin
+/plugins/devtools-common @backstage/maintainers @awanlin
+/plugins/events-* @backstage/maintainers @backstage/events-maintainers
/plugins/home @backstage/home-maintainers
/plugins/home-* @backstage/home-maintainers
/plugins/kubernetes @backstage/kubernetes-maintainers
@@ -70,17 +70,17 @@ yarn.lock @backstage/maintainers @backst
/plugins/notifications-* @backstage/maintainers @backstage/notifications-maintainers
/plugins/scaffolder-backend-module-notifications @backstage/maintainers @backstage/notifications-maintainers
/plugins/permission-* @backstage/permission-maintainers
-/plugins/scaffolder @backstage/maintainers @backstage/reviewers @backstage/scaffolder-maintainers
-/plugins/scaffolder-* @backstage/maintainers @backstage/reviewers @backstage/scaffolder-maintainers
+/plugins/scaffolder @backstage/maintainers @backstage/scaffolder-maintainers
+/plugins/scaffolder-* @backstage/maintainers @backstage/scaffolder-maintainers
/plugins/search @backstage/search-maintainers
/plugins/search-* @backstage/search-maintainers
/plugins/signals @backstage/maintainers @backstage/notifications-maintainers
/plugins/signals-* @backstage/maintainers @backstage/notifications-maintainers
/plugins/techdocs @backstage/techdocs-maintainers
/plugins/techdocs-* @backstage/techdocs-maintainers
-/plugins/user-settings @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers
-/plugins/user-settings-backend @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers
-/plugins/user-settings-common @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers
+/plugins/user-settings @backstage/maintainers @backstage/sda-se-reviewers
+/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers
+/plugins/user-settings-common @backstage/maintainers @backstage/sda-se-reviewers
/scripts @backstage/operations-maintainers
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 98073c5128..2157025b7b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,11 +1,23 @@
-This is a TypeScript project structured as a monorepo using Yarn workspaces.
+Backstage is an open platform for building developer portals. This is a TypeScript monorepo using Yarn workspaces.
+
+## Key Directories
+
+- `/packages`: Core framework packages (prefixed `@backstage/`)
+- `/plugins`: Plugin packages (prefixed `@backstage/plugin-*`)
+- `/packages/app` and `/packages/backend`: Example app for local development
+- `/packages/app`: Main example app using the new frontend system
+- `/packages/app-legacy`: Example app using the old frontend system
+- `/docs`: Documentation files
+
+Packages prefixed with `core-` (e.g., `@backstage/core-plugin-api`) are part of the old frontend system. Packages prefixed with `frontend-` (e.g., `@backstage/frontend-plugin-api`) are part of the new frontend system (NFS). Packages prefixed with `backend-` (e.g., `@backstage/backend-plugin-api`) are part of the backend system.
## Code Standards
The following files contain guidelines for the project:
+- `/CONTRIBUTING.md`: comprehensive contribution guidelines.
- `/STYLE.md`: guidelines for code style.
-- `/REVIEWING.md`: guidelines for pull requests.
+- `/REVIEWING.md`: guidelines for pull requests and writing changesets.
- `/SECURITY.md`: guidelines for security.
- `/docs/architecture-decisions/`: contains the architecture decisions for the project.
@@ -19,13 +31,27 @@ Before any of these commands can be run, you need to run `yarn install` in the p
- Code formatting: Use `yarn prettier --write ` to format code.
- Lint: Use `yarn lint --fix` in the project root to run the linter.
- API reports: Before submitting a pull request with changes to any package in the workspace, run `yarn build:api-reports` in the project root to generate API reports for all packages.
+- Dev server: Use `yarn start` to run the example app locally (frontend on :3000, backend on :7007).
+- Create: Use `yarn new` to scaffold new plugins, packages, or modules.
You MUST NOT create a release by running `yarn changesets version` or `yarn release` as part of any changes. Releases are created by separate workflows.
-All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. See the guidelines in `/REVIEWING.md` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages that below version `1.0.0`, or a `major` version bump for packages that are at version `1.0.0` or higher.
+All changes that affect the published version of packages in the `/packages` and `/plugins` directories must be accompanied by a changeset. Only non-private packages require changesets. See the guidelines in `/CONTRIBUTING.md#creating-changesets` for information on how to write good changesets. Changesets are stored in the `/.changeset` directory. Breaking changes must be accompanied by a `minor` version bump for packages below version `1.0.0`, or a `major` version bump for packages at version `1.0.0` or higher.
Never update ESLint, Prettier, or TypeScript configuration files unless specifically requested.
## Repository Structure
See `/docs/contribute/project-structure.md` for a detailed description of the repository structure.
+
+## Pull Request Reviews
+
+When reviewing a pull request you MUST calculate a priority score between 0 and 100, where a higher score indicates higher priority. You MUST include the score as an HTML comment at the very beginning of the "PR Overview" section, using the following format: ``.
+
+The score MUST be calculated as a sum of the following:
+
+- **60% Guideline Adherence**: How well does the pull request follow the project guidelines in `/STYLE.md`, `/REVIEWING.md`, `/CONTRIBUTING.md`, and `/SECURITY.md`? A pull request that fully adheres to all guidelines scores 60 points, while one that violates multiple guidelines scores closer to 0.
+
+- **40% Complexity**: How straightforward is the pull request? A simple bug fix or documentation update scores the full 40 points. A complex API change, new feature, or architectural modification scores closer to 0.
+
+Higher priority pull requests (higher scores) are those that are both well-crafted according to project standards AND easy to review and merge quickly.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..1fb8ca3ca3
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,15 @@
+version: 2
+updates:
+ - package-ecosystem: 'npm'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
+ ignore:
+ # These need to stay pinned to match the React/Node versions we support
+ - dependency-name: '@types/react'
+ - dependency-name: '@types/node'
+ # Ignored due to licensing issues. See #10992
+ - dependency-name: '@elastic/elasticsearch'
+ - dependency-name: 'event-source-polyfill'
+ # Disable version updates since we use Renovate for that
+ open-pull-requests-limit: 0
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index da3cf3e8fb..4e5ca932c0 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -18,6 +18,9 @@
// ignored due to licensing issues. See #10992
'@elastic/elasticsearch',
'event-source-polyfill',
+ // These need to stay pinned to match the React/Node versions we support
+ '@types/react',
+ '@types/node',
],
packageRules: [
{
diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt
index 6697fad858..f6f37b17cf 100644
--- a/.github/vale/config/vocabularies/Backstage/accept.txt
+++ b/.github/vale/config/vocabularies/Backstage/accept.txt
@@ -53,7 +53,6 @@ Chai
Chainguard
changeset
changesets
-Changesets
chanwit
Chanwit
CI/CD
@@ -68,12 +67,10 @@ Cobertura
codeblocks
Codecov
codehilite
-Codehilite
codemod
codemods
codeowners
codescene
-CodeScene
composability
composable
config
@@ -99,6 +96,7 @@ dataflow
dataloader
dayjs
debounce
+debounced
debounces
debuggability
declaratively
@@ -124,7 +122,6 @@ devs
dialogs
disambiguator
discoverability
-Discoverability
dls
Dockerfile
dockerfiles
@@ -135,7 +132,6 @@ Dominik
DOMPurify
don'ts
dynatrace
-Dynatrace
dyno
ecco
elasticsearch
@@ -156,12 +152,10 @@ faqs
featureful
Figma
firehydrant
-FireHydrant
Firekube
Firestore
Fiverr
flightcontrol
-Flightcontrol
Francesco
Frontside
Gaurav
@@ -187,7 +181,6 @@ haproxy
hardcoded
hardcoding
harness
-Harness
Helidon
Henneke
Heroku
@@ -268,6 +261,7 @@ maintainer's
maintainership
makefile
Matomo
+matchers
md
memcache
memoization
@@ -279,7 +273,6 @@ microsite
microtasks
middleware
minikube
-Minikube
Minio
misattributed
misconfiguration
@@ -288,7 +281,6 @@ mkdocs
Mkdocs
modularization
monorepo
-Monorepo
monorepos
monospace
morgan
@@ -325,15 +317,14 @@ Olausson
Oldsberg
onboarded
onboarding
-Onboarding
onelogin
openapi
OpenSearch
OpenShift
openssl
+opentelemetry
orgs
overridable
-Overridable
padding
paddings
pagerduty
@@ -343,12 +334,13 @@ parallelization
param
params
parseable
+passthrough
+passwordless
Patrik
pattison
Peloton
PEP
performant
-Performant
periskop
Periskop
permissioned
@@ -431,6 +423,7 @@ SCM
SCMs
scrollable
scrollbar
+scrollbars
sdks
seb
semlas
@@ -522,11 +515,15 @@ transpile
transpiled
transpiler
transpilers
+trixie
truthy
tsconfig
+TTLs
+Turbopack
TSDoc
typeahead
ui
+unassign
unbreak
Unconference
undici
@@ -536,6 +533,7 @@ unmount
unregister
unregistering
unregistration
+unreviewed
unstarred
untemplated
untracked
@@ -551,6 +549,7 @@ validator
validators
Valkey
varchar
+viewport
vite
VMware
Vodafone
@@ -576,3 +575,4 @@ zoomable
zsh
resizable
enums
+LLMs
diff --git a/.github/workflows/api-breaking-changes-comment.yml b/.github/workflows/api-breaking-changes-comment.yml
index 92823ad2be..aca727743a 100644
--- a/.github/workflows/api-breaking-changes-comment.yml
+++ b/.github/workflows/api-breaking-changes-comment.yml
@@ -23,7 +23,7 @@ jobs:
comment-cache-key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
disable-sudo: true
egress-policy: block
@@ -32,7 +32,7 @@ jobs:
- name: 'Download artifacts'
# Fetch output (zip archive) from the workflow run that triggered this workflow.
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -74,7 +74,7 @@ jobs:
- name: Cache Comment
if: ${{ steps.event.outputs.ACTION != 'closed' }}
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: comment.md
key: ${{ steps.hash.outputs.COMMENT_FILE_HASH }}
@@ -99,11 +99,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Fetch cached Manifests File
id: cache
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: comment.md
key: ${{ needs.setup.outputs.comment-cache-key }}
@@ -115,13 +115,13 @@ jobs:
with:
issue-number: ${{ needs.setup.outputs.pr-number }}
comment-author: 'github-actions[bot]'
- body-includes: API changes
+ body-includes: api-breaking-changes-comment
direction: last
- name: Create or Update Comment with API Changes
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
- issue-number: ${{ github.event.pull_request.number }}
+ issue-number: ${{ needs.setup.outputs.pr-number }}
body-path: comment.md
edit-mode: replace
diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml
index 934add0716..a96556bb45 100644
--- a/.github/workflows/api-breaking-changes.yml
+++ b/.github/workflows/api-breaking-changes.yml
@@ -14,11 +14,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch the commit that's merged into the base rather than the target ref
# This will let us diff only the contents of the PR, without fetching more history
@@ -27,19 +27,20 @@ jobs:
run: git fetch --depth 1 origin ${{ github.base_ref }}
- name: setup-node
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: linux-v22
- name: breaking changes check
run: |
yarn backstage-repo-tools repo schema openapi diff --since origin/${{ github.base_ref }} > comment.md
+ echo "" >> comment.md
- name: clone artifacts to current directory
run: |
diff --git a/.github/workflows/automate_area-labels.yml b/.github/workflows/automate_area-labels.yml
index a4ef0d2c29..cdf4a28b55 100644
--- a/.github/workflows/automate_area-labels.yml
+++ b/.github/workflows/automate_area-labels.yml
@@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
+ - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
sync-labels: true
diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml
index 257db3929a..adfa7d29ec 100644
--- a/.github/workflows/automate_changeset_feedback.yml
+++ b/.github/workflows/automate_changeset_feedback.yml
@@ -23,18 +23,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch the commit that's merged into the base rather than the target ref
# This will let us diff only the contents of the PR, without fetching more history
ref: 'refs/pull/${{ github.event.pull_request.number }}/merge'
- name: fetch base
run: git fetch --depth 1 origin ${{ github.base_ref }}
- - uses: backstage/actions/changeset-feedback@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ - uses: backstage/actions/changeset-feedback@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
name: Generate feedback
with:
diff-ref: 'origin/master'
diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml
index 138ba97b1c..c1ecde9194 100644
--- a/.github/workflows/automate_merge_message.yml
+++ b/.github/workflows/automate_merge_message.yml
@@ -24,11 +24,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.pull_request.merge_commit_sha }}'
@@ -44,7 +44,7 @@ jobs:
node generate.js ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} > message.txt
- name: Post Message
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
ISSUE_NUMBER: ${{ github.event.pull_request.number }}
with:
diff --git a/.github/workflows/automate_stale.yml b/.github/workflows/automate_stale.yml
index cdaa01fea1..34ab03f207 100644
--- a/.github/workflows/automate_stale.yml
+++ b/.github/workflows/automate_stale.yml
@@ -15,12 +15,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Stale check - base
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
+ uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
@@ -42,7 +42,7 @@ jobs:
operations-per-run: 100
- name: Stale check - bugs without repro
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
+ uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-issue-message: >
This bug report has been automatically marked as stale because it has not had
diff --git a/.github/workflows/ci-noop.yml b/.github/workflows/ci-noop.yml
index 12aafe18ee..d9851778c6 100644
--- a/.github/workflows/ci-noop.yml
+++ b/.github/workflows/ci-noop.yml
@@ -40,7 +40,7 @@ jobs:
name: Test ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7552898bd0..9ac6120d28 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,20 +28,32 @@ jobs:
name: Install ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ # Store the PR number for the Sync Pull Requests workflow
+ - name: Save PR context
+ if: matrix.node-version == '22.x'
+ run: |
+ mkdir -p ./context
+ echo "${{ github.event.pull_request.number }}" > ./context/pr-number
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ if: matrix.node-version == '22.x'
+ with:
+ name: pr-context
+ path: context/
+
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
@@ -64,20 +76,20 @@ jobs:
name: Verify ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
@@ -121,12 +133,19 @@ jobs:
- name: verify doc links
run: node scripts/verify-links.js
+ - name: verify plugin directory
+ run: node scripts/verify-plugin-directory.js
- name: build all packages
run: yarn backstage-cli repo build --all
- # For now BUI has a custom build script and needs to be built separately
- - name: build BUI
- run: yarn --cwd packages/ui build
+ # Build docs-ui to ensure documentation site builds successfully
+ - name: install docs-ui dependencies
+ working-directory: docs-ui
+ run: yarn install
+
+ - name: build docs-ui
+ working-directory: docs-ui
+ run: yarn build
- name: verify type dependencies
run: yarn lint:type-deps
@@ -203,14 +222,14 @@ jobs:
env:
CI: true
- NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules
+ NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules
INTEGRATION_TEST_GITHUB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITHUB_TOKEN }}
INTEGRATION_TEST_GITLAB_TOKEN: ${{ secrets.INTEGRATION_TEST_GITLAB_TOKEN }}
INTEGRATION_TEST_BITBUCKET_TOKEN: ${{ secrets.INTEGRATION_TEST_BITBUCKET_TOKEN }}
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: fetch master branch
run: git fetch origin master
@@ -220,13 +239,13 @@ jobs:
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
@@ -236,7 +255,7 @@ jobs:
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
- name: restore backstage-cli cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .cache/backstage-cli
key: ${{ runner.os }}-v${{ matrix.node-version }}-backstage-cli-${{ github.run_id }}
@@ -260,7 +279,7 @@ jobs:
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
- name: save backstage-cli cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
if: always()
with:
path: .cache/backstage-cli
diff --git a/.github/workflows/cleanup_patch-files.yml b/.github/workflows/cleanup_patch-files.yml
deleted file mode 100644
index 5fdb5d09df..0000000000
--- a/.github/workflows/cleanup_patch-files.yml
+++ /dev/null
@@ -1,160 +0,0 @@
-name: Cleanup Patch Files
-on:
- push:
- branches:
- - 'patch/v*'
-
-concurrency:
- group: cleanup-patch-files
- cancel-in-progress: false
-
-jobs:
- cleanup:
- name: Cleanup Patch Files
- runs-on: ubuntu-latest
- if: github.repository == 'backstage/backstage'
- permissions:
- contents: write
- steps:
- - name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
- with:
- egress-policy: audit
-
- - name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- with:
- fetch-depth: 0
- token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
-
- - name: Configure Git
- run: |
- git config --global user.email noreply@backstage.io
- git config --global user.name 'Github patch cleanup workflow'
-
- - name: Extract PR numbers from commit messages
- id: extract-pr-numbers
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
- with:
- github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
- script: |
- const { execSync } = require('child_process');
-
- // Extract PR numbers from commits pushed to the patch branch
- // Commits have messages in format: "Patch from PR #123"
- const beforeSha = context.payload.before;
- const afterSha = context.payload.after;
-
- let commitRange;
- if (beforeSha && beforeSha !== '0000000000000000000000000000000000000000') {
- commitRange = `${beforeSha}..${afterSha}`;
- } else {
- // First push to branch, check recent commits
- commitRange = `-n 20 ${afterSha}`;
- }
-
- // Get commit messages from git log
- let commitMessages;
- try {
- commitMessages = execSync(
- `git log --format=%B ${commitRange}`,
- { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }
- );
- } catch (error) {
- console.log('No commits found in range');
- commitMessages = '';
- }
-
- // Extract PR numbers from commit messages matching "Patch from PR #123"
- const prNumbers = new Set();
- const prPattern = /Patch from PR #(\d+)/g;
- let match;
-
- while ((match = prPattern.exec(commitMessages)) !== null) {
- prNumbers.add(parseInt(match[1], 10));
- }
-
- // Convert to sorted array
- const prNumbersArray = Array.from(prNumbers).sort((a, b) => a - b);
-
- if (prNumbersArray.length > 0) {
- console.log(`Found PR numbers: ${prNumbersArray.join(', ')}`);
- core.setOutput('pr_numbers', JSON.stringify(prNumbersArray));
- core.setOutput('has_pr_numbers', 'true');
- } else {
- console.log('No PR numbers found in commit messages');
- core.setOutput('pr_numbers', '[]');
- core.setOutput('has_pr_numbers', 'false');
- }
-
- - name: Checkout master
- if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true'
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- with:
- ref: master
- token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
-
- - name: Delete patch files
- if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true'
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
- env:
- PR_NUMBERS: ${{ steps.extract-pr-numbers.outputs.pr_numbers }}
- REF_NAME: ${{ github.ref_name }}
- with:
- github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
- script: |
- const { execSync } = require('child_process');
- const fs = require('fs');
- const path = require('path');
-
- const prNumbers = JSON.parse(process.env.PR_NUMBERS);
- const patchesDir = path.join(process.cwd(), '.patches');
-
- if (!fs.existsSync(patchesDir)) {
- console.log('No .patches directory found, nothing to clean up');
- return;
- }
-
- const deletedFiles = [];
-
- // Delete patch files for each PR number
- for (const prNum of prNumbers) {
- const patchFile = path.join(patchesDir, `pr-${prNum}.txt`);
- if (fs.existsSync(patchFile)) {
- fs.unlinkSync(patchFile);
- deletedFiles.push(patchFile);
- console.log(`Deleted ${patchFile}`);
- } else {
- console.log(`Patch file ${patchFile} not found, skipping`);
- }
- }
-
- if (deletedFiles.length === 0) {
- console.log('No patch files to delete');
- return;
- }
-
- - name: Commit and push changes
- if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true'
- run: |
- # Check if there are any changes to commit
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
-
- git add .patches/
- git commit -m "chore: remove applied patch files after patch release merge
-
- Removed patch files for PRs included in patch release to ${{ github.ref_name }}"
-
- git push origin master
-
- - name: Summary
- if: steps.extract-pr-numbers.outputs.has_pr_numbers == 'true'
- run: |
- echo "## Patch Files Cleanup Summary" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Patch release to ${{ github.ref_name }} has been merged." >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Patch files have been removed from the master branch." >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml
index b695c88c90..73c5202fb3 100644
--- a/.github/workflows/cron.yml
+++ b/.github/workflows/cron.yml
@@ -10,11 +10,11 @@ jobs:
timeout-minutes: 10
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: backstage/actions/cron@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ - uses: backstage/actions/cron@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml
index 0cf49b4b8a..ad7029af14 100644
--- a/.github/workflows/deploy_docker-image.yml
+++ b/.github/workflows/deploy_docker-image.yml
@@ -20,24 +20,24 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: backstage
ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }}
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
@@ -52,17 +52,17 @@ jobs:
working-directory: ./example-app
- name: Login to GitHub Container Registry
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build and push
- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: './example-app'
file: ./example-app/packages/backend/Dockerfile
diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml
index 3bd4e2d124..1de546aaa5 100644
--- a/.github/workflows/deploy_microsite.yml
+++ b/.github/workflows/deploy_microsite.yml
@@ -23,12 +23,12 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: find latest release
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: find-release
with:
script: |
@@ -54,55 +54,44 @@ jobs:
result-encoding: string
- name: checkout latest release
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/tags/${{ steps.find-release.outputs.result }}
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- - name: build API reference
- run: yarn build:api-docs
-
- - name: upload API reference
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- with:
- name: stable-reference
- path: docs/reference/
- if-no-files-found: error
- retention-days: 1
-
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
- name: restore package-docs cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-
- - name: build API reference (beta)
+ - name: build API reference
run: yarn backstage-repo-tools package-docs
- - name: upload API reference (beta)
+ - name: upload API reference
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
- name: stable-reference-beta
+ name: stable-reference
path: type-docs/
if-no-files-found: error
retention-days: 1
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
- name: save package-docs cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
if: always()
with:
path: .cache/package-docs
@@ -135,58 +124,47 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: checkout master
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- - name: build API reference
- run: yarn build:api-docs
-
- - name: upload API reference
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- with:
- name: next-reference
- path: docs/reference/
- if-no-files-found: error
- retention-days: 1
-
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
- name: restore package-docs cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-
- - name: build API reference (beta)
+ - name: build API reference
run: yarn backstage-repo-tools package-docs
- - name: upload API reference (beta)
+ - name: upload API reference
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
- name: next-reference-beta
+ name: next-reference
path: type-docs/
if-no-files-found: error
retention-days: 1
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
- name: save package-docs cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
if: always()
with:
path: .cache/package-docs
@@ -240,19 +218,19 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
# Stable docs
- name: checkout latest release
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/tags/${{ needs.stable.outputs.release }}
@@ -260,23 +238,31 @@ jobs:
run: yarn install --immutable
working-directory: microsite
- - name: download stable reference
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- with:
- name: stable-reference
- path: docs/reference
-
- name: download stable OpenAPI API docs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: stable-openapi-docs
path: docs
- - name: grab lastest releases docs
+ - name: grab latest releases docs
run: |
git fetch origin master --depth 1
git checkout FETCH_HEAD -- docs/releases
+ - name: create missing generated file
+ run: |
+ mkdir -p docs/reference
+ cat > docs/reference/index.md <> $GITHUB_ENV
- name: Update GitHub Issue
- uses: actions/github-script@v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/pr-review-comment-trigger.yaml b/.github/workflows/pr-review-comment-trigger.yaml
deleted file mode 100644
index 95c9c921f5..0000000000
--- a/.github/workflows/pr-review-comment-trigger.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-# This workflow is used to trigger the "PR Review Comment" workflow. This chaining is needed
-# because workflows triggered by pull_request_review_comment do not have access to secrets.
-
-name: PR Review Comment Trigger
-on:
- pull_request_review_comment:
- types:
- - created
-
-permissions:
- contents: read
-
-jobs:
- trigger:
- runs-on: ubuntu-latest
-
- # The "PR Review Comment" workflow will check the success status of this workflow and only mark
- # the PR for re-review if this workflow was successful, which is when the author leaves a review comment.
- if: github.repository == 'backstage/backstage' && github.event.comment.user.id == github.event.pull_request.user.id
-
- steps:
- - name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
- with:
- egress-policy: audit
-
- - name: Save PR number
- env:
- PR_NUMBER: ${{ github.event.pull_request.number }}
- run: |
- mkdir -p ./pr
- echo $PR_NUMBER > ./pr/pr_number
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- with:
- name: pr_number-${{ github.event.pull_request.number }}
- path: pr/
- overwrite: true
diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml
deleted file mode 100644
index 3c2c34ee38..0000000000
--- a/.github/workflows/pr-review-comment.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-# This workflow is triggered by "PR Review Comment Trigger"
-
-name: PR Review Comment
-on:
- workflow_run:
- workflows: [PR Review Comment Trigger]
- types:
- - completed
-
-jobs:
- re-review:
- runs-on: ubuntu-latest
-
- # The triggering workflow will report success if the PR needs re-review
- if: github.repository == 'backstage/backstage' && github.event.workflow_run.conclusion == 'success'
-
- steps:
- # Inspired by https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
- - name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
- with:
- egress-policy: audit
-
- - name: Read PR Number
- id: pr-number
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: context.payload.workflow_run.id,
- });
- const [artifact] = allArtifacts.data.artifacts.filter(artifact => artifact.name.startsWith('pr_number-'))
- if (!artifact) {
- throw new Error('No PR Number artifact available')
- }
-
- const prNumber = artifact.name.slice('pr_number-'.length)
- core.setOutput('pr-number', prNumber);
-
- - uses: backstage/actions/re-review@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
- with:
- app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
- private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
- installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }}
- project-id: PVT_kwDOBFKqdc02LQ
- issue-number: ${{ steps.pr-number.outputs.pr-number }}
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
deleted file mode 100644
index 36988f7d07..0000000000
--- a/.github/workflows/pr.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: PR
-on:
- pull_request_target:
- types:
- - opened
- - synchronize
- - reopened
- - closed
- issue_comment:
- types:
- - created
-
-jobs:
- sync:
- runs-on: ubuntu-latest
-
- # Avoid running on issue comments
- if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request )
- steps:
- - name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
- with:
- egress-policy: audit
-
- - name: PR sync
- uses: backstage/actions/pr-sync@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
- with:
- github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
- app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
- private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
- installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }}
- project-id: PVT_kwDOBFKqdc02LQ
- auto-assign: false
- owning-teams: '@backstage/techdocs-core'
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 96cb3368be..c1c55f7abc 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -29,12 +29,12 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: 'Checkout code'
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
- uses: github/codeql-action/upload-sarif@497990dfed22177a82ba1bbab381bc8f6d27058f # v3.31.6
+ uses: github/codeql-action/upload-sarif@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
with:
sarif_file: results.sarif
diff --git a/.github/workflows/sync_canon.yml b/.github/workflows/sync_bui-docs.yml
similarity index 76%
rename from .github/workflows/sync_canon.yml
rename to .github/workflows/sync_bui-docs.yml
index 9da6c80b98..2a12f8f72d 100644
--- a/.github/workflows/sync_canon.yml
+++ b/.github/workflows/sync_bui-docs.yml
@@ -8,26 +8,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- name: Checkout backstage/docs-ui
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: backstage/docs-ui
path: bui-external-docs
diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml
index 9719a34886..9fc82de604 100644
--- a/.github/workflows/sync_code-formatting.yml
+++ b/.github/workflows/sync_code-formatting.yml
@@ -10,22 +10,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch changes to previous commit - required for 'only_changed' in Prettier action
fetch-depth: 0
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml
index b4672960ed..fb51949fd1 100644
--- a/.github/workflows/sync_dependabot-changesets.yml
+++ b/.github/workflows/sync_dependabot-changesets.yml
@@ -11,12 +11,12 @@ jobs:
if: github.actor == 'dependabot[bot]' && github.repository == 'backstage/backstage'
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
ref: ${{ github.head_ref }}
@@ -26,7 +26,7 @@ jobs:
git config --global user.email noreply@backstage.io
git config --global user.name 'Github changeset workflow'
- name: Generate changeset
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { promises: fs } = require('fs');
diff --git a/.github/workflows/sync_patch-release.yml b/.github/workflows/sync_patch-release.yml
index b4e5cd3c6e..e126fe2ab9 100644
--- a/.github/workflows/sync_patch-release.yml
+++ b/.github/workflows/sync_patch-release.yml
@@ -5,11 +5,6 @@ on:
- master
paths:
- '.patches/**'
- pull_request:
- branches:
- - master
- paths:
- - '.patches/**'
concurrency:
group: sync-patch-release
@@ -25,17 +20,23 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 20000
fetch-tags: true
token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
+ - name: Use Node.js 22.x
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ with:
+ node-version: 22.x
+ registry-url: https://registry.npmjs.org/ # Needed for auth
+
- name: Configure Git
run: |
git config --global user.email noreply@backstage.io
@@ -55,7 +56,7 @@ jobs:
- name: Find existing PR
id: find-pr
if: steps.check-patches.outputs.has_patches == 'true'
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
script: |
@@ -82,7 +83,7 @@ jobs:
- name: Close PR and delete branch if no patches
if: steps.check-patches.outputs.has_patches == 'false'
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
script: |
@@ -131,7 +132,7 @@ jobs:
- name: Read patch files for PR metadata
if: steps.check-patches.outputs.has_patches == 'true'
id: read-patches
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
script: |
@@ -203,7 +204,7 @@ jobs:
- name: Create or update PR
if: steps.check-patches.outputs.has_patches == 'true'
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_EXISTS: ${{ steps.find-pr.outputs.pr_exists }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
diff --git a/.github/workflows/sync_pull-requests-scheduled.yml b/.github/workflows/sync_pull-requests-scheduled.yml
new file mode 100644
index 0000000000..08e6e3b0c0
--- /dev/null
+++ b/.github/workflows/sync_pull-requests-scheduled.yml
@@ -0,0 +1,94 @@
+name: Sync Pull Requests Scheduled
+
+on:
+ schedule:
+ # Every 30 minutes, processing a rotating batch of PRs
+ - cron: '*/30 * * * *'
+ workflow_dispatch:
+
+permissions: {}
+
+jobs:
+ plan:
+ permissions:
+ pull-requests: read
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ matrix: ${{ steps.batch.outputs.matrix }}
+ count: ${{ steps.batch.outputs.count }}
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
+ with:
+ egress-policy: audit
+
+ - name: Determine PR batch
+ id: batch
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ // 48 slots per day (one per 30-minute cron window).
+ // Each PR is assigned a fixed slot via pr.number % 48, so
+ // opening/closing other PRs never shifts the assignment.
+ const totalSlots = 48;
+ const now = new Date();
+ const currentSlot =
+ now.getUTCHours() * 2 + (now.getUTCMinutes() >= 30 ? 1 : 0);
+
+ const prs = await github.paginate(github.rest.pulls.list, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ per_page: 100,
+ });
+
+ const batch = prs.filter(
+ pr => pr.number % totalSlots === currentSlot,
+ );
+
+ core.info(
+ `Slot ${currentSlot}/${totalSlots}: ` +
+ `${batch.length} PRs to process out of ${prs.length} open`,
+ );
+
+ const matrix = {
+ include: batch.map(pr => ({ prNumber: String(pr.number) })),
+ };
+
+ core.setOutput('matrix', JSON.stringify(matrix));
+ core.setOutput('count', String(batch.length));
+
+ sync:
+ needs: plan
+ if: needs.plan.outputs.count > 0
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ strategy:
+ matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
+ fail-fast: false
+ max-parallel: 1
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
+ with:
+ egress-policy: audit
+
+ - name: Backstage PR automation
+ uses: backstage/actions/pr-automation@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
+ with:
+ app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
+ private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
+ installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }}
+ project-owner: backstage
+ project-number: '14'
+ pr-number: ${{ matrix.prNumber }}
+ action: synchronize
+ required-checks: |
+ DCO
+ E2E Linux 22.x
+ E2E Linux 24.x
+ Test 22.x
+ Test 24.x
+ Verify 22.x
+ Verify 24.x
diff --git a/.github/workflows/sync_pull-requests-trigger.yml b/.github/workflows/sync_pull-requests-trigger.yml
new file mode 100644
index 0000000000..152837b811
--- /dev/null
+++ b/.github/workflows/sync_pull-requests-trigger.yml
@@ -0,0 +1,51 @@
+name: Sync Pull Requests Trigger
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - closed
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+ - ready_for_review
+ - converted_to_draft
+ pull_request_review:
+ types: [submitted, dismissed]
+ issue_comment:
+ types: [created]
+
+jobs:
+ trigger:
+ if: >
+ github.repository == 'backstage/backstage' &&
+ github.event.sender.type != 'Bot' &&
+ (github.event.pull_request || github.event.issue.pull_request)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
+ with:
+ egress-policy: audit
+
+ - name: Save PR context
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
+ LABEL_ADDED: ${{ github.event.action == 'labeled' && github.event.label.name || '' }}
+ REVIEW_STATE: ${{ github.event.review.state }}
+ ACTOR: ${{ github.actor }}
+ ACTION: ${{ github.event.action }}
+ run: |
+ mkdir -p ./context
+ echo "$PR_NUMBER" > ./context/pr-number
+ echo "$LABEL_ADDED" > ./context/label-added
+ echo "$REVIEW_STATE" > ./context/review-state
+ echo "$ACTOR" > ./context/actor
+ echo "$ACTION" > ./context/action
+
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: pr-context
+ path: context/
+ overwrite: true
diff --git a/.github/workflows/sync_pull-requests.yml b/.github/workflows/sync_pull-requests.yml
new file mode 100644
index 0000000000..ca2524fe1c
--- /dev/null
+++ b/.github/workflows/sync_pull-requests.yml
@@ -0,0 +1,70 @@
+name: Sync Pull Requests
+
+on:
+ workflow_run:
+ workflows: ['Sync Pull Requests Trigger', 'CI', 'E2E Linux']
+ types:
+ - completed
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ # Only run on the trigger workflow, or if the source workflow was triggered by a pull request
+ if: >
+ github.event.workflow_run.name == 'Sync Pull Requests Trigger' ||
+ github.event.workflow_run.event == 'pull_request'
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
+ with:
+ egress-policy: audit
+
+ - name: Download PR context
+ id: download
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ continue-on-error: true
+ with:
+ name: pr-context
+ path: ./context
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ run-id: ${{ github.event.workflow_run.id }}
+
+ - name: Read context
+ if: steps.download.outcome == 'success'
+ id: context
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ const fs = require('fs');
+ function read(path, fallback) {
+ try { return fs.readFileSync(path, 'utf8').trim(); }
+ catch { return fallback; }
+ }
+ core.setOutput('pr-number', read('./context/pr-number', ''));
+ core.setOutput('label-added', read('./context/label-added', ''));
+ core.setOutput('review-state', read('./context/review-state', ''));
+ core.setOutput('actor', read('./context/actor', ''));
+ core.setOutput('action', read('./context/action', 'synchronize'));
+
+ - name: Backstage PR automation
+ if: steps.context.outputs.pr-number
+ uses: backstage/actions/pr-automation@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
+ with:
+ app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }}
+ private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }}
+ installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }}
+ project-owner: backstage
+ project-number: '14'
+ pr-number: ${{ steps.context.outputs.pr-number }}
+ label-added: ${{ steps.context.outputs.label-added }}
+ review-state: ${{ steps.context.outputs.review-state }}
+ actor: ${{ steps.context.outputs.actor }}
+ action: ${{ steps.context.outputs.action }}
+ required-checks: |
+ DCO
+ E2E Linux 22.x
+ E2E Linux 24.x
+ Test 22.x
+ Test 24.x
+ Verify 22.x
+ Verify 24.x
diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml
index ce7ff266c0..dd145df153 100644
--- a/.github/workflows/sync_release-manifest.yml
+++ b/.github/workflows/sync_release-manifest.yml
@@ -8,24 +8,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# 'v' prefix is added here for the tag, we keep it out of the manifest logic
ref: v${{ github.event.client_payload.version }}
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
@@ -35,7 +35,7 @@ jobs:
# Checkout backstage/versions into /backstage/versions, which is where store the output
- name: Checkout versions
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: backstage/versions
path: versions
@@ -71,7 +71,7 @@ jobs:
git push
- name: Dispatch update-helper update
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml
index 99644e41fe..8575cab760 100644
--- a/.github/workflows/sync_renovate-changesets.yml
+++ b/.github/workflows/sync_renovate-changesets.yml
@@ -11,12 +11,12 @@ jobs:
if: github.actor == 'renovate[bot]' && github.repository == 'backstage/backstage'
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2
ref: ${{ github.head_ref }}
@@ -26,7 +26,7 @@ jobs:
git config --global user.email noreply@backstage.io
git config --global user.name 'Github changeset workflow'
- name: Generate changeset
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { promises: fs } = require("fs");
diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml
index 97f7e8ed14..75ad266a8b 100644
--- a/.github/workflows/sync_snyk-github-issues.yml
+++ b/.github/workflows/sync_snyk-github-issues.yml
@@ -12,19 +12,19 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml
index 8b76338645..a8b364fbb8 100644
--- a/.github/workflows/sync_snyk-monitor.yml
+++ b/.github/workflows/sync_snyk-monitor.yml
@@ -25,11 +25,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Monitor and Synchronize Snyk Policies
uses: snyk/actions/node@9adf32b1121593767fc3c057af55b55db032dc04 # master
with:
@@ -58,6 +58,6 @@ jobs:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
NODE_OPTIONS: --max-old-space-size=7168
- name: Upload Snyk report
- uses: github/codeql-action/upload-sarif@497990dfed22177a82ba1bbab381bc8f6d27058f # v3.31.6
+ uses: github/codeql-action/upload-sarif@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
with:
sarif_file: snyk.sarif
diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml
index a5a035d3a3..7df18100b0 100644
--- a/.github/workflows/sync_version-packages.yml
+++ b/.github/workflows/sync_version-packages.yml
@@ -12,20 +12,32 @@ jobs:
create-release-pr:
name: Create Changeset PR
runs-on: ubuntu-latest
+
+ env:
+ CI: true
+ NODE_OPTIONS: --max-old-space-size=8192
+
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 20000
fetch-tags: true
token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
+ - name: Use Node.js 22.x
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ with:
+ node-version: 22.x
+ registry-url: https://registry.npmjs.org/ # Needed for auth
+
- name: Install Dependencies
run: yarn --immutable
+
- name: Create Release Pull Request
uses: backstage/changesets-action@a39baf18913e669734ffb00c2fd9900472cfa240 # v2.3.2
with:
diff --git a/.github/workflows/verify_accessibility-noop.yml b/.github/workflows/verify_accessibility-noop.yml
index ee696ebc7f..51caba5acd 100644
--- a/.github/workflows/verify_accessibility-noop.yml
+++ b/.github/workflows/verify_accessibility-noop.yml
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml
index 82d26ca510..12456caf8e 100644
--- a/.github/workflows/verify_accessibility.yml
+++ b/.github/workflows/verify_accessibility.yml
@@ -20,17 +20,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- name: run Lighthouse CI
diff --git a/.github/workflows/verify_chromatic-noop.yml b/.github/workflows/verify_chromatic-noop.yml
index 35015def52..cb4c96902a 100644
--- a/.github/workflows/verify_chromatic-noop.yml
+++ b/.github/workflows/verify_chromatic-noop.yml
@@ -20,7 +20,7 @@ jobs:
name: Chromatic
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/verify_chromatic.yml b/.github/workflows/verify_chromatic.yml
index a9d83b01e3..74ff9595f4 100644
--- a/.github/workflows/verify_chromatic.yml
+++ b/.github/workflows/verify_chromatic.yml
@@ -24,23 +24,23 @@ jobs:
name: Chromatic
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 10000 # Required to retrieve git history
- name: Use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: Install dependencies
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml
index d06c8ea270..a919dff7b0 100644
--- a/.github/workflows/verify_codeql.yml
+++ b/.github/workflows/verify_codeql.yml
@@ -42,12 +42,12 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout repository
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -55,7 +55,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@497990dfed22177a82ba1bbab381bc8f6d27058f # v3.31.6
+ uses: github/codeql-action/init@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -66,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@497990dfed22177a82ba1bbab381bc8f6d27058f # v3.31.6
+ uses: github/codeql-action/autobuild@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -80,4 +80,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@497990dfed22177a82ba1bbab381bc8f6d27058f # v3.31.6
+ uses: github/codeql-action/analyze@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml
index 8810f05a16..af42f25390 100644
--- a/.github/workflows/verify_docs-quality.yml
+++ b/.github/workflows/verify_docs-quality.yml
@@ -12,11 +12,11 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Vale does not support file excludes, so we use the script to generate a list of files instead
# The action also does not allow args or a local config file to be passed in, so the files array
diff --git a/.github/workflows/verify_e2e-linux-noop.yml b/.github/workflows/verify_e2e-linux-noop.yml
index 152f4cc2c0..38ee55f72c 100644
--- a/.github/workflows/verify_e2e-linux-noop.yml
+++ b/.github/workflows/verify_e2e-linux-noop.yml
@@ -29,7 +29,7 @@ jobs:
name: E2E Linux ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml
index 436995ddc2..c9a0696054 100644
--- a/.github/workflows/verify_e2e-linux.yml
+++ b/.github/workflows/verify_e2e-linux.yml
@@ -43,11 +43,23 @@ jobs:
name: E2E Linux ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ # Store the PR number for the Sync Pull Requests workflow
+ - name: Save PR context
+ if: matrix.node-version == '22.x' && github.event_name == 'pull_request'
+ run: |
+ mkdir -p ./context
+ echo "${{ github.event.pull_request.number }}" > ./context/pr-number
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ if: matrix.node-version == '22.x' && github.event_name == 'pull_request'
+ with:
+ name: pr-context
+ path: context/
+
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure Git
run: |
@@ -55,12 +67,12 @@ jobs:
git config --global user.name 'GitHub e2e user'
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml
index 07c751e098..0ea20acc46 100644
--- a/.github/workflows/verify_e2e-techdocs.yml
+++ b/.github/workflows/verify_e2e-techdocs.yml
@@ -32,21 +32,21 @@ jobs:
name: Techdocs
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.9'
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
diff --git a/.github/workflows/verify_e2e-windows-noop.yml b/.github/workflows/verify_e2e-windows-noop.yml
index f04748a950..4759555905 100644
--- a/.github/workflows/verify_e2e-windows-noop.yml
+++ b/.github/workflows/verify_e2e-windows-noop.yml
@@ -25,7 +25,7 @@ jobs:
name: E2E Windows ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml
index 933bbb0840..dc9e7ebd9e 100644
--- a/.github/workflows/verify_e2e-windows.yml
+++ b/.github/workflows/verify_e2e-windows.yml
@@ -28,12 +28,12 @@ jobs:
env:
CI: true
- NODE_OPTIONS: --max-old-space-size=8192 --no-node-snapshot --experimental-vm-modules
+ NODE_OPTIONS: --max-old-space-size=8192 --experimental-vm-modules
name: E2E Windows ${{ matrix.node-version }}
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
@@ -44,7 +44,7 @@ jobs:
git config --global core.autocrlf false
git config --global core.eol lf
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure Git
run: |
@@ -52,13 +52,13 @@ jobs:
git config --global user.name 'GitHub e2e user'
- name: use node.js ${{ matrix.node-version }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: setup python
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
@@ -80,7 +80,7 @@ jobs:
uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # latest
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }}
diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml
index 681ac2c07a..84d666f00e 100644
--- a/.github/workflows/verify_fossa.yml
+++ b/.github/workflows/verify_fossa.yml
@@ -14,12 +14,12 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Fossa
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
diff --git a/.github/workflows/verify_microsite-noop.yml b/.github/workflows/verify_microsite-noop.yml
index 70686e3bd9..acab96962c 100644
--- a/.github/workflows/verify_microsite-noop.yml
+++ b/.github/workflows/verify_microsite-noop.yml
@@ -21,7 +21,7 @@ jobs:
name: Microsite
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml
index 7b735877bc..71276ec85d 100644
--- a/.github/workflows/verify_microsite.yml
+++ b/.github/workflows/verify_microsite.yml
@@ -28,12 +28,12 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: find latest release
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
id: find-release
with:
script: |
@@ -59,53 +59,42 @@ jobs:
result-encoding: string
- name: checkout latest release
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: refs/tags/${{ steps.find-release.outputs.result }}
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- - name: build API reference
- run: yarn build:api-docs
-
- - name: upload API reference
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- with:
- name: stable-reference
- path: docs/reference/
- if-no-files-found: error
- retention-days: 1
-
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
- name: restore package-docs cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-stable-
- - name: build API reference (beta)
+ - name: build API reference
run: yarn backstage-repo-tools package-docs
- - name: upload API reference (beta)
+ - name: upload API reference
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
- name: stable-reference-beta
+ name: stable-reference
path: type-docs/
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
- name: save package-docs cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
if: always()
with:
path: .cache/package-docs
@@ -137,56 +126,45 @@ jobs:
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: checkout master
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: yarn install
- uses: backstage/actions/yarn-install@b3c1841fd69e1658ac631afafd0fb140a2309024 # v0.6.17
+ uses: backstage/actions/yarn-install@c4dd040f963edd226462b5f32efd319ffca12ac5 # v0.7.7
with:
cache-prefix: ${{ runner.os }}-v22.x
- - name: build API reference
- run: yarn build:api-docs
-
- - name: upload API reference
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- with:
- name: next-reference
- path: docs/reference/
- if-no-files-found: error
- retention-days: 1
-
# Use the lower-level cache actions for the success cache, so that we can store the cache even on failed builds
- name: restore package-docs cache
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: .cache/package-docs
key: ${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-v${{ matrix.node-version }}-package-docs-next-
- - name: build API reference (beta)
+ - name: build API reference
run: yarn backstage-repo-tools package-docs
- - name: upload API reference (beta)
+ - name: upload API reference
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
- name: next-reference-beta
+ name: next-reference
path: type-docs/
# Always save success cache even if there were failures, that way it can be used in re-triggered builds
- name: save package-docs cache
- uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
if: always()
with:
path: .cache/package-docs
@@ -234,17 +212,17 @@ jobs:
name: Microsite
steps:
- name: Harden Runner
- uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3
+ uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Use Node.js 22.x
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 22.x
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.9'
@@ -267,23 +245,31 @@ jobs:
- name: verify yarn dependency duplicates
run: node scripts/verify-lockfile-duplicates.js
- - name: download stable reference
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
- with:
- name: stable-reference
- path: docs/reference
-
- name: download stable OpenAPI API docs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: stable-openapi-docs
path: docs
- - name: grab lastest releases docs
+ - name: grab latest releases docs
run: |
git fetch origin master --depth 1
git checkout FETCH_HEAD -- docs/releases
+ - name: create missing generated file
+ run: |
+ mkdir -p docs/reference
+ cat > docs/reference/index.md < ` in the root of the repository.
+Once a patch has been applied and merged, manually delete the corresponding patch file from this directory. The patch script will automatically skip patches that have already been applied to the target branch, so it's safe to re-run the script even if some patches are already present.
+
## GitHub Workflow
A GitHub workflow automatically keeps a "Patch Release" PR in sync with the patches listed in this directory. When you add, modify, or remove patch files, the workflow will:
diff --git a/.patches/create-pr-patch.js b/.patches/create-pr-patch.js
index 528d4fc404..381810a6ff 100755
--- a/.patches/create-pr-patch.js
+++ b/.patches/create-pr-patch.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node
-const fs = require('fs');
-const path = require('path');
+const fs = require('node:fs');
+const path = require('node:path');
const prNumber = process.argv[2];
const description = process.argv.slice(3).join(' ');
diff --git a/.patches/pr-32973.txt b/.patches/pr-32973.txt
new file mode 100644
index 0000000000..6d83c5d66d
--- /dev/null
+++ b/.patches/pr-32973.txt
@@ -0,0 +1 @@
+Fixes the search component to register the first search that happens on initial navigation to the search page.
\ No newline at end of file
diff --git a/.storybook/main.ts b/.storybook/main.ts
index 788df83ba9..f891cd9692 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -1,7 +1,14 @@
-import type { StorybookConfig } from '@storybook/react-vite';
+// This file has been automatically migrated to valid ESM format by Storybook.
+import { defineMain } from '@storybook/react-vite/node';
+import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
import { join, dirname, posix } from 'path';
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const require = createRequire(import.meta.url);
+
const isChromatic = process.env.STORYBOOK_STORY_SET === 'chromatic';
// All stories for full development
@@ -19,34 +26,38 @@ const allStories = isChromatic
];
const rootPath = '../';
-const storiesSrcMdx = 'src/**/*.mdx';
const storiesSrcGlob = 'src/**/*.stories.@(js|jsx|mjs|ts|tsx)';
const getStoriesPath = (element: string, pattern: string) =>
posix.join(rootPath, element, pattern);
-const stories = allStories.flatMap(element => [
- getStoriesPath(element, storiesSrcMdx),
+const stories = allStories.map(element =>
getStoriesPath(element, storiesSrcGlob),
-]);
+);
// Resolve absolute path of a package. Needed in monorepos.
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
-const config: StorybookConfig = {
+export default defineMain({
stories,
addons: [
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-themes'),
getAbsolutePath('@storybook/addon-docs'),
getAbsolutePath('@storybook/addon-a11y'),
+ getAbsolutePath('@storybook/addon-vitest'),
+ getAbsolutePath('@storybook/addon-mcp'),
],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {},
},
+ features: {
+ experimentalComponentsManifest: true,
+ experimentalCodeExamples: true, // optional
+ },
viteFinal: async (config, { configType }) => {
// Add Node.js polyfills for browser compatibility
//
@@ -122,6 +133,4 @@ const config: StorybookConfig = {
return config;
},
-};
-
-export default config;
+});
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 8f987ebab1..6a0177cafe 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,8 +1,12 @@
-import React, { useEffect } from 'react';
+import addonA11y from '@storybook/addon-a11y';
+import addonDocs from '@storybook/addon-docs';
+import addonThemes from '@storybook/addon-themes';
+import addonLinks from '@storybook/addon-links';
+import { definePreview } from '@storybook/react-vite';
+import { useEffect } from 'react';
import { TestApiProvider } from '@backstage/test-utils';
-import { Content, AlertDisplay } from '@backstage/core-components';
+import { AlertDisplay } from '@backstage/core-components';
import { apis } from './support/apis';
-import type { Decorator, Preview } from '@storybook/react-vite';
import { useGlobals } from 'storybook/preview-api';
import { UnifiedThemeProvider, themes } from '@backstage/theme';
import { allModes } from './modes';
@@ -16,7 +20,8 @@ import './storybook.css';
// Custom themes
import './themes/spotify.css';
-const preview: Preview = {
+export default definePreview({
+ tags: ['manifest'],
globalTypes: {
themeMode: {
name: 'Theme Mode',
@@ -28,7 +33,6 @@ const preview: Preview = {
{ value: 'light', icon: 'circlehollow', title: 'Light' },
{ value: 'dark', icon: 'circle', title: 'Dark' },
],
- showName: true,
dynamicTitle: true,
},
},
@@ -42,18 +46,17 @@ const preview: Preview = {
{ value: 'backstage', title: 'Backstage' },
{ value: 'spotify', title: 'Spotify' },
],
- showName: true,
dynamicTitle: true,
},
},
},
+
initialGlobals: {
themeMode: 'light',
themeName: 'backstage',
},
- parameters: {
- layout: 'fullscreen',
+ parameters: {
backgrounds: {
disable: true,
},
@@ -72,7 +75,7 @@ const preview: Preview = {
},
viewport: {
- viewports: {
+ options: {
initial: {
name: 'Initial',
styles: { width: '320px', height: '100%' },
@@ -101,7 +104,15 @@ const preview: Preview = {
// 'dark spotify': allModes['dark spotify'],
},
},
+
+ a11y: {
+ // 'todo' - show a11y violations in the test UI only
+ // 'error' - fail CI on a11y violations
+ // 'off' - skip a11y checks entirely
+ test: 'todo',
+ },
},
+
decorators: [
Story => {
const [globals] = useGlobals();
@@ -121,10 +132,10 @@ const preview: Preview = {
};
}, [selectedTheme, selectedThemeName]);
- document.body.style.backgroundColor = 'var(--bui-bg)';
+ document.body.style.backgroundColor = 'var(--bui-bg-app)';
const docsStoryElements = document.getElementsByClassName('docs-story');
Array.from(docsStoryElements).forEach(element => {
- (element as HTMLElement).style.backgroundColor = 'var(--bui-bg)';
+ (element as HTMLElement).style.backgroundColor = 'var(--bui-bg-app)';
});
return (
@@ -132,14 +143,12 @@ const preview: Preview = {
{/* @ts-ignore */}
-
-
-
+
);
},
],
-};
-export default preview;
+ addons: [addonLinks(), addonThemes(), addonDocs(), addonA11y()],
+});
diff --git a/.storybook/storybook.css b/.storybook/storybook.css
index e69c4a8eb1..e0f0b68678 100644
--- a/.storybook/storybook.css
+++ b/.storybook/storybook.css
@@ -5,15 +5,15 @@
--sb-panel-left: 0;
--sb-panel-right: 0;
--sb-sidebar-border: none;
- --sb-sidebar-border-right: 1px solid var(--bui-border);
+ --sb-sidebar-border-right: 1px solid var(--bui-border-2);
--sb-sidebar-bg: #000;
--sb-options-border: none;
- --sb-options-border-left: 1px solid var(--bui-border);
+ --sb-options-border-left: 1px solid var(--bui-border-2);
--sb-content-padding-inline: 250px;
}
[data-theme-mode='dark'] {
- --sb-sidebar-bg: var(--bui-bg-surface-1);
+ --sb-sidebar-bg: var(--bui-bg-neutral-1);
}
[data-theme-name='spotify'] {
@@ -24,15 +24,15 @@
--sb-panel-right: 8px;
--sb-sidebar-border: none;
--sb-sidebar-border-right: none;
- --sb-sidebar-bg: var(--bui-bg-surface-1);
+ --sb-sidebar-bg: var(--bui-bg-neutral-1);
--sb-options-border: none;
--sb-options-border-left: none;
--sb-content-padding-inline: 258px;
}
[data-theme-name='spotify'][data-theme-mode='light'] {
- --sb-sidebar-border: 1px solid var(--bui-border);
- --sb-sidebar-border-right: 1px solid var(--bui-border);
- --sb-options-border: 1px solid var(--bui-border);
- --sb-options-border-left: 1px solid var(--bui-border);
+ --sb-sidebar-border: 1px solid var(--bui-border-2);
+ --sb-sidebar-border-right: 1px solid var(--bui-border-2);
+ --sb-options-border: 1px solid var(--bui-border-2);
+ --sb-options-border-left: 1px solid var(--bui-border-2);
}
diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css
index bcb1ded9ff..b72683f53e 100644
--- a/.storybook/themes/spotify.css
+++ b/.storybook/themes/spotify.css
@@ -174,7 +174,7 @@
}
.bui-TableHeader .bui-TableRow {
- border-bottom: 1px solid var(--bui-border);
+ border-bottom: 1px solid var(--bui-border-2);
}
.bui-TableHead {
@@ -216,22 +216,17 @@
}
[data-theme-mode='light'][data-theme-name='spotify'] {
- --bui-bg: var(--bui-gray-1);
- --bui-bg-surface-1: var(--bui-white);
- --bui-bg-surface-2: var(--bui-gray-2);
--bui-bg-solid: #1ed760;
--bui-bg-solid-hover: #3be477;
--bui-bg-solid-pressed: #1abc54;
- --bui-bg-solid-disabled: var(--bui-gray-2);
+ --bui-bg-solid-disabled: #0f6c30;
--bui-fg-primary: var(--bui-black);
- --bui-fg-secondary: var(--bui-gray-7);
+ --bui-fg-secondary: #757575;
--bui-fg-solid: var(--bui-black);
+ --bui-fg-solid-disabled: #62ab7c;
- --bui-border: var(--bui-gray-3);
- --bui-border-hover: rgba(0, 0, 0, 0.3);
- --bui-border-pressed: rgba(0, 0, 0, 0.5);
- --bui-border-disabled: rgba(0, 0, 0, 0.1);
+ --bui-border-2: #d9d9d9;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
@@ -239,47 +234,36 @@
--bui-ring: rgba(0, 0, 0, 0.2);
.bui-HeaderToolbarWrapper {
- border: 1px solid var(--bui-border);
+ border: 1px solid var(--bui-border-2);
}
.bui-HeaderTabsWrapper {
- border: 1px solid var(--bui-border);
+ border: 1px solid var(--bui-border-2);
}
}
[data-theme-mode='dark'][data-theme-name='spotify'] {
- --bui-bg: var(--bui-black);
- --bui-bg-surface-1: var(--bui-gray-1);
- --bui-bg-surface-2: var(--bui-gray-2);
+ --bui-bg-app: var(--bui-black);
+
--bui-bg-solid: #1ed760;
--bui-bg-solid-hover: #3be477;
--bui-bg-solid-pressed: #1abc54;
--bui-bg-solid-disabled: #0f6c30;
- --bui-bg-tint: #242424;
- --bui-bg-tint-hover: #202020;
- --bui-bg-tint-pressed: #292929;
- --bui-bg-tint-disabled: rgba(255, 255, 255, 0.8);
+
--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: var(--bui-white);
- --bui-fg-link-hover: var(--bui-white);
- --bui-fg-disabled: var(--bui-gray-5);
+ --bui-fg-secondary: #9e9e9e;
+ --bui-fg-disabled: #575757;
--bui-fg-solid: var(--bui-black);
- --bui-fg-solid-disabled: #083618;
- --bui-fg-tint: var(--bui-white);
- --bui-fg-tint-disabled: var(--bui-gray-5);
+ --bui-fg-solid-disabled: #072f15;
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
- --bui-border: var(--bui-gray-3);
- --bui-border-hover: rgba(255, 255, 255, 0.4);
- --bui-border-pressed: rgba(255, 255, 255, 0.5);
- --bui-border-disabled: rgba(255, 255, 255, 0.2);
+ --bui-border-2: #373737;
--bui-border-danger: #f87a7a;
--bui-border-warning: #e36d05;
--bui-border-success: #53db83;
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 2b3cad0b25..9a0e2fb0fd 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -91,7 +91,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. |
| [VMware](https://www.vmware.com) | [Waldir Montoya](https://github.com/waldirmontoya25), [Kris Applegate](https://github.com/krisapplegate), [Jamie Klassen](https://github.com/jamieklassen) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal |
| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality |
-| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. |
+| [IKEA Retail](https://www.ingka.com) | [Björn Ramberg](https://github.com/bjornramberg), [Martin Norin](https://github.com/martin-norin) | Centralized developer portal, supporting the internal engineering community with self serve access, documentation, templates, catalog and more. |
| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. |
| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. |
| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@kurtaking](https://github.com/kurtaking), [@pc-bob](https://github.com/pc-bob) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. |
@@ -288,3 +288,5 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
| [Sophotech](https://sopho.tech) | [@archy-rock3t-cloud](https://github.com/archy-rock3t-cloud), [Artem Muterko](mailto:artem@sopho.tech) | Custom Developer Platform based on Backstage, providing a service catalog, infrastructure templates, and integrated tooling to give developers a self-service experience. |
| [Swiss Mobiliar Insurance Company Ltd.](https://www.mobiliar.ch/) | [Patrick Wyler](mailto:patrick.wyler@mobiliar.ch) and [Beat Winistörfer](mailto:beat.winistoerfer@mobiliar.ch) | The portal provides a unified interface for accessing all relevant DevOps information previously scattered across various locations, enhancing accessibility and clarity for all IT employees. It relies on an internal graph database that enhances the Backstage software catalog with many additional elements. Significant effort has been invested in the visual representation of information through graphs and diagrams, facilitating analysis and improving the understanding of dependencies. |
| [DB Systel GmbH](https://www.dbsystel.de/dbsystel-en/) | [DB Systel GmbH](https://github.com/dbsystel) | Deutsche Bahn's Internal Developer Portal leverages Backstage and Crossplane.io to deliver a fully GitOps-driven onboarding experience for engineering teams across the company. Our Scaffolder ecosystem accelerates platform setup (Artifactory, GitLab, OpenShift), service bootstrapping, automated testing, AI-ready backends, and modern frontend development. We also make heavy use of custom catalog modules and frontend plugins that support developers in checking their provisioned resources or security and compliance of their code. |
+| [Danske StatsBaner (DSB)](https://www.dsb.dk/om-dsb/job-og-karriere/) | [Isaac Livingston](mailto:isjl@dsb.dk), [Paula-Catalina Curca](mailto:PACU@DSB.DK), [Muhammad Salman Khan](mailto:MUSK@DSB.DK) | DSB's portal is a central catalog of all our microservices, APIs, and systems. Developers can easily create new components with templates that run in our CNCF based developer platform |
+| [NN Group](https://www.nn-group.com/) | [Flavia Naezer (Product Owner)](mailto:flavia.naezer@nn-group.com), [Vijai Ramcharan (Principal Engineer)](mailto:vijai.ramcharan@nn-group.com) | Our Backstage powered IDP supports engineers and other makers and shapers in our large financial enterprise with the Software Catalog, a Store for infrastructure, TechDocs and Portal Platform with mini App-Store.|
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ba40be86c8..adf04c48e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,6 +17,7 @@ If you need help, just jump into our [Discord chatroom](https://discord.gg/backs
- [Accessibility](#accessibility)
- [Get Started!](#get-started)
- [Coding Guidelines](#coding-guidelines)
+- [AI Use Policy and Guidelines](#ai-use-policy-and-guidelines)
- [Documentation Guidelines](#documentation-guidelines)
- [Package Scripts](#package-scripts)
- [Local configuration](#local-configuration)
@@ -128,6 +129,37 @@ If there are any updates in `markdown` file please make sure to run `yarn run li
The Backstage development environment does not require any specific editor, but it is intended to be used with one that has built-in linting and type-checking. The development server does not include any checks by default, but they can be enabled using the `--check` flag. Note that using the flag may consume more system resources and slow things down.
+## AI Use Policy and Guidelines
+
+Our goal in the Backstage project is to develop an excellent software system. This requires careful attention to detail in every change we integrate. Maintainer time and attention is very limited, so it's important that changes you ask us to review represent your _best_ work.
+
+You are encouraged to use tools that help you write good code, including AI tools. However, you always need to understand and explain the changes you're proposing to make, whether or not you used an LLM as part of your process to produce them. The answer to "Why did you make change X?" should never be "I'm not sure. The AI did it."
+
+**Do not submit an AI-generated PR you haven't personally understood and tested**, as this wastes maintainers' time. PRs that appear to violate this guideline will be closed without review. If you do submit a largely AI-generated PR, clearly mark it as such in the description—maintainers may still close it without further review if it does not seem worthwhile.
+
+### Using AI as a Coding Assistant
+
+1. Don't skip **becoming familiar with the part of the codebase** you're working on. This will let you write better prompts and validate their output if you use an LLM. Code assistants can be a useful search engine/discovery tool in this process, but don't trust claims they make about how Backstage works. LLMs are often wrong, even about details that are clearly answered in the [Backstage documentation](https://backstage.io/docs).
+2. Split up your changes into **coherent commits**, even if an LLM generates them all in one go. See our section on [Creating Changesets](#creating-changesets) for guidance on documenting your changes.
+3. Don't simply ask an LLM to add **code comments**, as it will likely produce a bunch of text that unnecessarily explains what's already clear from the code. If using an LLM to generate comments, be really specific in your request, demand succinctness, and carefully edit the result.
+
+### Using AI for Communication
+
+Backstage contributors are expected to communicate with intention, to avoid wasting maintainer time with long, sloppy writing. We strongly prefer clear and concise communication about points that actually require discussion over long AI-generated comments.
+
+When you use an LLM to write a message for you, it remains **your responsibility** to read through the whole thing and make sure that it makes sense to you and represents your ideas concisely. A good rule of thumb is that if you can't make yourself carefully read some LLM output that you generated, nobody else wants to read it either.
+
+Here are some concrete guidelines for using LLMs as part of your communication workflows:
+
+1. When writing a pull request description, **do not include anything that's obvious** from looking at your changes directly (e.g., files changed, functions updated, etc.). Instead, focus on the _why_ behind your changes. Don't ask an LLM to generate a PR description on your behalf based on your code changes, as it will simply regurgitate the information that's already there.
+2. Similarly, when responding to a pull request comment, **explain _your_ reasoning**. Don't prompt an LLM to re-describe what can already be seen from the code.
+3. Verify that **everything you write is accurate**, whether or not an LLM generated any part of it. Backstage's maintainers will be unable to review your contributions if you misrepresent your work (e.g., wrongly describing your code changes, their effect, or your testing process).
+4. Complete all parts of the **PR description template**, including screenshots and the checklist. Don't simply overwrite the template with LLM output.
+5. **Clarity and succinctness** are much more important than perfect grammar, so you shouldn't feel obliged to pass your writing through an LLM. If you do ask an LLM to clean up your writing style, be sure it does _not_ make it longer in the process. Demand succinctness in your prompt.
+6. Quoting an LLM answer is usually less helpful than linking to **relevant primary sources**, like source code, [Backstage documentation](https://backstage.io/docs), or reference materials. If you do need to quote an LLM answer in a discussion, clearly distinguish LLM output from your own thoughts.
+
+If you have questions about AI usage or need help, feel free to ask in [Discord](https://discord.gg/backstage-687207715902193673).
+
## Documentation Guidelines
Contributing to the docs is one of the best ways to start getting involved with Backstage. The documentation site is often the first stop for anyone using or exploring Backstage, so even small improvements can have a big impact!
@@ -349,7 +381,7 @@ Or from the Settings UI look for the "Git: Always Sign Off" setting and check th
## API Reports
-Backstage uses [API Extractor](https://api-extractor.com/) and TSDoc comments to generate API Reports in Markdown format. These reports are what drive the [API Reference documentation](https://backstage.io/docs/reference/). What this means is that if you are making changes to the API or adding a new plugin then you will need either generate a new API Report or update an existing API Report. If you don't do this the CI build will fail when you create your Pull Request.
+Backstage uses [API Extractor](https://api-extractor.com/) and TSDoc comments to generate API Reports in Markdown format. What this means is that if you are making changes to the API or adding a new plugin then you will need either generate a new API Report or update an existing API Report. If you don't do this the CI build will fail when you create your Pull Request.
There are two ways you can do this:
@@ -400,7 +432,7 @@ Once you've submitted a Pull Request (PR) the various bots will come out and do
- checking for commits for their DCO (Developer Certificate of Origin)
- kick off the various CI builds
-Once these steps are completed, it's just a matter of being patient. As the reviewers have time, they will begin reviewing your PR. When the review process begins, there may be a few layers to this, but the general rule is that you need approval from one of the core maintainers and one from the specific area impacted by your PR. You may also have someone from the community review your changes. This can really help speed things up as they may catch some early items making the review for the maintainers simpler. Once you have the two (2) approvals, it's ready to be merged, a task that is also performed by the maintainers.
+Once these steps are completed, it's just a matter of being patient. Reviews are coordinated by maintainers and reviewers based on project area ownership. Prioritization is driven by a number of different factors, but some important ones are size (smaller get higher priority) and alignment with current roadmap priorities. Timely responses to review comments also help keep the pull request moving faster. You may also have someone from the reviewers group review your pull request and request changes. Approval from a member of the reviewers group will greatly increase the priority of your pull request.
### Review Tips
diff --git a/LABELS.md b/LABELS.md
index f1af06d4fa..cd2d19e6d5 100644
--- a/LABELS.md
+++ b/LABELS.md
@@ -97,6 +97,27 @@ These labels indicate a workflow status for the owners.
- `stale` - The issue or pull request has not seen any activity for a while and will be closed if no further activity is seen.
- `no stale` - The issue or pull request should not be closed due to inactivity.
+## Pull Request Labels
+
+The following labels indicate the status of a pull request:
+
+- `waiting-for:review` - The pull request needs a review and will be visible in the review queue unless already assigned an owner.
+- `waiting-for:author` - Changes have been requested by a reviewer and the pull request will not receive a review until the changes are made. A comment made on the pull request by the author will also push it back into the review queue.
+- `waiting-for:decision` - The pull request has been marked as more complex and needs a decision from the owners. Progress can still be made and discussion can continue, but expect the review to take longer. These pull requests are often good candidates to bring to a [SIG](https://github.com/backstage/community/tree/main/sigs) meeting.
+- `waiting-for:merge` - The pull request has been approved and is awaiting merge. If you have write access and authored the pull request you can merge it yourself. If you do not have access to merge and the pull request has not been merged within a day of approval, please notify the assigned reviewer.
+
+The following labels indicate the size of a pull request:
+
+- `size:tiny` - Tiny pull requests receive a higher priority for reviews.
+- `size:small` - Small pull requests receive a slightly higher priority for reviews.
+- `size:medium` - Medium-sized pull requests receive no change in priority for reviews.
+- `size:large` - Large pull requests receive a slightly lower priority for reviews.
+- `size:huge` - Huge pull requests receive a lower priority for reviews.
+
+The following additional labels also apply to pull requests:
+
+- `reviewer-approved` - The pull request has been approved by a member of the reviewers group. This pull request receives a much higher priority for reviews.
+
## Common Issue Filters
This is a collection of common issue filters that can help you find issues that you are looking for or that match your interests and skills.
@@ -106,7 +127,7 @@ This is a collection of common issue filters that can help you find issues that
These issues are ideal for new contributors to get started and don't require much familiarity with Backstage.
- [Backend](https://github.com/backstage/backstage/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20label%3A%22domain%3Abackend%22%20)
-- [Documentation](https://github.com/backstage/backstage/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20label%3A%22domain%3Adocs%22%20)
+- [Documentation](https://github.com/backstage/backstage/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20label%3A%22area%3Adocumentation%22)
- [Tooling](https://github.com/backstage/backstage/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20label%3A%22domain%3Atooling%22%20)
- [Web](https://github.com/backstage/backstage/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22good%20first%20issue%22%20label%3A%22domain%3Aweb%22%20)
@@ -115,7 +136,7 @@ These issues are ideal for new contributors to get started and don't require muc
These issues generally require some familiarity with Backstage and the codebase, and are either open for or require contributions from the community.
- [Backend]()
-- [Documentation]()
+- [Documentation]()
- [Tooling]()
- [Web]()
diff --git a/OWNERS.md b/OWNERS.md
index 44b4ebca15..e09a0a24c3 100644
--- a/OWNERS.md
+++ b/OWNERS.md
@@ -5,11 +5,13 @@
Team: @backstage/maintainers
-| Maintainer | Organization | GitHub | Discord |
-| --------------- | ------------ | ----------------------------------------------- | ------------- |
-| Patrik Oldsberg | Spotify | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` |
-| Fredrik Adelöw | Spotify | [freben](https://github.com/freben) | `freben#3926` |
-| Ben Lambert | Spotify | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` |
+| Maintainer | Organization | GitHub | Discord |
+| --------------- | ------------ | ----------------------------------------------------- | ------------- |
+| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
+| Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` |
+| Ben Lambert | Spotify | [benjdlambert](https://github.com/benjdlambert) | `blam#2159` |
+| Fredrik Adelöw | Spotify | [freben](https://github.com/freben) | `freben#3926` |
+| Patrik Oldsberg | Spotify | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` |
## Project Areas
@@ -17,7 +19,7 @@ These are the separate project areas of Backstage, each with their own project a
### Auth
-Team: @backstage/auth-maintainers
+Team: @backstage/auth-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aauth-maintainers)
Scope: The Backstage auth plugin and modules, as well as client-side implementations.
@@ -32,7 +34,7 @@ Scope: The Backstage auth plugin and modules, as well as client-side implementat
### Catalog
-Team: @backstage/catalog-maintainers
+Team: @backstage/catalog-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Acatalog-maintainers)
Scope: The catalog plugin and catalog model
@@ -61,7 +63,7 @@ Scope: Tooling and Community Repo Maintainers for the Backstage [Community Plugi
### Design System
-Team: @backstage/design-system-maintainers
+Team: @backstage/design-system-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Adesign-system-maintainers)
Scope: The Backstage design system, component library, as well as surrounding tooling such as Storybook.
@@ -69,22 +71,23 @@ Scope: The Backstage design system, component library, as well as surrounding to
| ------------------- | ------------ | ------------- | --------------------------------------------- | ------------- |
| Charles de Dreuille | Spotify | | [cdedreuille](https://github.com/cdedreuille) | `cdedreuille` |
| Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip` |
+| Johan Persson | Spotify | | [Naycon](https://github.com/Naycon) | `naycon` |
### Documentation
-Team: @backstage/documentation-maintainers
+Team: @backstage/documentation-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Adocumentation-maintainers)
Scope: The Backstage Documentation and Microsite, excluding the plugins listing
| Name | Organization | GitHub | Discord |
| --------------- | ------------- | ----------------------------------------------------- | ------------- |
-| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
+| André Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
| Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` |
| Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` |
### Framework
-Team: @backstage/framework-maintainers
+Team: @backstage/framework-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aframework-maintainers)
Scope: The Backstage core framework, including all revisions of the frontend and backend systems, as well as the App plugin.
@@ -111,7 +114,7 @@ Scope: The Backstage [Helm Chart(s)](https://github.com/backstage/charts).
### Home
-Team: @backstage/home-maintainers
+Team: @backstage/home-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Ahome-maintainers)
Scope: The Backstage home page and information architecture
@@ -126,7 +129,7 @@ Scope: The Backstage home page and information architecture
### Kubernetes
-Team: @backstage/kubernetes-maintainers
+Team: @backstage/kubernetes-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Akubernetes-maintainers)
Scope: The Kubernetes plugin and the base it provides for other plugins to build upon.
@@ -136,7 +139,7 @@ Scope: The Kubernetes plugin and the base it provides for other plugins to build
### Operations
-Team: @backstage/operations-maintainers
+Team: @backstage/operations-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aoperations-maintainers)
Scope: The management and operation of the main Backstage repository and release process, along with the surrounding tooling.
@@ -151,7 +154,7 @@ Scope: The management and operation of the main Backstage repository and release
### Permission Framework
-Team: @backstage/permission-maintainers
+Team: @backstage/permission-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Apermission-maintainers)
Scope: The Permission Framework and plugins integrating with the permission framework
@@ -166,7 +169,7 @@ Scope: The Permission Framework and plugins integrating with the permission fram
### Search
-Team: @backstage/search-maintainers
+Team: @backstage/search-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Asearch-maintainers)
Scope: The Backstage Search plugin
@@ -178,7 +181,7 @@ Scope: The Backstage Search plugin
### TechDocs
-Team: @backstage/techdocs-maintainers
+Team: @backstage/techdocs-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Atechdocs-maintainers)
Scope: The TechDocs plugin and related tooling
@@ -192,7 +195,7 @@ Scope: The TechDocs plugin and related tooling
### Tooling
-Team: @backstage/tooling-maintainers
+Team: @backstage/tooling-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Atooling-maintainers)
Scope: All published Backstage CLI tools in the main `backstage` repository that do not belong to other areas, including `@backstage/cli` and `@backstage/repo-tools`.
@@ -212,7 +215,7 @@ These incubating project areas have shared ownership with @backstage/maintainers
### Auditor
-Team: @backstage/auditor-maintainers
+Team: @backstage/auditor-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aauditor-maintainers)
Scope: The auditor core service, along with auditor usage in the main repository
@@ -222,7 +225,7 @@ Scope: The auditor core service, along with auditor usage in the main repository
### Events
-Team: @backstage/events-maintainers
+Team: @backstage/events-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aevents-maintainers)
Scope: The Events plugin and library, along with events modules in the main repository
@@ -232,7 +235,7 @@ Scope: The Events plugin and library, along with events modules in the main repo
### Notifications
-Team: @backstage/notifications-maintainers
+Team: @backstage/notifications-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Anotifications-maintainers)
Scope: The Notifications and Signals plugins and libraries
@@ -242,7 +245,7 @@ Scope: The Notifications and Signals plugins and libraries
### OpenAPI Tooling
-Team: @backstage/openapi-tooling-maintainers
+Team: @backstage/openapi-tooling-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Aopenapi-tooling-maintainers)
Scope: Tooling for frontend and backend schema-first OpenAPI development.
@@ -252,7 +255,7 @@ Scope: Tooling for frontend and backend schema-first OpenAPI development.
### Scaffolder
-Team: @backstage/scaffolder-maintainers
+Team: @backstage/scaffolder-maintainers — [Review board](https://github.com/orgs/backstage/projects/14/views/1?filterQuery=no%3Aassignee+reviewers%3Ascaffolder-maintainers)
Scope: The Scaffolder frontend and backend plugins, and related tooling.
@@ -267,6 +270,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling.
| Niklas Gustavsson | Spotify | [protocol7](https://github.com/protocol7) | |
| Dave Zolotusky | Spotify | [dzolotusky](https://github.com/dzolotusky) | |
| Pia Nilsson | Spotify | [pianilsson](https://github.com/pianilsson) | |
+| Stefan Särne | Spotify | [ssarne](https://github.com/ssarne) | |
## Organization Members
@@ -274,13 +278,12 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling.
| ------------------------------ | ------------------------- | ----------------------------------------------------- | ------------------------------ |
| Adam Harvey | Okta | [adamdmharvey](https://github.com/adamdmharvey) | `adamharvey_` |
| Alex Crome | | [afscrome](https://github.com/afscrome) | `afscrome` |
-| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` |
| Andrew Thauer | Wealthsimple | [andrewthauer](https://github.com/andrewthauer) | `andrewthauer#3060` |
-| Aramis Sennyey | DoorDash | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` |
| Brian Fletcher | Roadie.io | [punkle](https://github.com/punkle) | `Brian Fletcher#7051` |
| Carlos Esteban Lopez Jaramillo | VMWare | [luchillo17](https://github.com/luchillo17) | `luchillo17#8777` |
| David Tuite | Roadie.io | [dtuite](https://github.com/dtuite) | `David Tuite (roadie.io)#1010` |
| Deepankumar Loganathan | | [deepan10](https://github.com/deepan10) | `deepan10` |
+| Gabriel Dugny | Believe | [GabDug](https://github.com/GabDug) | `GabDug` |
| Heikki Hellgren | OP Financial Group | [drodil](https://github.com/drodil) | `deathammer` |
| Himanshu Mishra | Harness.io | [OrkoHunter](https://github.com/OrkoHunter) | `OrkoHunter#1520` |
| Irma Solakovic | Roadie.io | [Irma12](https://github.com/Irma12) | `Irma#7629` |
diff --git a/README-fr_FR.md b/README-fr_FR.md
index 27a7cd1153..50646617c4 100644
--- a/README-fr_FR.md
+++ b/README-fr_FR.md
@@ -10,7 +10,6 @@ Français \| [English](README.md) \| [한국어](README-ko_kr.md) \| [中文版]

[](https://codecov.io/gh/backstage/backstage)
[](https://github.com/backstage/backstage/releases)
-[](https://app.uffizzi.com/ephemeral-environments/backstage/backstage)
[](https://bestpractices.coreinfrastructure.org/projects/7678)
[](https://securityscorecards.dev/viewer/?uri=github.com/backstage/backstage)
@@ -56,7 +55,7 @@ Si vous voulez contribuer et vous impliquer dans notre communauté, voici les re
- [Discord chatroom](https://discord.gg/backstage-687207715902193673) - Pour obtenir de l'aide ou discuter du projet
- [Contributing to Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - Rendez-vous ici si vous souhaitez contribuer
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Contribuez à définir la direction technique
-- [FAQ](https://backstage.io/docs/FAQ)
+- [FAQ](https://backstage.io/docs/faq)
- [Code of Conduct](CODE_OF_CONDUCT.md) - C'est comme ça qu'on fonctionne
- [Adopters](ADOPTERS.md) - Les entreprises utilisant déjà Backstage
- [Blog](https://backstage.io/blog/) - Annonces et mises à jour
@@ -66,7 +65,7 @@ Si vous voulez contribuer et vous impliquer dans notre communauté, voici les re
## Licence
-Copyright 2020-2025 © Les auteurs de Backstage. Tous droits réservés. La Linux Foundation détient des marques déposées et utilise des marques commerciales. Pour une liste des marques de commerce de la Linux Foundation, veuillez consulter notre page d'utilisation des marques: https://www.linuxfoundation.org/trademark-usage
+Copyright 2020-2026 © Les auteurs de Backstage. Tous droits réservés. La Linux Foundation détient des marques déposées et utilise des marques commerciales. Pour une liste des marques de commerce de la Linux Foundation, veuillez consulter notre page d'utilisation des marques: https://www.linuxfoundation.org/trademark-usage
Sous licence Apache, version 2.0: http://www.apache.org/licenses/LICENSE-2.0
diff --git a/README-ko_kr.md b/README-ko_kr.md
index e43aa60c43..2b063a39e7 100644
--- a/README-ko_kr.md
+++ b/README-ko_kr.md
@@ -2,7 +2,7 @@
# [Backstage](https://backstage.io)
-[English](README.md) \| 한국어
+한국어 \| [English](README.md) \| [中文版](README-zh_Hans.md) \| [Français](README-fr_FR.md)
[](https://opensource.org/licenses/Apache-2.0)
[](https://www.cncf.io/projects)
@@ -65,7 +65,7 @@ Backstage의 문서는 다음을 포함합니다:
## License
-Copyright 2020-2025 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage
+Copyright 2020-2026 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage
Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
diff --git a/README-zh_Hans.md b/README-zh_Hans.md
index 4661564725..cb8a4bc38f 100644
--- a/README-zh_Hans.md
+++ b/README-zh_Hans.md
@@ -2,7 +2,7 @@
# [Backstage](https://backstage.io)
-简体中文\| [English](README.md) \| [한국어](README-ko_kr.md)
+简体中文 \| [English](README.md) \| [한국어](README-ko_kr.md) \| [Français](README-fr_FR.md)
[](https://opensource.org/licenses/Apache-2.0)
[](https://www.cncf.io/projects)
@@ -55,7 +55,7 @@ Backstage 的文档包括:
- [Discord 聊天室](https://discord.gg/backstage-687207715902193673) - 获得支持或讨论项目
- [参与贡献 Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - 如果您想做出贡献,请从这里开始
- [RFCs](https://github.com/backstage/backstage/labels/rfc) - 帮助制定技术方向
-- [FAQ](https://backstage.io/docs/FAQ) - 常问问题
+- [FAQ](https://backstage.io/docs/faq) - 常问问题
- [行为准则](CODE_OF_CONDUCT.md) - 这是我们的行事方式
- [采纳者](ADOPTERS.md) - 已经在使用 Backstage 的公司
- [博客](https://backstage.io/blog/) - 公告和更新
@@ -65,7 +65,7 @@ Backstage 的文档包括:
## 许可
-版权所有 2020-2025 © Backstage 作者。版权所有。Linux 基金会已注册商标并使用商标。有关 Linux 基金会的商标列表,请参阅我们的商标使用页面:https://www.linuxfoundation.org/trademark-usage
+版权所有 2020-2026 © Backstage 作者。版权所有。Linux 基金会已注册商标并使用商标。有关 Linux 基金会的商标列表,请参阅我们的商标使用页面:https://www.linuxfoundation.org/trademark-usage
采用 Apache v2.0 许可:http://www.apache.org/licenses/LICENSE-2.0
diff --git a/README.md b/README.md
index e3e9bd77dd..68931a3fde 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ See the [GOVERNANCE.md](https://github.com/backstage/community/blob/main/GOVERNA
## License
-Copyright 2020-2025 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage
+Copyright 2020-2026 © The Backstage Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage
Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
diff --git a/REVIEWING.md b/REVIEWING.md
index 66af880f43..75f6a67eeb 100644
--- a/REVIEWING.md
+++ b/REVIEWING.md
@@ -2,6 +2,85 @@
This file provides pointers for reviewing pull requests. While the main audience are reviewers, this can also be useful if you are contributing to this repository.
+## Quick Links
+
+- [Incoming Reviews](https://github.com/orgs/backstage/projects/14/views/1)
+- [Personal Reviews](https://github.com/orgs/backstage/projects/14/views/2)
+- Project area boards in [OWNERS.md](./OWNERS.md)
+
+## Review Workflow
+
+Pull request reviews are coordinated by maintainers and reviewers based on project area ownership. Reviews are prioritized and tracked using automated labels and a GitHub Project board. This review process applies to all pull requests in the [main Backstage repository](https://github.com/backstage/backstage), other repositories may have their own review processes.
+
+All incoming pull request reviews are tracked on the [Incoming Reviews board](https://github.com/orgs/backstage/projects/14/views/1). This board can be used by members of the `@backstage/reviewers` group to find pull requests to review, as well as maintainers that want an overview of all incoming pull requests.
+
+Project area maintainers can use the same board but with additional filters applied to only show incoming pull requests for their project area. These filtered boards are linked to for each project area in [OWNERS.md](./OWNERS.md). There is also a [personal review board](https://github.com/orgs/backstage/projects/14/views/2) that can be used to track reviews that you have been assigned to.
+
+### Submitting Reviews
+
+When reviewing a pull request, always submit a formal review using either **"Approve"** or **"Request changes"**. Do not use "Comment" as a substitute for either of these — the review automation relies on the review state to manage labels and the review board, and only formal approvals and change requests are tracked.
+
+- Use **"Approve"** when you are satisfied with the changes and believe they are ready to be merged (for your area, at least).
+- Use **"Request changes"** when changes are necessary before the pull request can be merged. This signals clearly to the author what is expected and moves the pull request out of the review queue until the author responds.
+- Use **"Comment"** only to contribute to discussion, ask questions, or leave optional or minor suggestions that do not block the pull request.
+
+Being explicit with "Approve" or "Request changes" is important both for transparent communication with the author and for the automation to function correctly. The automation reads these review states to update labels and the review board accordingly, meaning a "Comment" review will not cause any status change.
+
+### Closing Pull Requests
+
+Pull requests that have been superseded, are no longer relevant, or otherwise are not worth moving forward should be closed with a comment explaining why. Closed pull requests are automatically removed from the review board by the automation.
+
+### Review Process for @backstage/reviewers
+
+Members of the `@backstage/reviewers` do not have any specific areas that they should focus on. They can choose to filter and focus on reviews from any part of the project. They do not assign themselves specific pull requests, but instead leave reviews directly on pull requests without any further process.
+
+Reviews from this group still have meaningful impact on the review process, as they are picked up by the automation. Approving reviews from a member of this group will add the `reviewer-approved` label to the pull request, which greatly increases its priority and visibility for project area maintainers. Likewise, requesting changes will add the `waiting-for:author` label to the pull request, removing it from the review queue until the author has commented or pushed new changes.
+
+As a reward for helping out with reviews, members who actively review pull requests will have their own pull requests prioritized higher on the review board. The more reviews you contribute, the higher the priority boost your own pull requests receive.
+
+### Review Process for Project Area Maintainers
+
+Project area maintainers are responsible for reviewing and ultimately merging or closing pull requests that target their project area. They should use the global or filtered board for their project area to find pull requests to review. When they find a pull request that they want to review, they should assign themselves to the pull request, removing it from the review queue and placing it on their personal review board.
+
+Once a pull request has been assigned a single owner, it is their responsibility to review and eventually merge or close the pull request. They manage all ongoing requests on their personal review board, typically prioritizing the ones at the top of the board marked with `waiting-for:review`. If a pull request remains assigned but unreviewed for 14 days, the automation will automatically unassign the reviewer and return the pull request to the review queue.
+
+### Merging Pull Requests
+
+When a pull request has been approved, the way it gets merged depends on who the author is:
+
+- **Outside contributions:** Project area maintainers and core maintainers should merge the pull request themselves once approved, since outside contributors do not have merge access.
+- **Maintainer contributions:** Within maintainer teams, it is common to let the author choose when to merge the pull request themselves, unless there is a reason to merge it immediately. The `waiting-for:merge` label will be applied automatically when the pull request is approved.
+
+### Cross-Area Pull Requests
+
+Some pull requests may require review from multiple project areas. In these cases the most relevant owner should assign themselves and coordinate with the other owners for additional reviews. If the most relevant owner is not clear, this is preferably solved in a discussion among the owners. Frequent conflicts should lead to a discussion whether `CODEOWNERS` should be updated to simplify the review process. If some owners are currently unavailable, other owners can assign themselves to the pull request and bring it to the point where they approve the changes for their area, and then send the pull request back to the review queue.
+
+## Pull Request Automation
+
+Pull request labels and the [review board](https://github.com/orgs/backstage/projects/14) are fully managed by automation. All fields on the review board are computed automatically and **should never be updated manually**. Likewise, pull request labels that are part of the automation should not be added or removed by hand, with one exception described below.
+
+The automation is triggered by pull request events (opened, updated, reopened, labeled, unlabeled), pull request reviews (submitted, dismissed), issue comments, and CI workflow completions. The source code is available in the [`backstage/actions`](https://github.com/backstage/actions) repository, with the workflow definitions in [`.github/workflows/sync_pull-requests-trigger.yml`](.github/workflows/sync_pull-requests-trigger.yml) and [`.github/workflows/sync_pull-requests.yml`](.github/workflows/sync_pull-requests.yml).
+
+### Labels
+
+The `waiting-for:*` status labels, `reviewer-approved` label, and `size:*` labels are all **automated** based on pull request reviews, author activity, and pull request size. See [LABELS.md](./LABELS.md#pull-request-labels) for the meaning of each label.
+
+The only exception is `waiting-for:decision`, which can and should be set **manually**. Use it when discussion or a decision is needed before the pull request can move forward, for example when the approach needs to be discussed in a [SIG](https://github.com/backstage/community/tree/main/sigs) meeting. Once set, this label will **stick until manually removed** — the automation will not override it. Only remove it once a decision has been made and the pull request is ready to proceed.
+
+### Review Board Priority
+
+The priority field on the review board is a number calculated automatically by the automation. It determines the sort order of pull requests on the board. The priority is influenced by several factors:
+
+- **Pull request size** — Smaller pull requests receive higher priority, where the priority reduces significantly for roughly every 500 lines added.
+- **Reviewer approval** — Pull requests with the `reviewer-approved` label receive a large priority boost.
+- **Author review contributions** — Authors who have themselves reviewed other pull requests in the repository receive a priority boost proportional to their review activity.
+- **Draft status** — Draft pull requests receive significantly reduced priority.
+- **CI status** — Pull requests where required CI checks are failing or still pending receive reduced priority. The required checks include DCO, tests, verification, and E2E tests.
+
+### Stale Review Handling
+
+If a pull request with the `waiting-for:review` label has been assigned to a reviewer but has not received a review for 14 days, the automation will automatically unassign the reviewer and return the pull request to the review queue. This ensures that pull requests do not get stuck due to reviewer unavailability.
+
## Code Style
See our code style documented at [STYLE.md](./STYLE.md).
@@ -62,6 +141,37 @@ Some things that changeset should NOT contain are:
- Documentation - changesets can describe new features, but it should not be relied on for documenting them. Documentation should either be placed in [TSDoc](https://tsdoc.org) comments, package README, or [./docs/](./docs/).
- Diffs of internal code, for example mirroring what the pull request changes _inside_ a plugin rather than public surfaces. This is not of interest to the reader of a package changelog. Sometimes, however, a small and concise diff can be used in a changeset to illustrate changes that the user will have to make in _their own_ Backstage installation as part of an upgrade, specifically when breaking changes are made to a package.
+### Backstage UI Changeset Format
+
+Changesets for `@backstage/ui` must follow a standardized format to enable proper documentation generation. See [`.changeset/README.md`](.changeset/README.md#backstage-ui-changeset-format) for the complete guide.
+
+**Required structure:**
+
+```markdown
+---
+'@backstage/ui': patch
+---
+
+Brief one-line summary of the change
+
+Optional detailed description with any markdown content.
+
+**Migration:**
+
+Migration instructions (only for breaking changes)
+
+**Affected components:** Button, ButtonIcon
+```
+
+**Key requirements:**
+
+1. **Affected components marker** - Must end with `**Affected components:**` followed by comma-separated component names
+2. **Migration marker** - Use `**Migration:**` (bold, with colon) for breaking changes only
+3. **No headings** - Never use `##`, `###`, or `####` inside changeset entries (these break semantic structure when the entry becomes a list item in CHANGELOG.md)
+4. **Bold markers** - Use bold text markers, not plain text, for reliable parsing
+
+These markers enable the documentation system to extract metadata for per-component changelogs and separate migration guides.
+
### When is a changeset needed?
In general our changeset feedback bot will take care of informing whether a changeset is needed or not, but there are some edge cases. Whether a changeset is needed depends mostly on what files have been changed, but sometimes also on the kind of change that has been made.
diff --git a/SECURITY.md b/SECURITY.md
index 5c6c8106d5..46f36f0a67 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -8,7 +8,7 @@ See our [Release & Versioning Policy](https://backstage.io/docs/overview/version
Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub.
-If you have questions about a potential vulnerability, please reach out on Discord by asking for a maintainer in the `#support` channel, or via direct message to a maintainer.
+If you have questions about a potential vulnerability, please reach out on Discord by asking for a maintainer in the `#security` channel, or via direct message to a maintainer.
## Remediation and Notification Process
diff --git a/app-config.yaml b/app-config.yaml
index c1a894dcc7..08cd0b693c 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -109,8 +109,12 @@ integrations:
token: ${GITLAB_TOKEN}
### Example for how to add a bitbucket cloud integration
# bitbucketCloud:
+ # # Using API token
# - username: ${BITBUCKET_USERNAME}
# token: ${BITBUCKET_API_TOKEN}
+ # # Using OAuth
+ # - clientId: ${BITBUCKET_CLIENT_ID}
+ # clientSecret: ${BITBUCKET_CLIENT_SECRET}
### Example for how to add your bitbucket server instance using the API:
# - host: server.bitbucket.com
# apiBaseUrl: server.bitbucket.com
@@ -185,15 +189,9 @@ catalog:
target: ../catalog-model/examples/acme-corp.yaml
rules:
- allow: [User, Group]
- # Example component for github-actions and TechDocs
- - type: file
- target: ../../plugins/github-actions/examples/sample.yaml
# Example component for TechDocs
- type: file
target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
- # Example component for ADRs
- - type: file
- target: ../../plugins/adr/examples/component/catalog-info.yaml
# Backstage example templates
- type: file
target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
@@ -220,6 +218,13 @@ auth:
enabled: true
allowedRedirectUriPatterns:
- cursor://*
+ - http://localhost:*
+ - http://127.0.0.1:*
+ experimentalClientIdMetadataDocuments:
+ enabled: true
+ allowedRedirectUriPatterns:
+ - http://127.0.0.1:*
+ - http://localhost:*
### Add auth.keyStore.provider to more granularly control how to store JWK data when running
# the auth-backend.
@@ -311,3 +316,8 @@ auth:
permission:
enabled: true
+
+devTools:
+ scheduledTasks:
+ plugins:
+ - catalog
diff --git a/beps/0001-notifications-system/notifications-architecture.drawio.svg b/beps/0001-notifications-system/notifications-architecture.drawio.svg
index 9c6bb00119..25b27642ca 100644
--- a/beps/0001-notifications-system/notifications-architecture.drawio.svg
+++ b/beps/0001-notifications-system/notifications-architecture.drawio.svg
@@ -1,537 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notification Backend
-
-
-
-
-
- Notification Backend
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Notification Frontend
-
-
-
-
-
- Notification Frontend
-
-
-
-
-
-
-
-
-
-
- Signal Backend
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Signal Frontend
-
-
-
-
-
-
-
-
-
-
-
-
- Plugin X Backend
-
-
-
-
-
-
-
-
-
-
- Plugin X Frontend
-
-
-
-
-
- Plugin X Frontend
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EventsService
-
-
-
-
-
-
-
-
-
-
-
-
- ...
-
-
-
-
-
-
-
-
-
-
- ...
-
-
-
-
-
-
-
-
-
-
- publish event
-
-
-
-
-
-
-
-
-
-
- broadcast
-
-
-
-
-
-
-
-
-
-
- ???
-
-
-
-
-
-
-
-
-
-
- server push
-
-
-
-
-
-
-
-
-
-
- POST
-
-
-
-
-
-
-
-
-
-
-
-
- External System
-
-
-
-
-
-
-
-
-
-
- email/slack/teams, etc.
-
-
-
-
-
- email/slack/teams, etc.
-
-
-
-
-
-
-
-
-
-
- Render
-
-
-
-
-
-
-
-
-
-
- Processor
-
-
-
-
-
-
-
-
-
-
- GET
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
- 3
-
-
-
-
-
-
-
-
-
-
- 1b
-
-
-
-
-
-
-
-
-
-
- 4
-
-
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
-
-
- 6
-
-
-
-
-
-
-
-
-
-
- 7b
-
-
-
-
-
-
-
-
-
-
- 7
-
-
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- Channel Callback
-
-
-
-
-
-
-
-
- Text is not SVG - cannot display
-
-
-
-
+Notification Backend Notification Frontend Signal Backend Signal Frontend Plugin X Backend Plugin X Frontend EventsService ... ... publish event broadcast ??? server push POST External System email/slack/teams, etc. Render Processor GET 1 3 1b 4 5 6 7b 7 2 Channel Callback Text is not SVG - cannot display
\ No newline at end of file
diff --git a/beps/0002-dynamic-frontend-plugins/README.md b/beps/0002-dynamic-frontend-plugins/README.md
index 4787f0fb7a..c783cf052e 100644
--- a/beps/0002-dynamic-frontend-plugins/README.md
+++ b/beps/0002-dynamic-frontend-plugins/README.md
@@ -301,7 +301,7 @@ An example of [integration with scalprum](https://github.com/backstage/backstage
**Dynamic Feature configuration**
-The dynamic remote loading can be added directly into the [`createApp`](https://backstage.io/docs/reference/frontend-defaults.createapp) function.
+The dynamic remote loading can be added directly into the [`createApp`](https://backstage.io/api/stable/functions/_backstage_frontend-defaults.createApp.html) function.
The current `feature` type can be expanded with a `DynamicFrontendFeature` type:
@@ -351,7 +351,7 @@ const scalprum = initialize({
});
```
-Because the [`appLoader`](https://backstage.io/docs/reference/frontend-defaults.createapp) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins.
+Because the [`appLoader`](https://backstage.io/api/stable/functions/_backstage_frontend-defaults.createApp.html) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins.
Initializing the dynamic feature is just a case of mapping the `DynamicFrontendFeature` to `FrontendFeature` via Scalprum:
diff --git a/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg b/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg
index b20b528c35..29d95b44bf 100644
--- a/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg
+++ b/beps/0003-auth-architecture-evolution/token-sequence-cookie.drawio.svg
@@ -1,203 +1 @@
-
-
-
-
-
-
-
-
-
-
-
- cookie flow
-
-
-
-
-
-
-
-
-
-
-
- Browser
-
-
-
-
-
-
-
-
-
-
-
-
- Techdocs
-
- Backend
-
-
-
-
-
- Techdocs...
-
-
-
-
-
-
-
-
-
-
-
-
- GET /cookie
-
-
-
-
-
-
-
-
-
-
-
- set cookie on response
-
-
-
-
-
- set cookie on response
-
-
-
-
-
-
-
-
-
-
-
- browser static content requests
-
-
-
-
-
- browser static content requests
-
-
-
-
-
-
-
-
-
-
- cookie token
-
-
-
-
-
-
-
-
-
-
- cookie token
-
-
-
-
-
-
-
-
-
-
- user identity token
-
-
-
-
-
- user identity token
-
-
-
-
-
-
-
-
-
-
-
-
- acquire cookie token
-
- based on user token
-
-
-
-
-
-
-
- acquire cookie token...
-
-
-
-
-
-
-
-
-
-
-
-
- Text is not SVG - cannot display
-
-
-
-
+cookie flow Browser Techdocs... GET /cookie set cookie on response browser static content requests
browser static content requests cookie token cookie token user identity token acquire cookie token based on user token
acquire cookie token... Text is not SVG - cannot display
\ No newline at end of file
diff --git a/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg b/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg
index f337f8b572..517d2c4d19 100644
--- a/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg
+++ b/beps/0003-auth-architecture-evolution/token-sequence-obo.drawio.svg
@@ -1,203 +1 @@
-
-
-
-
-
-
-
-
-
-
-
- Browser
-
-
-
-
-
-
-
-
-
-
-
-
- Scaffolder
-
- Backend
-
-
-
-
-
- Scaffolder...
-
-
-
-
-
-
-
-
-
-
-
-
-
- acquire service obo token
-
- based on user token
-
-
-
-
-
-
-
- acquire service obo token...
-
-
-
-
-
-
-
-
-
-
-
- POST /tasks
-
-
-
-
-
-
-
-
-
-
-
- success
-
-
-
-
-
-
-
-
-
-
-
- Catalog
-
- Backend
-
-
-
-
-
- Catalog...
-
-
-
-
-
-
-
-
-
-
-
-
- GET /entities/...
-
-
-
-
-
- GET /entities/...
-
-
-
-
-
-
-
-
-
-
-
- template entity
-
-
-
-
-
-
-
-
-
-
- user identity token
-
-
-
-
-
- user identity token
-
-
-
-
-
-
-
-
-
-
- service obo token
-
-
-
-
-
- service obo token
-
-
-
-
-
-
-
-
- Text is not SVG - cannot display
-
-
-
-
+Browser Scaffolder... acquire service obo token based on user token
acquire service obo token... POST /tasks success Catalog... GET /entities/... template entity user identity token service obo token Text is not SVG - cannot display
\ No newline at end of file
diff --git a/contrib/.devcontainer/README.md b/contrib/.devcontainer/README.md
deleted file mode 100644
index 4fca39b3ad..0000000000
--- a/contrib/.devcontainer/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Devcontainer Configuration
-
-[Devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) is a VSCode feature that allows developers
-to share a common workspace definition, by leveraging Docker to run the workspace, and using VSCode Remote Development extension, you can make sure the developer experience is the same across different computers
-
-## Benefits
-
-- Developers only need to have VSCode and Docker installed
-- All dependencies necessary to work in the repository are contained in the docker image
-- Makes it easier for new developers to start contributing to the project
-- No extra setup required
-
-## Known Issues
-
-- Performance is a known issue. Working inside devcontainers require developers to have a powerful computer. See [System Requirements](https://code.visualstudio.com/docs/devcontainers/containers#_system-requirements)
-
-## Usage
-
-Just copy the .devcontainer folder to the root of the directory, and VSCode will automatically detect the configuration and suggest that you re-open the repository using Devcontainer
diff --git a/contrib/.devcontainer/devcontainer.json b/contrib/.devcontainer/devcontainer.json
deleted file mode 100644
index 14e7d7f0ae..0000000000
--- a/contrib/.devcontainer/devcontainer.json
+++ /dev/null
@@ -1,27 +0,0 @@
-// For format details, see https://aka.ms/devcontainer.json. For config options, see the
-// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
-{
- "name": "Base Backstage Workspace",
- "build": { "dockerfile": "Dockerfile" },
- "features": {
- "ghcr.io/devcontainers/features/common-utils:1": {},
- "ghcr.io/devcontainers/features/docker-from-docker:1": {},
- "ghcr.io/devcontainers-contrib/features/mkdocs:2": {}
- },
-
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- "forwardPorts": [3000, 7007],
-
- // Use 'postCreateCommand' to run commands after the container is created.
- "postCreateCommand": "bash .devcontainer/postCreate.sh",
-
- // Configure tool-specific properties.
- "customizations": {
- "vscode": {
- "extensions": ["Intility.vscode-backstage"]
- }
- }
-
- // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
- // "remoteUser": "root"
-}
diff --git a/contrib/.devcontainer/postCreate.sh b/contrib/.devcontainer/postCreate.sh
deleted file mode 100755
index 88f403de5e..0000000000
--- a/contrib/.devcontainer/postCreate.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/bash
-yarn install
-export VIRTUAL_ENV=$HOME/venv
-python3 -m venv $VIRTUAL_ENV
-export PATH="$VIRTUAL_ENV/bin:$PATH"
-python3 -m pip install mkdocs-techdocs-core
\ No newline at end of file
diff --git a/contrib/catalog/ImmediateEntityProvider.ts b/contrib/catalog/ImmediateEntityProvider.ts
index b5580b775a..ba17f19c9d 100644
--- a/contrib/catalog/ImmediateEntityProvider.ts
+++ b/contrib/catalog/ImmediateEntityProvider.ts
@@ -1,3 +1,8 @@
+import {
+ coreServices,
+ createBackendModule,
+ LoggerService,
+} from '@backstage/backend-plugin-api';
import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
@@ -10,50 +15,18 @@ import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
-import { parseEntityYaml } from '@backstage/plugin-catalog-backend';
+import { parseEntityYaml } from '@backstage/plugin-catalog-node';
+import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import bodyParser from 'body-parser';
import express from 'express';
import Router from 'express-promise-router';
import lodash from 'lodash';
-import { Logger } from 'winston';
/**
* An entity provider attached to a router, that lets users perform direct
* manipulation of a set of entities using REST requests.
- *
- * @remarks
- *
- * Installation:
- *
- * Add it to the catalog builder in your
- * `packages/backend/src/plugins/catalog.ts`. Note that it BOTH adds a provider
- * and amends the catalog router:
- *
- * ```
- * const immediate = new ImmediateEntityProvider({
- * logger: env.logger,
- * handleEntity: (deferred) => {
- * // Optionally modify the incoming entity
- * },
- * });
- * builder.addEntityProvider(immediate);
- *
- * // ...
- *
- * return router.use('/immediate', immediate.getRouter());
- * ```
- *
- * API (assume a catalog prefix, e.g. `/api/catalog`):
- *
- * - `POST /immediate/entities`: Accepts a YAML document of entities, and
- * inserts or updates the entities that match that document. Returns 201 OK on
- * success.
- *
- * - `PUT /immediate/entities`: Accepts a YAML document of entities, and
- * replaces the entire set of entities managed by the provider with those
- * entities. Returns 201 OK on success.
*/
-export class ImmediateEntityProvider implements EntityProvider {
+class ImmediateEntityProvider implements EntityProvider {
private connection?: EntityProviderConnection;
private readonly entityValidator: (data: unknown) => Entity;
@@ -76,7 +49,7 @@ export class ImmediateEntityProvider implements EntityProvider {
router.use(bodyParser.raw({ type: '*/*' }));
- router.post('/entities', async (req, res) => {
+ router.post('/immediate/entities', async (req, res) => {
if (!this.connection) {
throw new Error(`Service is not yet initialized`);
}
@@ -89,7 +62,7 @@ export class ImmediateEntityProvider implements EntityProvider {
res.status(201).end();
});
- router.put('/entities', async (req, res) => {
+ router.put('/immediate/entities', async (req, res) => {
if (!this.connection) {
throw new Error(`Service is not yet initialized`);
}
@@ -151,19 +124,59 @@ export class ImmediateEntityProvider implements EntityProvider {
/**
* Options for {@link ImmediateEntityProvider}.
*/
-export interface ImmediateEntityProviderOptions {
+interface ImmediateEntityProviderOptions {
/**
- * The logger to use.
+ * The logger.
*/
- logger: Logger;
+ logger: LoggerService;
/**
- * An optional function to perform adjustments to, or validate, an incoming
- * entity before being stored. It is permitted to modify the deferred entity,
- * but the request is static and has had its body consumed.
+ * An optional callback function to perform adjustments to, or validate, an
+ * incoming entity before being stored. It is permitted to modify the deferred
+ * entity, but the request is static and has had its body consumed.
*/
handleEntity?: (
request: express.Request,
deferred: DeferredEntity,
) => void | Promise;
}
+
+/**
+ * Backend module that installs an immediate entity provider.
+ *
+ * @remarks
+ *
+ * Install it by doing `backend.add(immediateEntityProviderModule)` in your `packages/backend/src/index.ts` file.
+ *
+ * API:
+ *
+ * - `POST /api/catalog/immediate/entities`: Accepts a YAML document of entities, and
+ * inserts or updates the entities that match that document. Returns 201 OK on
+ * success.
+ *
+ * - `PUT /api/catalog/immediate/entities`: Accepts a YAML document of entities, and
+ * replaces the entire set of entities managed by the provider with those
+ * entities. Returns 201 OK on success.
+ */
+export const immediateEntityProviderModule = createBackendModule({
+ pluginId: 'catalog',
+ moduleId: 'immediate-entity-provider',
+ register(env) {
+ env.registerInit({
+ deps: {
+ logger: coreServices.logger,
+ router: coreServices.httpRouter,
+ catalogProcessing: catalogProcessingExtensionPoint,
+ },
+ async init({ logger, router, catalogProcessing }) {
+ const provider = new ImmediateEntityProvider({
+ logger,
+ // add handleEntity here if you need to modify incoming entities before they are stored
+ });
+
+ catalogProcessing.addEntityProvider(provider);
+ router.use(provider.getRouter());
+ },
+ });
+ },
+});
diff --git a/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts b/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts
index 44119947e6..dea29a9f54 100644
--- a/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts
+++ b/contrib/catalog/read-write-split/workspace/packages/backend/src/catalogService.ts
@@ -142,9 +142,13 @@ export class ReadWriteSplitCatalogService implements CatalogService {
}
async getLocations(
- options: CatalogRequestOptions,
+ request: {} | undefined,
+ options: CatalogServiceRequestOptions,
): Promise {
- return this.#catalogRead.getLocations(options);
+ return this.#catalogRead.getLocations(
+ request,
+ await this.#getOptions(options),
+ );
}
async getLocationById(
diff --git a/contrib/docs/tutorials/devcontainer.md b/contrib/docs/tutorials/devcontainer.md
new file mode 100644
index 0000000000..00027de289
--- /dev/null
+++ b/contrib/docs/tutorials/devcontainer.md
@@ -0,0 +1,199 @@
+# Developing Backstage with Dev Containers
+
+This guide will help you get started with developing Backstage using [Dev Containers](https://containers.dev/). Dev
+Containers provide a consistent, containerized development environment that works across different machines and
+operating systems.
+
+## What are Dev Containers?
+
+Dev Containers (Development Containers) allow you to use a Docker container as a full-featured development environment.
+All dependencies, tools, and configurations are defined in the container, so you don't need to install Node.js, Python,
+or other tools on your local machine.
+
+## Prerequisites
+
+Before you begin, make sure you have the following installed:
+
+1. **Docker Desktop** (or Docker Engine on Linux)
+
+- [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
+- [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
+- [Docker Engine for Linux](https://docs.docker.com/engine/install/)
+
+2. **Visual Studio Code (or IntelliJ IDEA Ultimate)**
+
+- [Download VS Code](https://code.visualstudio.com/)
+- [Download IntelliJ IDEA](https://www.jetbrains.com/idea/download/)
+
+**Note**: IntelliJ IDEA Community Edition does not support Dev Containers, so you will need the Ultimate (paid) edition
+to use this feature.
+
+3. **Dev Containers Extension**
+
+**For VS Code**:
+
+- Install from
+ the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
+- Or open VS Code and search for "Dev Containers" in the Extensions view (`Ctrl/Cmd + Shift + X`)
+
+**For IntelliJ IDEA**:
+
+- Dev Containers support is available in IntelliJ IDEA Ultimate edition out of the box,
+ no additional plugin is required
+
+### System Requirements
+
+The dev container requires:
+
+- **CPU**: At least 2 cores
+- **Memory**: At least 4GB of RAM
+- **Storage**: At least 32GB of free disk space
+
+## Getting Started
+
+### Option 1: Open the Repository in a Dev Container
+
+1. **Fork and clone the Backstage repository** (if you haven't already):
+
+- Go to https://github.com/backstage/backstage and click "Fork"
+- Clone your fork:
+
+ ```shell
+ git clone https://github.com//backstage.git
+ cd backstage
+ ```
+
+2. **Open in VS Code**:
+
+```shell
+code .
+```
+
+3. **Reopen in Container**:
+
+- VS Code should detect the `.devcontainer` folder and show a notification asking if you want to reopen the folder in
+ a container
+- Click "Reopen in Container"
+- Alternatively, open the Command Palette (`Ctrl/Cmd + Shift + P`) and run "Dev Containers: Reopen in Container"
+
+4. **Wait for the container to build**:
+
+- The first time you open the container, it will take several minutes to build the Docker image and install all
+ dependencies
+- VS Code will show progress in the bottom-right corner
+- The setup script will automatically run `yarn install` and install Python dependencies for TechDocs
+
+5. **You're ready to develop!**
+
+- Once the setup is complete, you'll see a welcome message in the terminal
+- Open a new terminal in VS Code (`Ctrl/Cmd + ~`) and run `yarn start` to launch Backstage
+
+### Option 2: Build and Open from Command Palette
+
+1. Open VS Code with the Backstage repository
+2. Open the Command Palette (`Ctrl/Cmd + Shift + P`)
+3. Run "Dev Containers: Rebuild and Reopen in Container"
+
+## What's Included
+
+The dev container comes pre-configured with:
+
+### Tools and Runtime
+
+- **Node.js**: Version suitable for Backstage development
+- **Python 3**: For building and previewing TechDocs
+- **Yarn**: Package manager
+- **Git**: Version control
+- **Chromium**: For running end-to-end tests
+- **MkDocs with TechDocs core**: For TechDocs development
+
+### VS Code Extensions
+
+The following extensions are automatically installed:
+
+- **Prettier**: Code formatter (format on save enabled)
+- **ESLint**: JavaScript/TypeScript linter
+- **Backstage**: Official Backstage extension for syntax highlighting and snippets
+
+### Port Forwarding
+
+The following ports are automatically forwarded to your local machine:
+
+- **3000**: Frontend application
+- **7007**: Backend API
+
+Additional ports, such as **9464** for the metrics endpoint, can be forwarded manually from the Ports panel in your
+editor, or may be auto-forwarded when detected.
+
+## Using the Dev Container
+
+### Starting Backstage
+
+Once the container is ready and dependencies are installed:
+
+```shell
+yarn start
+```
+
+This will start both the frontend (on port 3000) and backend (on port 7007). Access Backstage at:
+
+- **Frontend**: http://localhost:3000
+- **Backend API**: http://localhost:7007
+
+> **Note**: The first time you start Backstage, the backend may take a minute to fully start. You might need to refresh
+> your browser once the backend is ready.
+
+## Troubleshooting
+
+### Container build fails
+
+If the container fails to build, try:
+
+1. Make sure Docker Desktop is running
+2. Rebuild the container: Command Palette → "Dev Containers: Rebuild Container"
+3. Check Docker has enough resources allocated (CPU, memory, disk space)
+
+### Port already in use
+
+If ports 3000 or 7007 are already in use on your local machine:
+
+1. Stop any other Backstage instances or services using those ports
+2. Reload the VS Code window: Command Palette → "Developer: Reload Window"
+
+### Dependencies not installed
+
+If `yarn start` fails with missing dependencies:
+
+1. Run `yarn install` manually in the terminal
+2. Or rebuild the container to run the setup script again
+
+### Performance issues
+
+If the dev container feels slow:
+
+1. Increase Docker Desktop's resource allocation (Settings → Resources)
+2. Make sure your Docker disk image isn't full
+3. On Windows, ensure WSL 2 is enabled for better performance
+
+### Cannot connect to backend
+
+If the frontend shows connection errors:
+
+1. Wait a minute for the backend to fully start (check terminal logs)
+2. Ensure port 7007 is properly forwarded (check Ports panel in VS Code)
+3. Refresh your browser
+
+## Additional Resources
+
+- [Dev Containers Documentation](https://containers.dev/)
+- [VS Code Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers)
+- [Backstage Getting Started Guide](https://backstage.io/docs/getting-started/)
+- [Backstage Contributing Guide](../../../CONTRIBUTING.md)
+
+## Need Help?
+
+If you encounter issues not covered in this guide:
+
+- Check the [Backstage Discord](https://discord.gg/backstage) community
+- Search or create an issue on [GitHub](https://github.com/backstage/backstage/issues)
+- Review the [Backstage documentation](https://backstage.io/docs/)
diff --git a/docs-ui/.eslintrc.json b/docs-ui/.eslintrc.json
deleted file mode 100644
index be99640f75..0000000000
--- a/docs-ui/.eslintrc.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "extends": ["next/core-web-vitals", "next/typescript"],
- "rules": {
- "notice/notice": "off",
- "react/forbid-elements": "off",
- "jsx-a11y/alt-text": "off"
- }
-}
diff --git a/docs-ui/eslint.config.mjs b/docs-ui/eslint.config.mjs
new file mode 100644
index 0000000000..2cb66fd509
--- /dev/null
+++ b/docs-ui/eslint.config.mjs
@@ -0,0 +1,16 @@
+import { defineConfig, globalIgnores } from 'eslint/config';
+import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
+
+const eslintConfig = defineConfig([
+ ...nextCoreWebVitals,
+ globalIgnores(['.next/**', 'dist/**', 'node_modules/**']),
+ {
+ rules: {
+ 'notice/notice': 'off',
+ 'react/forbid-elements': 'off',
+ 'jsx-a11y/alt-text': 'off',
+ },
+ },
+]);
+
+export default eslintConfig;
diff --git a/docs-ui/package.json b/docs-ui/package.json
index 086cfb479e..5cb0aa3e44 100644
--- a/docs-ui/package.json
+++ b/docs-ui/package.json
@@ -3,19 +3,16 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "prebuild": "yarn sync:css",
"build": "next build",
- "lint": "next lint",
- "prestart": "yarn sync:css",
+ "lint": "eslint .",
"start": "next dev",
"sync:changelog": "node scripts/sync-changelog.mjs",
"sync:changelog:dry-run": "node scripts/sync-changelog.mjs --dry-run",
- "sync:css": "node scripts/sync-css.js",
- "sync:css:watch": "node scripts/sync-css.js --watch"
+ "sync:changelog:force": "node scripts/sync-changelog.mjs --force"
},
"resolutions": {
- "@types/react": "19.1.9",
- "@types/react-dom": "19.1.7"
+ "@types/react": "19.2.10",
+ "@types/react-dom": "19.2.3"
},
"dependencies": {
"@base-ui-components/react": "^1.0.0-beta.4",
@@ -24,33 +21,31 @@
"@lezer/highlight": "^1.2.1",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
- "@next/mdx": "15.5.6",
+ "@next/mdx": "16.1.6",
"@remixicon/react": "^4.6.0",
- "@storybook/react": "^8.6.12",
"@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"clsx": "^2.1.1",
"html-react-parser": "^5.2.5",
"motion": "^12.4.1",
- "next": "15.4.8",
+ "next": "16.1.6",
"next-mdx-remote-client": "^2.1.2",
"prop-types": "^15.8.1",
- "react": "19.1.1",
- "react-dom": "19.1.1",
- "shiki": "^3.13.0",
- "storybook": "^8.6.12"
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "shiki": "^3.13.0"
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
"@shikijs/transformers": "^3.13.0",
"@types/mdx": "^2.0.13",
"@types/node": "^22.13.14",
- "@types/react": "19.1.9",
- "@types/react-dom": "19.1.7",
- "chokidar": "^3.6.0",
- "eslint": "^8",
- "eslint-config-next": "15.5.6",
- "lightningcss": "^1.28.2",
+ "@types/react": "19.2.10",
+ "@types/react-dom": "19.2.3",
+ "eslint": "^9",
+ "eslint-config-next": "16.1.6",
+ "postcss": "^8.5.6",
+ "postcss-import": "^16.1.1",
"typescript": "^5",
"unified": "^11.0.4"
}
diff --git a/docs-ui/postcss.config.mjs b/docs-ui/postcss.config.mjs
new file mode 100644
index 0000000000..bd2bb9bb7c
--- /dev/null
+++ b/docs-ui/postcss.config.mjs
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ 'postcss-import': {},
+ },
+};
diff --git a/docs-ui/public/theme-backstage.css b/docs-ui/public/theme-backstage.css
deleted file mode 100644
index 59a5b986be..0000000000
--- a/docs-ui/public/theme-backstage.css
+++ /dev/null
@@ -1 +0,0 @@
-@layer tokens{: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}}@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}:where(a){color:inherit;text-decoration:none}@keyframes pulse{50%{opacity:.5}}body{background-color:var(--bui-bg);color:var(--bui-fg-primary);font-family:var(--bui-font-regular);font-weight:var(--bui-font-weight-regular);font-size:var(--bui-font-size-3);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}@layer components;@layer utilities{.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(--ml-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(--ml-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(--ml-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(--ml-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(--ml-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(--mr-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(--mr-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(--mr-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(--mr-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(--mr-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(--mt-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(--mt-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(--mt-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(--mt-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(--mt-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(--mb-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(--mb-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(--mb-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(--mb-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(--mb-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(--my-xs);margin-bottom:var(--my-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(--my-sm);margin-bottom:var(--my-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(--my-md);margin-bottom:var(--my-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(--my-lg);margin-bottom:var(--my-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(--my-xl);margin-bottom:var(--my-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(--mx-xs);margin-right:var(--mx-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(--mx-sm);margin-right:var(--mx-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(--mx-md);margin-right:var(--mx-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(--mx-lg);margin-right:var(--mx-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(--mx-xl);margin-right:var(--mx-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}}}
\ No newline at end of file
diff --git a/docs-ui/public/theme-spotify.css b/docs-ui/public/theme-spotify.css
deleted file mode 100644
index b0bf517701..0000000000
--- a/docs-ui/public/theme-spotify.css
+++ /dev/null
@@ -1 +0,0 @@
-@font-face{font-family:CircularSpTitle;font-weight:900;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Black-4588c99025b967475c31695b0743dd1a.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Black-506746f387a26f25aa3d023b3e501d34.woff)format("woff")}@font-face{font-family:CircularSpTitle;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-b2586b06a2e1522e9d879d84c2792a58.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSpTitle-Bold-1624fb2df28c20d7203d7fb86ce8b481.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:400;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Book-a00e99ef9996a3a157fb6b746856d04f.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Book-3f73da7d35bd81c706bce7bbb84964de.woff)format("woff")}@font-face{font-family:CircularSp;font-weight:700;font-display:swap;unicode-range:U+0,U+D,U+20-7E,U+A0-17E,U+18F,U+192,U+1A0-1A1,U+1AF-1B0,U+1B5-1B6,U+1C4-1C6,U+1F1-1F3,U+1FA-1FF,U+218-21B,U+237,U+259,U+2BB-2BC,U+2C6-2C7,U+2C9,U+2D8-2DD,U+300-301,U+303,U+309,U+323,U+394,U+3A9,U+3BC,U+3C0,U+1E80-1E85,U+1E8A-1E8B,U+1EA0-1EF9,U+1FD6,U+2007-2008,U+200B,U+2010-2011,U+2013-2014,U+2018-201A,U+201C-201E,U+2020-2022,U+2026,U+2030,U+2032-2033,U+2039-203A,U+2042,U+2044,U+2051,U+2070,U+2074-2079,U+2080-2089,U+20AB-20AC,U+2113,U+2117,U+2122,U+2126,U+2160-2169,U+216C-216F,U+2190-2193,U+2196-2199,U+21A9,U+21B0-21B5,U+21C6,U+2202,U+2206,U+220F,U+2211-2212,U+2215,U+2219-221A,U+221E,U+222B,U+2248,U+2260,U+2264-2265,U+22C5,U+24C5,U+25A0-25A1,U+25AF,U+25B2-25B3,U+25CA-25CB,U+25CF,U+262E,U+2713,U+2715,U+2780-2788,U+E000,U+E002,U+F6C3,U+FB00-FB04,U+FEFF,U+FF0C,U+FF0E,U+FF1A-FF1B,U+FFFF;src:url(https://encore.scdn.co/fonts/CircularSp-Bold-602e7aefc706aa36c6ec1324b9bbc461.woff2)format("woff2"),url(https://encore.scdn.co/fonts/CircularSp-Bold-856afe2da4ba4e61239b129e2c16d633.woff)format("woff")}[data-theme-name=spotify]{--bui-font-text:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-title:CircularSpTitle,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;--bui-font-regular:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva;& .bui-Button{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3)}& .bui-ButtonIcon{padding:0}& .bui-MenuPopup{box-sizing:border-box;max-width:21.25rem}& .bui-MenuSubmenuTrigger,& .bui-MenuItem{height:auto;min-height:2rem}& .bui-Text{font-family:var(--bui-font-text)}& .bui-Heading{font-family:var(--bui-font-title)}& .bui-TableRow{border-radius:var(--bui-radius-4);border:none}& .bui-TableRow:hover td:first-child{border-top-left-radius:var(--bui-radius-2);border-bottom-left-radius:var(--bui-radius-2)}& .bui-TableRow:hover td:last-child{border-top-right-radius:var(--bui-radius-2);border-bottom-right-radius:var(--bui-radius-2)}& .bui-TableBody:before,& .bui-TableBody:after{line-height:var(--bui-space-1);content:"";display:block}& .bui-TableHeader .bui-TableRow{border-bottom:1px solid var(--bui-border)}& .bui-TableHead{font-size:var(--bui-font-size-2);color:var(--bui-fg-secondary);font-weight:var(--bui-font-weight-regular)}& .bui-HeaderToolbar{padding-top:var(--bui-space-2);padding-inline:var(--bui-space-2)}& .bui-HeaderToolbarWrapper{border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-3);border:none}& .bui-HeaderToolbarControls{right:calc(var(--bui-space-3) - 1px)}& .bui-HeaderTabsWrapper{margin-top:var(--bui-space-2);margin-inline:var(--bui-space-2);border-radius:var(--bui-radius-3);padding-inline:var(--bui-space-1);border:none}& .bui-Input{border-radius:var(--bui-radius-3)}& .bui-Tag{border-radius:var(--bui-radius-full)}}[data-theme-mode=light][data-theme-name=spotify]{--bui-bg:var(--bui-gray-1);--bui-bg-surface-1:var(--bui-white);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:var(--bui-gray-2);--bui-fg-primary:var(--bui-black);--bui-fg-secondary:var(--bui-gray-7);--bui-fg-solid:var(--bui-black);--bui-border:var(--bui-gray-3);--bui-border-hover:#0000004d;--bui-border-pressed:#00000080;--bui-border-disabled:#0000001a;--bui-border-danger:#f87a7a;--bui-border-warning:#e36d05;--bui-border-success:#53db83;--bui-ring:#0003;& .bui-HeaderToolbarWrapper,& .bui-HeaderTabsWrapper{border:1px solid var(--bui-border)}}[data-theme-mode=dark][data-theme-name=spotify]{--bui-bg:var(--bui-black);--bui-bg-surface-1:var(--bui-gray-1);--bui-bg-surface-2:var(--bui-gray-2);--bui-bg-solid:#1ed760;--bui-bg-solid-hover:#3be477;--bui-bg-solid-pressed:#1abc54;--bui-bg-solid-disabled:#0f6c30;--bui-bg-tint:#242424;--bui-bg-tint-hover:#202020;--bui-bg-tint-pressed:#292929;--bui-bg-tint-disabled:#fffc;--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:var(--bui-white);--bui-fg-link-hover:var(--bui-white);--bui-fg-disabled:var(--bui-gray-5);--bui-fg-solid:var(--bui-black);--bui-fg-solid-disabled:#083618;--bui-fg-tint:var(--bui-white);--bui-fg-tint-disabled:var(--bui-gray-5);--bui-fg-danger:#e22b2b;--bui-fg-warning:#e36d05;--bui-fg-success:#1db954;--bui-border:var(--bui-gray-3);--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:#fff3}
\ No newline at end of file
diff --git a/docs-ui/scripts/sync-changelog.mjs b/docs-ui/scripts/sync-changelog.mjs
index 5eab3616dd..fa59db741b 100644
--- a/docs-ui/scripts/sync-changelog.mjs
+++ b/docs-ui/scripts/sync-changelog.mjs
@@ -1,5 +1,4 @@
#!/usr/bin/env node
-/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import path from 'path';
@@ -13,22 +12,18 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
- * Compare two semantic versions.
+ * Compare two semantic versions for sorting.
* Returns: 1 if versionA > versionB, -1 if versionA < versionB, 0 if equal
- * Handles pre-release versions (e.g., "0.9.0-next.2")
+ * Example: compareVersions("0.11.0", "0.10.0") => 1
*/
function compareVersions(versionA, versionB) {
// Strip 'v' prefix if present
versionA = versionA.replace(/^v/, '');
versionB = versionB.replace(/^v/, '');
- // Split into [major.minor.patch, prerelease]
- const [versionABase, versionAPre] = versionA.split('-');
- const [versionBBase, versionBPre] = versionB.split('-');
-
- // Compare base versions
- const versionAParts = versionABase.split('.').map(Number);
- const versionBParts = versionBBase.split('.').map(Number);
+ // Compare major.minor.patch versions
+ const versionAParts = versionA.split('.').map(Number);
+ const versionBParts = versionB.split('.').map(Number);
for (
let i = 0;
@@ -42,13 +37,7 @@ function compareVersions(versionA, versionB) {
if (versionAPart < versionBPart) return -1;
}
- // If base versions equal, compare pre-release
- if (!versionAPre && !versionBPre) return 0;
- if (!versionAPre) return 1; // versionA is release, versionB is pre-release
- if (!versionBPre) return -1; // versionA is pre-release, versionB is release
-
- // Both have pre-release, compare them
- return versionAPre.localeCompare(versionBPre);
+ return 0;
}
/**
@@ -58,15 +47,6 @@ function isNewerVersion(versionA, versionB) {
return compareVersions(versionA, versionB) > 0;
}
-/**
- * Normalize version to base version (remove pre-release suffix)
- * "0.9.0-next.3" → "0.9.0"
- * "0.9.0" → "0.9.0"
- */
-function normalizeVersion(version) {
- return version.split('-')[0];
-}
-
/**
* Read the existing changelog.ts and find the highest version number
* by scanning the changelogs/ directory for version files
@@ -169,6 +149,7 @@ function getValidComponents(changelogPath) {
/**
* Parse CHANGELOG.md and extract entries newer than sinceVersion
+ * Only processes main release versions (e.g., 0.11.0), skips pre-releases (e.g., 0.11.0-next.1)
*/
async function parseChangelogMd(changelogPath, sinceVersion, validComponents) {
const content = fs.readFileSync(changelogPath, 'utf-8');
@@ -181,14 +162,17 @@ async function parseChangelogMd(changelogPath, sinceVersion, validComponents) {
// Walk through the markdown AST
async function walk(node, depth = 0) {
- // Version headers (## 0.9.0)
+ // Version headers (## 0.11.0)
if (node.type === 'heading' && node.depth === 2) {
const versionText = extractText(node).trim();
- // Validate that this is actually a version number (X.Y.Z or X.Y.Z-pre.N)
- // Skip headings that are just markdown content within changelog entries
- if (/^\d+\.\d+\.\d+/.test(versionText)) {
+ // Only process main release versions (X.Y.Z format)
+ if (/^\d+\.\d+\.\d+$/.test(versionText)) {
currentVersion = versionText;
currentSection = null;
+ } else {
+ // Skip pre-release versions (e.g., 0.11.0-next.1) and non-version headings
+ currentVersion = null;
+ currentSection = null;
}
return;
}
@@ -301,9 +285,11 @@ async function parseListItem(
// Remove the commit SHA prefix from markdown
let description = fullMarkdown.replace(/^-?\s*[a-f0-9]+:\s*/, '').trim();
- // Auto-detect components from "Affected components:" line
+ // Extract components using bold marker (standard format)
let components = [];
- const componentMatch = description.match(/Affected components?:\s*([^\n]+)/i);
+ const componentMatch = description.match(
+ /\*\*Affected components:\*\*\s*([^\n]+)/,
+ );
if (componentMatch) {
const componentNames = componentMatch[1]
.split(',')
@@ -314,52 +300,64 @@ async function parseListItem(
.map(name => mapComponentName(name, validComponents))
.filter(Boolean);
- // Optionally strip "Affected components:" line from description
+ // Strip "**Affected components:**" line from description
description = description
- .replace(/\n*Affected components?:[ \t]*[^\n]+/i, '')
+ .replace(/\n*\*\*Affected components:\*\*[ \t]*[^\n]+/g, '')
+ .trim();
+ }
+
+ // Extract migration notes using bold marker (standard format)
+ let migration = null;
+ const migrationMatch = description.match(
+ /\*\*Migration:\*\*\s*\n([\s\S]+?)(?=\n\s*$|$)/,
+ );
+ if (migrationMatch) {
+ // Clean up indentation from list format (remove leading 2 spaces from each line)
+ migration = migrationMatch[1]
+ .split('\n')
+ .map(line => line.replace(/^ /, ''))
+ .join('\n')
+ .trim();
+ // Strip migration section from description
+ description = description
+ .replace(/\n*\*\*Migration:\*\*[\s\S]+$/, '')
.trim();
}
const prs = []; // Will be populated later by fetchPRNumbers()
- // Infer type from section and description
- const type = inferChangeType(section, description, version);
+ // Check if this is a breaking change
+ const breaking = isBreakingChange(section, description, version);
return {
- version: normalizeVersion(version),
+ version,
section,
commitSha,
description,
components,
prs,
- type,
+ breaking,
+ migration,
};
}
/**
- * Infer change type from section and description
+ * Determine if a change is breaking based on semver rules
+ * Breaking change rules (semver):
+ * - version >= 1.0.0: Major changes are breaking
+ * - version < 1.0.0: Major and Minor changes are breaking
*/
-function inferChangeType(section, description, version) {
- const isPre1 = version.startsWith('0.');
+function isBreakingChange(section, description, version) {
+ // Parse version to determine breaking change rules based on semver
+ const [major] = version.split('.').map(Number);
- // Check description keywords first
- if (description.match(/^(New|Add(ed)?)\s/i)) {
- return 'new';
- }
- if (description.includes('BREAKING')) {
- return 'breaking';
+ // Version >= 1.0.0: Only Major Changes are breaking
+ if (major >= 1) {
+ return section === 'Major Changes';
}
- // Infer from section
- if (section === 'Minor Changes') {
- return isPre1 ? 'breaking' : undefined;
- } else if (section === 'Major Changes') {
- return 'breaking';
- } else if (section === 'Patch Changes') {
- return 'fix';
- }
-
- return undefined;
+ // Version < 1.0.0: Both Major and Minor Changes are breaking
+ return section === 'Major Changes' || section === 'Minor Changes';
}
/**
@@ -414,8 +412,13 @@ async function fetchPRNumbers(entries, dryRun = false) {
/**
* Generate per-version changelog files in changelogs/ directory
*/
-async function generateVersionFiles(entries, changelogsDir, dryRun = false) {
- // Group entries by normalized version
+async function generateVersionFiles(
+ entries,
+ changelogsDir,
+ dryRun = false,
+ force = false,
+) {
+ // Group entries by version
const byVersion = {};
entries.forEach(entry => {
const version = entry.version;
@@ -434,8 +437,8 @@ async function generateVersionFiles(entries, changelogsDir, dryRun = false) {
const filePath = path.join(changelogsDir, fileName);
const varName = `changelog_${version.replace(/\./g, '_')}`;
- // Check if file already exists - skip if it does
- if (fs.existsSync(filePath)) {
+ // Check if file already exists - skip if it does (unless force flag)
+ if (fs.existsSync(filePath) && !force) {
skippedVersions.push(version);
// Still add to versionFiles array so main changelog.ts can import it
versionFiles.push({
@@ -456,7 +459,7 @@ async function generateVersionFiles(entries, changelogsDir, dryRun = false) {
.map(c => `'${c}'`)
.join(', ')}]`;
const prsStr = `[${entry.prs.map(pr => `'${pr}'`).join(', ')}]`;
- const typeStr = entry.type ? `type: '${entry.type}',` : '';
+ const breakingStr = entry.breaking ? `breaking: true,` : '';
const shaStr = entry.commitSha
? `commitSha: '${entry.commitSha}',`
: '';
@@ -467,12 +470,20 @@ async function generateVersionFiles(entries, changelogsDir, dryRun = false) {
.replace(/`/g, '\\`')
.replace(/\${/g, '\\${');
+ // Escape migration notes if present
+ const migrationStr = entry.migration
+ ? `migration: \`${entry.migration
+ .replace(/\\/g, '\\\\')
+ .replace(/`/g, '\\`')
+ .replace(/\${/g, '\\${')}\`,\n `
+ : '';
+
return ` {
components: ${componentsStr},
version: '${entry.version}',
prs: ${prsStr},
description: \`${descEscaped}\`,
- ${typeStr}
+ ${migrationStr}${breakingStr}
${shaStr}
}`;
})
@@ -802,6 +813,7 @@ async function findPRNumber(commitSha) {
async function main() {
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
+ const force = args.includes('--force');
const changelogTsPath = path.join(__dirname, '../src/utils/changelog.ts');
const changelogMdPath = path.join(
@@ -818,10 +830,15 @@ async function main() {
}
console.log('📋 Syncing UI component changelogs...\n');
+ if (force) {
+ console.log('⚠️ Force mode: Will overwrite existing version files\n');
+ }
- // Get last synced version
- const lastVersion = getLastSyncedVersion(changelogTsPath);
- console.log(`Last synced version: ${lastVersion || '(none)'}`);
+ // Get last synced version (null if force mode to process all)
+ const lastVersion = force ? null : getLastSyncedVersion(changelogTsPath);
+ console.log(
+ `Last synced version: ${lastVersion || '(none - processing all versions)'}`,
+ );
// Get valid components
const validComponents = getValidComponents(changelogTsPath);
@@ -834,26 +851,26 @@ async function main() {
lastVersion,
validComponents,
);
- console.log(`Found ${allEntries.length} total entries since ${lastVersion}`);
+ console.log(
+ `Found ${allEntries.length} total entries${
+ force ? '' : ` since ${lastVersion}`
+ }`,
+ );
// Read existing changelog content for duplicate detection
const existingContent = fs.readFileSync(changelogTsPath, 'utf-8');
// Filter to only new, non-duplicate entries
- const relevantEntries = allEntries.filter(e => {
- const hasComponents = e.components.length > 0 || e.components.length === 0;
- const notDuplicate = !isDuplicate(e, existingContent);
- return hasComponents && notDuplicate;
- });
+ const relevantEntries = allEntries.filter(
+ e => !isDuplicate(e, existingContent),
+ );
const duplicatesCount = allEntries.length - relevantEntries.length;
if (duplicatesCount > 0) {
console.log(`Skipped ${duplicatesCount} duplicate entries`);
}
- console.log(
- `Relevant entries (with or without components): ${relevantEntries.length}`,
- );
+ console.log(`Relevant entries: ${relevantEntries.length}`);
if (relevantEntries.length === 0) {
console.log('\n✅ No new entries to sync');
@@ -920,6 +937,7 @@ async function main() {
relevantEntries,
changelogsDir,
dryRun,
+ force,
);
// Generate main changelog.ts
diff --git a/docs-ui/scripts/sync-css.js b/docs-ui/scripts/sync-css.js
deleted file mode 100644
index 4763fdecc1..0000000000
--- a/docs-ui/scripts/sync-css.js
+++ /dev/null
@@ -1,149 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const { bundle } = require('lightningcss');
-const chokidar = require('chokidar');
-
-// Configuration
-const config = {
- UIPath: '../../packages/ui',
- publicPath: '../public',
- files: [
- {
- source: 'css/styles.css',
- destination: 'theme-backstage.css',
- name: 'Main Styles',
- },
- {
- source: '../../.storybook/themes/spotify.css',
- destination: 'theme-spotify.css',
- name: 'Spotify Theme',
- },
- ],
-};
-
-class CSSSync {
- constructor() {
- this.UIPath = path.resolve(__dirname, config.UIPath);
- this.publicPath = path.resolve(__dirname, config.publicPath);
- this.isWatching = process.argv.includes('--watch');
- }
-
- async syncFile(fileConfig) {
- const sourcePath = path.join(this.UIPath, fileConfig.source);
- const destPath = path.join(this.publicPath, fileConfig.destination);
-
- try {
- // Check if source file exists
- if (!fs.existsSync(sourcePath)) {
- console.warn(`⚠️ Source file not found: ${sourcePath}`);
- return false;
- }
-
- // Ensure destination directory exists
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
-
- // Bundle and optimize CSS
- const result = await bundle({
- filename: sourcePath,
- minify: true,
- });
-
- // Write to destination
- fs.writeFileSync(destPath, result.code);
-
- console.log(
- `✅ ${fileConfig.name}: ${fileConfig.source} → ${fileConfig.destination}`,
- );
- return true;
- } catch (error) {
- console.error(`❌ Error syncing ${fileConfig.name}:`, error.message);
- return false;
- }
- }
-
- async syncAll() {
- console.log('🔄 Syncing CSS files...\n');
-
- let successCount = 0;
- for (const fileConfig of config.files) {
- if (await this.syncFile(fileConfig)) {
- successCount++;
- }
- }
-
- console.log(
- `\n✨ Synced ${successCount}/${config.files.length} CSS files successfully!`,
- );
-
- if (successCount > 0) {
- console.log('\n📁 Available CSS files in public/:');
- config.files.forEach(file => {
- const destPath = path.join(this.publicPath, file.destination);
- if (fs.existsSync(destPath)) {
- const stats = fs.statSync(destPath);
- const size = (stats.size / 1024).toFixed(2);
- console.log(` • ${file.destination} (${size} KB)`);
- }
- });
- }
- }
-
- startWatching() {
- console.log('👀 Watching for CSS changes...\n');
-
- // Watch all source files
- const watchPaths = config.files.map(file =>
- path.join(this.UIPath, file.source),
- );
-
- const watcher = chokidar.watch(watchPaths, {
- ignored: /node_modules/,
- persistent: true,
- });
-
- watcher.on('change', async filePath => {
- console.log(
- `\n🔄 Change detected: ${path.relative(this.UIPath, filePath)}`,
- );
-
- // Find which file changed and sync it
- const fileConfig = config.files.find(file =>
- filePath.endsWith(file.source.replace(/\//g, path.sep)),
- );
-
- if (fileConfig) {
- await this.syncFile(fileConfig);
- }
- });
-
- watcher.on('error', error => console.error('❌ Watch error:', error));
-
- // Handle process termination
- process.on('SIGINT', () => {
- console.log('\n👋 Stopping CSS sync...');
- watcher.close();
- process.exit(0);
- });
- }
-
- async run() {
- console.log('🎨 BUI CSS Sync Tool\n');
- console.log(`📂 BUI path: ${this.UIPath}`);
- console.log(`📂 Public path: ${this.publicPath}\n`);
-
- // Initial sync
- await this.syncAll();
-
- // Watch for changes if requested
- if (this.isWatching) {
- this.startWatching();
- }
- }
-}
-
-// Run the sync tool
-const cssSync = new CSSSync();
-cssSync.run().catch(error => {
- console.error('❌ CSS Sync failed:', error);
- process.exit(1);
-});
diff --git a/docs-ui/src/app/components/[slug]/page.tsx b/docs-ui/src/app/components/[slug]/page.tsx
deleted file mode 100644
index 1422334471..0000000000
--- a/docs-ui/src/app/components/[slug]/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { components, layoutComponents } from '@/utils/data';
-
-export default async function Page({
- params,
-}: {
- params: Promise<{ slug: string }>;
-}) {
- const { slug } = await params;
-
- const { default: Component } = await import(`@/content/${slug}.mdx`);
-
- return ;
-}
-
-export function generateStaticParams() {
- const list = [...components, ...layoutComponents];
-
- return list.map(component => ({
- slug: component.slug,
- }));
-}
-
-export const dynamicParams = false;
diff --git a/docs-ui/src/app/components/accordion/components.tsx b/docs-ui/src/app/components/accordion/components.tsx
new file mode 100644
index 0000000000..95b312d17c
--- /dev/null
+++ b/docs-ui/src/app/components/accordion/components.tsx
@@ -0,0 +1,130 @@
+'use client';
+
+import {
+ Accordion,
+ AccordionTrigger,
+ AccordionPanel,
+ AccordionGroup,
+} from '../../../../../packages/ui/src/components/Accordion/Accordion';
+import { Box } from '../../../../../packages/ui/src/components/Box/Box';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+
+const Content = () => (
+
+
+ It's the edge of the world and all of Western civilization
+
+
+ The sun may rise in the East, at least it settled in a final location
+
+
+ It's understood that Hollywood sells Californication
+
+
+);
+
+export const Default = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const WithSubtitle = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const CustomTrigger = () => {
+ return (
+
+
+
+
+ Custom Multi-line Trigger
+
+
+ Click to expand additional details and configuration options
+
+
+
+
+
+
+
+ );
+};
+
+export const DefaultExpanded = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const GroupSingleOpen = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const GroupMultipleOpen = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/content/accordion.mdx b/docs-ui/src/app/components/accordion/page.mdx
similarity index 52%
rename from docs-ui/src/content/accordion.mdx
rename to docs-ui/src/app/components/accordion/page.mdx
index fc4bad1f8e..486c93ec80 100644
--- a/docs-ui/src/content/accordion.mdx
+++ b/docs-ui/src/app/components/accordion/page.mdx
@@ -1,35 +1,51 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { AccordionSnippet } from '@/snippets/stories-snippets';
import {
accordionPropDefs,
accordionTriggerPropDefs,
accordionPanelPropDefs,
accordionGroupPropDefs,
+} from './props-definition';
+import {
accordionUsageSnippet,
- accordionWithSubtitleSnippet,
- accordionCustomTriggerSnippet,
- accordionDefaultExpandedSnippet,
- accordionGroupSingleOpenSnippet,
- accordionGroupMultipleOpenSnippet,
-} from './accordion.props';
+ defaultSnippet,
+ withSubtitleSnippet,
+ customTriggerSnippet,
+ defaultExpandedSnippet,
+ groupSingleOpenSnippet,
+ groupMultipleOpenSnippet,
+} from './snippets';
+import {
+ Default,
+ WithSubtitle,
+ CustomTrigger,
+ DefaultExpanded,
+ GroupSingleOpen,
+ GroupMultipleOpen,
+} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { AccordionDefinition } from '../utils/definitions';
+import { AccordionDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ disclosure: 'https://react-aria.adobe.com/Disclosure',
+ disclosureGroup: 'https://react-aria.adobe.com/DisclosureGroup',
+};
}
- code={accordionUsageSnippet}
+ preview={ }
+ code={defaultSnippet}
/>
## Usage
@@ -44,6 +60,8 @@ Root container for the accordion. Renders a `` element.
+
+
### AccordionTrigger
Trigger component with built-in animated chevron icon. Renders a heading element (defaults to `
`, configurable via `level` prop) wrapping a ``.
@@ -62,66 +80,69 @@ Container for managing multiple accordions. Renders a `` element.
+
+
## Examples
### With Subtitle
-Here's a view when using both title and subtitle props.
+Accordions can display a subtitle below the title.
}
- code={accordionWithSubtitleSnippet}
+ preview={
}
+ code={withSubtitleSnippet}
+ layout="side-by-side"
/>
### Custom Trigger
-Here's a view when providing custom multi-line content as children.
+Pass custom content as children instead of using the title prop.
}
- code={accordionCustomTriggerSnippet}
+ preview={
}
+ code={customTriggerSnippet}
/>
### Default Expanded
-Here's a view when the panel is expanded by default.
-
}
- code={accordionDefaultExpandedSnippet}
+ preview={
}
+ code={defaultExpandedSnippet}
+ layout="side-by-side"
/>
### Group Single Open
-Here's a view when only one accordion can be open at a time.
+Use `AccordionGroup` to allow only one accordion open at a time.
}
- code={accordionGroupSingleOpenSnippet}
+ preview={
}
+ code={groupSingleOpenSnippet}
/>
### Group Multiple Open
-Here's a view when multiple accordions can be open simultaneously.
+Allows multiple panels to be open simultaneously.
}
- code={accordionGroupMultipleOpenSnippet}
+ preview={
}
+ code={groupMultipleOpenSnippet}
/>
diff --git a/docs-ui/src/app/components/accordion/props-definition.ts b/docs-ui/src/app/components/accordion/props-definition.ts
new file mode 100644
index 0000000000..b465526632
--- /dev/null
+++ b/docs-ui/src/app/components/accordion/props-definition.ts
@@ -0,0 +1,82 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const accordionPropDefs: Record
= {
+ children: {
+ type: 'enum',
+ values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
+ description:
+ 'Content of the accordion. Can be a render function to access expanded state.',
+ },
+ defaultExpanded: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the accordion is expanded on initial render.',
+ },
+ isExpanded: {
+ type: 'boolean',
+ description: 'Controls the expanded state (controlled mode).',
+ },
+ onExpandedChange: {
+ type: 'enum',
+ values: ['(isExpanded: boolean) => void'],
+ description: 'Called when the expanded state changes.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionTriggerPropDefs: Record = {
+ level: {
+ type: 'enum',
+ values: ['1', '2', '3', '4', '5', '6'],
+ default: '3',
+ description:
+ 'Heading level for accessibility (renders h1-h6). Match your page hierarchy.',
+ },
+ title: {
+ type: 'string',
+ description: 'Primary text displayed in the trigger.',
+ },
+ subtitle: {
+ type: 'string',
+ description: 'Secondary text displayed next to the title.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description:
+ 'Custom trigger content. When provided, title and subtitle are ignored.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionPanelPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content displayed when the accordion is expanded.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionGroupPropDefs: Record = {
+ allowsMultiple: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether multiple accordions can be expanded at the same time.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Accordion components to group together.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/accordion/snippets.ts b/docs-ui/src/app/components/accordion/snippets.ts
new file mode 100644
index 0000000000..83e89d1b64
--- /dev/null
+++ b/docs-ui/src/app/components/accordion/snippets.ts
@@ -0,0 +1,68 @@
+export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
+
+
+
+ Your content
+ `;
+
+export const defaultSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const withSubtitleSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const customTriggerSnippet = `
+
+
+
+ Custom Multi-line Trigger
+
+
+ Click to expand additional details and configuration options
+
+
+
+
+ Your content here
+
+ `;
+
+export const defaultExpandedSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const groupSingleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+ `;
+
+export const groupMultipleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+ `;
diff --git a/docs-ui/src/app/components/alert/components.tsx b/docs-ui/src/app/components/alert/components.tsx
new file mode 100644
index 0000000000..afc5eb654c
--- /dev/null
+++ b/docs-ui/src/app/components/alert/components.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import { Alert } from '../../../../../packages/ui/src/components/Alert/Alert';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { RiCloseLine, RiCloudLine } from '@remixicon/react';
+
+export const Default = () => {
+ return ;
+};
+
+export const StatusVariants = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const WithActions = () => {
+ return (
+
+
+ Dismiss
+
+ }
+ />
+ }
+ aria-label="Close"
+ />
+ }
+ />
+
+ );
+};
+
+export const WithActionsAndDescriptions = () => {
+ return (
+
+
+ Later
+
+
+ Update Now
+
+ >
+ }
+ />
+ );
+};
+
+export const LoadingStates = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const WithoutIcons = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const CustomIcon = () => {
+ return (
+ }
+ title="This alert uses a custom cloud icon instead of the default info icon."
+ />
+ );
+};
diff --git a/docs-ui/src/app/components/alert/page.mdx b/docs-ui/src/app/components/alert/page.mdx
new file mode 100644
index 0000000000..cdfdf184e0
--- /dev/null
+++ b/docs-ui/src/app/components/alert/page.mdx
@@ -0,0 +1,120 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { alertPropDefs } from './props-definition';
+import {
+ alertUsageSnippet,
+ defaultSnippet,
+ statusVariantsSnippet,
+ withDescriptionSnippet,
+ withActionsSnippet,
+ loadingStatesSnippet,
+ withoutIconsSnippet,
+ customIconSnippet,
+} from './snippets';
+import {
+ Default,
+ StatusVariants,
+ WithDescription,
+ WithActions,
+ LoadingStates,
+ WithoutIcons,
+ CustomIcon,
+} from './components';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { AlertDefinition } from '../../../utils/definitions';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Status Variants
+
+The Alert component supports four status variants, each with its own color theme.
+
+ }
+ code={statusVariantsSnippet}
+/>
+
+### With Description
+
+Add a description to provide additional context or details.
+
+ }
+ code={withDescriptionSnippet}
+/>
+
+### With Actions
+
+Include custom actions like buttons for interactive alerts.
+
+ }
+ code={withActionsSnippet}
+/>
+
+### Loading States
+
+The loading spinner replaces the icon to indicate an ongoing process.
+
+ }
+ code={loadingStatesSnippet}
+/>
+
+### Without Icons
+
+Disable icons for a simpler appearance.
+
+ }
+ code={withoutIconsSnippet}
+/>
+
+### Custom Icon
+
+Provide a custom icon element instead of the default status icon.
+
+ }
+ code={customIconSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/alert/props-definition.ts b/docs-ui/src/app/components/alert/props-definition.ts
new file mode 100644
index 0000000000..6f2650fe42
--- /dev/null
+++ b/docs-ui/src/app/components/alert/props-definition.ts
@@ -0,0 +1,76 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const alertPropDefs: Record = {
+ status: {
+ type: 'enum',
+ values: ['info', 'success', 'warning', 'danger'],
+ responsive: true,
+ default: 'info',
+ },
+ icon: {
+ type: 'enum',
+ values: ['boolean', 'React.ReactElement'],
+ responsive: false,
+ },
+ loading: {
+ type: 'enum',
+ values: ['boolean'],
+ responsive: false,
+ },
+ title: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ description: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ customActions: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ m: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mx: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ my: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mt: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mb: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ ml: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mr: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/alert/snippets.ts b/docs-ui/src/app/components/alert/snippets.ts
new file mode 100644
index 0000000000..739dfde9db
--- /dev/null
+++ b/docs-ui/src/app/components/alert/snippets.ts
@@ -0,0 +1,131 @@
+export const alertUsageSnippet = `import { Alert } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const statusVariantsSnippet = `
+
+
+
+
+ `;
+
+export const withDescriptionSnippet = `
+
+
+
+
+ `;
+
+export const withActionsSnippet = `
+
+ Dismiss
+
+ }
+ />
+ }
+ aria-label="Close"
+ />
+ }
+ />
+ `;
+
+export const withActionsAndDescriptionsSnippet = `
+
+ Later
+
+
+ Update Now
+
+ >
+ }
+/>`;
+
+export const loadingStatesSnippet = `
+
+
+
+ `;
+
+export const withoutIconsSnippet = `
+
+
+ `;
+
+export const customIconSnippet = `import { RiCloudLine } from '@remixicon/react';
+
+ }
+ title="This alert uses a custom cloud icon instead of the default info icon."
+/>`;
diff --git a/docs-ui/src/app/components/avatar/components.tsx b/docs-ui/src/app/components/avatar/components.tsx
new file mode 100644
index 0000000000..da257f13b9
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/components.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import { Avatar } from '../../../../../packages/ui/src/components/Avatar/Avatar';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+
+export const Default = () => {
+ return (
+
+ );
+};
+
+export const Fallback = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Purpose = () => {
+ return (
+
+
+ Informative (default)
+
+ Use when avatar appears alone. Announced as "Charles de
+ Dreuille" to screen readers:
+
+
+
+
+
+
+ Decoration
+
+ Use when avatar appears with adjacent text. Hidden from screen
+ readers:
+
+
+
+ Charles de Dreuille
+
+
+
+ );
+};
diff --git a/docs-ui/src/content/avatar.mdx b/docs-ui/src/app/components/avatar/page.mdx
similarity index 50%
rename from docs-ui/src/content/avatar.mdx
rename to docs-ui/src/app/components/avatar/page.mdx
index 047485c2cc..7020eb90c4 100644
--- a/docs-ui/src/content/avatar.mdx
+++ b/docs-ui/src/app/components/avatar/page.mdx
@@ -1,39 +1,37 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { AvatarSnippet } from '@/snippets/stories-snippets';
+import { avatarPropDefs } from './props-definition';
import {
- avatarPropDefs,
- snippetUsage,
- snippetSizes,
- snippetFallback,
- snippetPurpose,
-} from './avatar.props';
+ avatarUsageSnippet,
+ defaultSnippet,
+ sizesSnippet,
+ fallbackSnippet,
+ purposeSnippet,
+} from './snippets';
+import { Default, Sizes, Fallback, Purpose } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { AvatarDefinition } from '../utils/definitions';
+import { AvatarDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
- }
- code={` `}
-/>
+ } code={defaultSnippet} />
## Usage
-
+
## API reference
+Avatar also accepts all standard HTML div attributes (`onClick`, `onMouseEnter`, etc.) since it extends `React.ComponentPropsWithoutRef<'div'>`.
+
## Examples
### Sizes
@@ -44,8 +42,9 @@ Avatar sizes can be set using the `size` prop.
align="center"
py={4}
open
- preview={ }
- code={snippetSizes}
+ preview={ }
+ code={sizesSnippet}
+ layout="side-by-side"
/>
### Fallback
@@ -56,20 +55,16 @@ If the image is not available, the avatar will show the initials of the name.
align="center"
py={4}
open
- preview={ }
- code={snippetFallback}
+ preview={ }
+ code={fallbackSnippet}
+ layout="side-by-side"
/>
### The `purpose` prop
Control how the avatar is announced to screen readers using the `purpose` prop.
- }
- code={snippetPurpose}
-/>
+ } code={purposeSnippet} />
diff --git a/docs-ui/src/app/components/avatar/props-definition.tsx b/docs-ui/src/app/components/avatar/props-definition.tsx
new file mode 100644
index 0000000000..4b56b94be5
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/props-definition.tsx
@@ -0,0 +1,41 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const avatarPropDefs: Record = {
+ src: {
+ type: 'string',
+ description:
+ 'URL of the image to display. Pass an empty string to show initials fallback. Falls back to initials if the image fails to load.',
+ },
+ name: {
+ type: 'string',
+ required: true,
+ description:
+ 'Name of the person. Used for generating initials fallback and accessibility label.',
+ },
+ size: {
+ type: 'enum',
+ values: ['x-small', 'small', 'medium', 'large', 'x-large'],
+ default: 'medium',
+ responsive: true,
+ description:
+ 'Visual size. Smaller sizes show 1 initial, larger sizes show 2.',
+ },
+ purpose: {
+ type: 'enum',
+ values: ['informative', 'decoration'],
+ default: 'informative',
+ description: (
+ <>
+ Accessibility behavior. Use decoration when name appears in
+ adjacent text.
+ >
+ ),
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/avatar/snippets.ts b/docs-ui/src/app/components/avatar/snippets.ts
new file mode 100644
index 0000000000..2deb8037a6
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/snippets.ts
@@ -0,0 +1,52 @@
+export const avatarUsageSnippet = `import { Avatar } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const fallbackSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+export const purposeSnippet = `
+
+ Informative (default)
+
+ Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
+
+
+
+
+
+
+ Decoration
+
+ Use when avatar appears with adjacent text. Hidden from screen readers:
+
+
+
+ Charles de Dreuille
+
+
+ `;
diff --git a/docs-ui/src/app/components/box/components.tsx b/docs-ui/src/app/components/box/components.tsx
new file mode 100644
index 0000000000..dac66f1f59
--- /dev/null
+++ b/docs-ui/src/app/components/box/components.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { Box } from '../../../../../packages/ui/src/components/Box/Box';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+ );
+};
+
+export const Surface = () => {
+ return (
+
+
+ Surface 0
+
+
+ Surface 1
+
+
+ Surface 2
+
+
+ Surface 3
+
+
+ );
+};
+
+export const Responsive = () => {
+ return (
+
+ Resize to see change
+
+ );
+};
diff --git a/docs-ui/src/app/components/box/page.mdx b/docs-ui/src/app/components/box/page.mdx
new file mode 100644
index 0000000000..9b60e5244a
--- /dev/null
+++ b/docs-ui/src/app/components/box/page.mdx
@@ -0,0 +1,53 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { boxPropDefs } from './props-definition';
+import {
+ snippetUsage,
+ defaultSnippet,
+ boxSurfaceSnippet,
+ boxResponsiveSnippet,
+} from './snippets';
+import { Default, Surface, Responsive } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { BoxDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} align="center" />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Surface
+
+Use surface levels to create visual hierarchy.
+
+ } code={boxSurfaceSnippet} layout="side-by-side" />
+
+### Responsive props
+
+Props can accept breakpoint objects for responsive behavior.
+
+ }
+ code={boxResponsiveSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/box/props-definition.tsx b/docs-ui/src/app/components/box/props-definition.tsx
new file mode 100644
index 0000000000..3809bb9993
--- /dev/null
+++ b/docs-ui/src/app/components/box/props-definition.tsx
@@ -0,0 +1,48 @@
+import {
+ classNamePropDefs,
+ heightPropDefs,
+ positionPropDefs,
+ stylePropDefs,
+ widthPropDefs,
+ spacingGroupAll,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const boxPropDefs: Record = {
+ as: {
+ type: 'string',
+ default: 'div',
+ description:
+ 'HTML element to render. Accepts any valid HTML tag (div, span, section, etc.).',
+ },
+ surface: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
+ responsive: true,
+ description:
+ 'Background surface level for visual hierarchy. Higher numbers create elevation.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content to render inside the box.',
+ },
+ ...widthPropDefs,
+ ...heightPropDefs,
+ ...positionPropDefs,
+ display: {
+ type: 'enum',
+ values: ['none', 'flex', 'block', 'inline'],
+ responsive: true,
+ description: (
+ <>
+ Controls layout behavior. Use flex for flexbox layouts,{' '}
+ none to hide.
+ >
+ ),
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
diff --git a/docs-ui/src/app/components/box/snippets.ts b/docs-ui/src/app/components/box/snippets.ts
new file mode 100644
index 0000000000..e9af07dec5
--- /dev/null
+++ b/docs-ui/src/app/components/box/snippets.ts
@@ -0,0 +1,23 @@
+export const snippetUsage = `import { Box } from '@backstage/ui';
+
+
+ Content with padding and background
+ `;
+
+export const defaultSnippet = `
+
+ `;
+
+export const boxSurfaceSnippet = `
+ Surface 0
+ Surface 1
+ Surface 2
+ Surface 3
+ `;
+
+export const boxResponsiveSnippet = `
+ Content
+ `;
diff --git a/docs-ui/src/app/components/button-icon/components.tsx b/docs-ui/src/app/components/button-icon/components.tsx
new file mode 100644
index 0000000000..3ed421289c
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/components.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCloudLine } from '@remixicon/react';
+
+export const Variants = () => {
+ return (
+
+ } variant="primary" aria-label="Cloud" />
+ }
+ variant="secondary"
+ aria-label="Cloud"
+ />
+ }
+ variant="tertiary"
+ aria-label="Cloud"
+ />
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ } size="small" aria-label="Cloud" />
+ } size="medium" aria-label="Cloud" />
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ }
+ variant="primary"
+ aria-label="Cloud"
+ />
+ }
+ variant="secondary"
+ aria-label="Cloud"
+ />
+ }
+ variant="tertiary"
+ aria-label="Cloud"
+ />
+
+ );
+};
+
+export const Loading = () => {
+ return (
+ }
+ variant="primary"
+ loading
+ aria-label="Cloud"
+ />
+ );
+};
diff --git a/docs-ui/src/app/components/button-icon/page.mdx b/docs-ui/src/app/components/button-icon/page.mdx
new file mode 100644
index 0000000000..51ff0ea5a0
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/page.mdx
@@ -0,0 +1,80 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonIconPropDefs } from './props-definition';
+import {
+ buttonIconUsageSnippet,
+ variantsSnippet,
+ sizesSnippet,
+ disabledSnippet,
+ loadingSnippet,
+} from './snippets';
+import { Variants, Sizes, Disabled, Loading } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ButtonIconDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ button: 'https://react-aria.adobe.com/Button',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Variants
+
+ }
+ code={variantsSnippet}
+/>
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+### Loading
+
+Shows a spinner during async operations.
+
+ }
+ code={loadingSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button-icon/props-definition.tsx b/docs-ui/src/app/components/button-icon/props-definition.tsx
new file mode 100644
index 0000000000..05917cc2b4
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/props-definition.tsx
@@ -0,0 +1,64 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonIconPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Button size. Use small for toolbars, medium {' '}
+ for standalone actions.
+ >
+ ),
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description:
+ 'Icon element to display. Required for accessibility via aria-label.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Prevents interaction and applies disabled styling.',
+ },
+ loading: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Shows a spinner and disables the button.',
+ },
+ type: {
+ type: 'enum',
+ values: ['button', 'submit', 'reset'],
+ default: 'button',
+ description: 'HTML button type attribute.',
+ },
+ onSurface: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
+ responsive: true,
+ description: 'Surface context for correct color contrast.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/button-icon/snippets.ts b/docs-ui/src/app/components/button-icon/snippets.ts
new file mode 100644
index 0000000000..b1ab27b77b
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/snippets.ts
@@ -0,0 +1,23 @@
+export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
+import { RiCloseLine } from '@remixicon/react';
+
+ } aria-label="Close" />`;
+
+export const variantsSnippet = `
+ } variant="primary" aria-label="Cloud" />
+ } variant="secondary" aria-label="Cloud" />
+ } variant="tertiary" aria-label="Cloud" />
+ `;
+
+export const sizesSnippet = `
+ } size="small" aria-label="Cloud" />
+ } size="medium" aria-label="Cloud" />
+ `;
+
+export const disabledSnippet = `
+ } variant="primary" aria-label="Cloud" />
+ } variant="secondary" aria-label="Cloud" />
+ } variant="tertiary" aria-label="Cloud" />
+ `;
+
+export const loadingSnippet = ` } variant="primary" loading aria-label="Cloud" />`;
diff --git a/docs-ui/src/app/components/button-link/components.tsx b/docs-ui/src/app/components/button-link/components.tsx
new file mode 100644
index 0000000000..4282ebf7dd
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/components.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
+
+export const Variants = () => {
+ return (
+
+
+ }
+ variant="primary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="secondary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="tertiary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+ Small
+
+
+ Medium
+
+
+
+ );
+};
+
+export const WithIcons = () => {
+ return (
+
+
+ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ iconEnd={ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/button-link/page.mdx b/docs-ui/src/app/components/button-link/page.mdx
new file mode 100644
index 0000000000..35e4de68c5
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/page.mdx
@@ -0,0 +1,68 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonLinkPropDefs } from './props-definition';
+import {
+ buttonLinkUsageSnippet,
+ variantsSnippet,
+ sizesSnippet,
+ withIconsSnippet,
+ disabledSnippet,
+} from './snippets';
+import { Variants, Sizes, WithIcons, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ButtonLinkDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ link: 'https://react-aria.adobe.com/Link',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### With Icons
+
+ }
+ code={withIconsSnippet}
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button-link/props-definition.tsx b/docs-ui/src/app/components/button-link/props-definition.tsx
new file mode 100644
index 0000000000..deae5364df
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/props-definition.tsx
@@ -0,0 +1,74 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonLinkPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Link size. Use small for inline contexts,{' '}
+ medium for standalone.
+ >
+ ),
+ },
+ iconStart: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed before the link text.',
+ },
+ iconEnd: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed after the link text.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Prevents interaction and applies disabled styling.',
+ },
+ href: {
+ type: 'string',
+ required: true,
+ description: 'URL the link navigates to.',
+ },
+ target: {
+ type: 'enum',
+ values: ['_self', '_blank', '_parent', '_top'],
+ description: (
+ <>
+ Where to open the linked URL. Use _blank for external
+ links.
+ >
+ ),
+ },
+ onSurface: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
+ responsive: true,
+ description: 'Surface context for correct color contrast.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/button-link/snippets.ts b/docs-ui/src/app/components/button-link/snippets.ts
new file mode 100644
index 0000000000..7f1a69027c
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/snippets.ts
@@ -0,0 +1,68 @@
+export const buttonLinkUsageSnippet = `import { ButtonLink } from '@backstage/ui';
+
+Button `;
+
+export const variantsSnippet = `
+ }
+ variant="primary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="secondary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="tertiary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ `;
+
+export const sizesSnippet = `
+
+ Small
+
+
+ Medium
+
+ `;
+
+export const withIconsSnippet = `
+ } href="https://ui.backstage.io" target="_blank">
+ Button
+
+ } href="https://ui.backstage.io" target="_blank">
+ Button
+
+ }
+ iconEnd={ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ `;
+
+export const disabledSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
diff --git a/docs-ui/src/app/components/button/components.tsx b/docs-ui/src/app/components/button/components.tsx
new file mode 100644
index 0000000000..1fe13f6630
--- /dev/null
+++ b/docs-ui/src/app/components/button/components.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Variants = () => {
+ return (
+
+ }>
+ Button
+
+ }>
+ Button
+
+ }>
+ Button
+
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ Small
+ Medium
+
+ );
+};
+
+export const WithIcons = () => {
+ return (
+
+ }>Button
+ }>Button
+ } iconEnd={ }>
+ Button
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+ );
+};
+
+export const Destructive = () => {
+ return (
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+ );
+};
+
+export const Loading = () => {
+ return (
+
+ Load more items
+
+ );
+};
+
+export const AsLink = () => {
+ return (
+
+
+ Button
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/button/page.mdx b/docs-ui/src/app/components/button/page.mdx
new file mode 100644
index 0000000000..6434a7f4a4
--- /dev/null
+++ b/docs-ui/src/app/components/button/page.mdx
@@ -0,0 +1,147 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonPropDefs } from './props-definition';
+import {
+ variantsSnippet,
+ sizesSnippet,
+ withIconsSnippet,
+ disabledSnippet,
+ destructiveSnippet,
+ loadingSnippet,
+ asLinkSnippet,
+ buttonSnippetUsage,
+ buttonResponsiveSnippet,
+} from './snippets';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ButtonDefinition } from '../../../utils/definitions';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ Variants,
+ Sizes,
+ WithIcons,
+ Disabled,
+ Destructive,
+ Loading,
+ AsLink,
+} from './components';
+
+export const reactAriaUrls = {
+ button: 'https://react-spectrum.adobe.com/react-aria/Button.html',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Variants
+
+ }
+ code={variantsSnippet}
+ layout="side-by-side"
+/>
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With Icons
+
+Icons can appear before or after the label.
+
+ }
+ code={withIconsSnippet}
+ layout="side-by-side"
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+ layout="side-by-side"
+/>
+
+### Destructive
+
+Use the `destructive` prop for dangerous actions like delete or remove.
+
+ }
+ code={destructiveSnippet}
+ layout="side-by-side"
+/>
+
+### Loading
+
+Shows a spinner and disables interaction during async operations.
+
+ }
+ code={loadingSnippet}
+ layout="side-by-side"
+/>
+
+### Responsive
+
+Button props accept responsive breakpoint objects.
+
+
+
+### As Link
+
+If you want to use a button as a link, please use the `ButtonLink` component.
+
+ }
+ code={asLinkSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button/props-definition.tsx b/docs-ui/src/app/components/button/props-definition.tsx
new file mode 100644
index 0000000000..0f94fcffaf
--- /dev/null
+++ b/docs-ui/src/app/components/button/props-definition.tsx
@@ -0,0 +1,75 @@
+import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ destructive: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Applies destructive styling for dangerous actions like delete or remove.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Button size. Use small for dense layouts.
+ >
+ ),
+ },
+ iconStart: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed before the button text.',
+ },
+ iconEnd: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed after the button text.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Prevents interaction and applies disabled styling.',
+ },
+ loading: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Shows a spinner and disables the button.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Button label text or content.',
+ },
+ type: {
+ type: 'enum',
+ values: ['button', 'submit', 'reset'],
+ default: 'button',
+ description: 'HTML button type attribute.',
+ },
+ onSurface: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
+ responsive: true,
+ description: 'Surface context for correct color contrast.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/button/snippets.ts b/docs-ui/src/app/components/button/snippets.ts
new file mode 100644
index 0000000000..6b8aba7e26
--- /dev/null
+++ b/docs-ui/src/app/components/button/snippets.ts
@@ -0,0 +1,66 @@
+export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
+
+Click me `;
+
+export const buttonResponsiveSnippet = `
+ Responsive Button
+ `;
+
+export const variantsSnippet = `
+ }>
+ Button
+
+ }>
+ Button
+
+ }>
+ Button
+
+ `;
+
+export const sizesSnippet = `
+ Small
+ Medium
+ `;
+
+export const withIconsSnippet = `
+ }>Button
+ }>Button
+ } iconEnd={ }>
+ Button
+
+ `;
+
+export const disabledSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
+
+export const destructiveSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
+
+export const loadingSnippet = `
+ Load more items
+ `;
+
+export const asLinkSnippet = `
+
+ Button
+
+ `;
diff --git a/docs-ui/src/app/components/card/components.tsx b/docs-ui/src/app/components/card/components.tsx
new file mode 100644
index 0000000000..d10e4d7bce
--- /dev/null
+++ b/docs-ui/src/app/components/card/components.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import {
+ Card,
+ CardHeader,
+ CardBody,
+ CardFooter,
+} from '../../../../../packages/ui/src/components/Card/Card';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+
+export const Default = () => {
+ return (
+
+ Header
+ Body
+ Footer
+
+ );
+};
+
+export const HeaderAndBody = () => {
+ return (
+
+ Header
+ Body content without a footer
+
+ );
+};
+
+export const WithLongBody = () => {
+ return (
+
+
+ Header
+
+
+
+ This is the first paragraph of a long body text that demonstrates how
+ the Card component handles extensive content. The card should adjust
+ accordingly to display all the text properly while maintaining its
+ structure.
+
+
+ Here's a second paragraph that adds more content to our card
+ body. Having multiple paragraphs helps to visualize how spacing works
+ within the card component.
+
+
+ This third paragraph continues to add more text to ensure we have a
+ proper demonstration of a card with significant content. This makes it
+ easier to test scrolling behavior and overall layout when content
+ exceeds the initial view.
+
+
+
+ Footer
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/card/page.mdx b/docs-ui/src/app/components/card/page.mdx
new file mode 100644
index 0000000000..6f410fcd97
--- /dev/null
+++ b/docs-ui/src/app/components/card/page.mdx
@@ -0,0 +1,86 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ cardPropDefs,
+ cardHeaderPropDefs,
+ cardBodyPropDefs,
+ cardFooterPropDefs,
+} from './props-definition';
+import {
+ cardUsageSnippet,
+ defaultSnippet,
+ headerAndBodySnippet,
+ withLongBodySnippet,
+} from './snippets';
+import { Default, HeaderAndBody, WithLongBody } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { CardDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+All Card components extend `HTMLDivElement` attributes.
+
+### Card
+
+
+
+### CardHeader
+
+Fixed at the top of the card.
+
+
+
+### CardBody
+
+Scrollable content area that fills available space.
+
+
+
+### CardFooter
+
+Fixed at the bottom of the card.
+
+
+
+## Examples
+
+### Header and body only
+
+Cards can omit the footer section.
+
+ }
+ code={headerAndBodySnippet}
+/>
+
+### Scrollable body
+
+When body content exceeds the available height, CardBody scrolls while header and footer remain fixed.
+
+ }
+ code={withLongBodySnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/card/props-definition.ts b/docs-ui/src/app/components/card/props-definition.ts
new file mode 100644
index 0000000000..b3e3776648
--- /dev/null
+++ b/docs-ui/src/app/components/card/props-definition.ts
@@ -0,0 +1,38 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+const optionalChildrenPropDef: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description: 'Content to display inside the component.',
+ },
+};
+
+export const cardPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardHeaderPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardBodyPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardFooterPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/content/card.props.ts b/docs-ui/src/app/components/card/snippets.ts
similarity index 50%
rename from docs-ui/src/content/card.props.ts
rename to docs-ui/src/app/components/card/snippets.ts
index 70c71bdf56..6f956e5518 100644
--- a/docs-ui/src/content/card.props.ts
+++ b/docs-ui/src/app/components/card/snippets.ts
@@ -1,30 +1,4 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const cardPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardHeaderPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardBodyPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardFooterPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardUsageSnippet = `import { card } from '@backstage/ui';
+export const cardUsageSnippet = `import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui';
Header
@@ -32,13 +6,20 @@ export const cardUsageSnippet = `import { card } from '@backstage/ui';
Footer
`;
-export const cardDefaultSnippet = `
+export const defaultSnippet = `
Header
Body
Footer
`;
-export const cardLongBodySnippet = `
+export const headerAndBodySnippet = `
+ Header
+ Body content without a footer
+ `;
+
+export const withLongBodySnippet = `import { Text } from '@backstage/ui';
+
+
Header
@@ -65,19 +46,3 @@ export const cardLongBodySnippet = `Footer
`;
-
-export const cardListRowSnippet = `
-
- Header
-
-
- Hello world
- Hello world
- Hello world
- Hello world
- ...
-
-
- Footer
-
- `;
diff --git a/docs-ui/src/app/components/checkbox/components.tsx b/docs-ui/src/app/components/checkbox/components.tsx
new file mode 100644
index 0000000000..a826c9e02f
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/components.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { Checkbox } from '../../../../../packages/ui/src/components/Checkbox/Checkbox';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return Accept terms and conditions ;
+};
+
+export const AllVariants = () => {
+ return (
+
+ Unchecked
+ Checked
+ Disabled
+
+ Checked & Disabled
+
+
+ );
+};
diff --git a/docs-ui/src/content/checkbox.mdx b/docs-ui/src/app/components/checkbox/page.mdx
similarity index 50%
rename from docs-ui/src/content/checkbox.mdx
rename to docs-ui/src/app/components/checkbox/page.mdx
index c574e8fed4..9657b471da 100644
--- a/docs-ui/src/content/checkbox.mdx
+++ b/docs-ui/src/app/components/checkbox/page.mdx
@@ -1,29 +1,29 @@
import { PropsTable } from '@/components/PropsTable';
-import { CheckboxSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
+import { checkboxPropDefs } from './props-definition';
import {
- checkboxPropDefs,
checkboxUsageSnippet,
- checkboxDefaultSnippet,
- checkboxVariantsSnippet,
-} from './checkbox.props';
+ defaultSnippet,
+ allVariantsSnippet,
+} from './snippets';
+import { Default, AllVariants } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { CheckboxDefinition } from '../utils/definitions';
+import { CheckboxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ checkbox: 'https://react-aria.adobe.com/Checkbox',
+};
- }
- code={checkboxDefaultSnippet}
-/>
+ } code={defaultSnippet} />
## Usage
@@ -33,18 +33,18 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
## Examples
### All variants
-Here's a view when checkboxes have different variants.
-
}
- code={checkboxVariantsSnippet}
+ preview={ }
+ code={allVariantsSnippet}
/>
diff --git a/docs-ui/src/app/components/checkbox/props-definition.ts b/docs-ui/src/app/components/checkbox/props-definition.ts
new file mode 100644
index 0000000000..fd4b47ff8d
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/props-definition.ts
@@ -0,0 +1,49 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const checkboxPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ required: true,
+ description: 'Label displayed next to the checkbox.',
+ },
+ isSelected: {
+ type: 'boolean',
+ description: 'Controls checked state (controlled mode).',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'Initial checked state (uncontrolled mode).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Called when the checked state changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents interaction and applies disabled styling.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Marks the checkbox as required for form validation.',
+ },
+ isIndeterminate: {
+ type: 'boolean',
+ description: 'Shows a mixed state, typically for "select all" checkboxes.',
+ },
+ name: {
+ type: 'string',
+ description: 'Name attribute for form submission.',
+ },
+ value: {
+ type: 'string',
+ description: 'Value attribute for form submission.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/checkbox/snippets.ts b/docs-ui/src/app/components/checkbox/snippets.ts
new file mode 100644
index 0000000000..bef496195f
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/snippets.ts
@@ -0,0 +1,12 @@
+export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
+
+Accept terms `;
+
+export const defaultSnippet = `Accept terms and conditions `;
+
+export const allVariantsSnippet = `
+ Unchecked
+ Checked
+ Disabled
+ Checked & Disabled
+ `;
diff --git a/docs-ui/src/app/components/container/components.tsx b/docs-ui/src/app/components/container/components.tsx
new file mode 100644
index 0000000000..a11bbd9463
--- /dev/null
+++ b/docs-ui/src/app/components/container/components.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import { Container } from '../../../../../packages/ui/src/components/Container/Container';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+ Page content goes here
+
+ );
+};
+
+export const ResponsiveSpacing = () => {
+ return (
+
+ Content with vertical spacing
+
+ );
+};
diff --git a/docs-ui/src/app/components/container/page.mdx b/docs-ui/src/app/components/container/page.mdx
new file mode 100644
index 0000000000..e0f4014a07
--- /dev/null
+++ b/docs-ui/src/app/components/container/page.mdx
@@ -0,0 +1,46 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { containerPropDefs } from './props-definition';
+import {
+ containerUsageSnippet,
+ defaultSnippet,
+ containerResponsiveSnippet,
+} from './snippets';
+import { Default, ResponsiveSpacing } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ContainerDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## Core Concepts
+
+Container provides the standard page layout for plugin content. It constrains content to a maximum width, centers it horizontally, and adds consistent horizontal gutters. Use it once per page to wrap your main content area.
+
+## API Reference
+
+
+
+## Examples
+
+### Responsive spacing
+
+Vertical spacing props accept breakpoint objects.
+
+ } code={containerResponsiveSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/container/props-definition.ts b/docs-ui/src/app/components/container/props-definition.ts
new file mode 100644
index 0000000000..89a51eac5c
--- /dev/null
+++ b/docs-ui/src/app/components/container/props-definition.ts
@@ -0,0 +1,20 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ createSpacingGroup,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const containerPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content to render inside the container.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: createSpacingGroup(
+ ['my', 'mt', 'mb', 'py', 'pt', 'pb'],
+ 'Vertical spacing properties for controlling margin and padding.',
+ ),
+};
diff --git a/docs-ui/src/app/components/container/snippets.ts b/docs-ui/src/app/components/container/snippets.ts
new file mode 100644
index 0000000000..a314b52387
--- /dev/null
+++ b/docs-ui/src/app/components/container/snippets.ts
@@ -0,0 +1,13 @@
+export const containerUsageSnippet = `import { Container } from "@backstage/ui";
+
+
+ {/* Your plugin's main content */}
+ `;
+
+export const defaultSnippet = `
+ Page content goes here
+ `;
+
+export const containerResponsiveSnippet = `
+ Content with vertical spacing
+ `;
diff --git a/docs-ui/src/app/components/dialog/components.tsx b/docs-ui/src/app/components/dialog/components.tsx
new file mode 100644
index 0000000000..18ab1bf9c5
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/components.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import {
+ Dialog,
+ DialogTrigger,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+} from '../../../../../packages/ui/src/components/Dialog/Dialog';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField';
+import { Select } from '../../../../../packages/ui/src/components/Select/Select';
+
+export const Default = () => (
+
+ Open Dialog
+
+ Example Dialog
+
+ This is a basic dialog example.
+
+
+
+ Close
+
+
+ Save
+
+
+
+
+);
+
+export const PreviewFixedWidthAndHeight = () => (
+
+ Scrollable Dialog
+
+ Long Content Dialog
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
+ ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse
+ cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
+ cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum.
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem
+ accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
+ quae ab illo inventore veritatis et quasi architecto beatae vitae
+ dicta sunt explicabo.
+
+
+ Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut
+ fugit, sed quia consequuntur magni dolores eos qui ratione
+ voluptatem sequi nesciunt.
+
+
+ Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
+ consectetur, adipisci velit, sed quia non numquam eius modi tempora
+ incidunt ut labore et dolore magnam aliquam quaerat voluptatem.
+
+
+ Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis
+ suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
+
+
+
+
+
+ Cancel
+
+
+ Accept
+
+
+
+
+);
+
+export const PreviewWithForm = () => (
+
+ Create User
+
+ Create New User
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Create User
+
+
+
+
+);
diff --git a/docs-ui/src/content/dialog.mdx b/docs-ui/src/app/components/dialog/page.mdx
similarity index 69%
rename from docs-ui/src/content/dialog.mdx
rename to docs-ui/src/app/components/dialog/page.mdx
index e1e4ec436e..12ce2d4135 100644
--- a/docs-ui/src/content/dialog.mdx
+++ b/docs-ui/src/app/components/dialog/page.mdx
@@ -1,35 +1,46 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { DialogSnippet } from '@/snippets/stories-snippets';
+import {
+ Default,
+ PreviewFixedWidthAndHeight,
+ PreviewWithForm,
+} from './components';
import {
dialogPropDefs,
dialogTriggerPropDefs,
dialogHeaderPropDefs,
dialogBodyPropDefs,
dialogFooterPropDefs,
- dialogClosePropDefs,
+} from './props-definition';
+import {
dialogUsageSnippet,
dialogDefaultSnippet,
dialogFixedWidthAndHeightSnippet,
dialogWithFormSnippet,
dialogWithNoTriggerSnippet,
dialogCloseSnippet,
-} from './dialog.props';
+} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { DialogDefinition } from '../utils/definitions';
+import { DialogDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ dialogTrigger: 'https://react-aria.adobe.com/Modal#dialogtrigger',
+ modal: 'https://react-aria.adobe.com/Modal',
+};
}
+ preview={ }
code={dialogDefaultSnippet}
/>
@@ -45,18 +56,27 @@ Wraps a trigger element and the dialog content to handle open/close state.
+
+
### Dialog
The main dialog container that renders as a modal overlay.
+
+
### DialogHeader
Displays the dialog title with a built-in close button.
+
+
### DialogBody
The main content area of the dialog with optional scrolling.
@@ -82,18 +102,18 @@ Dialog with a fixed height body that scrolls when content overflows.
}
+ preview={ }
code={dialogFixedWidthAndHeightSnippet}
/>
### Dialog with Form
-Dialog containing form elements for user input.
+Forms can be embedded in the dialog body.
}
+ preview={ }
code={dialogWithFormSnippet}
/>
diff --git a/docs-ui/src/app/components/dialog/props-definition.ts b/docs-ui/src/app/components/dialog/props-definition.ts
new file mode 100644
index 0000000000..d5f2037f22
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/props-definition.ts
@@ -0,0 +1,92 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const dialogTriggerPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Trigger element and dialog content.',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open by default (controlled).',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open by default (uncontrolled).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description:
+ "Handler that is called when the overlay's open state changes.",
+ },
+};
+
+export const dialogPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Dialog content (DialogHeader, DialogBody, DialogFooter).',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open (controlled mode).',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Initial open state (uncontrolled mode).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Called when the open state changes.',
+ },
+ width: {
+ type: 'enum',
+ values: ['number', 'string'],
+ default: '400',
+ description: 'Fixed width in pixels (number) or CSS units (string).',
+ },
+ height: {
+ type: 'enum',
+ values: ['number', 'string'],
+ default: 'auto',
+ description: 'Fixed height in pixels (number) or CSS units (string).',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogHeaderPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Dialog title text.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogBodyPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Main content of the dialog.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogFooterPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Action buttons or footer content.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/dialog/snippets.ts b/docs-ui/src/app/components/dialog/snippets.ts
new file mode 100644
index 0000000000..440ff417e8
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/snippets.ts
@@ -0,0 +1,86 @@
+export const dialogUsageSnippet = `import {
+ Dialog,
+ DialogTrigger,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+} from '@backstage/ui';
+
+
+ Open Dialog
+
+ Title
+ Content
+
+ Close
+
+
+ `;
+
+export const dialogDefaultSnippet = `
+ Open Dialog
+
+ Example Dialog
+
+ This is a basic dialog example.
+
+
+ Close
+ Save
+
+
+ `;
+
+export const dialogFixedWidthAndHeightSnippet = `
+ Scrollable Dialog
+
+ Long Content Dialog
+
+ ...
+
+
+ Cancel
+ Accept
+
+
+ `;
+
+export const dialogWithFormSnippet = `
+ Create User
+
+ Create New User
+
+
+
+
+
+
+
+
+ Cancel
+ Create User
+
+
+ `;
+
+export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
+
+
+ Create New User
+
+ Your content
+
+
+ Cancel
+ Create User
+
+ `;
+
+export const dialogCloseSnippet = `Close `;
diff --git a/docs-ui/src/app/components/flex/components.tsx b/docs-ui/src/app/components/flex/components.tsx
new file mode 100644
index 0000000000..ce6cc8c05f
--- /dev/null
+++ b/docs-ui/src/app/components/flex/components.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const DirectionExample = () => {
+ return (
+
+ First
+ Second
+ Third
+
+ );
+};
+
+export const ResponsiveExample = () => {
+ return (
+
+ 1
+ 2
+ 3
+
+ );
+};
+
+export const AlignExample = () => {
+ return (
+
+ Start
+ Middle
+ End
+
+ );
+};
diff --git a/docs-ui/src/app/components/flex/page.mdx b/docs-ui/src/app/components/flex/page.mdx
new file mode 100644
index 0000000000..f321b54583
--- /dev/null
+++ b/docs-ui/src/app/components/flex/page.mdx
@@ -0,0 +1,75 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import { flexPropDefs } from './props-definition';
+import {
+ flexUsageSnippet,
+ defaultSnippet,
+ flexResponsiveSnippet,
+ flexAlignSnippet,
+ flexDirectionSnippet,
+} from './snippets';
+import {
+ Default,
+ ResponsiveExample,
+ AlignExample,
+ DirectionExample,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { FlexDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Direction
+
+ }
+ code={flexDirectionSnippet}
+ layout="side-by-side"
+/>
+
+### Responsive gap
+
+Gap values can be responsive using breakpoint objects.
+
+ }
+ code={flexResponsiveSnippet}
+ layout="side-by-side"
+/>
+
+### Alignment
+
+ }
+ code={flexAlignSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/flex/props-definition.tsx b/docs-ui/src/app/components/flex/props-definition.tsx
new file mode 100644
index 0000000000..2136dd0498
--- /dev/null
+++ b/docs-ui/src/app/components/flex/props-definition.tsx
@@ -0,0 +1,66 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ gapPropDefs,
+ spacingGroupAll,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const flexPropDefs: Record = {
+ direction: {
+ type: 'enum',
+ values: ['row', 'column', 'row-reverse', 'column-reverse'],
+ responsive: true,
+ description: (
+ <>
+ Main axis direction. Use row for horizontal,{' '}
+ column for vertical layouts.
+ >
+ ),
+ },
+ align: {
+ type: 'enum',
+ values: ['start', 'center', 'end', 'baseline', 'stretch'],
+ responsive: true,
+ description:
+ 'Cross-axis alignment. Controls how children align perpendicular to the main axis.',
+ },
+ justify: {
+ type: 'enum',
+ values: ['start', 'center', 'end', 'between'],
+ responsive: true,
+ description: (
+ <>
+ Main-axis distribution. Use between to space children
+ evenly with no edge gaps.
+ >
+ ),
+ },
+ gap: {
+ ...gapPropDefs.gap,
+ default: '4',
+ description:
+ 'Space between children. Accepts spacing scale values or responsive objects.',
+ },
+ surface: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
+ responsive: true,
+ description: (
+ <>
+ Surface level for theming. Use auto to increment from
+ parent context.
+ >
+ ),
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description: 'Content to render inside the flex container.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
diff --git a/docs-ui/src/app/components/flex/snippets.ts b/docs-ui/src/app/components/flex/snippets.ts
new file mode 100644
index 0000000000..8770880c79
--- /dev/null
+++ b/docs-ui/src/app/components/flex/snippets.ts
@@ -0,0 +1,31 @@
+export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
+
+
+ Item 1
+ Item 2
+ Item 3
+ `;
+
+export const defaultSnippet = `
+
+
+
+ `;
+
+export const flexDirectionSnippet = `
+ First
+ Second
+ Third
+ `;
+
+export const flexResponsiveSnippet = `
+ 1
+ 2
+ 3
+ `;
+
+export const flexAlignSnippet = `
+ Start
+ Middle
+ End
+ `;
diff --git a/docs-ui/src/app/components/grid/components.tsx b/docs-ui/src/app/components/grid/components.tsx
new file mode 100644
index 0000000000..0c0e5a0546
--- /dev/null
+++ b/docs-ui/src/app/components/grid/components.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { Grid } from '../../../../../packages/ui/src/components/Grid/Grid';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export const ResponsiveExample = () => {
+ return (
+
+ 1
+ 2
+ 3
+ 4
+
+ );
+};
+
+export const GridItemExample = () => {
+ return (
+
+
+ Spans 2 columns
+
+ 1 column
+ 1 column
+
+ );
+};
diff --git a/docs-ui/src/app/components/grid/page.mdx b/docs-ui/src/app/components/grid/page.mdx
new file mode 100644
index 0000000000..8862586830
--- /dev/null
+++ b/docs-ui/src/app/components/grid/page.mdx
@@ -0,0 +1,64 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { gridPropDefs, gridItemPropDefs } from './props-definition';
+import {
+ gridUsageSnippet,
+ defaultSnippet,
+ gridResponsiveSnippet,
+ gridItemSnippet,
+} from './snippets';
+import { Default, ResponsiveExample, GridItemExample } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { GridDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### Grid.Root
+
+The grid container. Defines column count and gap between items.
+
+
+
+### Grid.Item
+
+A grid child with column and row spanning control.
+
+
+
+## Examples
+
+### Responsive columns
+
+Column count can change at different breakpoints.
+
+ }
+ code={gridResponsiveSnippet}
+ open
+/>
+
+### Column spanning
+
+Use Grid.Item to span multiple columns.
+
+ } code={gridItemSnippet} open />
+
+
+
+
diff --git a/docs-ui/src/app/components/grid/props-definition.tsx b/docs-ui/src/app/components/grid/props-definition.tsx
new file mode 100644
index 0000000000..4622f8f614
--- /dev/null
+++ b/docs-ui/src/app/components/grid/props-definition.tsx
@@ -0,0 +1,115 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ gapPropDefs,
+ stylePropDefs,
+ spacingGroupAll,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+const columnValues = [
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ 'auto',
+];
+
+const surfaceValues = [
+ '0',
+ '1',
+ '2',
+ '3',
+ 'danger',
+ 'warning',
+ 'success',
+ 'auto',
+];
+
+export const gridPropDefs: Record = {
+ columns: {
+ type: 'enum',
+ values: columnValues,
+ default: 'auto',
+ responsive: true,
+ description: (
+ <>
+ Number of columns. Use 1-12 for fixed layouts, auto to fit
+ content.
+ >
+ ),
+ },
+ gap: {
+ ...gapPropDefs.gap,
+ default: '4',
+ description:
+ 'Space between items. Use higher values for separated layouts, lower for compact.',
+ },
+ surface: {
+ type: 'enum',
+ values: surfaceValues,
+ responsive: true,
+ description: (
+ <>
+ Surface level for theming. Use auto to increment from
+ parent context.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
+
+export const gridItemPropDefs: Record = {
+ colSpan: {
+ type: 'enum',
+ values: columnValues,
+ responsive: true,
+ description: 'Number of columns the item spans across.',
+ },
+ colStart: {
+ type: 'enum',
+ values: columnValues,
+ responsive: true,
+ description:
+ 'Starting column position. Use with colEnd for explicit placement.',
+ },
+ colEnd: {
+ type: 'enum',
+ values: columnValues,
+ responsive: true,
+ description:
+ 'Ending column position. Use with colStart for explicit placement.',
+ },
+ rowSpan: {
+ type: 'enum',
+ values: columnValues,
+ responsive: true,
+ description: 'Number of rows the item spans. Useful for tall content.',
+ },
+ surface: {
+ type: 'enum',
+ values: surfaceValues,
+ responsive: true,
+ description: (
+ <>
+ Surface level for theming. Use auto to increment from
+ parent context.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/grid/snippets.ts b/docs-ui/src/app/components/grid/snippets.ts
new file mode 100644
index 0000000000..2915a2ba4b
--- /dev/null
+++ b/docs-ui/src/app/components/grid/snippets.ts
@@ -0,0 +1,31 @@
+export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
+
+
+ Item 1
+ Item 2
+ Item 3
+ `;
+
+export const defaultSnippet = `
+
+
+
+
+
+
+ `;
+
+export const gridResponsiveSnippet = `
+ 1
+ 2
+ 3
+ 4
+ `;
+
+export const gridItemSnippet = `
+
+ Spans 2 columns
+
+ 1 column
+ 1 column
+ `;
diff --git a/docs-ui/src/app/components/header-page/components.tsx b/docs-ui/src/app/components/header-page/components.tsx
new file mode 100644
index 0000000000..791afc5bbe
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/components.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import {
+ MenuTrigger,
+ Menu,
+ MenuItem,
+} from '../../../../../packages/ui/src/components/Menu/Menu';
+import { MemoryRouter } from 'react-router-dom';
+import { RiMore2Line } from '@remixicon/react';
+
+const tabs = [
+ { 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' },
+];
+
+const breadcrumbs = [
+ { label: 'Home', href: '/' },
+ { label: 'Long Breadcrumb Name', href: '/long-breadcrumb' },
+ { label: 'Another Long Breadcrumb', href: '/another-long-breadcrumb' },
+ {
+ label: 'Yet Another Long Breadcrumb',
+ href: '/yet-another-long-breadcrumb',
+ },
+];
+
+export const WithEverything = () => (
+
+
+ Secondary
+ Primary
+ >
+ }
+ />
+
+);
+
+export const WithLongBreadcrumbs = () => (
+
+
+
+);
+
+export const WithTabs = () => (
+
+
+
+);
+
+export const WithCustomActions = () => (
+
+ Custom action}
+ />
+
+);
+
+export const WithMenu = () => (
+
+
+ } />
+
+ Settings
+ alert('logout')}>Logout
+
+
+ }
+ />
+
+);
diff --git a/docs-ui/src/app/components/header-page/page.mdx b/docs-ui/src/app/components/header-page/page.mdx
new file mode 100644
index 0000000000..ac99fb1af0
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/page.mdx
@@ -0,0 +1,66 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import {
+ WithEverything,
+ WithLongBreadcrumbs,
+ WithTabs,
+ WithCustomActions,
+ WithMenu,
+} from './components';
+import { headerPagePropDefs } from './props-definition';
+import {
+ usage,
+ defaultSnippet,
+ withTabs,
+ withBreadcrumbs,
+ withCustomActions,
+ withMenu,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { HeaderPageDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Breadcrumbs
+
+Labels are truncated at 240px.
+
+ } code={withBreadcrumbs} />
+
+### Tabs
+
+Tabs use React Router and highlight based on the current route.
+
+ } code={withTabs} />
+
+### Custom actions
+
+ } code={withCustomActions} />
+
+### With menu
+
+Use `customActions` to add a dropdown menu.
+
+ } code={withMenu} />
+
+
+
+
diff --git a/docs-ui/src/app/components/header-page/props-definition.tsx b/docs-ui/src/app/components/header-page/props-definition.tsx
new file mode 100644
index 0000000000..393141870f
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/props-definition.tsx
@@ -0,0 +1,69 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const headerPagePropDefs: Record = {
+ title: {
+ type: 'string',
+ description: 'Page heading displayed in the header.',
+ },
+ customActions: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Custom elements rendered in the actions area.',
+ },
+ tabs: {
+ type: 'complex',
+ description: 'Navigation tabs displayed below the title.',
+ complexType: {
+ name: 'HeaderTab[]',
+ properties: {
+ id: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier for the tab.',
+ },
+ label: {
+ type: 'string',
+ required: true,
+ description: 'Display text for the tab.',
+ },
+ href: {
+ type: 'string',
+ required: true,
+ description: 'URL to navigate to when tab is clicked.',
+ },
+ matchStrategy: {
+ type: "'exact' | 'prefix'",
+ required: false,
+ default: "'exact'",
+ description: (
+ <>
+ Route matching strategy. Use exact for exact path
+ match, prefix if pathname starts with href.
+ >
+ ),
+ },
+ },
+ },
+ },
+ breadcrumbs: {
+ type: 'complex',
+ description: 'Breadcrumb trail displayed above the title.',
+ complexType: {
+ name: 'HeaderPageBreadcrumb[]',
+ properties: {
+ label: {
+ type: 'string',
+ required: true,
+ description: 'Display text for the breadcrumb. Truncated at 240px.',
+ },
+ href: {
+ type: 'string',
+ required: true,
+ description: 'URL for the breadcrumb link.',
+ },
+ },
+ },
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/header-page/snippets.ts b/docs-ui/src/app/components/header-page/snippets.ts
new file mode 100644
index 0000000000..607675717c
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/snippets.ts
@@ -0,0 +1,56 @@
+export const usage = `import { HeaderPage } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = `
+ Secondary
+ Primary
+ >
+ }
+/>`;
+
+export const withBreadcrumbs = ` `;
+
+export const withTabs = ` `;
+
+export const withCustomActions = `Custom action}
+/>`;
+
+export const withMenu = `
+ } />
+
+ Settings
+ {}}>Logout
+
+
+ }
+/>`;
diff --git a/docs-ui/src/app/components/link/components.tsx b/docs-ui/src/app/components/link/components.tsx
new file mode 100644
index 0000000000..320dd52517
--- /dev/null
+++ b/docs-ui/src/app/components/link/components.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import { Link } from '../../../../../packages/ui/src/components/Link/Link';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Default = () => {
+ return (
+
+
+ Sign up for Backstage
+
+
+ );
+};
+
+export const ExternalLink = () => {
+ return (
+
+
+ Sign up for Backstage
+
+
+ );
+};
+
+export const AllVariants = () => {
+ return (
+
+
+
+ title-large
+
+
+ title-medium
+
+
+ title-small
+
+
+ title-x-small
+
+
+ body-large
+
+
+ body-medium
+
+
+ body-small
+
+
+ body-x-small
+
+
+
+ );
+};
+
+export const AllColors = () => {
+ return (
+
+
+
+ Primary
+
+
+ Secondary
+
+
+ Danger
+
+
+ Warning
+
+
+ Success
+
+
+ Info
+
+
+
+ );
+};
+
+export const Weight = () => {
+ return (
+
+
+
+ Regular
+
+
+ Bold
+
+
+
+ );
+};
+
+export const Standalone = () => {
+ return (
+
+
+
+ Default link
+
+
+ Standalone link
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/link/page.mdx b/docs-ui/src/app/components/link/page.mdx
new file mode 100644
index 0000000000..8715bb270b
--- /dev/null
+++ b/docs-ui/src/app/components/link/page.mdx
@@ -0,0 +1,107 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { linkPropDefs } from './props-definition';
+import {
+ linkUsageSnippet,
+ defaultSnippet,
+ externalLinkSnippet,
+ allVariantsSnippet,
+ allColorsSnippet,
+ weightSnippet,
+ standaloneSnippet,
+} from './snippets';
+import {
+ Default,
+ ExternalLink,
+ AllVariants,
+ AllColors,
+ Weight,
+ Standalone,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { LinkDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ link: 'https://react-aria.adobe.com/Link',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### External link
+
+Use `target="_blank"` to open links in a new tab.
+
+ }
+ code={externalLinkSnippet}
+ layout="side-by-side"
+/>
+
+### Variants
+
+ }
+ code={allVariantsSnippet}
+/>
+
+### Colors
+
+Status colors for contextual links.
+
+ }
+ code={allColorsSnippet}
+/>
+
+### Weight
+
+ }
+ code={weightSnippet}
+ layout="side-by-side"
+/>
+
+### Standalone
+
+Use `standalone` to remove the underline by default. The underline will appear on hover.
+
+ }
+ code={standaloneSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/link/props-definition.tsx b/docs-ui/src/app/components/link/props-definition.tsx
new file mode 100644
index 0000000000..f0aa95114e
--- /dev/null
+++ b/docs-ui/src/app/components/link/props-definition.tsx
@@ -0,0 +1,79 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const linkPropDefs: Record = {
+ href: {
+ type: 'string',
+ description:
+ 'URL the link navigates to. Supports internal and external URLs.',
+ },
+ target: {
+ type: 'enum',
+ values: ['_self', '_blank', '_parent', '_top'],
+ description: (
+ <>
+ Where to open the link. Use _blank for external links that
+ open in new tabs.
+ >
+ ),
+ },
+ title: {
+ type: 'string',
+ description: 'Tooltip text shown on hover. Useful for accessibility.',
+ },
+ variant: {
+ type: 'enum',
+ values: [
+ 'title-large',
+ 'title-medium',
+ 'title-small',
+ 'title-x-small',
+ 'body-large',
+ 'body-medium',
+ 'body-small',
+ 'body-x-small',
+ ],
+ default: 'body',
+ responsive: true,
+ description:
+ 'Typography style. Title variants for headings, body for paragraph text.',
+ },
+ weight: {
+ type: 'enum',
+ values: ['regular', 'bold'],
+ default: 'regular',
+ responsive: true,
+ description: (
+ <>
+ Font weight. Use bold for emphasis.
+ >
+ ),
+ },
+ color: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'danger', 'warning', 'success'],
+ default: 'primary',
+ responsive: true,
+ description:
+ 'Text color. Status colors (danger, warning, success) for contextual links.',
+ },
+ truncate: {
+ type: 'boolean',
+ description:
+ 'Truncates text with ellipsis when it overflows its container.',
+ default: 'false',
+ },
+ standalone: {
+ type: 'boolean',
+ description: 'Removes underline by default. Underline appears on hover.',
+ default: 'false',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/link/snippets.ts b/docs-ui/src/app/components/link/snippets.ts
new file mode 100644
index 0000000000..386ad9f1a0
--- /dev/null
+++ b/docs-ui/src/app/components/link/snippets.ts
@@ -0,0 +1,39 @@
+export const linkUsageSnippet = `import { Link } from '@backstage/ui';
+
+ Sign up for Backstage`;
+
+export const defaultSnippet = ` Sign up for Backstage`;
+
+export const externalLinkSnippet = `
+ Sign up for Backstage
+`;
+
+export const allVariantsSnippet = `
+ title-large
+ title-medium
+ title-small
+ title-x-small
+ body-large
+ body-medium
+ body-small
+ body-x-small
+ `;
+
+export const allColorsSnippet = `
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+ `;
+
+export const weightSnippet = `
+ Regular
+ Bold
+ `;
+
+export const standaloneSnippet = `
+ Default link
+ Standalone link
+ `;
diff --git a/docs-ui/src/app/components/menu/components.tsx b/docs-ui/src/app/components/menu/components.tsx
new file mode 100644
index 0000000000..fbb481f5ef
--- /dev/null
+++ b/docs-ui/src/app/components/menu/components.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import {
+ MenuTrigger,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuSeparator,
+ SubmenuTrigger,
+ MenuAutocomplete,
+ MenuAutocompleteListbox,
+ MenuListBoxItem,
+} from '../../../../../packages/ui/src/components/Menu/Menu';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { MemoryRouter } from 'react-router-dom';
+import {
+ RiChat1Line,
+ RiFileLine,
+ RiFolderLine,
+ RiImageLine,
+ RiSettingsLine,
+ RiShareBoxLine,
+} from '@remixicon/react';
+
+export const Preview = () => (
+
+
+ Menu
+
+ Edit
+ Duplicate
+ Rename
+
+ }>Share
+ }>Feedback
+
+
+ }>Settings
+
+ Edit
+ Duplicate
+ Rename
+
+
+
+
+
+);
+
+export const PreviewSubmenu = () => (
+
+
+ Menu
+
+ New File
+
+ Open Recent
+
+ File 1.txt
+ File 2.txt
+
+
+ Save
+
+
+
+);
+
+export const PreviewIcons = () => (
+
+
+ Menu
+
+ }>New File
+ }>New Folder
+ }>New Image
+
+
+
+);
+
+export const PreviewLinks = () => (
+
+
+ Menu
+
+ Home
+ About
+ Contact
+
+
+
+);
+
+export const PreviewSections = () => (
+
+
+ Menu
+
+
+ New
+ Open
+
+
+ Cut
+ Copy
+ Paste
+
+
+
+
+);
+
+export const PreviewSeparators = () => (
+
+
+ Menu
+
+ New
+ Open
+
+ Save
+ Save As...
+
+
+
+);
+
+export const PreviewAutocompleteMenu = () => (
+
+
+ Search
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
+
+export const PreviewAutocompleteListbox = () => (
+
+
+ Select
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
+
+export const PreviewAutocompleteListboxMultiple = () => (
+
+
+ Multi-select
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
diff --git a/docs-ui/src/app/components/menu/page.mdx b/docs-ui/src/app/components/menu/page.mdx
new file mode 100644
index 0000000000..f1de264d73
--- /dev/null
+++ b/docs-ui/src/app/components/menu/page.mdx
@@ -0,0 +1,229 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ Preview,
+ PreviewSubmenu,
+ PreviewIcons,
+ PreviewLinks,
+ PreviewSections,
+ PreviewSeparators,
+ PreviewAutocompleteMenu,
+ PreviewAutocompleteListbox,
+ PreviewAutocompleteListboxMultiple,
+} from './components';
+import {
+ menuTriggerPropDefs,
+ submenuTriggerPropDefs,
+ menuPropDefs,
+ menuListBoxPropDefs,
+ menuAutocompletePropDefs,
+ menuAutocompleteListboxPropDefs,
+ menuItemPropDefs,
+ menuListBoxItemPropDefs,
+ menuSectionPropDefs,
+ menuSeparatorPropDefs,
+} from './props-definition';
+import {
+ usage,
+ preview,
+ submenu,
+ icons,
+ sections,
+ separators,
+ links,
+ autocomplete,
+ autocompleteListbox,
+ autocompleteListboxMultiple,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { MenuDefinition } from '../../../utils/definitions';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ menuTrigger: 'https://react-aria.adobe.com/Menu#menutrigger',
+ submenuTrigger: 'https://react-aria.adobe.com/Menu#submenutrigger',
+ menu: 'https://react-aria.adobe.com/Menu',
+ menuItem: 'https://react-aria.adobe.com/Menu#menuitem',
+ menuSection: 'https://react-aria.adobe.com/Menu#menusection',
+ listBox: 'https://react-aria.adobe.com/ListBox',
+ listBoxItem: 'https://react-aria.adobe.com/ListBox#listboxitem',
+};
+
+
+
+ } code={preview} />
+
+## Usage
+
+
+
+### Triggers
+
+- `MenuTrigger` combines a trigger element with a menu popover.
+- `SubmenuTrigger` combines a `MenuItem` with a nested submenu.
+
+### Containers
+
+- `Menu` contains menu items or sections.
+- `MenuListBox` supports selection with checkmarks.
+- `MenuAutocomplete` adds a search input to filter items.
+- `MenuAutocompleteListbox` combines search with selection.
+
+### Items
+
+- `MenuItem` is an interactive action in the menu.
+- `MenuListBoxItem` is a selectable item in a list box.
+
+### Grouping
+
+- `MenuSection` groups items with a title.
+- `MenuSeparator` adds a horizontal divider.
+
+## API reference
+
+### MenuTrigger
+
+Accepts two children: the trigger element and a menu container.
+
+
+
+
+
+### SubmenuTrigger
+
+Accepts two children: a `MenuItem` and a menu container.
+
+
+
+
+
+### Menu
+
+
+
+
+
+### MenuListBox
+
+
+
+
+
+### MenuAutocomplete
+
+
+
+
+
+### MenuAutocompleteListbox
+
+
+
+
+
+### MenuItem
+
+
+
+
+
+### MenuListBoxItem
+
+
+
+
+
+### MenuSection
+
+
+
+
+
+### MenuSeparator
+
+
+
+
+
+## Examples
+
+### Nested navigation
+
+Submenus open to the right of their parent item.
+
+ }
+ code={submenu}
+/>
+
+### With icons
+
+ } code={icons} />
+
+### With links
+
+The `href` prop works with both internal and external links.
+
+ } code={links} />
+
+### With sections
+
+ }
+ code={sections}
+/>
+
+### With separators
+
+ }
+ code={separators}
+/>
+
+### With autocomplete
+
+ }
+ code={autocomplete}
+/>
+
+### With list box
+
+ }
+ code={autocompleteListbox}
+/>
+
+### Multiple selection
+
+Set `selectionMode="multiple"` to allow multiple selections.
+
+ }
+ code={autocompleteListboxMultiple}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/menu/props-definition.tsx b/docs-ui/src/app/components/menu/props-definition.tsx
new file mode 100644
index 0000000000..5e11b22737
--- /dev/null
+++ b/docs-ui/src/app/components/menu/props-definition.tsx
@@ -0,0 +1,364 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const menuTriggerPropDefs: Record = {
+ isOpen: {
+ type: 'boolean',
+ description: 'Controlled open state of the menu.',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Whether the menu is open by default.',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Handler called when the open state changes.',
+ },
+};
+
+export const submenuTriggerPropDefs: Record = {
+ delay: {
+ type: 'number',
+ default: '200',
+ description: 'Delay in milliseconds before the submenu opens on hover.',
+ },
+};
+
+export const menuPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'top start',
+ 'top end',
+ 'bottom start',
+ 'bottom end',
+ 'left start',
+ 'left end',
+ 'right start',
+ 'right end',
+ ],
+ description: 'Position of the menu relative to the trigger.',
+ },
+ onAction: {
+ type: 'enum',
+ values: ['(key: Key) => void'],
+ description:
+ 'Handler called when an item is activated. Receives the item key.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'How items can be selected.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of items that are disabled.',
+ },
+ virtualized: {
+ type: 'boolean',
+ default: false,
+ description: 'Enable virtualization for large lists.',
+ },
+ maxWidth: {
+ type: 'string',
+ description: 'Maximum width of the menu popover.',
+ },
+ maxHeight: {
+ type: 'string',
+ description: 'Maximum height of the menu popover.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuListBoxPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'top start',
+ 'top end',
+ 'bottom start',
+ 'bottom end',
+ 'left start',
+ 'left end',
+ 'right start',
+ 'right end',
+ ],
+ description: 'Position of the list box relative to the trigger.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'How items can be selected.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when selection changes.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of items that are disabled.',
+ },
+ virtualized: {
+ type: 'boolean',
+ default: false,
+ description: 'Enable virtualization for large lists.',
+ },
+ maxWidth: {
+ type: 'string',
+ description: 'Maximum width of the list box popover.',
+ },
+ maxHeight: {
+ type: 'string',
+ description: 'Maximum height of the list box popover.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuAutocompletePropDefs: Record = {
+ placeholder: {
+ type: 'string',
+ description: 'Placeholder text for the search input.',
+ },
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'top start',
+ 'top end',
+ 'bottom start',
+ 'bottom end',
+ 'left start',
+ 'left end',
+ 'right start',
+ 'right end',
+ ],
+ description: 'Position of the menu relative to the trigger.',
+ },
+ onAction: {
+ type: 'enum',
+ values: ['(key: Key) => void'],
+ description:
+ 'Handler called when an item is activated. Receives the item key.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'How items can be selected.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of items that are disabled.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when selection changes.',
+ },
+ virtualized: {
+ type: 'boolean',
+ default: false,
+ description: 'Enable virtualization for large lists.',
+ },
+ maxWidth: {
+ type: 'string',
+ description: 'Maximum width of the menu popover.',
+ },
+ maxHeight: {
+ type: 'string',
+ description: 'Maximum height of the menu popover.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuAutocompleteListboxPropDefs: Record = {
+ placeholder: {
+ type: 'string',
+ description: 'Placeholder text for the search input.',
+ },
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'top start',
+ 'top end',
+ 'bottom start',
+ 'bottom end',
+ 'left start',
+ 'left end',
+ 'right start',
+ 'right end',
+ ],
+ description: 'Position of the list box relative to the trigger.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'How items can be selected.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when selection changes.',
+ },
+ virtualized: {
+ type: 'boolean',
+ default: false,
+ description: 'Enable virtualization for large lists.',
+ },
+ maxWidth: {
+ type: 'string',
+ description: 'Maximum width of the list box popover.',
+ },
+ maxHeight: {
+ type: 'string',
+ description: 'Maximum height of the list box popover.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuItemPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ required: true,
+ description: 'Content displayed in the menu item.',
+ },
+ id: {
+ type: 'enum',
+ values: ['Key'],
+ description: 'Unique key for the item.',
+ },
+ iconStart: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the item content.',
+ },
+ color: {
+ type: 'enum',
+ values: ['primary', 'danger'],
+ description: (
+ <>
+ Color variant. Use danger for destructive actions.
+ >
+ ),
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when the item is clicked.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the item is disabled.',
+ },
+ textValue: {
+ type: 'string',
+ description: 'Text used for typeahead and accessibility.',
+ },
+ onAction: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Handler called when the item is activated.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuListBoxItemPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ required: true,
+ description: 'Content displayed in the list box item.',
+ },
+ id: {
+ type: 'enum',
+ values: ['Key'],
+ description: 'Unique key for the item.',
+ },
+ textValue: {
+ type: 'string',
+ description: 'Text used for typeahead and accessibility.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the item is disabled.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuSectionPropDefs: Record = {
+ title: {
+ type: 'string',
+ required: true,
+ description: 'Heading displayed above the section.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ required: true,
+ description: 'Menu items within the section.',
+ },
+ ...classNamePropDefs,
+};
+
+export const menuSeparatorPropDefs: Record = {
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/menu/snippets.ts b/docs-ui/src/app/components/menu/snippets.ts
new file mode 100644
index 0000000000..ee7e77e8c2
--- /dev/null
+++ b/docs-ui/src/app/components/menu/snippets.ts
@@ -0,0 +1,119 @@
+export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
+
+
+ Menu
+
+ Item 1
+ Item 2
+
+ `;
+
+export const preview = `
+ Menu
+
+ Edit
+ Duplicate
+ Rename
+
+ }>Share
+ }>Feedback
+
+
+ }>Settings
+
+ Edit
+ Duplicate
+ Rename
+
+
+
+ `;
+
+export const submenu = `
+ Menu
+
+ New File
+
+ Open Recent
+
+ File 1.txt
+ File 2.txt
+
+
+ Save
+
+ `;
+
+export const icons = `
+ Menu
+
+ }>New File
+ }>New Folder
+ }>New Image
+
+ `;
+
+export const sections = `
+ Menu
+
+
+ New
+ Open
+
+
+ Cut
+ Copy
+ Paste
+
+
+ `;
+
+export const separators = `
+ Menu
+
+ New
+ Open
+
+ Save
+ Save As...
+
+ `;
+
+export const links = `
+ Menu
+
+ Home
+ About
+ Contact
+
+ `;
+
+export const autocomplete = `
+ Search
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
+
+export const autocompleteListbox = `
+ Select
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
+
+export const autocompleteListboxMultiple = `
+ Multi-select
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
diff --git a/docs-ui/src/app/components/page.mdx b/docs-ui/src/app/components/page.mdx
index bd18005d19..399d9aafbf 100644
--- a/docs-ui/src/app/components/page.mdx
+++ b/docs-ui/src/app/components/page.mdx
@@ -1,173 +1,9 @@
-import { ComponentCards, ComponentCard } from '@/components/ComponentCards';
-import { LayoutComponents } from '@/components/LayoutComponents';
-import { CodeBlock } from '@/components/CodeBlock';
+import { ComponentGrid } from '@/components/ComponentGrid';
# Components
-## Layout Components
+Below is the full list of components available in the library. Each component is designed to be flexible, accessible,
+and easy to integrate into your plugins. We are actively working on adding more components, so check back regularly
+for updates.
-We built a couple of layout components to help you build responsive elements
-that will be consistent with the rest of your Backstage instance. These
-components are opinionated and use TypeScript to ensure that the props you
-provide are the ones coming from the theme.
-
-
- Hello World
-
- Project 1
- Project 2
-
-
-`}
-/>
-
-
-
-## Components
-
-### Actions
-
-
-
-
-
-
-
-
-### Content display
-
-
-
-
-
-
-### Selection and inputs
-
-
-
-
-
-
-
-
-
-
-
-### Navigation
-
-
-
-
-
-
-
-
-### Images and icons
-
-
-
-
-
-
-### Feedback indicators
-
-
-
-
-
-
-### Typography
-
-
-
-
+
diff --git a/docs-ui/src/app/components/password-field/components.tsx b/docs-ui/src/app/components/password-field/components.tsx
new file mode 100644
index 0000000000..a9f96b9267
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/components.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import { PasswordField } from '../../../../../packages/ui/src/components/PasswordField/PasswordField';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiLockLine } from '@remixicon/react';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
+
+export const WithIcon = () => {
+ return (
+ }
+ style={{ maxWidth: '300px' }}
+ />
+ );
+};
+
+export const Validation = () => {
+ return (
+
+ );
+};
diff --git a/docs-ui/src/app/components/password-field/page.mdx b/docs-ui/src/app/components/password-field/page.mdx
new file mode 100644
index 0000000000..2b79425453
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/page.mdx
@@ -0,0 +1,87 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { passwordFieldPropDefs } from './props-definition';
+import {
+ passwordFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+ withIconSnippet,
+ validationSnippet,
+} from './snippets';
+import {
+ WithLabel,
+ Sizes,
+ WithDescription,
+ WithIcon,
+ Validation,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { PasswordFieldDefinition } from '../../../utils/definitions';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ textField: 'https://react-aria.adobe.com/TextField',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### With description
+
+ }
+ code={withDescriptionSnippet}
+/>
+
+### With icon
+
+ }
+ code={withIconSnippet}
+/>
+
+### Validation
+
+ }
+ code={validationSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/password-field/props-definition.tsx b/docs-ui/src/app/components/password-field/props-definition.tsx
new file mode 100644
index 0000000000..63f0f56d25
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/props-definition.tsx
@@ -0,0 +1,77 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const passwordFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for dense layouts,{' '}
+ medium for prominent fields.
+ >
+ ),
+ },
+ label: {
+ type: 'string',
+ description: 'Visible label displayed above the input.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary text shown next to the label. If not provided and isRequired
+ is true, displays Required .
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Help text displayed below the label.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon rendered before the input.',
+ },
+ placeholder: {
+ type: 'string',
+ description: 'Text displayed when the input is empty.',
+ },
+ name: {
+ type: 'string',
+ description: 'Form field name for submission.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Whether the field is required for form submission.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the input is disabled.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Whether the input is read-only.',
+ },
+ value: {
+ type: 'string',
+ description: 'Controlled value of the input.',
+ },
+ defaultValue: {
+ type: 'string',
+ description: 'Default value for uncontrolled usage.',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(value: string) => void'],
+ description: 'Handler called when the input value changes.',
+ },
+ isInvalid: {
+ type: 'boolean',
+ description: 'Whether the field is in an invalid state.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/password-field/snippets.ts b/docs-ui/src/app/components/password-field/snippets.ts
new file mode 100644
index 0000000000..6bc4b9760b
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/snippets.ts
@@ -0,0 +1,33 @@
+export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+ `;
+
+export const withDescriptionSnippet = ` `;
+
+export const withIconSnippet = ` }
+/>`;
+
+export const validationSnippet = ` `;
diff --git a/docs-ui/src/app/components/plugin-header/components.tsx b/docs-ui/src/app/components/plugin-header/components.tsx
new file mode 100644
index 0000000000..b3d831e718
--- /dev/null
+++ b/docs-ui/src/app/components/plugin-header/components.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { PluginHeader } from '../../../../../packages/ui/src/components/PluginHeader/PluginHeader';
+import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { MemoryRouter } from 'react-router-dom';
+import {
+ RiHeartLine,
+ RiEmotionHappyLine,
+ RiCloudy2Line,
+} from '@remixicon/react';
+
+const tabs = [
+ { 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' },
+];
+
+const tabs2 = [
+ { id: 'banana', label: 'Banana', href: '/banana' },
+ { id: 'apple', label: 'Apple', href: '/apple' },
+ { id: 'orange', label: 'Orange', href: '/orange' },
+];
+
+export const WithAllOptionsAndTabs = () => (
+
+
+ } />
+ } />
+ } />
+ >
+ }
+ />
+
+);
+
+export const WithAllOptions = () => (
+
+
+ } />
+ } />
+ } />
+ >
+ }
+ />
+
+);
+
+export const WithHeaderPage = () => (
+
+ <>
+
+ Custom action}
+ />
+ >
+
+);
diff --git a/docs-ui/src/app/components/plugin-header/page.mdx b/docs-ui/src/app/components/plugin-header/page.mdx
new file mode 100644
index 0000000000..5f8e02bfc4
--- /dev/null
+++ b/docs-ui/src/app/components/plugin-header/page.mdx
@@ -0,0 +1,57 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import {
+ WithAllOptionsAndTabs,
+ WithAllOptions,
+ WithHeaderPage,
+} from './components';
+import { headerPropDefs } from './props-definition';
+import {
+ usage,
+ simple,
+ defaultSnippet,
+ withTabs,
+ withHeaderPage,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { PluginHeaderDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Simple plugin header
+
+ } code={simple} open />
+
+### Plugin header with tabs
+
+Tabs use React Router and highlight automatically based on the current route.
+
+ } code={withTabs} open />
+
+### Plugin header with HeaderPage
+
+Combine with [HeaderPage](/components/header-page) for multi-level navigation.
+
+ } code={withHeaderPage} open />
+
+
+
+
diff --git a/docs-ui/src/app/components/plugin-header/props-definition.tsx b/docs-ui/src/app/components/plugin-header/props-definition.tsx
new file mode 100644
index 0000000000..4a651cdb72
--- /dev/null
+++ b/docs-ui/src/app/components/plugin-header/props-definition.tsx
@@ -0,0 +1,63 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const headerPropDefs: Record = {
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the title.',
+ },
+ title: {
+ type: 'string',
+ description: 'Main heading text for the header.',
+ },
+ titleLink: {
+ type: 'string',
+ description: 'URL the title links to when clicked.',
+ },
+ customActions: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Custom elements rendered in the toolbar area.',
+ },
+ tabs: {
+ type: 'complex',
+ description: 'Navigation tabs displayed below the toolbar.',
+ complexType: {
+ name: 'HeaderTab[]',
+ properties: {
+ id: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier for the tab.',
+ },
+ label: {
+ type: 'string',
+ required: true,
+ description: 'Display text for the tab.',
+ },
+ href: {
+ type: 'string',
+ required: true,
+ description: 'URL to navigate to when tab is clicked.',
+ },
+ matchStrategy: {
+ type: "'exact' | 'prefix'",
+ required: false,
+ description: (
+ <>
+ Route matching strategy. Use exact for exact path
+ match, prefix if pathname starts with href.
+ >
+ ),
+ },
+ },
+ },
+ },
+ onTabSelectionChange: {
+ type: 'enum',
+ values: ['(key: Key) => void'],
+ description: 'Handler called when the selected tab changes.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/plugin-header/snippets.ts b/docs-ui/src/app/components/plugin-header/snippets.ts
new file mode 100644
index 0000000000..4e9518af73
--- /dev/null
+++ b/docs-ui/src/app/components/plugin-header/snippets.ts
@@ -0,0 +1,61 @@
+export const usage = `import { PluginHeader } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = `
+ } />
+ } />
+ } />
+ >
+ }
+/>`;
+
+export const simple = `
+ } />
+ } />
+ } />
+ >
+ }
+/>`;
+
+export const withTabs = ` `;
+
+export const withHeaderPage = `
+Custom action}
+/>`;
diff --git a/docs-ui/src/app/components/popover/components.tsx b/docs-ui/src/app/components/popover/components.tsx
new file mode 100644
index 0000000000..4135d88059
--- /dev/null
+++ b/docs-ui/src/app/components/popover/components.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { Popover } from '../../../../../packages/ui/src/components/Popover/Popover';
+import { DialogTrigger } from '../../../../../packages/ui/src/components/Dialog/Dialog';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+ Open Popover
+
+ Popover content
+
+
+ );
+};
+
+export const Placement = () => {
+ return (
+
+
+ Top
+
+ Content above trigger
+
+
+
+ Right
+
+ Content to the right
+
+
+
+ Bottom
+
+ Content below trigger
+
+
+
+ Left
+
+ Content to the left
+
+
+
+ );
+};
+
+export const HideArrow = () => {
+ return (
+
+ Open Popover
+
+ Popover without arrow
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/popover/page.mdx b/docs-ui/src/app/components/popover/page.mdx
new file mode 100644
index 0000000000..0d546cdc05
--- /dev/null
+++ b/docs-ui/src/app/components/popover/page.mdx
@@ -0,0 +1,74 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { dialogTriggerPropDefs, popoverPropDefs } from './props-definition';
+import {
+ popoverUsageSnippet,
+ defaultSnippet,
+ placementSnippet,
+ hideArrowSnippet,
+} from './snippets';
+import { Default, Placement, HideArrow } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { PopoverDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ dialogTrigger: 'https://react-aria.adobe.com/Modal#dialogtrigger',
+ popover: 'https://react-aria.adobe.com/Popover',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### DialogTrigger
+
+Wraps the trigger button and popover content.
+
+
+
+
+
+### Popover
+
+Displays floating content with automatic positioning.
+
+
+
+
+
+## Examples
+
+### Placement
+
+ }
+ code={placementSnippet}
+/>
+
+### Hidden arrow
+
+ }
+ code={hideArrowSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/popover/props-definition.ts b/docs-ui/src/app/components/popover/props-definition.ts
new file mode 100644
index 0000000000..563bfa7d66
--- /dev/null
+++ b/docs-ui/src/app/components/popover/props-definition.ts
@@ -0,0 +1,47 @@
+import { childrenPropDefs, classNamePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const dialogTriggerPropDefs: Record = {
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Whether the popover is open by default (uncontrolled).',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the popover is open (controlled).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Handler called when the popover open state changes.',
+ },
+ ...childrenPropDefs,
+};
+
+export const popoverPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: ['top', 'right', 'bottom', 'left'],
+ description:
+ 'The placement of the popover relative to the trigger element.',
+ },
+ offset: {
+ type: 'number',
+ default: '8',
+ description:
+ 'The distance in pixels between the popover and the trigger element.',
+ },
+ containerPadding: {
+ type: 'number',
+ default: '12',
+ description:
+ 'The minimum distance in pixels from the edge of the viewport.',
+ },
+ hideArrow: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether to hide the arrow pointing to the trigger element.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/popover/snippets.ts b/docs-ui/src/app/components/popover/snippets.ts
new file mode 100644
index 0000000000..986b33c770
--- /dev/null
+++ b/docs-ui/src/app/components/popover/snippets.ts
@@ -0,0 +1,49 @@
+export const popoverUsageSnippet = `import { DialogTrigger, Popover, Button, Text } from '@backstage/ui';
+
+
+ Open Popover
+
+ Popover content
+
+ `;
+
+export const defaultSnippet = `
+ Open Popover
+
+ Popover content
+
+ `;
+
+export const placementSnippet = `
+
+ Top
+
+ Content above trigger
+
+
+
+ Right
+
+ Content to the right
+
+
+
+ Bottom
+
+ Content below trigger
+
+
+
+ Left
+
+ Content to the left
+
+
+ `;
+
+export const hideArrowSnippet = `
+ Open Popover
+
+ Popover without arrow
+
+ `;
diff --git a/docs-ui/src/app/components/radio-group/components.tsx b/docs-ui/src/app/components/radio-group/components.tsx
new file mode 100644
index 0000000000..56870c0166
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/components.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import {
+ RadioGroup,
+ Radio,
+} from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
+
+export const Default = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const Horizontal = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const DisabledSingle = () => {
+ return (
+
+ Bulbasaur
+
+ Charmander
+
+ Squirtle
+
+ );
+};
+
+export const Validation = () => {
+ return (
+ (value === 'charmander' ? 'Nice try!' : null)}
+ >
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const ReadOnly = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
diff --git a/docs-ui/src/app/components/radio-group/page.mdx b/docs-ui/src/app/components/radio-group/page.mdx
new file mode 100644
index 0000000000..5cc296b959
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/page.mdx
@@ -0,0 +1,118 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { radioGroupPropDefs, radioPropDefs } from './props-definition';
+import {
+ radioGroupUsageSnippet,
+ defaultSnippet,
+ horizontalSnippet,
+ disabledSnippet,
+ disabledSingleSnippet,
+ validationSnippet,
+ readOnlySnippet,
+} from './snippets';
+import {
+ Default,
+ Horizontal,
+ Disabled,
+ DisabledSingle,
+ Validation,
+ ReadOnly,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { RadioGroupDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ radioGroup: 'https://react-aria.adobe.com/RadioGroup',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### RadioGroup
+
+
+
+
+
+### Radio
+
+Individual radio button within a group.
+
+
+
+
+
+## Examples
+
+### Horizontal
+
+ }
+ code={horizontalSnippet}
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+### Disabled single radio
+
+ }
+ code={disabledSingleSnippet}
+/>
+
+### Validation
+
+ }
+ code={validationSnippet}
+/>
+
+### Read only
+
+ }
+ code={readOnlySnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/radio-group/props-definition.tsx b/docs-ui/src/app/components/radio-group/props-definition.tsx
new file mode 100644
index 0000000000..54da42d93a
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/props-definition.tsx
@@ -0,0 +1,91 @@
+import {
+ classNamePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const radioGroupPropDefs: Record = {
+ label: {
+ type: 'string',
+ description: 'The visible label for the radio group.',
+ },
+ 'aria-label': {
+ type: 'string',
+ description:
+ 'Accessible label when a visible label is not provided. Either label, aria-label, or aria-labelledby is required.',
+ },
+ 'aria-labelledby': {
+ type: 'string',
+ description:
+ 'ID of an element that labels the radio group. Either label, aria-label, or aria-labelledby is required.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary label text. Defaults to Required when isRequired
+ is true.
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Helper text displayed below the label.',
+ },
+ orientation: {
+ type: 'enum',
+ values: ['horizontal', 'vertical'],
+ default: 'vertical',
+ description: 'The axis the radio buttons should align with.',
+ },
+ value: {
+ type: 'string',
+ description: 'The current value (controlled).',
+ },
+ defaultValue: {
+ type: 'string',
+ description: 'The default value (uncontrolled).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(value: string) => void'],
+ description: 'Handler called when the value changes.',
+ },
+ name: {
+ type: 'string',
+ description: 'The name of the radio group for form submission.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether all radio buttons in the group are disabled.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Whether the radio group is read-only.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Whether a selection is required before form submission.',
+ },
+ isInvalid: {
+ type: 'boolean',
+ description: 'Whether the radio group is in an invalid state.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const radioPropDefs: Record = {
+ value: {
+ type: 'string',
+ required: true,
+ description: 'The value of the radio button.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether this radio button is disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/radio-group/snippets.ts b/docs-ui/src/app/components/radio-group/snippets.ts
new file mode 100644
index 0000000000..d0b14ef5c4
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/snippets.ts
@@ -0,0 +1,57 @@
+export const radioGroupUsageSnippet = `import { RadioGroup, Radio } from '@backstage/ui';
+
+
+ Option 1
+ Option 2
+ `;
+
+export const defaultSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const horizontalSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const disabledSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const disabledSingleSnippet = `
+ Bulbasaur
+
+ Charmander
+
+ Squirtle
+ `;
+
+export const validationSnippet = ` (value === 'charmander' ? 'Nice try!' : null)}
+>
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const readOnlySnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
diff --git a/docs-ui/src/app/components/search-field/components.tsx b/docs-ui/src/app/components/search-field/components.tsx
new file mode 100644
index 0000000000..84a4c6018d
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/components.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
+
+export const StartCollapsed = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/search-field/page.mdx b/docs-ui/src/app/components/search-field/page.mdx
new file mode 100644
index 0000000000..425dbc8f62
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/page.mdx
@@ -0,0 +1,91 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { searchFieldPropDefs } from './props-definition';
+import {
+ searchFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+ startCollapsedSnippet,
+} from './snippets';
+import {
+ WithLabel,
+ Sizes,
+ WithDescription,
+ StartCollapsed,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { SearchFieldDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ searchField: 'https://react-aria.adobe.com/SearchField',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With description
+
+Add context below the input with the `description` prop.
+
+ }
+ code={withDescriptionSnippet}
+ layout="side-by-side"
+/>
+
+### Collapsible
+
+Use `startCollapsed` for space-constrained layouts where the field expands on focus.
+
+ }
+ code={startCollapsedSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/search-field/props-definition.tsx b/docs-ui/src/app/components/search-field/props-definition.tsx
new file mode 100644
index 0000000000..58d2cb0988
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/props-definition.tsx
@@ -0,0 +1,85 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const searchFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for inline or dense
+ layouts, medium for standalone fields.
+ >
+ ),
+ },
+ label: {
+ type: 'string',
+ description: 'The visible label for the search field.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary label text. Defaults to Required when isRequired
+ is true.
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Helper text displayed below the label.',
+ },
+ placeholder: {
+ type: 'string',
+ default: 'Search',
+ description: 'Placeholder text shown when the field is empty.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode', 'false'],
+ description:
+ 'Icon displayed before the input. Set to false to hide the icon.',
+ },
+ startCollapsed: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether the search field starts in a collapsed state. Expands on focus.',
+ },
+ name: {
+ type: 'string',
+ description: 'The name of the input for form submission.',
+ },
+ value: {
+ type: 'string',
+ description: 'The current value (controlled).',
+ },
+ defaultValue: {
+ type: 'string',
+ description: 'The default value (uncontrolled).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(value: string) => void'],
+ description: 'Handler called when the value changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the search field is disabled.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Whether the search field is read-only.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Whether a value is required before form submission.',
+ },
+ isInvalid: {
+ type: 'boolean',
+ description: 'Whether the search field is in an invalid state.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/search-field/snippets.ts b/docs-ui/src/app/components/search-field/snippets.ts
new file mode 100644
index 0000000000..e54bca1bf4
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/snippets.ts
@@ -0,0 +1,20 @@
+export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+ `;
+
+export const withDescriptionSnippet = ` `;
+
+export const startCollapsedSnippet = `
+
+
+ `;
diff --git a/docs-ui/src/app/components/select/components.tsx b/docs-ui/src/app/components/select/components.tsx
new file mode 100644
index 0000000000..1cbc323b84
--- /dev/null
+++ b/docs-ui/src/app/components/select/components.tsx
@@ -0,0 +1,150 @@
+'use client';
+
+import { Select } from '../../../../../packages/ui/src/components/Select/Select';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCloudLine } from '@remixicon/react';
+
+const fontOptions = [
+ { value: 'sans', label: 'Sans-serif' },
+ { value: 'serif', label: 'Serif' },
+ { value: 'mono', label: 'Monospace' },
+ { value: 'cursive', label: 'Cursive' },
+];
+
+const countries = [
+ { value: 'us', label: 'United States' },
+ { value: 'ca', label: 'Canada' },
+ { value: 'mx', label: 'Mexico' },
+ { value: 'uk', label: 'United Kingdom' },
+ { value: 'fr', label: 'France' },
+ { value: 'de', label: 'Germany' },
+ { value: 'it', label: 'Italy' },
+ { value: 'es', label: 'Spain' },
+ { value: 'jp', label: 'Japan' },
+ { value: 'cn', label: 'China' },
+ { value: 'in', label: 'India' },
+ { value: 'br', label: 'Brazil' },
+ { value: 'au', label: 'Australia' },
+];
+
+const skills = [
+ { value: 'react', label: 'React' },
+ { value: 'typescript', label: 'TypeScript' },
+ { value: 'javascript', label: 'JavaScript' },
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ { value: 'csharp', label: 'C#' },
+ { value: 'go', label: 'Go' },
+ { value: 'rust', label: 'Rust' },
+ { value: 'kotlin', label: 'Kotlin' },
+ { value: 'swift', label: 'Swift' },
+];
+
+export const Preview = () => (
+
+);
+
+export const WithLabelAndDescription = () => (
+
+);
+
+export const Sizes = () => (
+
+
+
+
+);
+
+export const WithIcon = () => (
+ }
+ style={{ width: 300 }}
+ />
+);
+
+export const Disabled = () => (
+
+);
+
+export const DisabledOption = () => (
+
+);
+
+export const Searchable = () => (
+
+);
+
+export const MultipleSelection = () => (
+
+);
+
+export const SearchableMultiple = () => (
+
+);
diff --git a/docs-ui/src/app/components/select/page.mdx b/docs-ui/src/app/components/select/page.mdx
new file mode 100644
index 0000000000..8e7df65ef9
--- /dev/null
+++ b/docs-ui/src/app/components/select/page.mdx
@@ -0,0 +1,147 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ Preview,
+ WithLabelAndDescription,
+ Sizes,
+ WithIcon,
+ Disabled,
+ DisabledOption,
+ Searchable,
+ MultipleSelection,
+ SearchableMultiple,
+} from './components';
+import { selectPropDefs } from './props-definition';
+import {
+ selectUsageSnippet,
+ selectDefaultSnippet,
+ selectDescriptionSnippet,
+ selectSizesSnippet,
+ selectDisabledSnippet,
+ selectResponsiveSnippet,
+ selectIconSnippet,
+ selectSearchableSnippet,
+ selectMultipleSnippet,
+ selectSearchableMultipleSnippet,
+ selectDisabledOptionsSnippet,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { SelectDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ select: 'https://react-aria.adobe.com/Select',
+};
+
+
+
+ }
+ code={selectDefaultSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Label and description
+
+ }
+ code={selectDescriptionSnippet}
+/>
+
+### Sizes
+
+ }
+ code={selectSizesSnippet}
+/>
+
+### With icon
+
+ }
+ code={selectIconSnippet}
+/>
+
+### Disabled
+
+ }
+ code={selectDisabledSnippet}
+/>
+
+### Disabled options
+
+ }
+ code={selectDisabledOptionsSnippet}
+/>
+
+### Searchable
+
+Enable filtering with the `searchable` prop.
+
+ }
+ code={selectSearchableSnippet}
+/>
+
+### Multiple selection
+
+ }
+ code={selectMultipleSnippet}
+/>
+
+### Searchable multiple
+
+Combine search and multiple selection.
+
+ }
+ code={selectSearchableMultipleSnippet}
+/>
+
+### Responsive
+
+Size can change at different breakpoints.
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/select/props-definition.tsx b/docs-ui/src/app/components/select/props-definition.tsx
new file mode 100644
index 0000000000..0d1ab22202
--- /dev/null
+++ b/docs-ui/src/app/components/select/props-definition.tsx
@@ -0,0 +1,137 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const selectPropDefs: Record = {
+ options: {
+ type: 'complex',
+ description: 'Array of options to display in the dropdown.',
+ complexType: {
+ name: 'SelectOption[]',
+ properties: {
+ value: {
+ type: 'string',
+ required: true,
+ description: 'Unique value for the option.',
+ },
+ label: {
+ type: 'string',
+ required: true,
+ description: 'Display text for the option.',
+ },
+ disabled: {
+ type: 'boolean',
+ required: false,
+ description: 'Whether the option is disabled.',
+ },
+ },
+ },
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['single', 'multiple'],
+ default: 'single',
+ description: 'Single or multiple selection mode.',
+ },
+ value: {
+ type: 'enum',
+ values: ['string', 'string[]'],
+ description:
+ 'Controlled selected value. String for single, array for multiple.',
+ },
+ defaultValue: {
+ type: 'enum',
+ values: ['string', 'string[]'],
+ description:
+ 'Initial value for uncontrolled usage. String for single, array for multiple.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(key: Key | null) => void', '(keys: Selection) => void'],
+ description: 'Called when selection changes.',
+ },
+ label: {
+ type: 'string',
+ description: 'Visible label above the select.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary text shown next to the label. If not provided and isRequired
+ is true, displays Required .
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Helper text displayed below the label.',
+ },
+ placeholder: {
+ type: 'string',
+ default: 'Select an option',
+ description: 'Text shown when no option is selected.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: 'Visual size of the select field.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the selected value.',
+ },
+ searchable: {
+ type: 'boolean',
+ default: false,
+ description: 'Enables search/filter functionality in the dropdown.',
+ },
+ searchPlaceholder: {
+ type: 'string',
+ default: 'Search...',
+ description:
+ 'Placeholder text for the search input when searchable is true.',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Controlled open state. Use with onOpenChange.',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Initial open state for uncontrolled usage.',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Called when the dropdown opens or closes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents user interaction when true.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of options that should be disabled.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Marks the field as required for form validation.',
+ },
+ isInvalid: {
+ type: 'boolean',
+ description: 'Displays the select in an error state.',
+ },
+ name: {
+ type: 'string',
+ description: 'Form field name for form submission.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/content/select.props.ts b/docs-ui/src/app/components/select/snippets.ts
similarity index 51%
rename from docs-ui/src/content/select.props.ts
rename to docs-ui/src/app/components/select/snippets.ts
index 05f0d895c9..ca16481eaa 100644
--- a/docs-ui/src/content/select.props.ts
+++ b/docs-ui/src/app/components/select/snippets.ts
@@ -1,115 +1,3 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const selectPropDefs: Record = {
- label: {
- type: 'string',
- responsive: false,
- },
- description: {
- type: 'string',
- responsive: false,
- },
- name: {
- type: 'string',
- responsive: false,
- required: true,
- },
- options: {
- type: 'enum',
- values: ['Array<{ value: string, label: string }>'],
- required: true,
- },
- selectionMode: {
- type: 'enum',
- values: ['single', 'multiple'],
- default: 'single',
- responsive: false,
- },
- placeholder: {
- type: 'string',
- default: 'Select an item',
- responsive: false,
- },
- icon: {
- type: 'enum',
- values: ['ReactNode'],
- responsive: false,
- },
- value: {
- type: 'enum',
- values: ['string', 'string[]'],
- responsive: false,
- description:
- 'Selected value (controlled). String for single selection, array for multiple.',
- },
- defaultValue: {
- type: 'enum',
- values: ['string', 'string[]'],
- responsive: false,
- description:
- 'Initial value (uncontrolled). String for single selection, array for multiple.',
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'small',
- responsive: true,
- },
- isOpen: {
- type: 'boolean',
- responsive: false,
- },
- defaultOpen: {
- type: 'boolean',
- responsive: false,
- },
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- responsive: false,
- },
- isDisabled: {
- type: 'boolean',
- responsive: false,
- },
- isRequired: {
- type: 'boolean',
- responsive: false,
- },
- isInvalid: {
- type: 'boolean',
- responsive: false,
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- responsive: false,
- },
- onSelectionChange: {
- type: 'enum',
- values: ['(key: Key | null) => void', '(keys: Selection) => void'],
- responsive: false,
- description:
- 'Handler called when selection changes. Single mode: receives Key | null. Multiple mode: receives Selection.',
- },
- searchable: {
- type: 'boolean',
- default: 'false',
- responsive: false,
- },
- searchPlaceholder: {
- type: 'string',
- default: 'Search...',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
export const selectUsageSnippet = `import { Select } from '@backstage/ui';
`;
export const selectDisabledSnippet = ` `;
@@ -209,3 +97,16 @@ export const selectSearchableMultipleSnippet = ` `;
+
+export const selectDisabledOptionsSnippet = ` `;
diff --git a/docs-ui/src/app/components/skeleton/components.tsx b/docs-ui/src/app/components/skeleton/components.tsx
new file mode 100644
index 0000000000..9f83a013db
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/components.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { Skeleton } from '../../../../../packages/ui/src/components/Skeleton/Skeleton';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Box } from '../../../../../packages/ui/src/components/Box/Box';
+
+export const CardPlaceholder = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const AvatarWithText = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Rounded = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/content/skeleton.mdx b/docs-ui/src/app/components/skeleton/page.mdx
similarity index 52%
rename from docs-ui/src/content/skeleton.mdx
rename to docs-ui/src/app/components/skeleton/page.mdx
index 07cd4b3332..b7d218af89 100644
--- a/docs-ui/src/content/skeleton.mdx
+++ b/docs-ui/src/app/components/skeleton/page.mdx
@@ -1,18 +1,18 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { SkeletonSnippet } from '@/snippets/stories-snippets';
+import { skeletonPropDefs } from './props-definition';
import {
- skeletonPropDefs,
skeletonUsageSnippet,
- skeletonDefaultSnippet,
- skeletonDemo1Snippet,
- skeletonDemo2Snippet,
-} from './skeleton.props';
+ cardPlaceholderSnippet,
+ avatarWithTextSnippet,
+ roundedSnippet,
+} from './snippets';
+import { CardPlaceholder, AvatarWithText, Rounded } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { SkeletonDefinition } from '../utils/definitions';
+import { SkeletonDefinition } from '../../../utils/definitions';
}
- code={skeletonDefaultSnippet}
+ preview={ }
+ code={cardPlaceholderSnippet}
/>
## Usage
@@ -32,32 +32,28 @@ import { SkeletonDefinition } from '../utils/definitions';
## API reference
+Skeleton extends standard HTML div attributes.
+
## Examples
-### Demo 1
-
-You can use a mix of different sizes to create a more complex skeleton.
+### Rounded
}
- code={skeletonDemo1Snippet}
+ layout="side-by-side"
open
+ preview={ }
+ code={roundedSnippet}
/>
-### Demo 2
-
-You can use a mix of different sizes to create a more complex skeleton.
+### Avatar with text
}
- code={skeletonDemo2Snippet}
+ layout="side-by-side"
open
+ preview={ }
+ code={avatarWithTextSnippet}
/>
diff --git a/docs-ui/src/app/components/skeleton/props-definition.ts b/docs-ui/src/app/components/skeleton/props-definition.ts
new file mode 100644
index 0000000000..8c14c4c0ec
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/props-definition.ts
@@ -0,0 +1,28 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const skeletonPropDefs: Record = {
+ width: {
+ type: 'string',
+ default: '80',
+ description:
+ 'The width of the skeleton. Accepts a number (pixels) or CSS string value.',
+ },
+ height: {
+ type: 'string',
+ default: '24',
+ description:
+ 'The height of the skeleton. Accepts a number (pixels) or CSS string value.',
+ },
+ rounded: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether to apply fully rounded corners (for circular shapes).',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/skeleton/snippets.ts b/docs-ui/src/app/components/skeleton/snippets.ts
new file mode 100644
index 0000000000..84bbec1616
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/snippets.ts
@@ -0,0 +1,30 @@
+export const skeletonUsageSnippet = `import { Skeleton } from '@backstage/ui';
+
+ `;
+
+export const cardPlaceholderSnippet = `
+
+
+
+
+
+ `;
+
+export const avatarWithTextSnippet = `
+
+
+
+
+
+
+
+
+ `;
+
+export const roundedSnippet = `
+
+
+
+
+
+ `;
diff --git a/docs-ui/src/app/components/switch/components.tsx b/docs-ui/src/app/components/switch/components.tsx
new file mode 100644
index 0000000000..604d9d68b1
--- /dev/null
+++ b/docs-ui/src/app/components/switch/components.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { Switch } from '../../../../../packages/ui/src/components/Switch/Switch';
+
+export const Default = () => {
+ return ;
+};
+
+export const Disabled = () => {
+ return ;
+};
diff --git a/docs-ui/src/app/components/switch/page.mdx b/docs-ui/src/app/components/switch/page.mdx
new file mode 100644
index 0000000000..1f893aea36
--- /dev/null
+++ b/docs-ui/src/app/components/switch/page.mdx
@@ -0,0 +1,42 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { switchPropDefs } from './props-definition';
+import { snippetUsage, defaultSnippet, disabledSnippet } from './snippets';
+import { Default, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { SwitchDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ switch: 'https://react-aria.adobe.com/Switch',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Disabled
+
+ } code={disabledSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/switch/props-definition.ts b/docs-ui/src/app/components/switch/props-definition.ts
new file mode 100644
index 0000000000..eeab3e64b8
--- /dev/null
+++ b/docs-ui/src/app/components/switch/props-definition.ts
@@ -0,0 +1,45 @@
+import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const switchPropDefs: Record = {
+ label: {
+ type: 'string',
+ description: 'Text label displayed next to the switch.',
+ },
+ isSelected: {
+ type: 'boolean',
+ description:
+ 'Controlled selected state. Use with onChange for controlled behavior.',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'Initial selected state for uncontrolled usage.',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Called when the switch state changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents user interaction when true.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Makes the switch non-interactive but still focusable.',
+ },
+ name: {
+ type: 'string',
+ description: 'Form field name for form submission.',
+ },
+ value: {
+ type: 'string',
+ description: 'Form field value submitted when selected.',
+ },
+ autoFocus: {
+ type: 'boolean',
+ description: 'Focuses the switch on mount.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/switch/snippets.ts b/docs-ui/src/app/components/switch/snippets.ts
new file mode 100644
index 0000000000..694f15106c
--- /dev/null
+++ b/docs-ui/src/app/components/switch/snippets.ts
@@ -0,0 +1,7 @@
+export const snippetUsage = `import { Switch } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const disabledSnippet = ` `;
diff --git a/docs-ui/src/app/components/table/components.tsx b/docs-ui/src/app/components/table/components.tsx
new file mode 100644
index 0000000000..d26deeb5d3
--- /dev/null
+++ b/docs-ui/src/app/components/table/components.tsx
@@ -0,0 +1,554 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ Table,
+ TableRoot,
+ TableHeader,
+ TableBody,
+ Column,
+ Row,
+ CellProfile,
+ CellText,
+ useTable,
+ type ColumnConfig,
+} from '../../../../../packages/ui/src/components/Table';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
+import {
+ RadioGroup,
+ Radio,
+} from '../../../../../packages/ui/src/components/RadioGroup';
+import { data as rockBandData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
+import { data as catalogData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data1';
+import { MemoryRouter } from 'react-router-dom';
+
+// =============================================================================
+// Types
+// =============================================================================
+
+type RockBandItem = (typeof rockBandData)[0];
+type CatalogItem = (typeof catalogData)[0];
+
+// =============================================================================
+// Hero Example
+// =============================================================================
+
+const heroColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ defaultWidth: '3fr',
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ defaultWidth: '3fr',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ defaultWidth: '1fr',
+ cell: item => ,
+ },
+ {
+ id: 'albums',
+ label: 'Albums',
+ defaultWidth: '1fr',
+ cell: item => ,
+ },
+];
+
+export function HeroExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => rockBandData,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+
+
+
+ );
+}
+
+// =============================================================================
+// Sorting Example
+// =============================================================================
+
+const sortingColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ isSortable: true,
+ cell: item => ,
+ },
+];
+
+export function SortingExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData,
+ paginationOptions: { pageSize: 5 },
+ initialSort: { column: 'name', direction: 'ascending' },
+ sortFn: (items, { column, direction }) => {
+ return [...items].sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ if (column === 'owner') {
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ } else {
+ aVal = String(a[column as keyof CatalogItem]);
+ bVal = String(b[column as keyof CatalogItem]);
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return direction === 'descending' ? -cmp : cmp;
+ });
+ },
+ });
+
+ return (
+
+
+
+ );
+}
+
+// =============================================================================
+// Search Example
+// =============================================================================
+
+const searchColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function SearchExample() {
+ const { tableProps, search } = useTable({
+ mode: 'complete',
+ getData: () => catalogData,
+ paginationOptions: { pageSize: 5 },
+ searchFn: (items, query) => {
+ const lowerQuery = query.toLocaleLowerCase('en-US');
+ return items.filter(
+ item =>
+ item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
+ );
+ },
+ });
+
+ return (
+
+
+
+ No results found for "{search.value}"
+ ) : (
+ No data available
+ )
+ }
+ {...tableProps}
+ />
+
+
+ );
+}
+
+// =============================================================================
+// Selection Example
+// =============================================================================
+
+const selectionColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function SelectionExample() {
+ const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
+ 'multiple',
+ );
+ const [selectionBehavior, setSelectionBehavior] = useState<
+ 'toggle' | 'replace'
+ >('toggle');
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+
+
+
+
+
+ Selection mode:
+
+ {
+ setSelectionMode(value as 'single' | 'multiple');
+ setSelected(new Set());
+ }}
+ >
+ single
+ multiple
+
+
+
+
+ Selection behavior:
+
+ {
+ setSelectionBehavior(value as 'toggle' | 'replace');
+ setSelected(new Set());
+ }}
+ >
+ toggle
+ replace
+
+
+
+
+
+ );
+}
+
+// =============================================================================
+// Row Actions Example
+// =============================================================================
+
+export function RowActionsExample() {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+ alert(`Clicked: ${item.name}`),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Empty State Example
+// =============================================================================
+
+const emptyStateColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function EmptyStateExample() {
+ const emptyData: CatalogItem[] = [];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => emptyData,
+ });
+
+ return (
+
+ No items yet. Create one to get started.}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Combined Example
+// =============================================================================
+
+const combinedColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => ,
+ },
+];
+
+export function CombinedExample() {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps, search } = useTable({
+ mode: 'offset',
+ getData: async ({ offset, pageSize, search: query, sort }) => {
+ // Simulate network delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Filter data
+ let filtered = catalogData;
+ if (query) {
+ const lowerQuery = query.toLocaleLowerCase('en-US');
+ filtered = filtered.filter(
+ item =>
+ item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
+ );
+ }
+
+ // Sort data
+ if (sort?.column) {
+ filtered = [...filtered].sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ if (sort.column === 'owner') {
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ } else {
+ aVal = String(a[sort.column as keyof CatalogItem]);
+ bVal = String(b[sort.column as keyof CatalogItem]);
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return sort.direction === 'descending' ? -cmp : cmp;
+ });
+ }
+
+ // Paginate
+ const data = filtered.slice(offset, offset + pageSize);
+
+ return { data, totalCount: filtered.length };
+ },
+ paginationOptions: { pageSize: 5, pageSizeOptions: [5, 10, 20] },
+ initialSort: { column: 'name', direction: 'ascending' },
+ });
+
+ return (
+
+
+
+ alert(`Clicked: ${item.name}`),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ emptyState={
+ search.value ? (
+ No results match your search
+ ) : (
+ No items available
+ )
+ }
+ {...tableProps}
+ />
+
+
+ );
+}
+
+// =============================================================================
+// Custom Row Example
+// =============================================================================
+
+const customRowColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ cell: item => ,
+ },
+];
+
+export function CustomRowExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+ (
+
+ {column => column.cell(item)}
+
+ )}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Primitives Example
+// =============================================================================
+
+export function PrimitivesExample() {
+ const items = catalogData.slice(0, 5);
+
+ return (
+
+
+
+ Name
+ Owner
+ Type
+
+
+ {items.map(item => (
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/docs-ui/src/app/components/table/page.mdx b/docs-ui/src/app/components/table/page.mdx
new file mode 100644
index 0000000000..6eb280cbe2
--- /dev/null
+++ b/docs-ui/src/app/components/table/page.mdx
@@ -0,0 +1,295 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ HeroExample,
+ SortingExample,
+ SearchExample,
+ SelectionExample,
+ RowActionsExample,
+ EmptyStateExample,
+ CombinedExample,
+ CustomRowExample,
+ PrimitivesExample,
+} from './components';
+import {
+ tableReturnColumns,
+ useTableOptionsPropDefs,
+ useTableReturnPropDefs,
+ tablePropDefs,
+ tablePaginationPropDefs,
+ columnConfigPropDefs,
+ cellTextPropDefs,
+ cellProfilePropDefs,
+ tableRootPropDefs,
+ columnPropDefs,
+ rowPropDefs,
+} from './props-definition';
+import {
+ tableUsageSnippet,
+ tableHeroSnippet,
+ tableConceptsSnippet,
+ tableSortingSnippet,
+ tablePaginationSnippet,
+ tableSearchSnippet,
+ tableSelectionSnippet,
+ tableRowActionsHrefSnippet,
+ tableRowActionsClickSnippet,
+ tableRowActionsDisabledSnippet,
+ tableEmptyStateSnippet,
+ tableOffsetPaginationSnippet,
+ tableCursorPaginationSnippet,
+ tableCombinedSnippet,
+ tableCustomRowSnippet,
+ tablePrimitivesSnippet,
+} from './snippets';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { TableDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ table: 'https://react-aria.adobe.com/Table#table',
+ tableHeader: 'https://react-aria.adobe.com/Table#tableheader',
+ tableBody: 'https://react-aria.adobe.com/Table#tablebody',
+ column: 'https://react-aria.adobe.com/Table#column',
+ row: 'https://react-aria.adobe.com/Table#row',
+ cell: 'https://react-aria.adobe.com/Table#cell',
+};
+
+
+
+ } code={tableHeroSnippet} />
+
+## Usage
+
+
+
+## Core Concepts
+
+The Table component is designed around a hook + component pattern:
+
+- **`useTable`** manages data fetching, pagination state, sorting, and filtering. It returns `tableProps` that you spread onto the Table component.
+- **`Table`** handles rendering - columns, rows, cells, and interactions.
+
+The hook supports three modes for different data scenarios:
+
+- **`complete`** - You have all data available upfront (client-side)
+- **`offset`** - Your API uses offset/limit pagination
+- **`cursor`** - Your API uses cursor-based pagination
+
+
+
+**Controlled vs uncontrolled state**
+
+By default, `useTable` manages sort, search, and filter state internally. Pass `initialSort`, `initialSearch`, or `initialFilter` to set starting values.
+
+For full control over state, use the controlled props instead:
+
+- `sort` / `onSortChange`
+- `search` / `onSearchChange`
+- `filter` / `onFilterChange`
+
+## Common Patterns
+
+### Sorting
+
+Columns can be made sortable by adding `isSortable: true` to the column configuration. When sortable, clicking a column header cycles through ascending, descending, and unsorted states. A visual indicator shows the current sort direction.
+
+With `mode: 'complete'`, sorting happens client-side. Provide a `sortFn` that receives the full dataset and the current sort descriptor, and returns the sorted array. You can also set an `initialSort` to define the default sort when the table first renders. For server-side sorting with `offset` or `cursor` modes, see [Server-Side Data](#server-side-data).
+
+ } code={tableSortingSnippet} />
+
+### Pagination
+
+Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically.
+
+
+
+### Search
+
+The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query.
+
+The search state is debounced internally, so rapid typing doesn't trigger excessive re-filtering. When the search query changes, pagination resets to the first page automatically.
+
+For server-side search with `offset` or `cursor` modes, the search query is passed to your `getData` function. See [Server-Side Data](#server-side-data).
+
+ } code={tableSearchSnippet} />
+
+### Row Selection
+
+Tables support row selection with two configuration options: `mode` and `behavior`.
+
+Selection mode controls how many rows can be selected:
+
+- `single` - Only one row can be selected at a time
+- `multiple` - Any number of rows can be selected, with a header checkbox for select-all
+
+Selection behavior controls the interaction style:
+
+- `toggle` - Checkboxes appear for selection. Click a checkbox to select/deselect.
+- `replace` - No checkboxes. Click a row to select it (replacing previous selection). Use Cmd/Ctrl+click to select multiple rows.
+
+Selection state can be managed in two ways:
+
+- **Controlled** - Pass both `selected` and `onSelectionChange` to fully manage state externally
+- **Uncontrolled** - Pass only `onSelectionChange` to let the Table manage state while receiving change notifications
+
+ } code={tableSelectionSnippet} />
+
+### Row Actions
+
+Rows can respond to user interaction in two ways: navigating to a URL or triggering a callback.
+
+Use `getHref` when rows should link to detail pages. The entire row becomes clickable and navigates on click. Links within cells remain independently clickable.
+
+
+
+Use `onClick` for custom actions like opening a panel or triggering a dialog.
+
+
+
+When combining row actions with selection, the interaction depends on selection behavior:
+
+- **`toggle`**: Clicking a row triggers its action when nothing is selected. Once any row is selected, clicking anywhere on the row toggles selection instead.
+- **`replace`**: Single click selects the row. Double-click triggers the row action.
+
+You can also disable specific rows from being clicked using `getIsDisabled`:
+
+
+
+ } code={tableRowActionsHrefSnippet} />
+
+### Empty State
+
+When the table has no data to display, provide an `emptyState` to show a helpful message instead of an empty table body. Consider providing different messages depending on context - an empty search result is different from a table with no data at all.
+
+ } code={tableEmptyStateSnippet} />
+
+## Server-Side Data
+
+When your data comes from an API with server-side pagination, use `offset` or `cursor` mode instead of `complete`. The key difference: instead of providing all data upfront, you provide a `getData` function that fetches data for the current page.
+
+### Offset Pagination
+
+Use `mode: 'offset'` when your API accepts `offset` and `limit` (or `skip` and `take`) parameters.
+
+
+
+The `signal` parameter is an `AbortSignal` - pass it to `fetch` so in-flight requests are cancelled when the user navigates away or triggers a new query.
+
+### Cursor Pagination
+
+Use `mode: 'cursor'` when your API uses cursor-based pagination (common with GraphQL or APIs that return `nextCursor`/`prevCursor` tokens).
+
+
+
+### Loading States
+
+When fetching data, the table shows a loading state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts.
+
+You can access these states via `tableProps.loading` and `tableProps.isStale` if you need to render additional loading indicators.
+
+## Combining Features
+
+Real-world tables often combine multiple features. Here's an example with search, sorting, pagination, and row selection working together.
+
+ } code={tableCombinedSnippet} />
+
+## Custom Tables
+
+For most use cases, `useTable` + `Table` provides everything you need. When you need more control, there are several ways you can customize your table.
+
+### Custom Row and Header Rendering
+
+Use a render function for `rowConfig` when you need to customize individual rows based on their data - for example, highlighting rows that require attention. Use `header` in column config to customize header cell content.
+
+ } code={tableCustomRowSnippet} />
+
+### Using Primitives Directly
+
+When the `Table` component doesn't support your use case, you can compose with the lower-level primitives: `TableRoot`, `TableHeader`, `TableBody`, `Column`, and `Row`.
+
+ } code={tablePrimitivesSnippet} />
+
+## API Reference
+
+### useTable
+
+The `useTable` hook manages data fetching, pagination, sorting, and filtering.
+
+**Options**
+
+
+
+**Return Value**
+
+
+
+### Table
+
+The main table component.
+
+
+
+### ColumnConfig
+
+
+
+### CellText
+
+
+
+
+
+### CellProfile
+
+
+
+
+
+### TablePagination
+
+
+
+### Primitives
+
+Low-level components for building custom table layouts.
+
+#### TableRoot
+
+
+
+
+
+#### TableHeader
+
+
+
+#### TableBody
+
+
+
+#### Column
+
+
+
+
+
+#### Row
+
+
+
+
+
+#### Cell
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx
new file mode 100644
index 0000000000..be24e86bf0
--- /dev/null
+++ b/docs-ui/src/app/components/table/props-definition.tsx
@@ -0,0 +1,461 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+// =============================================================================
+// PropsTable Column Configuration (Table docs use description instead of responsive)
+// =============================================================================
+
+// For return values (no default column)
+export const tableReturnColumns = [
+ { key: 'prop' as const, width: '15%' },
+ { key: 'type' as const, width: '25%' },
+ { key: 'description' as const, width: '60%' },
+];
+
+// =============================================================================
+// useTable Hook
+// =============================================================================
+
+export const useTableOptionsPropDefs: Record = {
+ mode: {
+ type: 'enum',
+ values: ['complete', 'offset', 'cursor'],
+ description: (
+ <>
+ Data fetching strategy (required). Use complete for
+ client-side data, offset for offset/limit APIs,{' '}
+ cursor for cursor-based pagination.
+ >
+ ),
+ },
+ getData: {
+ type: 'enum',
+ values: ['function'],
+ description:
+ 'Function that returns or fetches data (required for "offset" and "cursor" modes). For the "complete" mode, either this or `data` must be provided. Signature varies by mode.',
+ },
+ data: {
+ type: 'enum',
+ values: ['T[]'],
+ description:
+ 'The data for the table. Only applicable for "complete" mode, and either this or `getData` must be provided.',
+ },
+ paginationOptions: {
+ type: 'enum',
+ values: ['object'],
+ description: (
+ <>
+ Pagination configuration including pageSize ,{' '}
+ pageSizeOptions , and initialOffset .
+ >
+ ),
+ },
+ // Uncontrolled state
+ initialSort: {
+ type: 'enum',
+ values: ['SortDescriptor'],
+ description: 'Default sort configuration on first render (uncontrolled).',
+ },
+ initialSearch: {
+ type: 'string',
+ description: 'Default search value on first render (uncontrolled).',
+ },
+ initialFilter: {
+ type: 'enum',
+ values: ['TFilter'],
+ description: 'Default filter value on first render (uncontrolled).',
+ },
+ // Controlled state
+ sort: {
+ type: 'enum',
+ values: ['SortDescriptor'],
+ description: 'Current sort state (controlled).',
+ },
+ onSortChange: {
+ type: 'enum',
+ values: ['(sort: SortDescriptor) => void'],
+ description: 'Sort change handler (controlled).',
+ },
+ search: {
+ type: 'string',
+ description: 'Current search value (controlled).',
+ },
+ onSearchChange: {
+ type: 'enum',
+ values: ['(search: string) => void'],
+ description: 'Search change handler (controlled).',
+ },
+ filter: {
+ type: 'enum',
+ values: ['TFilter'],
+ description: 'Current filter value (controlled).',
+ },
+ onFilterChange: {
+ type: 'enum',
+ values: ['(filter: TFilter) => void'],
+ description: 'Filter change handler (controlled).',
+ },
+ // Client-side functions
+ sortFn: {
+ type: 'enum',
+ values: ['(data, sort) => data'],
+ description: (
+ <>
+ Client-side sort function. Only used with complete mode.
+ >
+ ),
+ },
+ searchFn: {
+ type: 'enum',
+ values: ['(data, query) => data'],
+ description: (
+ <>
+ Client-side search function. Only used with complete mode.
+ >
+ ),
+ },
+ filterFn: {
+ type: 'enum',
+ values: ['(data, filter) => data'],
+ description: (
+ <>
+ Client-side filter function. Only used with complete mode.
+ >
+ ),
+ },
+};
+
+export const useTableReturnPropDefs: Record = {
+ tableProps: {
+ type: 'enum',
+ values: ['object'],
+ description: (
+ <>
+ Props to spread onto the Table component. Includes data,
+ loading, error, pagination, and sort state.
+ >
+ ),
+ },
+ reload: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Function to trigger a data refetch.',
+ },
+ search: {
+ type: 'enum',
+ values: ['{ value, onChange }'],
+ description: (
+ <>
+ Search state object for binding to a SearchField component.
+ >
+ ),
+ },
+ filter: {
+ type: 'enum',
+ values: ['{ value, onChange }'],
+ description: 'Filter state object for binding to filter controls.',
+ },
+};
+
+// =============================================================================
+// Table Component
+// =============================================================================
+
+export const tablePropDefs: Record = {
+ columnConfig: {
+ type: 'enum',
+ values: ['ColumnConfig[]'],
+ description:
+ 'Array of column configurations defining how each column renders.',
+ },
+ data: {
+ type: 'enum',
+ values: ['T[]'],
+ description: 'Array of data items to display in the table.',
+ },
+ loading: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the table is in a loading state.',
+ },
+ isStale: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether the displayed data is stale while new data is loading.',
+ },
+ error: {
+ type: 'enum',
+ values: ['Error'],
+ description: 'Error object if data fetching failed.',
+ },
+ pagination: {
+ type: 'enum',
+ values: ['TablePaginationType'],
+ description: (
+ <>
+ Pagination configuration (required). Use{' '}
+ {'{ type: "none" }'} to disable or{' '}
+ {'{ type: "page", ...props }'} for pagination.
+ >
+ ),
+ },
+ sort: {
+ type: 'enum',
+ values: ['SortState'],
+ description: 'Sort state including current descriptor and change handler.',
+ },
+ rowConfig: {
+ type: 'enum',
+ values: ['RowConfig | RowRenderFn'],
+ description: (
+ <>
+ Row configuration object with getHref , onClick
+ , getIsDisabled , or a render function for custom rows.
+ >
+ ),
+ },
+ selection: {
+ type: 'enum',
+ values: ['TableSelection'],
+ description:
+ 'Selection configuration including mode, behavior, selected keys, and change handler.',
+ },
+ emptyState: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content to display when the table has no data.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+// =============================================================================
+// ColumnConfig
+// =============================================================================
+
+export const columnConfigPropDefs: Record = {
+ id: {
+ type: 'string',
+ description: 'Unique identifier for the column.',
+ },
+ label: {
+ type: 'string',
+ description: 'Display label for the column header.',
+ },
+ cell: {
+ type: 'enum',
+ values: ['(item) => ReactNode'],
+ description: 'Render function for cell content.',
+ },
+ header: {
+ type: 'enum',
+ values: ['() => ReactNode'],
+ description: 'Optional custom render function for the header cell.',
+ },
+ isSortable: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the column supports sorting.',
+ },
+ isHidden: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the column is hidden.',
+ },
+ isRowHeader: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether this column is the row header for accessibility.',
+ },
+ width: {
+ type: 'enum',
+ values: ['ColumnSize'],
+ description: 'Current width of the column.',
+ },
+ defaultWidth: {
+ type: 'enum',
+ values: ['ColumnSize'],
+ description: (
+ <>
+ Default width of the column (e.g., 1fr , 200px
+ ).
+ >
+ ),
+ },
+ minWidth: {
+ type: 'enum',
+ values: ['ColumnStaticSize'],
+ description: 'Minimum width of the column.',
+ },
+ maxWidth: {
+ type: 'enum',
+ values: ['ColumnStaticSize'],
+ description: 'Maximum width of the column.',
+ },
+};
+
+// =============================================================================
+// CellText
+// Note: Extends React Aria Cell props
+// =============================================================================
+
+export const cellTextPropDefs: Record = {
+ title: {
+ type: 'string',
+ description: 'Primary text content of the cell.',
+ },
+ description: {
+ type: 'string',
+ description: 'Secondary description text displayed below the title.',
+ },
+ color: {
+ type: 'enum',
+ values: ['TextColors'],
+ description: 'Text color variant.',
+ },
+ leadingIcon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the text content.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when the cell is clicked.',
+ },
+};
+
+// =============================================================================
+// CellProfile
+// Note: Extends React Aria Cell props
+// =============================================================================
+
+export const cellProfilePropDefs: Record = {
+ name: {
+ type: 'string',
+ description: 'Name to display.',
+ },
+ src: {
+ type: 'string',
+ description: 'URL of the avatar image.',
+ },
+ description: {
+ type: 'string',
+ description: 'Secondary text displayed below the name.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when clicked.',
+ },
+ color: {
+ type: 'enum',
+ values: ['TextColors'],
+ description: 'Text color variant.',
+ },
+};
+
+// =============================================================================
+// TablePagination
+// =============================================================================
+
+export const tablePaginationPropDefs: Record = {
+ pageSize: {
+ type: 'number',
+ description: 'Number of items per page.',
+ },
+ pageSizeOptions: {
+ type: 'enum',
+ values: ['number[]'],
+ description: 'Available page size options for the dropdown.',
+ },
+ offset: {
+ type: 'number',
+ description: 'Current offset (starting index) in the data.',
+ },
+ totalCount: {
+ type: 'number',
+ description: 'Total number of items across all pages.',
+ },
+ hasNextPage: {
+ type: 'boolean',
+ description: 'Whether there is a next page available.',
+ },
+ hasPreviousPage: {
+ type: 'boolean',
+ description: 'Whether there is a previous page available.',
+ },
+ onNextPage: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Handler called when navigating to the next page.',
+ },
+ onPreviousPage: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Handler called when navigating to the previous page.',
+ },
+ onPageSizeChange: {
+ type: 'enum',
+ values: ['(size: number) => void'],
+ description: 'Handler called when the page size changes.',
+ },
+ showPageSizeOptions: {
+ type: 'boolean',
+ default: 'true',
+ description: 'Whether to show the page size dropdown.',
+ },
+ getLabel: {
+ type: 'enum',
+ values: ['(props) => string'],
+ description: 'Custom function to generate the pagination label text.',
+ },
+};
+
+// =============================================================================
+// Primitives
+// =============================================================================
+
+export const tableRootPropDefs: Record = {
+ stale: {
+ type: 'boolean',
+ default: 'false',
+ description: (
+ <>
+ Whether the table data is stale (e.g., while fetching new data). Adds{' '}
+ aria-busy attribute.
+ >
+ ),
+ },
+};
+
+export const columnPropDefs: Record = {
+ isRowHeader: {
+ type: 'boolean',
+ description: 'Whether this column is a row header for accessibility.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Column header content.',
+ },
+};
+
+export const rowPropDefs: Record = {
+ id: {
+ type: 'enum',
+ values: ['string | number'],
+ description: 'Unique identifier for the row.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode | ((column) => ReactNode)'],
+ description:
+ 'Row content. Can be a render function receiving column config.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/table/snippets.ts b/docs-ui/src/app/components/table/snippets.ts
new file mode 100644
index 0000000000..ab4bf53a58
--- /dev/null
+++ b/docs-ui/src/app/components/table/snippets.ts
@@ -0,0 +1,359 @@
+// =============================================================================
+// Usage
+// =============================================================================
+
+export const tableUsageSnippet = `import { Table, useTable, CellText, type ColumnConfig } from '@backstage/ui';
+
+const columns: ColumnConfig[] = [
+ { id: 'name', label: 'Name', isRowHeader: true, cell: item => },
+ { id: 'owner', label: 'Owner', cell: item => },
+];
+
+function MyTable() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ });
+
+ return ;
+}`;
+
+// =============================================================================
+// Hero / Quick Start
+// =============================================================================
+
+export const tableHeroSnippet = `import { Table, CellText, useTable, type ColumnConfig } from '@backstage/ui';
+
+const data = [
+ { id: 1, name: 'Service A', owner: 'Team Alpha', type: 'service' },
+ { id: 2, name: 'Service B', owner: 'Team Beta', type: 'website' },
+ { id: 3, name: 'Library C', owner: 'Team Gamma', type: 'library' },
+];
+
+const columns: ColumnConfig[] = [
+ { id: 'name', label: 'Name', isRowHeader: true, cell: item => },
+ { id: 'owner', label: 'Owner', cell: item => },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+function MyTable() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ });
+
+ return ;
+}`;
+
+// =============================================================================
+// Core Concepts
+// =============================================================================
+
+export const tableConceptsSnippet = `// What useTable returns
+const {
+ tableProps, // Spread onto
+ reload, // Trigger data refetch
+ search, // { value, onChange } for search input
+ filter, // { value, onChange } for filters
+} = useTable({ ... });`;
+
+// =============================================================================
+// Common Patterns
+// =============================================================================
+
+export const tableSortingSnippet = `const columns: ColumnConfig- [] = [
+ { id: 'name', label: 'Name', isSortable: true, cell: item =>
},
+ { id: 'owner', label: 'Owner', isSortable: true, cell: item => },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ initialSort: { column: 'name', direction: 'ascending' },
+ sortFn: (items, { column, direction }) => {
+ return [...items].sort((a, b) => {
+ const aVal = String(a[column]);
+ const bVal = String(b[column]);
+ const cmp = aVal.localeCompare(bVal);
+ return direction === 'descending' ? -cmp : cmp;
+ });
+ },
+});
+
+return ;`;
+
+export const tablePaginationSnippet = `const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ paginationOptions: {
+ pageSize: 10,
+ pageSizeOptions: [10, 25, 50],
+ },
+});`;
+
+export const tableSearchSnippet = `const { tableProps, search } = useTable({
+ mode: 'complete',
+ data,
+ searchFn: (items, query) => {
+ const lowerQuery = query.toLowerCase();
+ return items.filter(item =>
+ item.name.toLowerCase().includes(lowerQuery) ||
+ item.owner.toLowerCase().includes(lowerQuery)
+ );
+ },
+});
+
+return (
+ <>
+
+
+ >
+);`;
+
+export const tableSelectionSnippet = `const [selected, setSelected] = useState | 'all'>(new Set());
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+});
+
+return (
+
+);`;
+
+export const tableRowActionsHrefSnippet = ` \`/catalog/\${item.namespace}/\${item.name}\`
+ }}
+ {...tableProps}
+/>`;
+
+export const tableRowActionsClickSnippet = ` openDetailPanel(item)
+ }}
+ {...tableProps}
+/>`;
+
+export const tableRowActionsDisabledSnippet = ` openDetailPanel(item),
+ getIsDisabled: item => item.status === 'archived',
+ }}
+ {...tableProps}
+/>`;
+
+export const tableEmptyStateSnippet = `const { tableProps, search } = useTable({
+ mode: 'complete',
+ data,
+ searchFn: (items, query) => { /* ... */ },
+});
+
+return (
+ No results match "{search.value}"
+ : No items yet. Create one to get started.
+ }
+ {...tableProps}
+ />
+);`;
+
+// =============================================================================
+// Server-Side Data
+// =============================================================================
+
+export const tableOffsetPaginationSnippet = `const { tableProps } = useTable({
+ mode: 'offset',
+ getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
+ const response = await fetch(
+ \`/api/items?offset=\${offset}&limit=\${pageSize}&q=\${search}\`,
+ { signal }
+ );
+ const { items, totalCount } = await response.json();
+
+ return {
+ data: items,
+ totalCount,
+ };
+ },
+ paginationOptions: {
+ pageSize: 20,
+ pageSizeOptions: [20, 50, 100],
+ },
+});`;
+
+export const tableCursorPaginationSnippet = `const { tableProps } = useTable({
+ mode: 'cursor',
+ getData: async ({ cursor, pageSize, sort, search, signal }) => {
+ const response = await fetch(
+ \`/api/items?cursor=\${cursor ?? ''}&limit=\${pageSize}&q=\${search}\`,
+ { signal }
+ );
+ const { items, nextCursor, prevCursor, totalCount } = await response.json();
+
+ return {
+ data: items,
+ nextCursor,
+ prevCursor,
+ totalCount, // optional - enables "X of Y" display
+ };
+ },
+ paginationOptions: {
+ pageSize: 20,
+ pageSizeOptions: [20, 50, 100],
+ },
+});`;
+
+// =============================================================================
+// Combining Features
+// =============================================================================
+
+export const tableCombinedSnippet = `interface TypeFilter {
+ type: string | null;
+}
+
+const columns: ColumnConfig- [] = [
+ { id: 'name', label: 'Name', isRowHeader: true, isSortable: true,
+ cell: item =>
},
+ { id: 'owner', label: 'Owner', isSortable: true,
+ cell: item => },
+ { id: 'type', label: 'Type', isSortable: true,
+ cell: item => },
+];
+
+function ItemsTable() {
+ const [selected, setSelected] = useState | 'all'>(new Set());
+
+ const { tableProps, search, filter } = useTable- ({
+ mode: 'offset',
+ initialSort: { column: 'name', direction: 'ascending' },
+ getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
+ const params = new URLSearchParams({
+ offset: String(offset),
+ limit: String(pageSize),
+ q: search,
+ ...(sort && { sortBy: sort.column, sortDir: sort.direction }),
+ ...(filter?.type && { type: filter.type }),
+ });
+
+ const response = await fetch(\`/api/items?\${params}\`, { signal });
+ const { items, totalCount } = await response.json();
+
+ return { data: items, totalCount };
+ },
+ });
+
+ return (
+
+
+
+ filter.onChange({ type: value || null })}
+ />
+
+ openDetailPanel(item),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ emptyState={
+ search.value || filter.value?.type
+ ? No results match your filters
+ : No items available
+ }
+ {...tableProps}
+ />
+
+ );
+}`;
+
+// =============================================================================
+// Custom Tables
+// =============================================================================
+
+export const tableCustomRowSnippet = `import { Fragment } from 'react';
+
+const columns: ColumnConfig- [] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ header: () =>
Name (required) ,
+ cell: item => ,
+ },
+ { id: 'owner', label: 'Owner', cell: item => },
+ { id: 'lifecycle', label: 'Lifecycle', cell: item => },
+];
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data,
+});
+
+ (
+
+ {column => (
+ {column.cell(item)}
+ )}
+
+ )}
+ {...tableProps}
+/>`;
+
+export const tablePrimitivesSnippet = `
+
+ Name
+ Owner
+ Type
+
+
+ {items.map(item => (
+
+
+
+
+
+ ))}
+
+ `;
diff --git a/docs-ui/src/app/components/tabs/components.tsx b/docs-ui/src/app/components/tabs/components.tsx
new file mode 100644
index 0000000000..8fbf3d4924
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/components.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import {
+ Tabs,
+ TabList,
+ Tab,
+ TabPanel,
+} from '../../../../../packages/ui/src/components/Tabs';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Default = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const DefaultSelectedKey = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const DisabledTabs = () => {
+ return (
+
+
+
+ Tab 1
+
+ Tab 2 (Disabled)
+
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const Orientation = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/tabs/page.mdx b/docs-ui/src/app/components/tabs/page.mdx
new file mode 100644
index 0000000000..6925127943
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/page.mdx
@@ -0,0 +1,116 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ tabsPropDefs,
+ tabListPropDefs,
+ tabPropDefs,
+ tabPanelPropDefs,
+} from './props-definition';
+import {
+ tabsUsageSnippet,
+ defaultSnippet,
+ defaultSelectedKeySnippet,
+ disabledTabsSnippet,
+ orientationSnippet,
+ urlNavigationSnippet,
+} from './snippets';
+import {
+ Default,
+ DefaultSelectedKey,
+ DisabledTabs,
+ Orientation,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TabsDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ tabs: 'https://react-aria.adobe.com/Tabs',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### Tabs
+
+Container that groups the tab list and panels.
+
+
+
+
+
+### TabList
+
+Container for the tab buttons.
+
+
+
+
+
+### Tab
+
+Individual tab button. Add `href` to enable URL-based tab selection.
+
+
+
+
+
+### TabPanel
+
+Content panel associated with a tab.
+
+
+
+
+
+## Examples
+
+### Default selected
+
+ }
+ code={defaultSelectedKeySnippet}
+/>
+
+### Disabled tabs
+
+ }
+ code={disabledTabsSnippet}
+/>
+
+### Vertical orientation
+
+ }
+ code={orientationSnippet}
+/>
+
+### URL-based navigation
+
+Add `href` to Tab components to enable URL-based tab selection. The active tab is determined by the current route.
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/tabs/props-definition.ts b/docs-ui/src/app/components/tabs/props-definition.ts
new file mode 100644
index 0000000000..e1c1f056cb
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/props-definition.ts
@@ -0,0 +1,83 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const tabsPropDefs: Record = {
+ orientation: {
+ type: 'enum',
+ values: ['horizontal', 'vertical'],
+ default: 'horizontal',
+ description:
+ 'Layout direction. Use horizontal for top navigation, vertical for sidebar-style.',
+ },
+ selectedKey: {
+ type: 'string',
+ description: 'The currently selected tab key (controlled).',
+ },
+ defaultSelectedKey: {
+ type: 'string',
+ description: 'The default selected tab key (uncontrolled).',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(key: Key) => void'],
+ description: 'Handler called when the selected tab changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Disables all tabs. Use when an entire section is unavailable.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of tabs that should be disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tabListPropDefs: Record = {
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tabPropDefs: Record = {
+ id: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier matching the corresponding TabPanel.',
+ },
+ href: {
+ type: 'string',
+ description:
+ 'URL to navigate to. When set, tab selection is controlled by the current route.',
+ },
+ matchStrategy: {
+ type: 'enum',
+ values: ['exact', 'prefix'],
+ default: 'exact',
+ description:
+ 'URL matching strategy. Use exact for leaf routes, prefix for parent routes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Disables this tab. Use for temporarily unavailable options.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tabPanelPropDefs: Record = {
+ id: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier matching the corresponding Tab.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/tabs/snippets.ts b/docs-ui/src/app/components/tabs/snippets.ts
new file mode 100644
index 0000000000..651a0a89af
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/snippets.ts
@@ -0,0 +1,71 @@
+export const tabsUsageSnippet = `import { Tabs, TabList, Tab, TabPanel } from '@backstage/ui';
+
+
+
+ Tab 1
+ Tab 2
+
+ Content 1
+ Content 2
+ `;
+
+export const defaultSnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+ `;
+
+export const defaultSelectedKeySnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const disabledTabsSnippet = `
+
+ Tab 1
+ Tab 2 (Disabled)
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const orientationSnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const urlNavigationSnippet = `
+
+ Overview
+ Profile
+ Security
+
+ Overview content
+ Profile content
+ Security content
+ `;
diff --git a/docs-ui/src/app/components/tag-group/components.tsx b/docs-ui/src/app/components/tag-group/components.tsx
new file mode 100644
index 0000000000..a8455c1d1f
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/components.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import {
+ TagGroup,
+ Tag,
+} from '../../../../../packages/ui/src/components/TagGroup/TagGroup';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+import { useState } from 'react';
+import { useListData } from 'react-stately';
+import type { Selection } from 'react-aria-components';
+import {
+ RiAccountCircleLine,
+ RiBugLine,
+ RiEyeLine,
+ RiHeartLine,
+} from '@remixicon/react';
+
+interface ListItem {
+ id: string;
+ name: string;
+ icon: React.ReactNode;
+ isDisabled?: boolean;
+}
+
+const initialList: ListItem[] = [
+ { id: 'banana', name: 'Banana', icon: },
+ {
+ id: 'apple',
+ name: 'Apple',
+ icon: ,
+ isDisabled: true,
+ },
+ { id: 'orange', name: 'Orange', icon: , isDisabled: true },
+ { id: 'pear', name: 'Pear', icon: },
+ { id: 'grape', name: 'Grape', icon: },
+ { id: 'pineapple', name: 'Pineapple', icon: },
+ { id: 'strawberry', name: 'Strawberry', icon: },
+];
+
+export const Default = () => (
+
+
+ {initialList.map(item => (
+ {item.name}
+ ))}
+
+
+);
+
+export const WithLink = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
+
+export const WithIcon = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
+
+export const Sizes = () => (
+
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+
+);
+
+export const RemovingTags = () => {
+ const [selected, setSelected] = useState(new Set(['travel']));
+ const list = useListData({
+ initialItems: initialList,
+ });
+
+ return (
+
+
+ aria-label="Tag Group"
+ items={list.items}
+ onRemove={keys => list.remove(...keys)}
+ selectedKeys={selected}
+ onSelectionChange={setSelected}
+ >
+ {item => {item.name} }
+
+
+ );
+};
+
+export const Disabled = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
diff --git a/docs-ui/src/app/components/tag-group/page.mdx b/docs-ui/src/app/components/tag-group/page.mdx
new file mode 100644
index 0000000000..1b283a8170
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/page.mdx
@@ -0,0 +1,91 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ Default,
+ WithLink,
+ WithIcon,
+ Sizes,
+ RemovingTags,
+ Disabled,
+} from './components';
+import { tagGroupPropDefs, tagPropDefs } from './props-definition';
+import {
+ usage,
+ preview,
+ withLink,
+ disabled,
+ withIcons,
+ sizes,
+ removingTags,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TagGroupDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ tagGroup: 'https://react-aria.adobe.com/TagGroup',
+};
+
+
+
+ } code={preview} />
+
+## Usage
+
+
+
+## API reference
+
+### TagGroup
+
+Container for a collection of tags.
+
+
+
+
+
+### Tag
+
+Individual tag item within a group.
+
+
+
+
+
+## Examples
+
+### With links
+
+ } code={withLink} />
+
+### With icons
+
+ } code={withIcons} />
+
+### Sizes
+
+ } code={sizes} />
+
+### Removable
+
+ }
+ code={removingTags}
+/>
+
+### Disabled
+
+ } code={disabled} />
+
+
+
+
diff --git a/docs-ui/src/app/components/tag-group/props-definition.tsx b/docs-ui/src/app/components/tag-group/props-definition.tsx
new file mode 100644
index 0000000000..3f64b03e8c
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/props-definition.tsx
@@ -0,0 +1,89 @@
+import {
+ classNamePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const tagGroupPropDefs: Record = {
+ items: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Item objects in the collection.',
+ },
+ renderEmptyState: {
+ type: 'enum',
+ values: ['() => ReactNode'],
+ description: 'Content to display when the collection is empty.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'The type of selection allowed.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['all', 'Iterable'],
+ description: 'The currently selected keys (controlled).',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['all', 'Iterable'],
+ description: 'The initial selected keys (uncontrolled).',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of tags that should be disabled.',
+ },
+ onRemove: {
+ type: 'enum',
+ values: ['(keys: Set) => void'],
+ description: 'Handler called when a tag is removed.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when the selection changes.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tagPropDefs: Record = {
+ id: {
+ type: 'string',
+ description: 'Unique identifier for the tag.',
+ },
+ textValue: {
+ type: 'string',
+ description:
+ 'Text value for accessibility. Derived from children if string.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when the tag is clicked.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the tag text.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ description: (
+ <>
+ Visual size of the tag. Use small for inline or dense
+ layouts, medium for standalone tags.
+ >
+ ),
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the tag is disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/tag-group/snippets.ts b/docs-ui/src/app/components/tag-group/snippets.ts
new file mode 100644
index 0000000000..c939c69d14
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/snippets.ts
@@ -0,0 +1,62 @@
+export const usage = `import { TagGroup, Tag } from '@backstage/ui';
+
+
+ Tag 1
+ Tag 2
+ `;
+
+export const preview = `
+ Banana
+ Apple
+ Orange
+ Pear
+ Grape
+ Pineapple
+ Strawberry
+ `;
+
+export const withLink = `
+ Banana
+ Apple
+ Orange
+ `;
+
+export const disabled = `
+ Banana
+ Apple
+ Orange
+ Pear
+ `;
+
+export const withIcons = `
+ }>Banana
+ }>Apple
+ }>Orange
+ }>Pear
+ `;
+
+export const sizes = `
+
+ }>Banana
+ }>Apple
+
+
+ }>Banana
+ }>Apple
+
+ `;
+
+export const removingTags = `const list = useListData({
+ initialItems: [
+ { id: 'banana', name: 'Banana' },
+ { id: 'apple', name: 'Apple' },
+ { id: 'orange', name: 'Orange' },
+ ],
+});
+
+ list.remove(...keys)}
+>
+ {item => {item.name} }
+ `;
diff --git a/docs-ui/src/app/components/text-field/components.tsx b/docs-ui/src/app/components/text-field/components.tsx
new file mode 100644
index 0000000000..6f6c8019ea
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/components.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiSparklingLine } from '@remixicon/react';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ }
+ />
+ }
+ />
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
diff --git a/docs-ui/src/app/components/text-field/page.mdx b/docs-ui/src/app/components/text-field/page.mdx
new file mode 100644
index 0000000000..87d1087814
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/page.mdx
@@ -0,0 +1,70 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { textFieldPropDefs } from './props-definition';
+import {
+ textFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+} from './snippets';
+import { WithLabel, Sizes, WithDescription } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TextFieldDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ textField: 'https://react-aria.adobe.com/TextField',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With description
+
+ }
+ code={withDescriptionSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/text-field/props-definition.tsx b/docs-ui/src/app/components/text-field/props-definition.tsx
new file mode 100644
index 0000000000..54589e1dab
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/props-definition.tsx
@@ -0,0 +1,85 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const textFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for dense layouts,{' '}
+ medium for prominent fields.
+ >
+ ),
+ },
+ label: {
+ type: 'string',
+ description: 'Visible label displayed above the input.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary text shown next to the label. If not provided and isRequired
+ is true, displays Required .
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Help text displayed below the label.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon rendered before the input.',
+ },
+ type: {
+ type: 'enum',
+ values: ['text', 'email', 'tel', 'url'],
+ default: 'text',
+ description:
+ 'HTML input type. Use SearchField for search inputs and PasswordField for passwords.',
+ },
+ placeholder: {
+ type: 'string',
+ description: 'Text displayed when the input is empty.',
+ },
+ name: {
+ type: 'string',
+ description: 'Form field name for submission.',
+ },
+ isRequired: {
+ type: 'boolean',
+ description: 'Whether the field is required for form submission.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the input is disabled.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Whether the input is read-only.',
+ },
+ value: {
+ type: 'string',
+ description: 'Controlled value of the input.',
+ },
+ defaultValue: {
+ type: 'string',
+ description: 'Default value for uncontrolled usage.',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(value: string) => void'],
+ description: 'Handler called when the input value changes.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/text-field/snippets.ts b/docs-ui/src/app/components/text-field/snippets.ts
new file mode 100644
index 0000000000..617884dcfe
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/snippets.ts
@@ -0,0 +1,31 @@
+export const textFieldUsageSnippet = `import { TextField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+ }
+ />
+ }
+ />
+ `;
+
+export const withDescriptionSnippet = ` `;
diff --git a/docs-ui/src/app/components/text/components.tsx b/docs-ui/src/app/components/text/components.tsx
new file mode 100644
index 0000000000..8021a1dde6
--- /dev/null
+++ b/docs-ui/src/app/components/text/components.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return This is a text component ;
+};
+
+export const Variants = () => {
+ return (
+
+ Title Large
+ Title Medium
+ Title Small
+ Title X-Small
+ Body Large
+ Body Medium
+ Body Small
+ Body X-Small
+
+ );
+};
+
+export const Colors = () => {
+ return (
+
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+
+ );
+};
+
+export const Weights = () => {
+ return (
+
+ Regular weight
+ Bold weight
+
+ );
+};
+
+export const AsElement = () => {
+ return (
+
+ Paragraph element
+ Span element
+ Div element
+
+ );
+};
+
+export const Truncate = () => {
+ return (
+
+
+ This is a very long text that will be truncated when it exceeds the
+ container width
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/text/page.mdx b/docs-ui/src/app/components/text/page.mdx
new file mode 100644
index 0000000000..9a93f38160
--- /dev/null
+++ b/docs-ui/src/app/components/text/page.mdx
@@ -0,0 +1,56 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { textPropDefs } from './props-definition';
+import {
+ textUsageSnippet,
+ defaultSnippet,
+ variantsSnippet,
+ weightsSnippet,
+ colorsSnippet,
+ truncateSnippet,
+} from './snippets';
+import { Default, Variants, Weights, Colors, Truncate } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TextDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Variants
+
+ } code={variantsSnippet} />
+
+### Weights
+
+ } code={weightsSnippet} />
+
+### Colors
+
+Status colors for contextual messaging.
+
+ } code={colorsSnippet} />
+
+### Truncate
+
+ } code={truncateSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/text/props-definition.tsx b/docs-ui/src/app/components/text/props-definition.tsx
new file mode 100644
index 0000000000..8a25af2de7
--- /dev/null
+++ b/docs-ui/src/app/components/text/props-definition.tsx
@@ -0,0 +1,81 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const textPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: [
+ 'title-large',
+ 'title-medium',
+ 'title-small',
+ 'title-x-small',
+ 'body-large',
+ 'body-medium',
+ 'body-small',
+ 'body-x-small',
+ ],
+ default: 'body-medium',
+ responsive: true,
+ description:
+ 'Typography style. Title variants for headings, body for paragraph text.',
+ },
+ weight: {
+ type: 'enum',
+ values: ['regular', 'bold'],
+ default: 'regular',
+ responsive: true,
+ description: (
+ <>
+ Font weight. Use bold for emphasis.
+ >
+ ),
+ },
+ color: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'danger', 'warning', 'success'],
+ default: 'primary',
+ responsive: true,
+ description:
+ 'Text color. Status colors (danger, warning, success) for contextual messaging.',
+ },
+ as: {
+ type: 'enum',
+ values: [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'p',
+ 'span',
+ 'label',
+ 'div',
+ 'strong',
+ 'em',
+ 'small',
+ 'legend',
+ ],
+ default: 'span',
+ description:
+ 'HTML element to render. Use heading tags for semantic structure.',
+ },
+ truncate: {
+ type: 'boolean',
+ default: 'false',
+ description: (
+ <>
+ Truncates text with ellipsis when it overflows its container. Requires{' '}
+ display: block to work.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/text/snippets.ts b/docs-ui/src/app/components/text/snippets.ts
new file mode 100644
index 0000000000..b8cc26bd6b
--- /dev/null
+++ b/docs-ui/src/app/components/text/snippets.ts
@@ -0,0 +1,43 @@
+export const textUsageSnippet = `import { Text } from '@backstage/ui';
+
+This is a text component `;
+
+export const defaultSnippet = `This is a text component `;
+
+export const variantsSnippet = `
+ Title Large
+ Title Medium
+ Title Small
+ Title X-Small
+ Body Large
+ Body Medium
+ Body Small
+ Body X-Small
+ `;
+
+export const colorsSnippet = `
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+ `;
+
+export const weightsSnippet = `
+ Regular weight
+ Bold weight
+ `;
+
+export const asElementSnippet = `
+ Paragraph element
+ Span element
+ Div element
+ `;
+
+export const truncateSnippet = `
+
+ This is a very long text that will be truncated when it exceeds the
+ container width
+
+
`;
diff --git a/docs-ui/src/app/components/toggle-button-group/components.tsx b/docs-ui/src/app/components/toggle-button-group/components.tsx
new file mode 100644
index 0000000000..a6da51f862
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/components.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { ToggleButtonGroup } from '../../../../../packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup';
+import { ToggleButton } from '../../../../../packages/ui/src/components/ToggleButton/ToggleButton';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import {
+ RiCloudLine,
+ RiStarFill,
+ RiStarLine,
+ RiArrowRightSLine,
+} from '@remixicon/react';
+
+export const ToggleButtonGroupSurfaces = () => (
+
+
+ Default
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 0
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 1
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 2
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 3
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+);
+
+export const ToggleButtonGroupSingle = () => (
+
+ Dogs
+ Cats
+ Birds
+
+);
+
+export const ToggleButtonGroupMultiple = () => (
+
+ Frontend
+ Backend
+ Platform
+
+);
+
+export const ToggleButtonGroupVertical = () => (
+
+ Morning
+ Afternoon
+ Evening
+
+);
+
+export const ToggleButtonGroupDisabled = () => (
+
+ Cat
+ Dog
+ Bird
+
+);
+
+export const ToggleButtonGroupDisallowEmpty = () => (
+
+ One
+ Two
+ Three
+
+);
+
+export const ToggleButtonGroupIcons = () => (
+
+ } />
+ }
+ />
+ }>
+ Star
+
+ }>
+ Next
+
+
+);
+
+export const ToggleButtonGroupIconsOnly = () => (
+
+ } />
+ } />
+ } />
+
+);
diff --git a/docs-ui/src/app/components/toggle-button-group/page.mdx b/docs-ui/src/app/components/toggle-button-group/page.mdx
new file mode 100644
index 0000000000..462ee8680b
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/page.mdx
@@ -0,0 +1,152 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { toggleButtonGroupPropDefs } from './props-definition';
+import {
+ toggleButtonGroupHeroSnippet,
+ toggleButtonGroupUsageSnippet,
+ toggleButtonGroupSurfacesSnippet,
+ toggleButtonGroupSingleSnippet,
+ toggleButtonGroupMultipleSnippet,
+ toggleButtonGroupIconsSnippet,
+ toggleButtonGroupIconsOnlySnippet,
+ toggleButtonGroupDisallowEmptySnippet,
+ toggleButtonGroupVerticalSnippet,
+ toggleButtonGroupDisabledSnippet,
+} from './snippets';
+import {
+ ToggleButtonGroupSingle,
+ ToggleButtonGroupMultiple,
+ ToggleButtonGroupVertical,
+ ToggleButtonGroupDisabled,
+ ToggleButtonGroupDisallowEmpty,
+ ToggleButtonGroupIcons,
+ ToggleButtonGroupIconsOnly,
+ ToggleButtonGroupSurfaces,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ToggleButtonGroupDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ toggleButtonGroup: 'https://react-aria.adobe.com/ToggleButtonGroup',
+};
+
+
+
+ }
+ code={toggleButtonGroupHeroSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Surfaces
+
+ }
+ code={toggleButtonGroupSurfacesSnippet}
+/>
+
+### Single selection
+
+ }
+ code={toggleButtonGroupSingleSnippet}
+ layout="side-by-side"
+/>
+
+### Multiple selection
+
+ }
+ code={toggleButtonGroupMultipleSnippet}
+ layout="side-by-side"
+/>
+
+### Icons and text
+
+ }
+ code={toggleButtonGroupIconsSnippet}
+ layout="side-by-side"
+/>
+
+### Icons only
+
+ }
+ code={toggleButtonGroupIconsOnlySnippet}
+ layout="side-by-side"
+/>
+
+### Disallow empty selection
+
+ }
+ code={toggleButtonGroupDisallowEmptySnippet}
+ layout="side-by-side"
+/>
+
+### Vertical orientation
+
+ }
+ code={toggleButtonGroupVerticalSnippet}
+ layout="side-by-side"
+/>
+
+### Disabled
+
+ }
+ code={toggleButtonGroupDisabledSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/toggle-button-group/props-definition.ts b/docs-ui/src/app/components/toggle-button-group/props-definition.ts
new file mode 100644
index 0000000000..8c8632709e
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/props-definition.ts
@@ -0,0 +1,51 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const toggleButtonGroupPropDefs: Record = {
+ selectionMode: {
+ type: 'enum',
+ values: ['single', 'multiple'],
+ description: 'Whether to allow single or multiple selection.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'The currently selected keys (controlled).',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'The initial selected keys (uncontrolled).',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when the selection changes.',
+ },
+ orientation: {
+ type: 'enum',
+ values: ['horizontal', 'vertical'],
+ default: 'horizontal',
+ description: 'The layout orientation of the button group.',
+ },
+ disallowEmptySelection: {
+ type: 'boolean',
+ description:
+ 'Whether to prevent deselecting when at least one button is selected.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Disables all buttons in the group.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of buttons that should be disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/toggle-button-group/snippets.ts b/docs-ui/src/app/components/toggle-button-group/snippets.ts
new file mode 100644
index 0000000000..ce5d70784a
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/snippets.ts
@@ -0,0 +1,121 @@
+export const toggleButtonGroupHeroSnippet = `
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, ToggleButton } from '@backstage/ui';
+
+
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupSingleSnippet = `
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupMultipleSnippet = `
+ Frontend
+ Backend
+ Platform
+ `;
+
+export const toggleButtonGroupVerticalSnippet = `
+ Morning
+ Afternoon
+ Evening
+ `;
+
+export const toggleButtonGroupDisabledSnippet = `
+ Cat
+ Dog
+ Bird
+ `;
+
+export const toggleButtonGroupDisallowEmptySnippet = `
+ One
+ Two
+ Three
+ `;
+
+export const toggleButtonGroupIconsSnippet = `import { RiCloudLine, RiStarFill, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
+
+
+ } />
+ }
+ />
+ }>
+ Star
+
+ }>
+ Next
+
+ `;
+
+export const toggleButtonGroupIconsOnlySnippet = `import { RiCloudLine, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
+
+
+ } />
+ } />
+ } />
+ `;
+
+export const toggleButtonGroupSurfacesSnippet = `
+
+ Default
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 0
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 1
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 2
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 3
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+ `;
diff --git a/docs-ui/src/app/components/toggle-button/components.tsx b/docs-ui/src/app/components/toggle-button/components.tsx
new file mode 100644
index 0000000000..6fa5aada98
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/components.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { ToggleButton } from '../../../../../packages/ui/src/components/ToggleButton/ToggleButton';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCheckLine } from '@remixicon/react';
+import { useState } from 'react';
+
+export const Default = () => {
+ const [isSelected, setIsSelected] = useState(false);
+ return (
+
+ Toggle
+
+ );
+};
+
+export const WithIcon = () => {
+ const [isSelected, setIsSelected] = useState(false);
+ return (
+ }
+ >
+ With Icon
+
+ );
+};
+
+export const Sizes = () => {
+ const [small, setSmall] = useState(false);
+ const [medium, setMedium] = useState(false);
+
+ return (
+
+
+ Small
+
+
+ Medium
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ {}}>
+ Disabled Off
+
+ {}}>
+ Disabled On
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/toggle-button/page.mdx b/docs-ui/src/app/components/toggle-button/page.mdx
new file mode 100644
index 0000000000..4b6aad90d1
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/page.mdx
@@ -0,0 +1,68 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { toggleButtonPropDefs } from './props-definition';
+import {
+ toggleButtonUsageSnippet,
+ defaultSnippet,
+ withIconSnippet,
+ sizesSnippet,
+ disabledSnippet,
+} from './snippets';
+import { Default, WithIcon, Sizes, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ToggleButtonDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ toggleButton: 'https://react-aria.adobe.com/ToggleButton',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### With icon
+
+ }
+ code={withIconSnippet}
+/>
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/toggle-button/props-definition.ts b/docs-ui/src/app/components/toggle-button/props-definition.ts
new file mode 100644
index 0000000000..7af4521bcb
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/props-definition.ts
@@ -0,0 +1,55 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+
+export const toggleButtonPropDefs: Record = {
+ isSelected: {
+ type: 'boolean',
+ description:
+ 'Whether the button is selected. Controls the visual pressed state.',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'The default selected state (uncontrolled).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Handler called when the button is pressed.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description:
+ 'Visual weight. Use small for compact layouts, medium for emphasis.',
+ },
+ iconStart: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description:
+ 'Icon displayed before the button text. Sized to match button.',
+ },
+ iconEnd: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed after the button text. Sized to match button.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Prevents interaction and dims the button.',
+ },
+ onSurface: {
+ type: 'enum',
+ values: ['Surface'],
+ responsive: true,
+ description:
+ 'Surface the button is placed on. Defaults to context surface.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode', '(renderProps) => ReactNode'],
+ description: 'Button label. Can be a function for dynamic content.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/toggle-button/snippets.ts b/docs-ui/src/app/components/toggle-button/snippets.ts
new file mode 100644
index 0000000000..d913e0702e
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/snippets.ts
@@ -0,0 +1,42 @@
+export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstage/ui';
+import { useState } from 'react';
+
+const [isSelected, setIsSelected] = useState(false);
+
+
+ Toggle
+ `;
+
+export const defaultSnippet = `const [isSelected, setIsSelected] = useState(false);
+
+
+ Toggle
+ `;
+
+export const withIconSnippet = `const [isSelected, setIsSelected] = useState(false);
+
+ }
+>
+ With Icon
+`;
+
+export const sizesSnippet = `
+
+ Small
+
+
+ Medium
+
+ `;
+
+export const disabledSnippet = `
+ {}}>
+ Disabled Off
+
+ {}}>
+ Disabled On
+
+ `;
diff --git a/docs-ui/src/app/components/tooltip/components.tsx b/docs-ui/src/app/components/tooltip/components.tsx
new file mode 100644
index 0000000000..67fc8c8d30
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/components.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import {
+ TooltipTrigger,
+ Tooltip,
+} from '../../../../../packages/ui/src/components/Tooltip/Tooltip';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+ Hover me
+ Helpful information
+
+ );
+};
+
+export const Placement = () => {
+ return (
+
+
+ Top
+ Top tooltip
+
+
+ Right
+ Right tooltip
+
+
+ Bottom
+ Bottom tooltip
+
+
+ Left
+ Left tooltip
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/tooltip/page.mdx b/docs-ui/src/app/components/tooltip/page.mdx
new file mode 100644
index 0000000000..8a9711d56c
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/page.mdx
@@ -0,0 +1,65 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { tooltipTriggerPropDefs, tooltipPropDefs } from './props-definition';
+import {
+ tooltipUsageSnippet,
+ defaultSnippet,
+ placementSnippet,
+} from './snippets';
+import { Default, Placement } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TooltipDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ tooltip: 'https://react-aria.adobe.com/Tooltip',
+};
+
+
+
+ } code={defaultSnippet} align="center" />
+
+## Usage
+
+
+
+## API reference
+
+### TooltipTrigger
+
+Wraps the trigger element and tooltip content.
+
+
+
+
+
+### Tooltip
+
+The tooltip content that appears on hover or focus.
+
+
+
+
+
+## Examples
+
+### Placement
+
+The tooltip appears in the specified direction relative to the trigger.
+
+ }
+ code={placementSnippet}
+ align="center"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/tooltip/props-definition.tsx b/docs-ui/src/app/components/tooltip/props-definition.tsx
new file mode 100644
index 0000000000..16dc9133dd
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/props-definition.tsx
@@ -0,0 +1,79 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+} from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const tooltipTriggerPropDefs: Record = {
+ delay: {
+ type: 'number',
+ default: 600,
+ description: 'Milliseconds before tooltip appears on hover.',
+ },
+ closeDelay: {
+ type: 'number',
+ default: 0,
+ description: 'Milliseconds before tooltip hides after leaving trigger.',
+ },
+ trigger: {
+ type: 'enum',
+ values: ['focus'],
+ description: (
+ <>
+ Set to focus for focus-only tooltips that do not appear on
+ hover.
+ >
+ ),
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Controlled open state. Use with onOpenChange.',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Initial open state for uncontrolled usage.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents the tooltip from appearing.',
+ },
+ ...childrenPropDefs,
+};
+
+export const tooltipPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'top start',
+ 'top end',
+ 'bottom',
+ 'bottom start',
+ 'bottom end',
+ 'left',
+ 'left top',
+ 'left bottom',
+ 'right',
+ 'right top',
+ 'right bottom',
+ ],
+ default: 'top',
+ description:
+ 'Position relative to the trigger element. Compound placements include alignment.',
+ },
+ offset: {
+ type: 'number',
+ default: 4,
+ description: 'Distance in pixels from the trigger element.',
+ },
+ containerPadding: {
+ type: 'number',
+ default: 12,
+ description: 'Padding from viewport edge when repositioning.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/tooltip/snippets.ts b/docs-ui/src/app/components/tooltip/snippets.ts
new file mode 100644
index 0000000000..68b7d1b0c7
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/snippets.ts
@@ -0,0 +1,30 @@
+export const tooltipUsageSnippet = `import { TooltipTrigger, Tooltip, Button } from '@backstage/ui';
+
+
+ Hover me
+ Helpful information
+ `;
+
+export const defaultSnippet = `
+ Hover me
+ Helpful information
+ `;
+
+export const placementSnippet = `
+
+ Top
+ Top tooltip
+
+
+ Right
+ Right tooltip
+
+
+ Bottom
+ Bottom tooltip
+
+
+ Left
+ Left tooltip
+
+ `;
diff --git a/docs-ui/src/app/components/visually-hidden/components.tsx b/docs-ui/src/app/components/visually-hidden/components.tsx
new file mode 100644
index 0000000000..8298465240
--- /dev/null
+++ b/docs-ui/src/app/components/visually-hidden/components.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { VisuallyHidden } from '../../../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+
+ This text is followed by a paragraph that is visually hidden but
+ accessible to screen readers. Try using a screen reader to hear it, or
+ inspect the DOM to see it's there.
+
+
+ This content is visually hidden but accessible to screen readers
+
+
+ );
+};
+
+export const ExampleUsage = () => {
+ return (
+
+
+ Footer links
+
+
+ About us
+
+
+ Jobs
+
+
+ Terms and Conditions
+
+
+ );
+};
diff --git a/docs-ui/src/content/visually-hidden.mdx b/docs-ui/src/app/components/visually-hidden/page.mdx
similarity index 56%
rename from docs-ui/src/content/visually-hidden.mdx
rename to docs-ui/src/app/components/visually-hidden/page.mdx
index 72ffc7a1d4..c72250231f 100644
--- a/docs-ui/src/content/visually-hidden.mdx
+++ b/docs-ui/src/app/components/visually-hidden/page.mdx
@@ -1,16 +1,16 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { VisuallyHiddenSnippet } from '@/snippets/stories-snippets';
+import { visuallyHiddenPropDefs } from './props-definition';
import {
- visuallyHiddenPropDefs,
visuallyHiddenUsageSnippet,
- visuallyHiddenDefaultSnippet,
- visuallyHiddenExampleUsageSnippet,
-} from './visually-hidden.props';
+ defaultSnippet,
+ exampleUsageSnippet,
+} from './snippets';
+import { Default, ExampleUsage } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { VisuallyHiddenDefinition } from '../utils/definitions';
+import { VisuallyHiddenDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
- }
- code={visuallyHiddenDefaultSnippet}
-/>
+ } code={defaultSnippet} />
## Usage
@@ -33,17 +28,17 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
+This component also accepts all standard HTML `div` attributes.
+
## Examples
-### Example Usage
-
-Here's an example of providing screen reader context for a list of links in a footer.
+### Hidden headings
}
- code={visuallyHiddenExampleUsageSnippet}
+ preview={ }
+ code={exampleUsageSnippet}
open
/>
diff --git a/docs-ui/src/app/components/visually-hidden/props-definition.ts b/docs-ui/src/app/components/visually-hidden/props-definition.ts
new file mode 100644
index 0000000000..140ed45fa4
--- /dev/null
+++ b/docs-ui/src/app/components/visually-hidden/props-definition.ts
@@ -0,0 +1,17 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const visuallyHiddenPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description:
+ 'Content to hide visually while remaining accessible to screen readers.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/content/visually-hidden.props.ts b/docs-ui/src/app/components/visually-hidden/snippets.ts
similarity index 63%
rename from docs-ui/src/content/visually-hidden.props.ts
rename to docs-ui/src/app/components/visually-hidden/snippets.ts
index 18a1c503d1..21ce6a04de 100644
--- a/docs-ui/src/content/visually-hidden.props.ts
+++ b/docs-ui/src/app/components/visually-hidden/snippets.ts
@@ -1,26 +1,10 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const visuallyHiddenPropDefs: Record = {
- children: {
- type: 'enum',
- values: ['ReactNode'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
export const visuallyHiddenUsageSnippet = `import { VisuallyHidden } from '@backstage/ui';
This content is visually hidden but accessible to screen readers
`;
-export const visuallyHiddenDefaultSnippet = `
+export const defaultSnippet = `
This text is followed by a paragraph that is visually hidden but
accessible to screen readers. Try using a screen reader to hear it, or
@@ -31,7 +15,7 @@ export const visuallyHiddenDefaultSnippet = `
`;
-export const visuallyHiddenExampleUsageSnippet = `
+export const exampleUsageSnippet = `
Footer links
diff --git a/docs-ui/src/app/get-started/installation/page.mdx b/docs-ui/src/app/get-started/installation/page.mdx
new file mode 100644
index 0000000000..cdb14d5924
--- /dev/null
+++ b/docs-ui/src/app/get-started/installation/page.mdx
@@ -0,0 +1,46 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { Banner } from '@/components/Banner';
+import { snippet } from './snippets';
+
+# Installation
+
+## Import BUI's global styles
+
+Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
+First, you'll need to install the package using a package manager. For example, if you're using Yarn:
+
+
+
+ );`}
+/>
+
+
+
+## Use BUI components
+
+As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
+again in your plugin as this will be handled at the root of your application. To get started, just add the library to
+your plugin and import the components you need.
+
+
+
+
diff --git a/docs-ui/src/app/get-started/installation/snippets.ts b/docs-ui/src/app/get-started/installation/snippets.ts
new file mode 100644
index 0000000000..7e63005172
--- /dev/null
+++ b/docs-ui/src/app/get-started/installation/snippets.ts
@@ -0,0 +1,6 @@
+export const snippet = `import { Flex, Button, Text } from '@backstage/ui';
+
+
+ Hello World
+ Click me
+ ;`;
diff --git a/docs-ui/src/app/icon.svg b/docs-ui/src/app/icon.svg
index b50d29e902..18824daaa7 100644
--- a/docs-ui/src/app/icon.svg
+++ b/docs-ui/src/app/icon.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/docs-ui/src/app/layout.tsx b/docs-ui/src/app/layout.tsx
index ad056626d8..015ee8346c 100644
--- a/docs-ui/src/app/layout.tsx
+++ b/docs-ui/src/app/layout.tsx
@@ -8,8 +8,8 @@ import { MobileBottomNav } from '@/components/MobileBottomNav';
import styles from './layout.module.css';
import '../css/globals.css';
-import '/public/theme-backstage.css';
-import '/public/theme-spotify.css';
+import '../css/theme-backstage.css';
+import '../css/theme-spotify.css';
export const metadata: Metadata = {
title: 'Backstage UI',
diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx
index 9a796b401b..712a5ac5c6 100644
--- a/docs-ui/src/app/page.mdx
+++ b/docs-ui/src/app/page.mdx
@@ -1,59 +1,78 @@
+import { ComponentGrid } from '@/components/ComponentGrid';
import { CodeBlock } from '@/components/CodeBlock';
-import { Banner } from '@/components/Banner';
-import { snippet } from './snippets';
+import { ColorFamily } from '@/components/ColorFamily';
+import {
+ surfacesSnippet,
+ adaptiveSnippet,
+ customCardSnippet,
+ customTokensSnippet,
+ colorPickerSnippet,
+} from './snippets';
-# Welcome to Backstage UI
+# Get Started with BUI
Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS.
This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI
is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users.
-## Import BUI's global styles
+Backstage UI is installed by default on every instance of Backstage, so you can start using it right away.
+If your setup doesn't include it yet, follow the [installation guide](/get-started/installation) to get started.
-Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
-First, you'll need to install the package using a package manager. For example, if you're using Yarn:
+## Layout containers
-
+[`Box`](/components/box), [`Flex`](/components/flex), [`Grid`](/components/grid), and [`Card`](/components/card) are the foundation of every layout in Backstage UI.
+Each one offers a set of utility props that map directly to our design tokens, so you can build consistent
+layouts without writing any CSS. When nested, they also act as surfaces and automatically increment the
+background depth so visual hierarchy is handled for you.
);`}
+ title="Nested surfaces with automatic styling"
+ code={surfacesSnippet}
/>
-
+## Adaptive components
-## Use BUI components
-
-As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
-again in your plugin as this will be handled at the root of your application. To get started, just add the library to
-your plugin and import the components you need.
+Components like [`Card`](/components/card), [`Button`](/components/button), [`Text`](/components/text), and others are **adaptive components**. They
+automatically adjust their colors, borders, and backgrounds to match the surface they live on. Drop a
+[`Button`](/components/button) inside a [`Card`](/components/card) inside a [`Box`](/components/box) and each component styles itself appropriately
+without any extra configuration.
-
+## The neutral scale background colors
-## Support
+
-Now that you have the basics down, you can start building your plugin using the new design system.
-Please familiarise yourself first with our theming principles. This will help you understand the core concepts of the design system.
-If you have any questions, please reach out to us on [Discord](https://discord.gg/MUpMjP2).
+## Creating custom components
+
+As much as possible we would like you to use components directly without creating custom components. If you need to create a custom component, you should use the components provided by Backstage UI.
+
+
+
+If you need to build custom components outside of BUI, you can use our [design tokens](/tokens) as CSS variables to ensure your styles stay consistent with the rest of the system.
+
+
+
+When building custom interactive components, we strongly recommend using [React Aria](https://react-spectrum.adobe.com/react-aria/) as your foundation. React Aria provides all the necessary accessibility features out of the box — keyboard navigation, focus management, ARIA attributes, and screen reader support — so you can focus on styling and logic without worrying about compliance.
+
+
## Philosophy
diff --git a/docs-ui/src/app/snippets.ts b/docs-ui/src/app/snippets.ts
index 7e63005172..e16463a085 100644
--- a/docs-ui/src/app/snippets.ts
+++ b/docs-ui/src/app/snippets.ts
@@ -1,6 +1,39 @@
-export const snippet = `import { Flex, Button, Text } from '@backstage/ui';
+export const surfacesSnippet = `
+
+ Hello World
+
+
+ Hello World
+
+ `;
-
+export const adaptiveSnippet = `
+ {/* automatically set background to neutral-2 */}
+ Button with background set to neutral-3
+
+ `;
+
+export const customCardSnippet = `
Hello World
- Click me
- ;`;
+`;
+
+export const customTokensSnippet = ``;
+
+export const colorPickerSnippet = `import { ColorPicker, ColorArea, ColorSlider, ColorField } from 'react-aria-components';
+
+function MyColorPicker() {
+ return (
+
+
+
+
+
+ );
+}`;
diff --git a/docs-ui/src/app/tokens/page.mdx b/docs-ui/src/app/tokens/page.mdx
index 8e9031c81c..f359c10525 100644
--- a/docs-ui/src/app/tokens/page.mdx
+++ b/docs-ui/src/app/tokens/page.mdx
@@ -107,8 +107,6 @@ the value, you add an object with the value and the breakpoint prefix.
## Base colors
-These colors are used for special purposes like ring, scrollbar, ...
-
@@ -133,62 +131,15 @@ These colors are used for special purposes like ring, scrollbar, ...
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
+## Neutral 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.
+These colors form a layered neutral scale for your application backgrounds.
+`--bui-bg-app` is the base background color of your app. Each subsequent level
+(1 through 4) represents an elevated layer on top of the previous one, with hover,
+pressed, and disabled variants for interactive states.
@@ -200,22 +151,129 @@ color of your app.
- --bui-bg
+ --bui-bg-app
+
+
+ The base background color of your Backstage instance.
- The background color of your Backstage instance.
- --bui-bg-surface-1
+ --bui-bg-neutral-1
+
+
+ First elevated layer. Use for cards, dialogs, and panels.
- Use for any panels or elevated surfaces.
- --bui-bg-surface-2
+ --bui-bg-neutral-1-hover
- Use for any panels or elevated surfaces.
+ Hover state for elements on neutral-1.
+
+
+ --bui-bg-neutral-1-pressed
+
+ Pressed state for elements on neutral-1.
+
+
+
+ --bui-bg-neutral-1-disabled
+
+ Disabled state for elements on neutral-1.
+
+
+
+ --bui-bg-neutral-2
+
+
+ Second elevated layer. Use for elements on top of neutral-1.
+
+
+
+
+ --bui-bg-neutral-2-hover
+
+ Hover state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-2-pressed
+
+ Pressed state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-2-disabled
+
+ Disabled state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-3
+
+
+ Third elevated layer. Use for elements on top of neutral-2.
+
+
+
+
+ --bui-bg-neutral-3-hover
+
+ Hover state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-3-pressed
+
+ Pressed state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-3-disabled
+
+ Disabled state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-4
+
+
+ Fourth elevated layer. Use for elements on top of neutral-3.
+
+
+
+
+ --bui-bg-neutral-4-hover
+
+ Hover state for elements on neutral-4.
+
+
+
+ --bui-bg-neutral-4-pressed
+
+ Pressed state for elements on neutral-4.
+
+
+
+ --bui-bg-neutral-4-disabled
+
+ Disabled state for elements on neutral-4.
+
+
+
+
+## Solid background colors
+
+
+
+
+ Prop
+ Description
+
+
+
--bui-bg-solid
@@ -240,30 +298,19 @@ color of your app.
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.
-
+
+
+
+## Status background colors
+
+
+
+
+ Prop
+ Description
+
+
+
--bui-bg-danger
@@ -282,6 +329,12 @@ color of your app.
Used to show success information.
+
+
+ --bui-bg-info
+
+ Used to show informational content.
+
@@ -315,22 +368,6 @@ are prefixed with `fg` to make it easier to identify.
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
@@ -347,33 +384,45 @@ are prefixed with `fg` to make it easier to identify.
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
+ Used for error states and destructive actions.
+
+
+
+ --bui-fg-warning
+
+
+ Used for warning states and cautionary information.
+
+
+
+
+ --bui-fg-success
+
+ Used for success states and positive feedback.
+
+
+
+ --bui-fg-info
+
+
+ Used for informational content and neutral status.
+
+
+
+
+ --bui-fg-danger-on-bg
+
It should be used on top of danger background colors.
- --bui-fg-warning
+ --bui-fg-warning-on-bg
It should be used on top of warning background colors.
@@ -381,12 +430,20 @@ are prefixed with `fg` to make it easier to identify.
- --bui-fg-success
+ --bui-fg-success-on-bg
It should be used on top of success background colors.
+
+
+ --bui-fg-info-on-bg
+
+
+ It should be used on top of info background colors.
+
+
@@ -405,31 +462,15 @@ low contrast to help as a separator with the different background colors.
- --bui-border
+ --bui-border-1
- It should be used on top of `--bui-bg-surface-1`.
+ Subtle border for low-contrast separators.
- --bui-border-hover
+ --bui-border-2
-
- Used when the component is interactive and hovered.
-
-
-
-
- --bui-border-pressed
-
-
- Used when the component is interactive and focused.
-
-
-
-
- --bui-border-disabled
-
- Used when the component is disabled.
+ It should be used on top of `--bui-bg-neutral-1`.
@@ -449,6 +490,12 @@ low contrast to help as a separator with the different background colors.
It should be used on top of `--bui-bg-success`.
+
+
+ --bui-border-info
+
+ It should be used on top of `--bui-bg-info`.
+
diff --git a/docs-ui/src/components/Changelog/index.tsx b/docs-ui/src/components/Changelog/index.tsx
index d7a547b901..c1f3a5660b 100644
--- a/docs-ui/src/components/Changelog/index.tsx
+++ b/docs-ui/src/components/Changelog/index.tsx
@@ -1,76 +1,21 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
+import { Badge, BreakingBadge, generateChangelogMarkdown } from './utils';
export function Changelog() {
- // Group changelog entries by version
- const groupedChangelog = changelog.reduce((acc, entry) => {
- if (!acc[entry.version]) {
- acc[entry.version] = [];
- }
- acc[entry.version].push(entry);
- return acc;
- }, {} as Record);
+ const content = generateChangelogMarkdown(changelog, {
+ showComponentBadges: true,
+ });
- // Sort versions in descending order
- const sortedVersions = Object.keys(groupedChangelog).sort((a, b) =>
- b.localeCompare(a),
+ return (
+
);
-
- const content = sortedVersions
- .map(version => {
- const entries = groupedChangelog[version];
-
- // Group entries by bump type
- const groupedByBump = entries.reduce((acc, entry) => {
- const bumpType = entry.type || 'other';
- if (!acc[bumpType]) {
- acc[bumpType] = [];
- }
- acc[bumpType].push(entry);
- return acc;
- }, {} as Record);
-
- // Define the order of bump types
- const bumpOrder = ['breaking', 'new', 'fix', 'other'];
-
- const bumpSections = bumpOrder
- .filter(bumpType => groupedByBump[bumpType]?.length > 0)
- .map(bumpType => {
- const bumpEntries = groupedByBump[bumpType];
- let sectionTitle = 'Other Changes';
- if (bumpType === 'breaking') {
- sectionTitle = 'Breaking Changes';
- } else if (bumpType === 'new') {
- sectionTitle = 'New Features';
- } else if (bumpType === 'fix') {
- sectionTitle = 'Bug Fixes';
- }
-
- return `### ${sectionTitle}
-
- ${bumpEntries
- .map(e => {
- const prs =
- e.prs.length > 0
- ? e.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ')
- : '';
- return `- ${e.description}${prs ? ` ${prs}` : ''}`;
- })
- .join('\n')}`;
- })
- .join('\n\n');
-
- return `## Version ${version}
-
- ${bumpSections}`;
- })
- .join('\n');
-
- return ;
}
diff --git a/docs-ui/src/components/Changelog/utils.tsx b/docs-ui/src/components/Changelog/utils.tsx
new file mode 100644
index 0000000000..d2daed1176
--- /dev/null
+++ b/docs-ui/src/components/Changelog/utils.tsx
@@ -0,0 +1,185 @@
+import type { ChangelogProps } from '@/utils/types';
+
+// Badge Components
+export const Badge = ({
+ children,
+ variant = 'gray',
+}: {
+ children: React.ReactNode;
+ variant?: 'red' | 'gray';
+}) => {
+ const colors = {
+ red: {
+ backgroundColor: 'var(--badge-red-bg)',
+ color: 'var(--badge-red-color)',
+ },
+ gray: {
+ backgroundColor: 'var(--badge-gray-bg)',
+ color: 'var(--badge-gray-color)',
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const BreakingBadge = () => Breaking ;
+
+// Utility Functions
+export const toTitleCase = (kebabCase: string): string => {
+ return kebabCase
+ .split('-')
+ .map(word => word.charAt(0).toLocaleUpperCase('en-US') + word.slice(1))
+ .join(' ');
+};
+
+export const groupByVersion = (
+ entries: ChangelogProps[],
+): Record => {
+ return entries.reduce((acc, entry) => {
+ if (!acc[entry.version]) {
+ acc[entry.version] = [];
+ }
+ acc[entry.version].push(entry);
+ return acc;
+ }, {} as Record);
+};
+
+export const sortVersions = (versions: string[]): string[] => {
+ return versions.sort((a, b) => {
+ const aParts = a.split('.').map(Number);
+ const bParts = b.split('.').map(Number);
+
+ for (let i = 0; i < 3; i++) {
+ if (bParts[i] !== aParts[i]) {
+ return bParts[i] - aParts[i]; // Descending order
+ }
+ }
+ return 0;
+ });
+};
+
+export const formatPRLinks = (prs: string[]): string => {
+ if (prs.length === 0) return '';
+
+ return prs
+ .map(pr => `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`)
+ .join(', ');
+};
+
+export const generateEntryMarkdown = (
+ entry: ChangelogProps,
+ options: { showComponentBadges?: boolean } = {},
+): string => {
+ const { showComponentBadges = true } = options;
+ const prs = formatPRLinks(entry.prs);
+
+ // Prepend component names as badges if available and requested
+ const componentBadges =
+ showComponentBadges && entry.components.length > 0
+ ? entry.components
+ .map(c => `${toTitleCase(c)} `)
+ .join(' ') + ' '
+ : '';
+
+ // Add breaking badge if this is a breaking change
+ const breakingBadge = entry.breaking ? ' ' : '';
+
+ // Remove **BREAKING**: text from description since we show it as a badge
+ const description = entry.description.replace(/\*\*BREAKING\*\*:?\s*/, '');
+
+ // Build the entry
+ let entryMarkdown = `- ${componentBadges}${breakingBadge}${description}`;
+ if (prs) {
+ entryMarkdown += ` ${prs}`;
+ }
+
+ // Add migration if present (should already be in description, but check)
+ if (entry.migration && !entry.description.includes('**Migration:**')) {
+ entryMarkdown += `\n\n Migration Guide:\n\n ${entry.migration
+ .split('\n')
+ .join('\n ')}`;
+ }
+
+ return entryMarkdown;
+};
+
+export interface GenerateChangelogOptions {
+ showComponentBadges?: boolean;
+ headingLevel?: number;
+}
+
+export const generateChangelogMarkdown = (
+ entries: ChangelogProps[],
+ options: GenerateChangelogOptions = {},
+): string => {
+ const { showComponentBadges = true, headingLevel = 2 } = options;
+
+ // Group changelog entries by version
+ const groupedChangelog = groupByVersion(entries);
+
+ // Sort versions in descending order (semantic versioning)
+ const sortedVersions = sortVersions(Object.keys(groupedChangelog));
+
+ // Generate heading prefix based on level (e.g., "##" for level 2, "###" for level 3)
+ const versionHeading = '#'.repeat(headingLevel);
+ const sectionHeading = '#'.repeat(headingLevel + 1);
+
+ const content = sortedVersions
+ .map(version => {
+ const versionEntries = groupedChangelog[version];
+
+ // Group entries: Breaking vs Everything Else
+ const breakingChanges = versionEntries.filter(e => e.breaking);
+ const otherChanges = versionEntries.filter(e => !e.breaking);
+
+ const sections = [];
+
+ // Breaking changes section
+ if (breakingChanges.length > 0) {
+ sections.push({
+ title: 'Breaking Changes',
+ entries: breakingChanges,
+ });
+ }
+
+ // All other changes section
+ if (otherChanges.length > 0) {
+ sections.push({
+ title: 'Changes',
+ entries: otherChanges,
+ });
+ }
+
+ const bumpSections = sections
+ .map(({ title, entries: sectionEntries }) => {
+ const entriesMarkdown = sectionEntries
+ .map(e => generateEntryMarkdown(e, { showComponentBadges }))
+ .join('\n\n');
+
+ return `${sectionHeading} ${title}\n\n${entriesMarkdown}`;
+ })
+ .join('\n\n');
+
+ return `${versionHeading} Version ${version}
+
+ ${bumpSections}`;
+ })
+ .join('\n');
+
+ return content;
+};
diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx
index ff86433268..92068d04f8 100644
--- a/docs-ui/src/components/ChangelogComponent/index.tsx
+++ b/docs-ui/src/components/ChangelogComponent/index.tsx
@@ -2,34 +2,32 @@ import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
import type { Component } from '@/utils/changelog';
+import {
+ Badge,
+ BreakingBadge,
+ generateChangelogMarkdown,
+} from '../Changelog/utils';
export const ChangelogComponent = ({ component }: { component: Component }) => {
const componentChangelog = changelog.filter(c =>
c.components.includes(component),
);
+ const content = `## Changelog
+
+${generateChangelogMarkdown(componentChangelog, {
+ showComponentBadges: false,
+ headingLevel: 3,
+})}`;
+
return (
{
- const prs =
- change.prs.length > 0
- ? change.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ')
- : '';
- return `- \`${change.version}\` - ${change.description}${
- prs ? ` ${prs}` : ''
- }`;
- })
- .join('\n')}`}
+ components={{
+ ...formattedMDXComponents,
+ Badge,
+ BreakingBadge,
+ }}
+ source={content}
/>
);
};
diff --git a/docs-ui/src/components/Chip/styles.module.css b/docs-ui/src/components/Chip/styles.module.css
index 01ee29311d..d752ae2602 100644
--- a/docs-ui/src/components/Chip/styles.module.css
+++ b/docs-ui/src/components/Chip/styles.module.css
@@ -1,11 +1,9 @@
.chip {
- display: inline-flex;
- align-items: center;
+ display: inline-block;
font-family: monospace;
font-size: 13px;
border-radius: 6px;
- padding: 0px 8px;
- height: 24px;
+ padding: 4px 8px;
margin-right: 4px;
background-color: #f0f0f0;
color: #5d5d5d;
diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.module.css b/docs-ui/src/components/ColorFamily/ColorFamily.module.css
new file mode 100644
index 0000000000..f38e481383
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/ColorFamily.module.css
@@ -0,0 +1,94 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+}
+
+@media (min-width: 768px) {
+ .wrapper {
+ flex-direction: row;
+ align-items: flex-start;
+ }
+}
+
+.visual {
+ flex: 1;
+ min-width: 0;
+}
+
+.text {
+ flex: 1;
+ min-width: 0;
+}
+
+.base {
+ border-radius: 12px;
+ padding: 16px;
+}
+
+.baseLabel {
+ display: block;
+ font-size: 13px;
+ color: var(--bui-fg-primary);
+ margin-bottom: 12px;
+}
+
+.level {
+ border-radius: 10px;
+ padding: 12px;
+}
+
+.levelHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.level > .level > .levelHeader:last-child,
+.level:not(:has(> .level)) > .levelHeader {
+ margin-bottom: 0;
+}
+
+.levelLabel {
+ font-size: 13px;
+ color: var(--bui-fg-primary);
+ white-space: nowrap;
+}
+
+.chips {
+ display: flex;
+ gap: 6px;
+}
+
+.chip {
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--bui-fg-primary);
+ white-space: nowrap;
+}
+
+.description {
+ color: var(--primary);
+ margin-top: 0;
+ margin-bottom: 1rem;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.description:last-child {
+ margin-bottom: 0;
+}
+
+.link {
+ color: var(--link);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.tsx b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
new file mode 100644
index 0000000000..07b8779503
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
@@ -0,0 +1,97 @@
+'use client';
+
+import styles from './ColorFamily.module.css';
+
+const StateChip = ({
+ level,
+ state,
+ label,
+}: {
+ level: number;
+ state: string;
+ label: string;
+}) => (
+
+ {label}
+
+);
+
+const NeutralLevel = ({
+ level,
+ children,
+ height,
+}: {
+ level: number;
+ children?: React.ReactNode;
+ height?: number;
+}) => (
+
+
+
Neutral {level}
+
+
+
+
+
+
+ {children}
+
+);
+
+export const ColorFamily = () => {
+ return (
+
+
+
+ Neutral 0
+
+
+
+
+
+
+
+
+
+
+
+ BUI uses a layered neutral scale from 0 to 4. Each level nests inside
+ the previous one, automatically incrementing the background depth.
+ This creates clear visual hierarchy without manually picking colors.
+
+
+ Neutral 0 is the application background and should only be used once,
+ at the root of your app. All other surfaces build on top of it.
+
+
+ Each level can be interactive or{' '}
+ non-interactive . A Card, for example, can be flat
+ (just a surface) or fully clickable with hover and pressed states. The
+ three interaction states (hover, pressed, disabled) are built into
+ every neutral level.
+
+
+ Explore the full list of color tokens on the{' '}
+
+ tokens page
+
+ .
+
+
+
+ );
+};
diff --git a/docs-ui/src/components/ColorFamily/index.ts b/docs-ui/src/components/ColorFamily/index.ts
new file mode 100644
index 0000000000..12f7fb4008
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/index.ts
@@ -0,0 +1 @@
+export { ColorFamily } from './ColorFamily';
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.module.css b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
new file mode 100644
index 0000000000..dbbdb72ceb
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
@@ -0,0 +1,154 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 15vh;
+
+ &[data-entering] {
+ animation: overlayFadeIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: overlayFadeOut 150ms ease-in;
+ }
+}
+
+@keyframes overlayFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes overlayFadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+.modal {
+ width: min(90vw, 500px);
+ max-height: min(70vh, 400px);
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+ display: flex;
+ flex-direction: column;
+ outline: none;
+ overflow: hidden;
+
+ &[data-entering] {
+ animation: modalScaleIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: modalScaleOut 150ms ease-in;
+ }
+}
+
+@keyframes modalScaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+@keyframes modalScaleOut {
+ from {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+}
+
+.dialog {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: min(70vh, 400px);
+ outline: none;
+ overflow: hidden;
+}
+
+.searchField {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 1rem;
+ color: var(--primary);
+ font-family: inherit;
+
+ &::placeholder {
+ color: var(--secondary);
+ }
+}
+
+.menu {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ outline: none;
+ border: none;
+}
+
+.menuItem {
+ padding: 8px 12px;
+ border-radius: 8px;
+ color: var(--primary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ outline: none;
+
+ &[data-focused] {
+ background-color: var(--action);
+ }
+
+ &[data-pressed] {
+ background-color: var(--action);
+ }
+}
+
+.empty {
+ padding: 24px 12px;
+ text-align: center;
+ color: var(--secondary);
+ font-size: 0.875rem;
+}
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.tsx b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
new file mode 100644
index 0000000000..bcc534372a
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import {
+ Autocomplete,
+ Dialog,
+ Input,
+ Label,
+ Menu,
+ MenuItem,
+ Modal,
+ ModalOverlay,
+ SearchField,
+ useFilter,
+} from 'react-aria-components';
+import { useRouter } from 'next/navigation';
+import { components } from '@/utils/data';
+import styles from './CommandPalette.module.css';
+
+interface CommandPaletteProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+const items = components.map(c => ({ id: c.slug, ...c }));
+
+export const CommandPalette = ({
+ isOpen,
+ onOpenChange,
+}: CommandPaletteProps) => {
+ const router = useRouter();
+ const { contains } = useFilter({ sensitivity: 'base' });
+
+ return (
+
+
+
+
+
+ Search
+
+
+ {
+ router.push(`/components/${key}`);
+ onOpenChange(false);
+ }}
+ renderEmptyState={() => (
+ No components found.
+ )}
+ >
+ {item => (
+
+ {item.title}
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/components/CommandPalette/index.ts b/docs-ui/src/components/CommandPalette/index.ts
new file mode 100644
index 0000000000..95c1305982
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/index.ts
@@ -0,0 +1 @@
+export { CommandPalette } from './CommandPalette';
diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.module.css b/docs-ui/src/components/ComponentCards/ComponentCards.module.css
deleted file mode 100644
index 8eb9dbd3d0..0000000000
--- a/docs-ui/src/components/ComponentCards/ComponentCards.module.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.grid {
- display: grid;
- grid-template-columns: repeat(1, 1fr);
- gap: 1rem;
- margin-top: 1rem;
- margin-bottom: 3rem;
-}
-
-@media (min-width: 768px) {
- .grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (min-width: 1024px) {
- .grid {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-@media (min-width: 1280px) {
- .grid {
- grid-template-columns: repeat(4, 1fr);
- }
-}
-
-.card {
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- background-color: var(--bg);
- border-radius: 8px;
- border: 1px solid var(--border);
- padding: 16px;
- gap: 4px;
- min-height: 120px;
-
- &:hover {
- background-color: var(--bg-hover);
- }
-}
-
-.title {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- color: var(--primary);
-}
-
-.description {
- margin: 0;
- font-size: 14px;
- color: var(--secondary);
-}
diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.tsx b/docs-ui/src/components/ComponentCards/ComponentCards.tsx
deleted file mode 100644
index 0e4d22c02a..0000000000
--- a/docs-ui/src/components/ComponentCards/ComponentCards.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import Link from 'next/link';
-import styles from './ComponentCards.module.css';
-
-export const ComponentCards = ({ children }: { children: React.ReactNode }) => {
- return {children}
;
-};
-
-export const ComponentCard = ({
- title,
- description,
- href,
-}: {
- title: string;
- description: string;
- href: string;
-}) => {
- return (
-
- {title}
- {description}
-
- );
-};
diff --git a/docs-ui/src/components/ComponentCards/index.ts b/docs-ui/src/components/ComponentCards/index.ts
deleted file mode 100644
index 4bc4beaae3..0000000000
--- a/docs-ui/src/components/ComponentCards/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ComponentCards';
diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css
new file mode 100644
index 0000000000..a6dd54a864
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css
@@ -0,0 +1,29 @@
+.grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+}
+
+@media (min-width: 768px) {
+ .grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+.item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 0;
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--link);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ }
+}
diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx
new file mode 100644
index 0000000000..eb0f043420
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import Link from 'next/link';
+import { components } from '@/utils/data';
+import styles from './ComponentGrid.module.css';
+
+export const ComponentGrid = () => {
+ return (
+
+ {components.map(item => (
+
+ {item.title}
+
+ ))}
+
+ );
+};
diff --git a/docs-ui/src/components/ComponentGrid/index.ts b/docs-ui/src/components/ComponentGrid/index.ts
new file mode 100644
index 0000000000..f0fe06477a
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/index.ts
@@ -0,0 +1 @@
+export * from './ComponentGrid';
diff --git a/docs-ui/src/components/CustomTheme/customTheme.tsx b/docs-ui/src/components/CustomTheme/customTheme.tsx
index ae765d7d88..539113140a 100644
--- a/docs-ui/src/components/CustomTheme/customTheme.tsx
+++ b/docs-ui/src/components/CustomTheme/customTheme.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState, useCallback } from 'react';
+import { useEffect, useState, useCallback, useSyncExternalStore } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { sass } from '@codemirror/lang-sass';
import styles from './styles.module.css';
@@ -14,6 +14,10 @@ const defaultTheme = `:root {
--bui-bg-solid: #000;
}`;
+// Stable server snapshots for useSyncExternalStore
+const serverIsClient = false;
+const serverDefaultTheme = defaultTheme;
+
const myTheme = createTheme({
theme: 'light',
settings: {
@@ -46,12 +50,41 @@ const myTheme = createTheme({
});
export const CustomTheme = () => {
- const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(true);
- const [customTheme, setCustomTheme] = useState(undefined);
const { selectedThemeName } = usePlayground();
const [savedMessage, setSavedMessage] = useState('Save');
+ // SSR-safe client detection
+ const isClient = useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => serverIsClient,
+ );
+
+ // SSR-safe localStorage access for custom theme
+ const customThemeFromStorage = useSyncExternalStore(
+ callback => {
+ window.addEventListener('storage', callback);
+ return () => window.removeEventListener('storage', callback);
+ },
+ () => {
+ const stored = localStorage.getItem('customThemeCss');
+ if (!stored) {
+ localStorage.setItem('customThemeCss', defaultTheme);
+ return defaultTheme;
+ }
+ return stored;
+ },
+ () => serverDefaultTheme,
+ );
+
+ const [customTheme, setCustomTheme] = useState(customThemeFromStorage);
+
+ // Sync from storage when it changes
+ useEffect(() => {
+ setCustomTheme(customThemeFromStorage);
+ }, [customThemeFromStorage]);
+
const updateStyleElement = (theme: string) => {
let styleElement = document.getElementById(
'custom-theme-style',
@@ -66,16 +99,11 @@ export const CustomTheme = () => {
styleElement.textContent = theme;
};
+ // Apply custom theme to DOM
useEffect(() => {
- if (selectedThemeName === 'custom') {
- let storedTheme = localStorage.getItem('customThemeCss');
- if (!storedTheme) {
- storedTheme = defaultTheme;
- localStorage.setItem('customThemeCss', storedTheme);
- }
- setCustomTheme(storedTheme);
- updateStyleElement(storedTheme);
- } else {
+ if (selectedThemeName === 'custom' && customTheme && isClient) {
+ updateStyleElement(customTheme);
+ } else if (isClient) {
const styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
@@ -83,11 +111,7 @@ export const CustomTheme = () => {
styleElement.remove();
}
}
- }, [selectedThemeName]);
-
- useEffect(() => {
- setIsClient(true);
- }, []);
+ }, [selectedThemeName, customTheme, isClient]);
const handleSave = () => {
if (customTheme) {
diff --git a/docs-ui/src/components/DecorativeBox/index.tsx b/docs-ui/src/components/DecorativeBox/index.tsx
index a5c20725bc..8af067459e 100644
--- a/docs-ui/src/components/DecorativeBox/index.tsx
+++ b/docs-ui/src/components/DecorativeBox/index.tsx
@@ -1,5 +1,17 @@
+'use client';
+
+import { CSSProperties, ReactNode } from 'react';
import styles from './styles.module.css';
-export const DecorativeBox = () => {
- return
;
+interface DecorativeBoxProps {
+ children?: ReactNode;
+ style?: CSSProperties;
+}
+
+export const DecorativeBox = ({ children, style }: DecorativeBoxProps) => {
+ return (
+
+ {children}
+
+ );
};
diff --git a/docs-ui/src/components/DecorativeBox/styles.module.css b/docs-ui/src/components/DecorativeBox/styles.module.css
index 84a6b579a0..a2a5c9407a 100644
--- a/docs-ui/src/components/DecorativeBox/styles.module.css
+++ b/docs-ui/src/components/DecorativeBox/styles.module.css
@@ -1,8 +1,14 @@
.box {
min-width: 64px;
min-height: 64px;
+ padding: 8px 12px;
background-color: #eaf2fd;
border-radius: 4px;
box-shadow: 0 0 0 1px #2563eb;
background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232563eb' fill-opacity='0.3' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
+ color: #2563eb;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css b/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css
deleted file mode 100644
index 9009cdb6a4..0000000000
--- a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.layoutComponents {
- display: flex;
- justify-content: flex-start;
- gap: 1rem;
- flex-wrap: wrap;
- margin-top: 2rem;
-
- & svg rect {
- transition: fill 0.2s ease-in-out;
- }
-
- & .box {
- display: flex;
- flex-direction: column;
- width: calc(50% - 0.5rem);
- margin-bottom: 1rem;
- align-items: flex-start;
- }
-
- & .content {
- flex: none;
- background-color: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- width: 100%;
- height: 180px;
- transition: all 0.2s ease-in-out;
- margin-bottom: 0.75rem;
- display: flex;
- align-items: center;
- justify-content: center;
-
- &:hover {
- transform: translateY(-4px);
- }
- }
-
- & .title {
- font-size: 16px;
- transition: color 0.2s ease-in-out;
- margin-bottom: 0.25rem;
- }
-
- & .description {
- font-size: 16px;
- color: var(--secondary);
- }
-}
diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx b/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx
deleted file mode 100644
index 0bf822526e..0000000000
--- a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { BoxSvg } from './svgs/box';
-import { FlexSvg } from './svgs/flex';
-import { GridSvg } from './svgs/grid';
-import { ContainerSvg } from './svgs/container';
-import styles from './LayoutComponents.module.css';
-import Link from 'next/link';
-
-export const LayoutComponents = () => {
- return (
-
-
-
-
-
-
Box
-
- The most basic layout component
-
-
-
-
-
-
-
Flex
-
- Arrange your components vertically
-
-
-
-
-
-
-
Grid
-
- Arrange your components in a grid
-
-
-
-
-
-
-
Container
-
- A container for your components
-
-
-
- );
-};
diff --git a/docs-ui/src/components/LayoutComponents/index.ts b/docs-ui/src/components/LayoutComponents/index.ts
deleted file mode 100644
index 8efb793bbd..0000000000
--- a/docs-ui/src/components/LayoutComponents/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LayoutComponents } from './LayoutComponents';
diff --git a/docs-ui/src/components/Navigation/Navigation.module.css b/docs-ui/src/components/Navigation/Navigation.module.css
index f5e1312ab1..efba109f97 100644
--- a/docs-ui/src/components/Navigation/Navigation.module.css
+++ b/docs-ui/src/components/Navigation/Navigation.module.css
@@ -49,16 +49,13 @@
}
.sectionTitle {
- font-size: 0.6875rem;
- font-weight: 500;
- padding: 0 12px 4px;
- color: var(--secondary);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--primary);
margin-top: 40px;
- text-transform: uppercase;
-
- &:first-child {
- margin-top: 12px;
- }
+ padding: 8px 12px;
+ margin-bottom: 8px;
}
.line {
diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx
index 13fa87661f..bad77e942e 100644
--- a/docs-ui/src/components/Navigation/Navigation.tsx
+++ b/docs-ui/src/components/Navigation/Navigation.tsx
@@ -2,7 +2,6 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { Fragment } from 'react';
import clsx from 'clsx';
import {
RiCollageLine,
@@ -12,26 +11,13 @@ import {
RiServiceLine,
RiStackLine,
} from '@remixicon/react';
-import { components, layoutComponents } from '@/utils/data';
+import { components } from '@/utils/data';
import styles from './Navigation.module.css';
interface NavigationProps {
onLinkClick?: () => void;
}
-const data = [
- {
- title: 'Layout Components',
- content: layoutComponents,
- url: '/components',
- },
- {
- title: 'Components',
- content: components,
- url: '/components',
- },
-];
-
export const Navigation = ({ onLinkClick }: NavigationProps) => {
const pathname = usePathname();
@@ -55,16 +41,6 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
Tokens
-
-
-
- Components
-
-
@@ -89,35 +65,31 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
- {data.map(section => {
+
+
+ Components
+
+ {components.map(item => {
+ const isActive = pathname === `/components/${item.slug}`;
+
return (
-
- {section.title}
-
- {section.content.map(item => {
- const isActive = pathname === `${section.url}/${item.slug}`;
-
- return (
-
- {item.title}
-
- {item.status === 'alpha' && 'Alpha'}
- {item.status === 'beta' && 'Beta'}
- {item.status === 'inProgress' && 'In Progress'}
- {item.status === 'stable' && 'Stable'}
- {item.status === 'deprecated' && 'Deprecated'}
-
-
- );
+
+ onClick={onLinkClick}
+ >
+ {item.title}
+
+ {item.status === 'alpha' && 'Alpha'}
+ {item.status === 'beta' && 'Beta'}
+ {item.status === 'inProgress' && 'In Progress'}
+ {item.status === 'stable' && 'Stable'}
+ {item.status === 'deprecated' && 'Deprecated'}
+
+
);
})}
>
diff --git a/docs-ui/src/components/PropsTable/PropsTable.tsx b/docs-ui/src/components/PropsTable/PropsTable.tsx
index 86b7b274ef..a8af2ad116 100644
--- a/docs-ui/src/components/PropsTable/PropsTable.tsx
+++ b/docs-ui/src/components/PropsTable/PropsTable.tsx
@@ -3,77 +3,133 @@
import * as Table from '../Table';
import { Chip } from '../Chip';
import { TypePopup } from './TypePopup';
+import { SpacingPopup } from './SpacingPopup';
+import { SpacingGroupRow } from './SpacingGroupRow';
import { PropDef } from '@/utils/propDefs';
-// Use the proper PropDef type
type PropData = PropDef;
-// Modify the PropsTable component to use the new type
+type ColumnType = 'prop' | 'type' | 'default' | 'description' | 'responsive';
+
+interface ColumnConfig {
+ key: ColumnType;
+ width: string;
+}
+
+const defaultColumns: ColumnConfig[] = [
+ { key: 'prop' as const, width: '15%' },
+ { key: 'type' as const, width: '25%' },
+ { key: 'default' as const, width: '15%' },
+ { key: 'description' as const, width: '45%' },
+];
+
+const columnLabels: Record = {
+ prop: 'Prop',
+ type: 'Type',
+ default: 'Default',
+ description: 'Description',
+ responsive: 'Responsive',
+};
+
export const PropsTable = >({
data,
+ columns = defaultColumns,
}: {
data: T;
+ columns?: ColumnConfig[];
}) => {
if (!data) return null;
+ const renderCell = (
+ propName: string,
+ propData: PropData,
+ column: ColumnType,
+ ) => {
+ const enumValues =
+ Array.isArray(propData.values) &&
+ propData.values.map(t => {t} );
+
+ switch (column) {
+ case 'prop':
+ return {propName} ;
+
+ case 'type':
+ return (
+
+ {propData.type === 'string' && string }
+ {propData.type === 'number' && number }
+ {propData.type === 'boolean' && boolean }
+ {propData.type === 'enum' && enumValues}
+ {propData.type === 'spacing' && Array.isArray(propData.values) && (
+
+ )}
+ {propData.type === 'complex' && propData.complexType && (
+
+ )}
+ {propData.type === 'enum | string' && (
+ <>
+ {enumValues}
+ string
+ >
+ )}
+
+ );
+
+ case 'default':
+ return propData.default ? {propData.default} : '-';
+
+ case 'description':
+ return propData.description ? (
+ {propData.description}
+ ) : null;
+
+ case 'responsive':
+ return {propData.responsive ? 'Yes' : 'No'} ;
+
+ default:
+ return null;
+ }
+ };
+
return (
- Prop
- Type
- Default
-
- Responsive
-
+ {columns.map(col => (
+
+ {columnLabels[col.key]}
+
+ ))}
{Object.keys(data).map(n => {
- const enumValues =
- Array.isArray(data[n].values) &&
- data[n].values.map(t => {t} );
+ const propData = data[n];
+ // Handle spacing-group type
+ if (propData.type === 'spacing-group' && propData.spacingGroup) {
+ return (
+
+ );
+ }
+
+ // Handle regular props
return (
-
- {n}
-
-
-
- {data[n].type === 'string' && string }
- {data[n].type === 'number' && number }
- {data[n].type === 'boolean' && boolean }
- {data[n].type === 'enum' && enumValues}
- {data[n].type === 'spacing' && (
- <>
- 0.5, 1, 1.5, 2, 3, ..., 14
- string
- >
- )}
- {data[n].type === 'complex' && data[n].complexType && (
-
- )}
- {data[n].type === 'enum | string' && (
- <>
- {enumValues}
- string
- >
- )}
-
-
-
- {data[n].default ? data[n].default : '-'}
-
-
- {data[n].responsive ? 'Yes' : 'No'}
-
+ {columns.map(col => (
+
+ {renderCell(n, propData, col.key)}
+
+ ))}
);
})}
diff --git a/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css b/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css
new file mode 100644
index 0000000000..71ade80b47
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css
@@ -0,0 +1,96 @@
+.expandButton {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ font-family: inherit;
+ color: inherit;
+ text-align: left;
+ gap: 0.5rem;
+}
+
+.expandButton:hover {
+ opacity: 0.8;
+}
+
+.propName {
+ font-family: monospace;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.count {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-weight: normal;
+}
+
+.icon {
+ color: var(--text-secondary);
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+}
+
+.expandedContent {
+ padding: 0.5rem 0;
+}
+
+.twoColumnLayout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+}
+
+.column {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.columnTitle {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 0.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.propsGrid {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.propRow {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 1rem;
+ align-items: start;
+}
+
+.propRowName {
+ min-width: 80px;
+}
+
+.propRowDescription {
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
+
+.propRowDefault {
+ font-size: 12px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
diff --git a/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx b/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx
new file mode 100644
index 0000000000..b3cc8de26d
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import { useState } from 'react';
+import * as Table from '../Table';
+import { Chip } from '../Chip';
+import { SpacingPopup } from './SpacingPopup';
+import type { SpacingGroupDef } from '@/utils/propDefs';
+import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react';
+import styles from './SpacingGroupRow.module.css';
+
+interface SpacingGroupRowProps {
+ spacingGroup: SpacingGroupDef;
+ description?: React.ReactNode;
+ columns: Array<{ key: string; width: string }>;
+}
+
+export const SpacingGroupRow = ({
+ spacingGroup,
+ description,
+ columns,
+}: SpacingGroupRowProps) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ // Separate padding and margin props
+ const paddingProps = spacingGroup.props.filter(prop =>
+ prop.name.startsWith('p'),
+ );
+ const marginProps = spacingGroup.props.filter(prop =>
+ prop.name.startsWith('m'),
+ );
+
+ const renderCell = (columnKey: string) => {
+ switch (columnKey) {
+ case 'prop':
+ return (
+ setIsExpanded(!isExpanded)}
+ aria-expanded={isExpanded}
+ >
+
+ Spacing props{' '}
+
+ ({spacingGroup.props.length})
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ );
+
+ case 'type':
+ return ;
+
+ case 'default':
+ return '-';
+
+ case 'description':
+ return description ? (
+ {description}
+ ) : null;
+
+ case 'responsive':
+ return {spacingGroup.responsive ? 'Yes' : 'No'} ;
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+ <>
+
+ {columns.map(col => (
+
+ {renderCell(col.key)}
+
+ ))}
+
+ {isExpanded && (
+
+
+
+
+ {/* Padding Column */}
+ {paddingProps.length > 0 && (
+
+
Padding
+
+ {paddingProps.map(prop => (
+
+
+ {prop.name}
+
+
+ {prop.description}
+
+ {prop.default && (
+
+ Default: {prop.default}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Margin Column */}
+ {marginProps.length > 0 && (
+
+
Margin
+
+ {marginProps.map(prop => (
+
+
+ {prop.name}
+
+
+ {prop.description}
+
+ {prop.default && (
+
+ Default: {prop.default}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/docs-ui/src/components/PropsTable/SpacingPopup.module.css b/docs-ui/src/components/PropsTable/SpacingPopup.module.css
new file mode 100644
index 0000000000..892bc14bc6
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingPopup.module.css
@@ -0,0 +1,77 @@
+.button {
+ display: inline-flex;
+ align-items: center;
+ font-family: monospace;
+ font-size: 13px;
+ border-radius: 6px;
+ padding: 0px 8px;
+ height: 24px;
+ margin-right: 4px;
+ background-color: #f0f0f0;
+ color: #5d5d5d;
+ cursor: pointer;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ transition: background-color 150ms ease;
+}
+
+.button:hover {
+ background-color: #e0e0e0;
+}
+
+[data-theme-mode='dark'] .button {
+ background-color: #2c2c2c;
+ color: #fff;
+}
+
+[data-theme-mode='dark'] .button:hover {
+ background-color: #3c3c3c;
+}
+
+.popover {
+ border: 1px solid var(--border);
+ box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
+ border-radius: 6px;
+ background: var(--bg);
+ color: var(--primary);
+ outline: none;
+ transition: transform 200ms, opacity 200ms;
+ padding: 1rem;
+ margin-top: 6px;
+ max-width: 400px;
+ --origin: translateY(-8px);
+
+ &[data-entering],
+ &[data-exiting] {
+ transform: var(--origin);
+ opacity: 0;
+ }
+}
+
+.arrow svg {
+ display: block;
+ fill: var(--bg);
+ stroke: var(--border);
+ stroke-width: 1px;
+ transform: rotate(180deg);
+}
+
+.title {
+ font-size: 14px;
+ font-weight: 500;
+ margin-bottom: 0.75rem;
+}
+
+.grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ margin-bottom: 0.75rem;
+}
+
+.note {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-style: italic;
+}
diff --git a/docs-ui/src/components/PropsTable/SpacingPopup.tsx b/docs-ui/src/components/PropsTable/SpacingPopup.tsx
new file mode 100644
index 0000000000..9515ec4e5a
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingPopup.tsx
@@ -0,0 +1,47 @@
+import { Chip } from '../Chip';
+import {
+ Button,
+ Dialog,
+ DialogTrigger,
+ OverlayArrow,
+ Popover,
+} from 'react-aria-components';
+import styles from './SpacingPopup.module.css';
+
+interface SpacingPopupProps {
+ values: string[];
+}
+
+export const SpacingPopup = ({ values }: SpacingPopupProps) => {
+ // Display abbreviated list: first, second, ..., last
+ const firstValue = values[0];
+ const secondValue = values[1];
+ const lastValue = values[values.length - 1];
+
+ return (
+
+
{firstValue}
+
{secondValue}
+
+ ...
+
+
+
+
+
+
+
+ All spacing values
+
+ {values.map(value => (
+ {value}
+ ))}
+
+ Also accepts custom string values
+
+
+
+
{lastValue}
+
+ );
+};
diff --git a/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx b/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx
new file mode 100644
index 0000000000..37656c482d
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx
@@ -0,0 +1,26 @@
+import styles from './styles.module.css';
+
+interface ReactAriaLinkProps {
+ /** The React Aria component name (e.g., "Cell", "Table") */
+ component: string;
+ /** The documentation URL */
+ href: string;
+}
+
+/**
+ * Displays a standardized note linking to React Aria documentation.
+ *
+ * Usage:
+ *
+ */
+export function ReactAriaLink({ component, href }: ReactAriaLinkProps) {
+ return (
+
+ Inherits all{' '}
+
+ React Aria {component}
+ {' '}
+ props.
+
+ );
+}
diff --git a/docs-ui/src/components/ReactAriaLink/index.ts b/docs-ui/src/components/ReactAriaLink/index.ts
new file mode 100644
index 0000000000..8fa7b70e73
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/index.ts
@@ -0,0 +1 @@
+export { ReactAriaLink } from './ReactAriaLink';
diff --git a/docs-ui/src/components/ReactAriaLink/styles.module.css b/docs-ui/src/components/ReactAriaLink/styles.module.css
new file mode 100644
index 0000000000..b74436fee3
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/styles.module.css
@@ -0,0 +1,15 @@
+.note {
+ font-size: 1rem;
+ color: var(--secondary);
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.note a {
+ color: var(--link);
+ text-decoration: none;
+}
+
+.note a:hover {
+ text-decoration: underline;
+}
diff --git a/docs-ui/src/components/Roadmap/Roadmap.tsx b/docs-ui/src/components/Roadmap/Roadmap.tsx
deleted file mode 100644
index 52f96e0f76..0000000000
--- a/docs-ui/src/components/Roadmap/Roadmap.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { RoadmapItem } from './list';
-
-export const Roadmap = ({ list }: { list: RoadmapItem[] }) => {
- const orderList = ['inProgress', 'notStarted', 'completed'];
- return (
-
- {list
- .sort(
- (a, b) => orderList.indexOf(a.status) - orderList.indexOf(b.status),
- )
- .map(Item)}
-
- );
-};
-
-const Item = ({
- title,
- status = 'notStarted',
-}: {
- title: string;
- status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
-}) => {
- return (
-
-
-
- {status === 'notStarted' && 'Not Started'}
- {status === 'inProgress' && 'In Progress'}
- {status === 'inReview' && 'Ready for Review'}
- {status === 'completed' && 'Completed'}
-
-
- );
-};
diff --git a/docs-ui/src/components/Roadmap/index.ts b/docs-ui/src/components/Roadmap/index.ts
deleted file mode 100644
index c7006073d6..0000000000
--- a/docs-ui/src/components/Roadmap/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Roadmap } from './Roadmap';
diff --git a/docs-ui/src/components/Roadmap/list.ts b/docs-ui/src/components/Roadmap/list.ts
deleted file mode 100644
index 24b9197cf5..0000000000
--- a/docs-ui/src/components/Roadmap/list.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-export type RoadmapItem = {
- title: string;
- status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
-};
-
-export const list: RoadmapItem[] = [
- {
- title: 'Remove Vanilla Extract and use pure CSS instead',
- status: 'inProgress',
- },
- {
- title: 'Add collapsing across breakpoints for the Inline component',
- status: 'notStarted',
- },
- {
- title: 'Add reversing the order for the Inline component',
- status: 'notStarted',
- },
- {
- title: 'Set up Storybook',
- status: 'completed',
- },
- {
- title: 'Set up iconography',
- status: 'completed',
- },
- {
- title: 'Set up global tokens',
- status: 'inProgress',
- },
- {
- title: 'Set up theming system',
- status: 'inProgress',
- },
- {
- title: 'Create first pass at box component',
- status: 'completed',
- },
- {
- title: 'Create first pass at stack component',
- status: 'completed',
- },
- {
- title: 'Create first pass at inline component',
- status: 'completed',
- },
-];
diff --git a/docs-ui/src/components/Roadmap/styles.css b/docs-ui/src/components/Roadmap/styles.css
deleted file mode 100644
index 63f32312e7..0000000000
--- a/docs-ui/src/components/Roadmap/styles.css
+++ /dev/null
@@ -1,100 +0,0 @@
-.roadmap {
- display: flex;
- flex-direction: column;
-}
-
-.roadmap .roadmap-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- border-bottom: 1px solid #e0e0e0;
- padding: 8px 0px;
-}
-
-.roadmap .roadmap-item .left {
- display: flex;
- align-items: center;
- padding-left: 12px;
- gap: 12px;
-}
-
-.roadmap .roadmap-item .dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: #e0e0e0;
-}
-
-.roadmap .roadmap-item.notStarted {
- color: #000;
-}
-
-.roadmap .roadmap-item.inProgress {
- color: #000;
-}
-
-.roadmap .roadmap-item.inReview {
- color: #000;
-}
-
-.roadmap .roadmap-item.completed .title {
- color: #a2a2a2;
- text-decoration: line-through;
-}
-
-.roadmap .roadmap-item.notStarted .dot {
- background-color: #d1d1d1;
-}
-
-.roadmap .roadmap-item.inProgress .dot {
- background-color: #ffd000;
-}
-
-.roadmap .roadmap-item.inReview .dot {
- background-color: #4ed14a;
-}
-
-.roadmap .roadmap-item.completed .dot {
- background-color: #4ed14a;
-}
-
-.roadmap .roadmap-item .title {
- font-size: 16px;
-}
-
-.roadmap .roadmap-item .pill {
- display: inline-flex;
- align-items: center;
- height: 24px;
- padding: 0px 8px;
- border-radius: 40px;
- font-size: 12px;
- font-weight: 600;
- margin-left: 8px;
- border-style: solid;
- border-width: 1px;
-}
-
-.roadmap .roadmap-item.notStarted .pill {
- background-color: #f2f2f2;
- border-color: #cdcdcd;
- color: #888888;
-}
-
-.roadmap .roadmap-item.inProgress .pill {
- background-color: #fff2b9;
- border-color: #ffd000;
- color: #d79927;
-}
-
-.roadmap .roadmap-item.inReview .pill {
- background-color: #d7f9d7;
- border-color: #4ed14a;
- color: #3a9837;
-}
-
-.roadmap .roadmap-item.completed .pill {
- background-color: #d7f9d7;
- border-color: #4ed14a;
- color: #3a9837;
-}
diff --git a/docs-ui/src/components/Snippet/client.tsx b/docs-ui/src/components/Snippet/client.tsx
index 04284c3249..2041218a12 100644
--- a/docs-ui/src/components/Snippet/client.tsx
+++ b/docs-ui/src/components/Snippet/client.tsx
@@ -12,6 +12,7 @@ interface SnippetProps {
py?: number;
open?: boolean;
height?: string | number;
+ layout?: 'stacked' | 'side-by-side';
}
export const SnippetClient = ({
@@ -22,9 +23,28 @@ export const SnippetClient = ({
py = 2,
open = false,
height = 'auto',
+ layout = 'stacked',
}: SnippetProps) => {
const [isOpen, setIsOpen] = useState(open);
+ if (layout === 'side-by-side') {
+ return (
+
+ );
+ }
+
return (
{
return (
);
};
diff --git a/docs-ui/src/components/Snippet/styles.module.css b/docs-ui/src/components/Snippet/styles.module.css
index 3141564293..c6270670a7 100644
--- a/docs-ui/src/components/Snippet/styles.module.css
+++ b/docs-ui/src/components/Snippet/styles.module.css
@@ -7,7 +7,7 @@
.preview {
border-radius: 8px;
box-shadow: inset 0 0 0 1px var(--border);
- background-color: var(--bui-bg);
+ background-color: var(--bui-bg-app);
padding: 1px;
position: relative;
}
@@ -60,3 +60,43 @@
opacity: 1;
}
}
+
+/* Side-by-side layout */
+.sideBySide {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ align-items: stretch;
+}
+
+.sideBySideCode {
+ min-width: 0;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.codeWrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.codeWrapper > div {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+.sideBySidePreview {
+ border-radius: 8px;
+ box-shadow: inset 0 0 0 1px var(--border);
+ background-color: var(--bui-bg-app);
+ padding: 1px;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.sideBySidePreview > .previewContent {
+ flex: 1;
+}
diff --git a/docs-ui/src/components/Table/Table.tsx b/docs-ui/src/components/Table/Table.tsx
index 4d0e48dbce..a43bcd766a 100644
--- a/docs-ui/src/components/Table/Table.tsx
+++ b/docs-ui/src/components/Table/Table.tsx
@@ -45,12 +45,14 @@ export const Row = ({ children }: { children: ReactNode }) => {
export const Cell = ({
children,
style,
+ colSpan,
}: {
children: ReactNode;
style?: CSSProperties;
+ colSpan?: number;
}) => {
return (
-
+
{children}
);
diff --git a/docs-ui/src/components/TableOfContents/TableOfContents.tsx b/docs-ui/src/components/TableOfContents/TableOfContents.tsx
index d18265415b..429d677b80 100644
--- a/docs-ui/src/components/TableOfContents/TableOfContents.tsx
+++ b/docs-ui/src/components/TableOfContents/TableOfContents.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useLayoutEffect } from 'react';
import { usePathname } from 'next/navigation';
import styles from './TableOfContents.module.css';
@@ -17,6 +17,29 @@ export function TableOfContents() {
const [indicatorHeight, setIndicatorHeight] = useState(0);
const pathname = usePathname();
+ // Update indicator position when activeId changes
+ useLayoutEffect(() => {
+ if (!activeId) return;
+
+ // Use requestAnimationFrame to defer setState call
+ const rafId = requestAnimationFrame(() => {
+ const activeElement = document.querySelector(
+ `[data-toc-id="${activeId}"]`,
+ ) as HTMLElement;
+ if (activeElement) {
+ const list = activeElement.closest('ul');
+ if (list) {
+ const listRect = list.getBoundingClientRect();
+ const elementRect = activeElement.getBoundingClientRect();
+ setIndicatorTop(elementRect.top - listRect.top);
+ setIndicatorHeight(elementRect.height);
+ }
+ }
+ });
+
+ return () => cancelAnimationFrame(rafId);
+ }, [activeId, headings]);
+
useEffect(() => {
// Extract all H2 and H3 headings from the document
const elements = Array.from(
@@ -29,18 +52,6 @@ export function TableOfContents() {
level: parseInt(element.tagName.substring(1)),
}));
- setHeadings(headingData);
-
- // Set initial active heading (first visible heading or first heading)
- if (headingData.length > 0) {
- const viewportTop = window.scrollY + 100; // offset for header
- const visibleHeading = elements.find(element => {
- const rect = element.getBoundingClientRect();
- return rect.top + window.scrollY >= viewportTop - 200;
- });
- setActiveId(visibleHeading?.id || headingData[0].id);
- }
-
// Set up IntersectionObserver to track visible headings
const observerOptions = {
rootMargin: '-80px 0px -80% 0px',
@@ -62,29 +73,26 @@ export function TableOfContents() {
elements.forEach(element => observer.observe(element));
+ // Initialize headings and active ID after observer is set up
+ requestAnimationFrame(() => {
+ setHeadings(headingData);
+
+ // Set initial active heading (first visible heading or first heading)
+ if (headingData.length > 0) {
+ const viewportTop = window.scrollY + 100; // offset for header
+ const visibleHeading = elements.find(element => {
+ const rect = element.getBoundingClientRect();
+ return rect.top + window.scrollY >= viewportTop - 200;
+ });
+ setActiveId(visibleHeading?.id || headingData[0].id);
+ }
+ });
+
return () => {
elements.forEach(element => observer.unobserve(element));
};
}, [pathname]);
- // Update indicator position when activeId changes
- useEffect(() => {
- if (activeId) {
- const activeElement = document.querySelector(
- `[data-toc-id="${activeId}"]`,
- ) as HTMLElement;
- if (activeElement) {
- const list = activeElement.closest('ul');
- if (list) {
- const listRect = list.getBoundingClientRect();
- const elementRect = activeElement.getBoundingClientRect();
- setIndicatorTop(elementRect.top - listRect.top);
- setIndicatorHeight(elementRect.height);
- }
- }
- }
- }, [activeId, headings]);
-
const handleClick = (id: string) => {
const element = document.getElementById(id);
if (element) {
diff --git a/docs-ui/src/components/Theming/index.tsx b/docs-ui/src/components/Theming/index.tsx
index d06f51348b..9f67c3a111 100644
--- a/docs-ui/src/components/Theming/index.tsx
+++ b/docs-ui/src/components/Theming/index.tsx
@@ -1,11 +1,16 @@
-import { MDXRemote } from 'next-mdx-remote-client/rsc';
-import { formattedMDXComponents } from '@/mdx-components';
-import type {
- ComponentDefinition,
- DataAttributeValues,
-} from '../../../../packages/ui/src/types';
+'use client';
-export function Theming({ definition }: { definition: ComponentDefinition }) {
+import { formattedMDXComponents } from '@/mdx-components';
+import type { DataAttributeValues } from '../../../../packages/ui/src/types';
+
+interface ThemingProps {
+ definition: {
+ classNames: Record;
+ dataAttributes?: Record;
+ };
+}
+
+export function Theming({ definition }: ThemingProps) {
const classNames = definition.classNames;
const dataAttributes = definition.dataAttributes;
@@ -33,15 +38,28 @@ export function Theming({ definition }: { definition: ComponentDefinition }) {
...Object.values(classNames).slice(1),
];
+ // Use the same styled components from MDX, with fallbacks to HTML elements
+ const H2 = formattedMDXComponents.h2 || 'h2';
+ const P = formattedMDXComponents.p || 'p';
+ const Ul = formattedMDXComponents.ul || 'ul';
+ const Li = formattedMDXComponents.li || 'li';
+ const Code = formattedMDXComponents.code || 'code';
+
return (
- `- \`${selector}\``).join('\n')}
- `}
- />
+
+
Theming
+
+ Our theming system is based on a mix between CSS classes, CSS variables
+ and data attributes. If you want to customise this component, you can
+ use one of these class names below.
+
+
+ {classNamesArray.map((selector, index) => (
+
+ {selector}
+
+ ))}
+
+
);
}
diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css
index 581fc599b3..4f067bd7bd 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.module.css
+++ b/docs-ui/src/components/Toolbar/Toolbar.module.css
@@ -10,12 +10,10 @@
}
}
-.breadcrumb {
+.left {
display: flex;
align-items: center;
gap: 0.5rem;
- font-size: 0.875rem;
- font-weight: 500;
}
.logoMobile {
@@ -26,39 +24,6 @@
}
}
-.breadcrumbDesktop {
- display: none;
-
- @media (min-width: 768px) {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-}
-
-.breadcrumbLink {
- color: var(--secondary);
- text-decoration: none;
- cursor: pointer;
-
- &:hover {
- color: var(--primary);
- text-decoration: underline;
- transition: color 0.2s ease-in-out;
- text-decoration-thickness: 1px;
- text-underline-offset: 4px;
- }
-}
-
-.breadcrumbSeparator {
- color: var(--secondary);
- flex-shrink: 0;
-}
-
-.breadcrumbCurrent {
- color: var(--primary);
-}
-
.actions {
display: none;
@@ -119,6 +84,51 @@
}
}
+.searchButton {
+ display: none;
+ align-items: center;
+ gap: 6px;
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 32px;
+ padding-inline: 16px 12px;
+ min-width: 180px;
+ color: var(--secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ height: 32px;
+ cursor: pointer;
+ font-family: inherit;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: var(--action);
+ color: var(--primary);
+ transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
+ }
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.searchLabel {
+ display: inline;
+ flex: 1;
+ text-align: left;
+}
+
+.searchKbd {
+ display: inline;
+ font-family: inherit;
+ font-size: 0.6875rem;
+ font-weight: 500;
+ background-color: var(--action);
+ border-radius: 4px;
+ padding: 2px 5px;
+ color: var(--secondary);
+}
+
.buttonGroup {
display: flex;
align-items: center;
diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx
index e7defd5e8a..4a071bf45e 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.tsx
+++ b/docs-ui/src/components/Toolbar/Toolbar.tsx
@@ -1,10 +1,11 @@
'use client';
+import { useState, useEffect } from 'react';
import {
RiArrowDownSLine,
- RiArrowRightSLine,
RiGithubLine,
RiMoonLine,
+ RiSearchLine,
RiSunLine,
} from '@remixicon/react';
import {
@@ -19,10 +20,8 @@ import {
} from 'react-aria-components';
import styles from './Toolbar.module.css';
import { usePlayground } from '@/utils/playground-context';
-import { usePathname } from 'next/navigation';
-import Link from 'next/link';
-import { components, layoutComponents } from '@/utils/data';
import { Logo } from '@/components/Sidebar/Logo';
+import { CommandPalette } from '@/components/CommandPalette';
interface ToolbarProps {
version: string;
@@ -42,75 +41,36 @@ export const Toolbar = ({ version }: ToolbarProps) => {
setSelectedThemeName,
} = usePlayground();
- const pathname = usePathname();
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
- // Determine breadcrumb content based on current path
- const getBreadcrumb = () => {
- const allComponents = [...components, ...layoutComponents];
+ useEffect(() => {
+ const isMac = /mac(os|intosh)/i.test(navigator.userAgent);
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
+ e.preventDefault();
+ setIsCommandPaletteOpen(prev => !prev);
+ }
+ };
- // Root page
- if (pathname === '/') {
- return { section: null, title: 'Getting Started' };
- }
-
- // Components index page
- if (pathname === '/components') {
- return { section: null, title: 'Components' };
- }
-
- // Component detail pages
- if (pathname?.startsWith('/components/')) {
- const slug = pathname.split('/components/')[1];
- const component = allComponents.find(c => c.slug === slug);
- return {
- section: 'Components',
- sectionLink: '/components',
- title: component?.title || slug,
- };
- }
-
- // Tokens page
- if (pathname === '/tokens') {
- return { section: null, title: 'Tokens' };
- }
-
- // Changelog page
- if (pathname === '/changelog') {
- return { section: null, title: 'Changelog' };
- }
-
- return { section: null, title: '' };
- };
-
- const breadcrumb = getBreadcrumb();
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
return (
-
+
-
- {breadcrumb.section && breadcrumb.sectionLink ? (
- <>
-
- {breadcrumb.section}
-
-
-
- {breadcrumb.title}
-
- >
- ) : (
- {breadcrumb.title}
- )}
-
+
setIsCommandPaletteOpen(true)}
+ >
+
+ Search
+ ⌘K
+
{
+
);
};
diff --git a/docs-ui/src/content/accordion.props.ts b/docs-ui/src/content/accordion.props.ts
deleted file mode 100644
index aac983b0c3..0000000000
--- a/docs-ui/src/content/accordion.props.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const accordionPropDefs: Record
= {
- children: {
- type: 'enum',
- values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
- },
- defaultExpanded: {
- type: 'boolean',
- default: 'false',
- },
- isExpanded: {
- type: 'boolean',
- },
- onExpandedChange: {
- type: 'enum',
- values: ['(isExpanded: boolean) => void'],
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionTriggerPropDefs: Record = {
- level: {
- type: 'enum',
- values: ['1', '2', '3', '4', '5', '6'],
- default: '3',
- },
- title: {
- type: 'string',
- },
- subtitle: {
- type: 'string',
- },
- children: {
- type: 'enum',
- values: ['ReactNode'],
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionPanelPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionGroupPropDefs: Record = {
- allowsMultiple: {
- type: 'boolean',
- default: 'false',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
-
-
-
- Your content
- `;
-
-export const accordionWithSubtitleSnippet = `
-
-
- Your content here
-
- `;
-
-export const accordionCustomTriggerSnippet = `
-
-
- Custom Multi-line Trigger
-
- Click to expand additional details
-
-
-
-
- Your content here
-
- `;
-
-export const accordionDefaultExpandedSnippet = `
-
-
- Your content here
-
- `;
-
-export const accordionGroupSingleOpenSnippet = `
-
-
- Content 1
-
-
-
- Content 2
-
- `;
-
-export const accordionGroupMultipleOpenSnippet = `
-
-
- Content 1
-
-
-
- Content 2
-
- `;
diff --git a/docs-ui/src/content/avatar.props.ts b/docs-ui/src/content/avatar.props.ts
deleted file mode 100644
index 80621cdb89..0000000000
--- a/docs-ui/src/content/avatar.props.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const avatarPropDefs: Record = {
- src: {
- type: 'string',
- },
- name: {
- type: 'string',
- },
- size: {
- type: 'enum',
- values: ['x-small', 'small', 'medium', 'large', 'x-large'],
- default: 'medium',
- responsive: true,
- },
- purpose: {
- type: 'enum',
- values: ['informative', 'decoration'],
- default: 'informative',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const snippetUsage = `import { Avatar } from '@backstage/ui';
-
- `;
-
-export const snippetSizes = `
-
-
-
-
-
- `;
-
-export const snippetFallback = ` `;
-
-export const snippetPurpose = `
-
- Informative (default)
-
- Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
-
-
-
-
-
-
- Decoration
-
- Use when name appears adjacent to avatar. Hidden from screen readers to avoid redundancy:
-
-
-
- Charles de Dreuille
-
-
- `;
diff --git a/docs-ui/src/content/box.mdx b/docs-ui/src/content/box.mdx
deleted file mode 100644
index 66e99ec19a..0000000000
--- a/docs-ui/src/content/box.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { BoxSnippet } from '@/snippets/stories-snippets';
-import {
- boxPropDefs,
- snippetUsage,
- boxPreviewSnippet,
- boxSimpleSnippet,
- boxResponsiveSnippet,
-} from './box.props';
-import { spacingPropDefs } from '@/utils/propDefs';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { BoxDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={boxPreviewSnippet}
- align="center"
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Box
-
-This is the Box component, our lowest-level component. Here are all the
-available properties.
-
-
-
-Padding and margin are used to create space around your component using our
-predefined spacing tokens. We would recommend to use padding over margin to
-avoid collapsing margins but both are available.
-
-
-
-## Examples
-
-### Simple example
-
-A simple example of how to use the Box component.
-
-
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/box.props.ts b/docs-ui/src/content/box.props.ts
deleted file mode 100644
index 0ee45c80f4..0000000000
--- a/docs-ui/src/content/box.props.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- classNamePropDefs,
- displayPropDefs,
- heightPropDefs,
- positionPropDefs,
- stylePropDefs,
- widthPropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const boxPropDefs: Record = {
- as: {
- type: 'enum',
- values: ['div', 'span'],
- default: 'div',
- responsive: true,
- },
- ...widthPropDefs,
- ...heightPropDefs,
- ...positionPropDefs,
- ...displayPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const snippetUsage = `import { Box } from '@backstage/ui';
-
- `;
-
-export const boxPreviewSnippet = `
-
- `;
-
-export const boxSimpleSnippet = `Hello World `;
-
-export const boxResponsiveSnippet = `
- Hello World
- `;
diff --git a/docs-ui/src/content/button-icon.mdx b/docs-ui/src/content/button-icon.mdx
deleted file mode 100644
index b0595cc9bc..0000000000
--- a/docs-ui/src/content/button-icon.mdx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonIconSnippet } from '@/snippets/stories-snippets';
-import {
- buttonIconPropDefs,
- buttonIconUsageSnippet,
- buttonIconDefaultSnippet,
- buttonIconVariantsSnippet,
- buttonIconSizesSnippet,
- buttonIconDisabledSnippet,
- buttonIconLoadingSnippet,
- buttonIconResponsiveSnippet,
- buttonIconAsLinkSnippet,
-} from './button-icon.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonIconDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={buttonIconDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonIconVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonIconSizesSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonIconDisabledSnippet}
-/>
-
-### Loading
-
-Here's a view when buttons are in a loading state.
-
- }
- code={buttonIconLoadingSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/button-icon.props.ts b/docs-ui/src/content/button-icon.props.ts
deleted file mode 100644
index b063484d27..0000000000
--- a/docs-ui/src/content/button-icon.props.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const buttonIconPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- icon: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- loading: { type: 'boolean', default: 'false', responsive: false },
- type: {
- type: 'enum',
- values: ['button', 'submit', 'reset'],
- default: 'button',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
-
- `;
-
-export const buttonIconDefaultSnippet = `
- } variant="primary" />
- } variant="secondary" />
- `;
-
-export const buttonIconVariantsSnippet = `
- } variant="primary" />
- } variant="secondary" />
- `;
-
-export const buttonIconSizesSnippet = `
- } size="small" />
- } size="medium" />
- `;
-
-export const buttonIconDisabledSnippet = ` } isDisabled />`;
-
-export const buttonIconLoadingSnippet = ` } variant="primary" loading={isLoading} onPress={handleClick} />`;
-
-export const buttonIconResponsiveSnippet = ` } variant={{ initial: 'primary', lg: 'secondary' }} />`;
-
-export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
-
-
- Button
- `;
diff --git a/docs-ui/src/content/button-link.mdx b/docs-ui/src/content/button-link.mdx
deleted file mode 100644
index 4f3f376aee..0000000000
--- a/docs-ui/src/content/button-link.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonLinkSnippet } from '@/snippets/stories-snippets';
-import {
- buttonLinkPropDefs,
- buttonLinkSnippetUsage,
- buttonLinkVariantsSnippet,
- buttonLinkSizesSnippet,
- buttonLinkIconsSnippet,
- buttonLinkDisabledSnippet,
- buttonLinkResponsiveSnippet,
-} from './button-link.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonLinkDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={buttonLinkVariantsSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonLinkVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonLinkSizesSnippet}
-/>
-
-### With Icons
-
-Here's a view when buttons have icons.
-
- }
- code={buttonLinkIconsSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonLinkDisabledSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/button-link.props.ts b/docs-ui/src/content/button-link.props.ts
deleted file mode 100644
index 2bd4e2196a..0000000000
--- a/docs-ui/src/content/button-link.props.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const buttonLinkPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
- iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- href: { type: 'string', responsive: false },
- hrefLang: { type: 'string', responsive: false },
- target: {
- type: 'enum',
- values: ['HTMLAttributeAnchorTarget'],
- default: '_self',
- responsive: false,
- },
- rel: { type: 'string', responsive: false },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ui';
-
- `;
-
-export const buttonLinkVariantsSnippet = `
- } variant="primary">
- Button
-
- } variant="secondary">
- Button
-
- `;
-
-export const buttonLinkSizesSnippet = `
- Small
- Medium
- `;
-
-export const buttonLinkIconsSnippet = `
- }>Button
- }>Button
- }
- iconEnd={ }>
- Button
-
- `;
-
-export const buttonLinkDisabledSnippet = `
-
- Primary
-
-
- Secondary
-
- `;
-
-export const buttonLinkResponsiveSnippet = `
- Responsive Button
- `;
diff --git a/docs-ui/src/content/button.mdx b/docs-ui/src/content/button.mdx
deleted file mode 100644
index d161c97edf..0000000000
--- a/docs-ui/src/content/button.mdx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets';
-import {
- buttonPropDefs,
- buttonSnippetUsage,
- buttonVariantsSnippet,
- buttonSizesSnippet,
- buttonIconsSnippet,
- buttonDisabledSnippet,
- buttonLoadingSnippet,
- buttonResponsiveSnippet,
- buttonAsLinkSnippet,
-} from './button.props';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonDefinition } from '../utils/definitions';
-
-
-
- }
- code={buttonVariantsSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonSizesSnippet}
-/>
-
-### With Icons
-
-Here's a view when buttons have icons.
-
- }
- code={buttonIconsSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonDisabledSnippet}
-/>
-
-### Loading
-
-Here's a view when buttons are in a loading state.
-
- }
- code={buttonLoadingSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-### As Link
-
-If you want to use a button as a link, please use the `ButtonLink` component.
-
- }
- code={buttonAsLinkSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/button.props.ts b/docs-ui/src/content/button.props.ts
deleted file mode 100644
index e650a9f2e9..0000000000
--- a/docs-ui/src/content/button.props.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const buttonPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
- iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- loading: { type: 'boolean', default: 'false', responsive: false },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- type: {
- type: 'enum',
- values: ['button', 'submit', 'reset'],
- default: 'button',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
-
- `;
-
-export const buttonVariantsSnippet = `
-
- Button
-
-
- Button
-
- `;
-
-export const buttonSizesSnippet = `
- Small
- Medium
- `;
-
-export const buttonIconsSnippet = `
- }>Button
- }>Button
- } iconEnd={ }>Button
- `;
-
-export const buttonDisabledSnippet = `
-
- Primary
-
-
- Secondary
-
- `;
-
-export const buttonResponsiveSnippet = `
- Responsive Button
- `;
-
-export const buttonLoadingSnippet = `
- Load more items
- `;
-
-export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
-
-
- Button
- `;
diff --git a/docs-ui/src/content/card.mdx b/docs-ui/src/content/card.mdx
deleted file mode 100644
index c38a07e920..0000000000
--- a/docs-ui/src/content/card.mdx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CardSnippet } from '@/snippets/stories-snippets';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import {
- cardPropDefs,
- cardUsageSnippet,
- cardDefaultSnippet,
- cardHeaderPropDefs,
- cardBodyPropDefs,
- cardFooterPropDefs,
- cardLongBodySnippet,
- cardListRowSnippet,
-} from './card.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { CardDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={cardDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Card
-
-A card component that can be used to display content in a box.
-
-
-
-### CardHeader
-
-To display a header in a card, use the `CardHeader` component. This will be fixed at the top of the card.
-
-
-
-### CardBody
-
-To display content in a card, use the `CardBody` component. This will automatically fill the card.
-
-
-
-### CardFooter
-
-To display a footer in a card, use the `CardFooter` component. This will be fixed at the bottom of the card.
-
-
-
-## Examples
-
-### With long body
-
-Here's a view when card has a long body.
-
- }
- code={cardLongBodySnippet}
- open
-/>
-
-### With list
-
-Here's a view when card has a list.
-
- }
- code={cardListRowSnippet}
- open
-/>
-
-
-
-
diff --git a/docs-ui/src/content/checkbox.props.ts b/docs-ui/src/content/checkbox.props.ts
deleted file mode 100644
index ce0f1033ff..0000000000
--- a/docs-ui/src/content/checkbox.props.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const checkboxPropDefs: Record = {
- children: {
- type: 'enum',
- values: ['React.ReactNode'],
- responsive: false,
- },
- isSelected: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- defaultSelected: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- onChange: {
- type: 'enum',
- values: ['(isSelected: boolean) => void'],
- responsive: false,
- },
- isDisabled: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- isRequired: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- name: {
- type: 'string',
- responsive: false,
- },
- value: {
- type: 'string',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
-
-Accept terms `;
-
-export const checkboxDefaultSnippet = `Accept terms and conditions `;
-
-export const checkboxVariantsSnippet = `
- Unchecked
- Checked
- Disabled
- Checked & Disabled
- `;
diff --git a/docs-ui/src/content/container.mdx b/docs-ui/src/content/container.mdx
deleted file mode 100644
index 56c2d5d405..0000000000
--- a/docs-ui/src/content/container.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { ContainerSnippet } from '@/snippets/stories-snippets';
-import {
- containerPropDefs,
- containerUsageSnippet,
- containerDefaultSnippet,
- containerSimpleSnippet,
- containerResponsiveSnippet,
-} from './container.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ContainerDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={containerDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Simple
-
-A simple example of how to use the Container component.
-
-
-
-### Responsive padding & margin
-
-The Container component also supports responsive values, making it easy to
-create responsive designs.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/container.props.ts b/docs-ui/src/content/container.props.ts
deleted file mode 100644
index a4d23a8495..0000000000
--- a/docs-ui/src/content/container.props.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- gapPropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const containerPropDefs: Record = {
- ...gapPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const containerUsageSnippet = `import { Container } from "@backstage/ui";
-
-Hello World! `;
-
-export const containerDefaultSnippet = `
-
- `;
-
-export const containerSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const containerResponsiveSnippet = `
- Hello World
- Hello World
- Hello World
- `;
diff --git a/docs-ui/src/content/dialog.props.ts b/docs-ui/src/content/dialog.props.ts
deleted file mode 100644
index 162ec307dc..0000000000
--- a/docs-ui/src/content/dialog.props.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const dialogTriggerPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- isOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (controlled).',
- },
- defaultOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (uncontrolled).',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- description:
- "Handler that is called when the overlay's open state changes.",
- },
-};
-
-export const dialogPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- isOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (controlled).',
- },
- defaultOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (uncontrolled).',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- description:
- "Handler that is called when the overlay's open state changes.",
- },
- width: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- height: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogHeaderPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogBodyPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- height: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogFooterPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogClosePropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary', 'tertiary'],
- default: 'secondary',
- responsive: false,
- },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogUsageSnippet = `import {
- Dialog,
- DialogTrigger,
- DialogHeader,
- DialogBody,
- DialogFooter,
-} from '@backstage/ui';
-
-
- Open Dialog
-
- Title
- Content
-
- Close
-
-
- `;
-
-export const dialogDefaultSnippet = `
- Open Dialog
-
- Example Dialog
-
- This is a basic dialog example.
-
-
- Close
- Save
-
-
- `;
-
-export const dialogFixedWidthAndHeightSnippet = `
- Scrollable Dialog
-
- Long Content Dialog
-
- ...
-
-
- Cancel
- Accept
-
-
- `;
-
-export const dialogWithFormSnippet = `
- Create User
-
- Create New User
-
-
-
-
-
- Admin
- User
- Viewer
-
-
-
-
- Cancel
- Create User
-
-
- `;
-
-export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
-
-
- Create New User
-
- Your content
-
-
- Cancel
- Create User
-
- `;
-
-export const dialogCloseSnippet = `Close `;
diff --git a/docs-ui/src/content/flex.mdx b/docs-ui/src/content/flex.mdx
deleted file mode 100644
index 36284f1f0b..0000000000
--- a/docs-ui/src/content/flex.mdx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { FlexSnippet } from '@/snippets/stories-snippets';
-import {
- flexPropDefs,
- flexUsageSnippet,
- flexDefaultSnippet,
- flexFAQ1Snippet,
- flexSimpleSnippet,
- flexResponsiveSnippet,
- flexAlignSnippet,
-} from './flex.props';
-import { spacingPropDefs } from '@/utils/propDefs';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { FlexDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={flexDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-The grid component also accepts all the spacing props from the Box component.
-
-
-
-## Examples
-
-### Simple
-
-A simple example of how to use the Flex component.
-
-
-
-### Responsive
-
-The Flex component also supports responsive values, making it easy to create
-responsive designs.
-
-
-
-### Align
-
-The Flex component also supports responsive alignment, making it easy to
-create responsive designs.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/flex.props.ts b/docs-ui/src/content/flex.props.ts
deleted file mode 100644
index cc4c2d5a75..0000000000
--- a/docs-ui/src/content/flex.props.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const flexPropDefs: Record = {
- align: {
- type: 'enum',
- values: ['start', 'center', 'end', 'baseline', 'stretch'],
- responsive: true,
- },
- direction: {
- type: 'enum',
- values: ['row', 'column', 'row-reverse', 'column-reverse'],
- responsive: true,
- },
- justify: {
- type: 'enum',
- values: ['start', 'center', 'end', 'between'],
- responsive: true,
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
-
- `;
-
-export const flexDefaultSnippet = `
-
-
-
- `;
-
-export const flexSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const flexResponsiveSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const flexAlignSnippet = `
- Hello World
- Hello World
- Hello World
- `;
diff --git a/docs-ui/src/content/grid.mdx b/docs-ui/src/content/grid.mdx
deleted file mode 100644
index 5f179b9e14..0000000000
--- a/docs-ui/src/content/grid.mdx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { GridSnippet } from '@/snippets/stories-snippets';
-import { spacingPropDefs } from '@/utils/propDefs';
-import {
- gridPropDefs,
- gridItemPropDefs,
- gridUsageSnippet,
- gridDefaultSnippet,
- gridSimpleSnippet,
- gridComplexSnippet,
- gridMixingRowsSnippet,
- gridResponsiveSnippet,
- gridStartEndSnippet,
-} from './grid.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { GridDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={gridDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Grid.Root
-
-This is the grid container component. It will help to define the number of
-columns that will be used in the grid. You can also define the gap between the
-columns. All values are responsive.
-
-
-
-The grid component also accepts all the spacing props from the Box component.
-
-
-
-### Grid.Item
-
-If you need more control over the columns, you can use the grid item
-component. This will give you access to `rowSpan`, `colSpan`, `start` and
-`end`. All values are responsive. This component is optional, you can use any
-elements directly if you prefer.
-
-
-
-## Examples
-
-### Simple grid
-
-A simple grid with 3 columns and a gap of md.
-
-
-
-### Complex grid
-
-You can also use the grid item to create more complex layouts. In this example
-the first column will span 1 column and the second column will span 2 columns.
-
-
-
-### Mixing rows and columns
-
-The grid item component also supports the `rowSpan` prop, which allows you to
-span multiple rows within the grid layout. In this example, the first item
-will span 2 rows to achieve a dynamic and flexible grid structure.
-
-
-
-### Responsive grid
-
-The grid component also supports responsive values, making it easy to create
-responsive designs.
-
-
-
-### Start and End
-
-The start and end props can be used to position the item in the grid.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/grid.props.ts b/docs-ui/src/content/grid.props.ts
deleted file mode 100644
index 6f037cd64f..0000000000
--- a/docs-ui/src/content/grid.props.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-const columnsValues = [
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
-];
-
-export const gridPropDefs: Record = {
- columns: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- default: 'auto',
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const gridItemPropDefs: Record = {
- colSpan: {
- type: 'enum | string',
- values: [...columnsValues, 'full'],
- responsive: true,
- },
- rowSpan: {
- type: 'enum | string',
- values: [...columnsValues, 'full'],
- responsive: true,
- },
- colStart: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- },
- colEnd: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
-
- `;
-
-export const gridDefaultSnippet = `
-
-
-
- `;
-
-export const gridSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const gridComplexSnippet = `
-
- Hello World
-
-
- Hello World
-
- `;
-
-export const gridMixingRowsSnippet = `
-
- Hello World
-
-
- Hello World
-
-
- Hello World
-
- `;
-
-export const gridResponsiveSnippet = `
-
-
-
- Hello World
-
- `;
-
-export const gridStartEndSnippet = `
-
- Hello World
-
- `;
diff --git a/docs-ui/src/content/header-page.mdx b/docs-ui/src/content/header-page.mdx
deleted file mode 100644
index 12ef2c32ee..0000000000
--- a/docs-ui/src/content/header-page.mdx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { HeaderPageSnippet } from '@/snippets/stories-snippets';
-import {
- propDefs,
- usage,
- simple,
- defaultSnippet,
- withTabs,
- withBreadcrumbs,
- withCustomActions,
- withMenuItems,
-} from './header-page.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { HeaderPageDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={defaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### With Breadcrumbs
-
-You can add breadcrumbs to the header page to help users navigate to the previous page. The `breadcrumbs`
-prop is an array of objects with a `label` and `href` property. By default we truncate the breadcrumb label to 240px.
-
- }
- code={withBreadcrumbs}
-/>
-
-### With Tabs
-
-You can add tabs to the header page to help users navigate to the different sections of the page. The `tabs`
-prop is an array of objects with a `label` and `href` property.
-
- }
- code={withTabs}
-/>
-
-### With Custom Actions
-
-You can add custom actions to the header page to help users navigate to the different sections of the page. The `customActions`
-prop is a React node.
-
- }
- code={withCustomActions}
-/>
-
-### With Menu Items
-
-You can add menu items to the header page to help users navigate to the different sections of the page. The `menuItems`
-prop is an array of objects with a `label`, `value` and `onClick` property.
-
- }
- code={withMenuItems}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/header-page.props.ts b/docs-ui/src/content/header-page.props.ts
deleted file mode 100644
index fca9497b58..0000000000
--- a/docs-ui/src/content/header-page.props.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const propDefs: Record = {
- title: {
- type: 'string',
- default: 'Your plugin',
- },
- customActions: {
- type: 'enum',
- values: ['ReactNode'],
- },
- menuItems: {
- type: 'complex',
- complexType: {
- name: 'MenuItem[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the menu item',
- },
- value: {
- type: 'string',
- required: true,
- description: 'Unique value for the menu item',
- },
- onClick: {
- type: '() => void',
- required: false,
- description: 'Callback function when menu item is clicked',
- },
- },
- },
- },
- tabs: {
- type: 'complex',
- complexType: {
- name: 'HeaderTab[]',
- properties: {
- id: {
- type: 'string',
- required: true,
- description: 'Unique identifier for the tab',
- },
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the tab',
- },
- href: {
- type: 'string',
- required: false,
- description: 'URL to navigate to when tab is clicked',
- },
- matchStrategy: {
- type: "'exact' | 'prefix'",
- required: false,
- description: 'How to match the current route to highlight the tab',
- },
- },
- },
- },
- breadcrumbs: {
- type: 'complex',
- complexType: {
- name: 'Breadcrumb[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the breadcrumb',
- },
- href: {
- type: 'string',
- required: true,
- description: 'URL for the breadcrumb link',
- },
- },
- },
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { HeaderPage } from '@backstage/ui';
-
- `;
-
-export const defaultSnippet = `Custom action}
-/>`;
-
-export const withBreadcrumbs = ` `;
-
-export const withTabs = ` `;
-
-export const withCustomActions = `Custom action}
-/>`;
-
-export const withMenuItems = ` {} },
- { label: 'Invite new members', value: 'invite-new-members', onClick: () => {} },
- ]}
-/>`;
diff --git a/docs-ui/src/content/header.mdx b/docs-ui/src/content/header.mdx
deleted file mode 100644
index eb505d7a62..0000000000
--- a/docs-ui/src/content/header.mdx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { HeaderSnippet } from '@/snippets/stories-snippets';
-import {
- propDefs,
- usage,
- simple,
- defaultSnippet,
- withTabs,
- withBreadcrumbs,
- withHeaderPage,
-} from './header.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { HeaderDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={defaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Simple header
-
-A simple example of how to use the Header component.
-
- }
- code={simple}
- open
-/>
-
-### Header with tabs
-
-A simple example of how to use the Header component with tabs. All links are using React Router
-under the hood and will be active when you are on the corresponding page.
-
- }
-code={withTabs}
-open
-/>
-
-### Header with breadcrumbs
-
-Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
-
- }
- code={withBreadcrumbs}
- open
-/>
-
-### Header with HeaderPage
-
-You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation.
-
- }
- code={withHeaderPage}
- open
-/>
-
-
-
-
diff --git a/docs-ui/src/content/header.props.ts b/docs-ui/src/content/header.props.ts
deleted file mode 100644
index af9a745fc5..0000000000
--- a/docs-ui/src/content/header.props.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const propDefs: Record = {
- icon: {
- type: 'enum',
- values: ['ReactNode'],
- },
- title: {
- type: 'string',
- default: 'Your plugin',
- },
- titleLink: {
- type: 'string',
- default: '/',
- },
- customActions: {
- type: 'enum',
- values: ['ReactNode'],
- },
- menuItems: {
- type: 'complex',
- complexType: {
- name: 'MenuItem[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the menu item',
- },
- value: {
- type: 'string',
- required: true,
- description: 'Unique value for the menu item',
- },
- onClick: {
- type: '() => void',
- required: false,
- description: 'Callback function when menu item is clicked',
- },
- },
- },
- },
- tabs: {
- type: 'complex',
- complexType: {
- name: 'HeaderTab[]',
- properties: {
- id: {
- type: 'string',
- required: true,
- description: 'Unique identifier for the tab',
- },
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the tab',
- },
- href: {
- type: 'string',
- required: false,
- description: 'URL to navigate to when tab is clicked',
- },
- matchStrategy: {
- type: "'exact' | 'prefix'",
- required: false,
- description: 'How to match the current route to highlight the tab',
- },
- },
- },
- },
- onTabSelectionChange: {
- type: 'enum',
- values: ['(key: string) => void'],
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { Header } from '@backstage/ui';
-
-`;
-
-export const defaultSnippet = `
- } />
- } />
- } />
- >
- }
-/>`;
-
-export const simple = `
- } />
- } />
- } />
- >
- }
-/>`;
-
-export const withTabs = `
-
- } />
- } />
- } />
- >
- }
- tabs={[
- { id: 'overview', label: 'Overview' },
- { id: 'checks', label: 'Checks' },
- { id: 'tracks', label: 'Tracks' },
- { id: 'campaigns', label: 'Campaigns' },
- { id: 'integrations', label: 'Integrations' },
- ]}
-/>
-`;
-
-export const withBreadcrumbs = ``;
-
-export const withHeaderPage = `
-Custom action}
-/>`;
diff --git a/docs-ui/src/content/link.mdx b/docs-ui/src/content/link.mdx
deleted file mode 100644
index 486c06b298..0000000000
--- a/docs-ui/src/content/link.mdx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import {
- MenuSnippet,
- ButtonSnippet,
- LinkSnippet,
-} from '@/snippets/stories-snippets';
-import {
- linkPropDefs,
- linkUsageSnippet,
- linkDefaultSnippet,
- linkVariantsSnippet,
- linkWeightsSnippet,
- linkColorsSnippet,
- linkRouterSnippet,
- linkTruncateSnippet,
-} from './link.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { LinkDefinition } from '../utils/definitions';
-
-
-
- }
- code={linkDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Router Integration
-
-The `Link` component handles both internal and external navigation. It automatically detects whether the provided URL is internal (relative path) or external (absolute URL with protocol) and renders the appropriate element:
-
-- **Internal routes**: Uses `react-router-dom`'s `Link` component for client-side navigation
-- **External URLs**: Renders a standard `` element for traditional navigation
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when links have different variants.
-
- }
- code={linkVariantsSnippet}
-/>
-
-### Weights
-
-Here's a view when links have different weights.
-
- }
- code={linkWeightsSnippet}
-/>
-
-### Colors
-
-Here's a view when links have different colors.
-
- }
- code={linkColorsSnippet}
-/>
-
-### Truncate
-
-The `Link` component has a `truncate` prop that can be used to truncate the text.
-
- }
- code={linkTruncateSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/link.props.ts b/docs-ui/src/content/link.props.ts
deleted file mode 100644
index 748e64cc15..0000000000
--- a/docs-ui/src/content/link.props.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const linkPropDefs: Record = {
- href: {
- type: 'string',
- },
- variant: {
- type: 'enum',
- values: [
- 'title-large',
- 'title-medium',
- 'title-small',
- 'title-x-small',
- 'body-large',
- 'body-medium',
- 'body-small',
- 'body-x-small',
- ],
- default: 'body-medium',
- responsive: true,
- },
- weight: {
- type: 'enum',
- values: ['regular', 'bold'],
- default: 'regular',
- responsive: true,
- },
- color: {
- type: 'enum',
- values: ['primary', 'secondary', 'danger', 'warning', 'success'],
- default: 'primary',
- responsive: true,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const linkUsageSnippet = `import { Link } from '@backstage/ui';
-
- Sign up for Backstage`;
-
-export const linkDefaultSnippet = ` Sign up for Backstage`;
-
-export const linkVariantsSnippet = `
- ...
- ...
- ...
- ...
- ...
- ...
- ...
- ...
- `;
-
-export const linkWeightsSnippet = `
-
-
- `;
-
-export const linkColorsSnippet = `
- I am primary
- I am secondary
- I am danger
- I am warning
- I am success
- `;
-
-export const linkRouterSnippet = `import { Link } from '@backstage/ui';
-
-// Internal route
- Home
-
-// External URL
- Backstage
-`;
-
-export const linkTruncateSnippet = ` ...`;
diff --git a/docs-ui/src/content/menu.mdx b/docs-ui/src/content/menu.mdx
deleted file mode 100644
index 9ede0885e2..0000000000
--- a/docs-ui/src/content/menu.mdx
+++ /dev/null
@@ -1,237 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { MenuSnippet } from '@/snippets/stories-snippets';
-import {
- usage,
- preview,
- submenu,
- icons,
- sections,
- separators,
- links,
- autocomplete,
- autocompleteListbox,
- autocompleteListboxMultiple,
- menuTriggerPropDefs,
- submenuTriggerPropDefs,
- menuPropDefs,
- menuListBoxPropDefs,
- menuAutocompletePropDefs,
- menuAutocompleteListboxPropDefs,
- menuItemPropDefs,
- menuListBoxItemPropDefs,
- menuSectionPropDefs,
- menuSeparatorPropDefs,
-} from './menu.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { MenuDefinition } from '../utils/definitions';
-
-
-
- }
- code={preview}
-/>
-
-## Usage
-
-
-
-### Triggers
-
-- `MenuTrigger` is a wrapper component that combines a button or other trigger element with a menu displayed in a popover.
-- `SubmenuTrigger` is a wrapper component that combines a `MenuItem` with a menu displayed in a popover.
-
-### Containers
-
-- `Menu` is a container component that contains a list of menu items or sections.
-- `MenuListBox` is a container component that contains a list of menu items or sections.
-- `MenuAutocomplete` is a container component that contains a list of menu items or sections.
-- `MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
-
-### Items
-
-- `MenuItem` is an individual interactive item in the menu.
-- `MenuListBoxItem` is an individual interactive item in the menu list box.
-
-### Separators
-
-- `MenuSeparator` is a component that renders a horizontal line to separate menu items.
-- `MenuSection` is a component that renders a section in the menu.
-
-## API reference
-
-### MenuTrigger
-
-`MenuTrigger` accepts exactly two children: the first child should be the trigger element, and second child should be
-one of the menu containers containing the menu.
-
-
-
-### SubmenuTrigger
-
-The `SubmenuTrigger` accepts exactly two children: the first child should be the `MenuItem` which triggers opening
-of the submenu, and second child should be one of the menu containers containing the submenu.
-
-
-
-### Menu
-
-`Menu` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuListBox
-
-`MenuListBox` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuAutocomplete
-
-`MenuAutocomplete` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuAutocompleteListbox
-
-`MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuItem
-
-`MenuItem` is an individual interactive item in the menu.
-
-
-
-### MenuListBoxItem
-
-`MenuListBoxItem` is an individual interactive item in the menu list box.
-
-
-
-### MenuSection
-
-`MenuSection` is a component that renders a section in the menu.
-
-
-
-### MenuSeparator
-
-`MenuSeparator` is a component that renders a horizontal line to separate menu items.
-
-
-
-## Examples
-
-### Nested navigation
-
-You can nest menus to create a more complex navigation structure. It is important to use the `placement` prop to ensure
-the submenu is displayed in the correct position. The best practice is to use the `right top` placement for the submenu.
-
- }
- code={submenu}
-/>
-
-### With icons
-
-You can use the `iconStart` prop to add an icon to the menu item.
-
- }
- code={icons}
-/>
-
-### With links
-
-You can use the `href` prop to add a link to the menu item. This is using our router provider under the hood
-to work for both internal and external links.
-
- }
- code={links}
-/>
-
-### With sections
-
-You can use the `MenuSection` component to add a section to the menu.
-
- }
- code={sections}
-/>
-
-### With separators
-
-You can use the `MenuSeparator` component to add a separator to the menu.
-
- }
- code={separators}
-/>
-
-### With autocomplete
-
-You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
-
- }
- code={autocomplete}
-/>
-
-### With list box
-
-You can use the `MenuListBox` component to add a list box to the menu.
-
- }
- code={autocompleteListbox}
-/>
-
-### With list box with multiple selection
-
-You can use the `MenuListBox` component to add a list box to the menu. You can also use the `selectionMode` prop to
-allow multiple selection.
-
- }
- code={autocompleteListboxMultiple}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/menu.props.ts b/docs-ui/src/content/menu.props.ts
deleted file mode 100644
index 2c7d76dd5d..0000000000
--- a/docs-ui/src/content/menu.props.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-const placementValues = [
- 'bottom',
- 'bottom left',
- 'bottom right',
- 'bottom start',
- 'bottom end',
- 'top',
- 'top left',
- 'top right',
- 'top start',
- 'top end',
- 'left',
- 'left top',
- 'left bottom',
- 'start',
- 'start top',
- 'start bottom',
- 'right',
- 'right top',
- 'right bottom',
- 'end',
- 'end top',
- 'end bottom',
-];
-
-export const menuTriggerPropDefs: Record = {
- isOpen: {
- type: 'boolean',
- },
- defaultOpen: {
- type: 'boolean',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- },
-};
-
-export const submenuTriggerPropDefs: Record = {
- delay: {
- type: 'number',
- default: '200',
- },
-};
-
-export const menuPropDefs: Record = {
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- },
- selectionMode: {
- type: 'enum',
- values: ['none', 'single', 'multiple'],
- },
- selectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- defaultSelectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuListBoxPropDefs: Record = {
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- },
- selectionMode: {
- type: 'enum',
- values: ['none', 'single', 'multiple'],
- },
- selectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- defaultSelectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuAutocompletePropDefs: Record = {
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuAutocompleteListboxPropDefs: Record = {
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuItemPropDefs: Record = {
- id: {
- type: 'enum',
- values: ['Key'],
- },
- value: {
- type: 'string',
- },
- textValue: {
- type: 'string',
- },
- isDisabled: {
- type: 'boolean',
- },
- href: {
- type: 'string',
- },
- onAction: {
- type: 'enum',
- values: ['(event) => void'],
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuListBoxItemPropDefs: Record = {
- id: {
- type: 'enum',
- values: ['Key'],
- },
- value: {
- type: 'string',
- },
- textValue: {
- type: 'string',
- },
- isDisabled: {
- type: 'boolean',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuSectionPropDefs: Record = {
- title: {
- type: 'string',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuSeparatorPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
-
-
- Menu
-
- Apple
- Banana
- Blueberry
-
-
- Vegetables
-
- Carrot
- Tomato
- Potato
-
-
-
- `;
-
-export const preview = `
- Menu
-
- {options.map(option => (
- {option.label}
- ))}
-
- `;
-
-export const submenu = `
- Menu
-
- Edit
- Duplicate
-
- Submenu
-
- Edit
- Duplicate
- Rename
-
- Share
- Move
-
- }>Feedback
-
-
-
- `;
-
-export const icons = `
- Menu
-
- }>Copy
- }>Rename
- }>Send feedback
-
- `;
-
-export const sections = `
- Menu
-
-
- }>Profile
- }>Settings
-
-
- }>Help Center
- }>
- Contact Support
-
- }>Feedback
-
-
- `;
-
-export const separators = `
- Menu
-
- Edit
- Duplicate
- Rename
-
- Share
- Move
-
- }>Feedback
-
- `;
-
-export const links = `
- Menu
-
- Internal link
-
- External link
-
- Email link
-
- `;
-
-export const autocomplete = `
- Menu
-
- Create new file...
- Create new folder...
- Assign to...
- Assign to me
- Change status...
- Change priority...
- Add label...
- Remove label...
-
- `;
-
-export const autocompleteListbox = `const [selected, setSelected] = useState(
- new Set(['blueberry']),
-);
-
-
- Selected: {Array.from(selected).join(', ')}
-
- Menu
-
- {options.map(option => (
-
- {option.label}
-
- ))}
-
-
- `;
-
-export const autocompleteListboxMultiple = `const [selected, setSelected] = useState(
- new Set(['blueberry', 'cherry']),
-);
-
-
- Selected: {Array.from(selected).join(', ')}
-
- Menu
-
- {options.map(option => (
-
- {option.label}
-
- ))}
-
-
- `;
diff --git a/docs-ui/src/content/password-field.mdx b/docs-ui/src/content/password-field.mdx
deleted file mode 100644
index 8dda2b4f2a..0000000000
--- a/docs-ui/src/content/password-field.mdx
+++ /dev/null
@@ -1,65 +0,0 @@
-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';
-import { PasswordFieldDefinition } from '../utils/definitions';
-
-
-
- }
- 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/password-field.props.ts b/docs-ui/src/content/password-field.props.ts
deleted file mode 100644
index 493405b5fd..0000000000
--- a/docs-ui/src/content/password-field.props.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-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/radio-group.mdx b/docs-ui/src/content/radio-group.mdx
deleted file mode 100644
index ba9352c026..0000000000
--- a/docs-ui/src/content/radio-group.mdx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { RadioGroupSnippet } from '@/snippets/stories-snippets';
-import {
- radioGroupPropDefs,
- radioGroupUsageSnippet,
- radioGroupDefaultSnippet,
- radioGroupDescriptionSnippet,
- radioGroupHorizontalSnippet,
- radioGroupDisabledSnippet,
- radioGroupDisabledSingleSnippet,
- radioGroupValidationSnippet,
- radioGroupReadOnlySnippet,
-} from './radio-group.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { CodeBlock } from '@/components/CodeBlock';
-import { RadioGroupDefinition } from '../utils/definitions';
-
-
-
- }
- code={radioGroupDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Horizontal
-
-Here's a simple TextField with a description.
-
- }
- code={radioGroupHorizontalSnippet}
-/>
-
-### Disabled
-
-You can disable the entire radio group by adding the `isDisabled` prop to the `RadioGroup` component.
-
- }
- code={radioGroupDisabledSnippet}
-/>
-
-### Disabled Single radio
-
-You can disable a single radio by adding the `isDisabled` prop to the `Radio` component.
-
- }
- code={radioGroupDisabledSingleSnippet}
-/>
-
-### Validation
-
-Here's an example of a radio group with errors.
-
- }
- code={radioGroupValidationSnippet}
-/>
-
-### Read only
-
-You can make the radio group read only by adding the `isReadOnly` prop to the `RadioGroup` component.
-
- }
- code={radioGroupReadOnlySnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/radio-group.props.ts b/docs-ui/src/content/radio-group.props.ts
deleted file mode 100644
index 3ed08f1053..0000000000
--- a/docs-ui/src/content/radio-group.props.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const radioGroupPropDefs: 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 radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/ui';
-
- `;
-
-export const radioGroupDefaultSnippet = ` `;
-
-export const radioGroupDescriptionSnippet = ` `;
-
-export const radioGroupHorizontalSnippet = ` `;
-
-export const radioGroupDisabledSnippet = `
- Bulbasaur
- Charmander
- Squirtle
- `;
-
-export const radioGroupDisabledSingleSnippet = `
- Bulbasaur
- Charmander
- Squirtle
- `;
-
-export const radioGroupValidationSnippet = ` (value === \'charmander\' ? \'Nice try!\' : null)>
- Bulbasaur
- Charmander
- Squirtle
- `;
-
-export const radioGroupReadOnlySnippet = `
- Bulbasaur
- Charmander
- Squirtle
- `;
diff --git a/docs-ui/src/content/search-field.mdx b/docs-ui/src/content/search-field.mdx
deleted file mode 100644
index 6b33feea42..0000000000
--- a/docs-ui/src/content/search-field.mdx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { SearchFieldSnippet } from '@/snippets/stories-snippets';
-import {
- searchFieldPropDefs,
- searchFieldUsageSnippet,
- searchFieldDefaultSnippet,
- searchFieldSizesSnippet,
- searchFieldDescriptionSnippet,
- searchFieldCollapsibleSnippet,
-} from './search-field.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { CodeBlock } from '@/components/CodeBlock';
-import { SearchFieldDefinition } from '../utils/definitions';
-
-
-
- }
- code={searchFieldDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Sizes
-
-We support two different sizes: `small`, `medium`.
-
- }
- code={searchFieldSizesSnippet}
-/>
-
-### With description
-
-Here's a simple SearchField with a description.
-
- }
- code={searchFieldDescriptionSnippet}
-/>
-
-### Collapsible
-
-You can make the SearchField collapsible by setting the `startCollapsed` prop to `true`.
-
- }
- code={searchFieldCollapsibleSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/search-field.props.ts b/docs-ui/src/content/search-field.props.ts
deleted file mode 100644
index b11ae3931c..0000000000
--- a/docs-ui/src/content/search-field.props.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const searchFieldPropDefs: 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,
- },
- startCollapsed: {
- type: 'boolean',
- default: 'false',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui';
-
- `;
-
-export const searchFieldDefaultSnippet = ` `;
-
-export const searchFieldSizesSnippet = `
- } />
- } />
- `;
-
-export const searchFieldDescriptionSnippet = ` `;
-
-export const searchFieldCollapsibleSnippet = ` `;
diff --git a/docs-ui/src/content/select.mdx b/docs-ui/src/content/select.mdx
deleted file mode 100644
index 436129d749..0000000000
--- a/docs-ui/src/content/select.mdx
+++ /dev/null
@@ -1,138 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { SelectSnippet } from '@/snippets/stories-snippets';
-import {
- selectPropDefs,
- selectUsageSnippet,
- selectDefaultSnippet,
- selectDescriptionSnippet,
- selectSizesSnippet,
- selectDisabledSnippet,
- selectResponsiveSnippet,
- selectIconSnippet,
- selectSearchableSnippet,
- selectMultipleSnippet,
- selectSearchableMultipleSnippet,
-} from './select.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { SelectDefinition } from '../utils/definitions';
-
-
-
- }
- code={selectDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### With Label and description
-
-Select component with label and description.
-
- }
- code={selectDescriptionSnippet}
-/>
-
-### Sizes
-
-Here's a view when the selects have different sizes.
-
- }
- code={selectSizesSnippet}
-/>
-
-### With Icon
-
-Here's a view when the select has an icon.
-
- }
-code={selectIconSnippet}
-/>
-
-### Disabled
-
-Here's a view when the select is disabled.
-
- }
- code={selectDisabledSnippet}
-/>
-
-### Searchable
-
-Here's a view when the select has searchable filtering.
-
- }
- code={selectSearchableSnippet}
-/>
-
-### Multiple Selection
-
-Here's a view when the select allows multiple selections.
-
- }
- code={selectMultipleSnippet}
-/>
-
-### Searchable with Multiple Selection
-
-Here's a view when the select combines search and multiple selection.
-
- }
- code={selectSearchableMultipleSnippet}
-/>
-
-### Responsive
-
-Here's a view when the select is responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/skeleton.props.ts b/docs-ui/src/content/skeleton.props.ts
deleted file mode 100644
index ab79268d15..0000000000
--- a/docs-ui/src/content/skeleton.props.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const skeletonPropDefs: Record = {
- width: {
- type: 'number',
- default: '80',
- responsive: false,
- },
- height: {
- type: 'number',
- default: '24',
- responsive: false,
- },
- rounded: {
- type: 'boolean',
- default: 'false',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const skeletonUsageSnippet = `import { Flex, Skeleton } from '@backstage/ui';
-
-
-
-
-
- `;
-
-export const skeletonDefaultSnippet = `
-
-
-
- `;
-
-export const skeletonDemo1Snippet = `
-
-
-
-
-
-
-
-
-
-
- `;
-
-export const skeletonDemo2Snippet = `
-
-
-
- `;
diff --git a/docs-ui/src/content/switch.mdx b/docs-ui/src/content/switch.mdx
deleted file mode 100644
index 965cc7832b..0000000000
--- a/docs-ui/src/content/switch.mdx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { SwitchSnippet } from '@/snippets/stories-snippets';
-import { switchPropDefs, snippetUsage } from './switch.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { SwitchDefinition } from '../utils/definitions';
-
-
-
- }
- code={` `}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Disabled
-
-A switch can be disabled using the `isDisabled` prop.
-
- }
- code={` `}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/switch.props.ts b/docs-ui/src/content/switch.props.ts
deleted file mode 100644
index 5576436102..0000000000
--- a/docs-ui/src/content/switch.props.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const switchPropDefs: Record = {
- autoFocus: {
- type: 'boolean',
- },
- defaultSelected: {
- type: 'boolean',
- },
- ...classNamePropDefs,
- isDisabled: {
- type: 'boolean',
- },
- isReadOnly: {
- type: 'boolean',
- },
- isSelected: {
- type: 'boolean',
- },
- label: {
- type: 'string',
- },
- name: {
- type: 'string',
- },
- onChange: {
- type: 'enum',
- values: ['(isSelected: boolean) => void'],
- },
- onFocus: {
- type: 'enum',
- values: ['(e: FocusEvent) => void'],
- },
- onBlur: {
- type: 'enum',
- values: ['(e: FocusEvent) => void'],
- },
- onFocusChange: {
- type: 'enum',
- values: ['(isFocused: boolean) => void'],
- },
- onKeyDown: {
- type: 'enum',
- values: ['(e: KeyboardEvent) => void'],
- },
- onKeyUp: {
- type: 'enum',
- values: ['(e: KeyboardEvent) => void'],
- },
- onHoverStart: {
- type: 'enum',
- values: ['(e: HoverEvent) => void'],
- },
- onHoverEnd: {
- type: 'enum',
- values: ['(e: HoverEvent) => void'],
- },
- onHoverChange: {
- type: 'enum',
- values: ['(isHovered: boolean) => void'],
- },
- ...stylePropDefs,
- value: {
- type: 'string',
- },
-};
-
-export const snippetUsage = `import { Switch } from '@backstage/ui';
-
- `;
diff --git a/docs-ui/src/content/table.mdx b/docs-ui/src/content/table.mdx
deleted file mode 100644
index 00b7290532..0000000000
--- a/docs-ui/src/content/table.mdx
+++ /dev/null
@@ -1,165 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { TableSnippet } from '@/snippets/stories-snippets';
-import {
- tablePropDefs,
- tableHeaderPropDefs,
- tableBodyPropDefs,
- tablePaginationPropDefs,
- tableUsageSnippet,
- tableBasicSnippet,
- tableRowClickSnippet,
- tableHybridSnippet,
- tableCellInteractionsSnippet,
- tablePaginationSnippet,
- tableSelectionActionsSnippet,
- tableSelectionModeSnippet,
- tableSelectionBehaviorSnippet,
- tableSortingSnippet,
- columnPropDefs,
- rowPropDefs,
- cellPropDefs,
-} from './table.props';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { TableDefinition } from '../utils/definitions';
-
-
-
- }
- code={tableBasicSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Table
-
-The main table component that renders data in a structured grid format.
-
-
-
-### TableHeader
-
-The header section of the table that contains the column definitions.
-
-
-
-### Column
-
-A column definition that describes how a column should be rendered.
-
-
-
-### TableBody
-
-The body section of the table that contains the rows.
-
-
-
-### Row
-
-A row definition that describes how a row should be rendered.
-
-
-
-### Cell
-
-A cell definition that describes how a cell should be rendered.
-
-
-
-### TablePagination
-
-A pagination component designed to work with the Table component.
-
-
-
-## Examples
-
-### Basic Table
-
-Coming soon.
-
-### Row selection
-
-Tables support row selection with two configuration options: selection mode and selection behavior.
-
-#### Selection mode
-
-Use `selectionMode` to control how many rows can be selected. With `single`, only one row can be selected at a time. With `multiple`, any number of rows can be selected, and a header checkbox provides select-all functionality.
-
- }
- code={tableSelectionModeSnippet}
-/>
-
-#### Selection behavior
-
-Use `selectionBehavior` to control how selection is indicated and interacted with. With `toggle`, checkboxes appear for selection. With `replace`, selection is indicated by row background color—click to select, Cmd/Ctrl+click for multiple.
-
- }
- code={tableSelectionBehaviorSnippet}
-/>
-
-#### With row actions
-
-With toggle behavior, clicking a row triggers its action when nothing is selected. Once any row is selected, clicking toggles selection instead.
-
-With replace behavior, clicking selects the row and double-clicking triggers the action.
-
- }
- code={tableSelectionActionsSnippet}
-/>
-
-### Row Clicks
-
-Coming soon.
-
-### Pagination
-
-Coming soon.
-
-### Sorting
-
-Coming soon.
-
-### Asynchronous loading
-
-Coming soon.
-
-### Empty state
-
-Coming soon.
-
-### Column resizing
-
-This feature is not available yet — let us know if you'd like us to explore it!
-
-### Column reordering
-
-This feature is not available yet — let us know if you'd like us to explore it!
-
-### Column pinning
-
-This feature is not available yet — let us know if you'd like us to explore it!
-
-### Column visibility
-
-This feature is not available yet — let us know if you'd like us to explore it!
-
-
-
-
diff --git a/docs-ui/src/content/table.props.ts b/docs-ui/src/content/table.props.ts
deleted file mode 100644
index 21bd5597c6..0000000000
--- a/docs-ui/src/content/table.props.ts
+++ /dev/null
@@ -1,377 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const tablePropDefs: Record = {
- selectionBehavior: {
- type: 'enum',
- values: ['toggle', 'replace'],
- default: 'toggle',
- description: 'How multiple selection should behave in the collection.',
- },
- disabledBehavior: {
- type: 'enum',
- values: ['selection', 'all'],
- default: 'selection',
- description:
- 'Whether disabledKeys applies to all interactions, or only selection.',
- },
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- description: 'A list of row keys to disable.',
- },
- selectionMode: {
- type: 'enum',
- values: ['single', 'multiple'],
- description: 'The type of selection that is allowed in the collection.',
- },
- selectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable