diff --git a/.changeset/deep-jobs-rush.md b/.changeset/deep-jobs-rush.md new file mode 100644 index 0000000000..4abb91822d --- /dev/null +++ b/.changeset/deep-jobs-rush.md @@ -0,0 +1,5 @@ +--- +"@cloudoperators/juno-ui-components": patch +--- + +feat(ui): add `Status` component diff --git a/docs/ux/datagrid.md b/docs/ux/datagrid.md index e7b12a38b4..eb0021d225 100644 --- a/docs/ux/datagrid.md +++ b/docs/ux/datagrid.md @@ -104,6 +104,10 @@ A DataGrid must handle the full range of data states gracefully. Do not render a - **Empty (filtered):** Distinguish between "no data exists" and "no data matches your filters" — these suggest different user actions - **Error:** Show an error state scoped to the DataGrid with a clear explanation and a retry option where possible. If present, refer users to the Reload/Refresh button described below. +Use the [`Status`](https://cloudoperators.github.io/juno/?path=/docs/components-status--docs) component for all of these states. Place it inside a `DataGridRow` and `DataGridCell` spanning all columns. `Status` automatically adapts its styling to the DataGrid context. See Storybook for usage examples. + +![A DataGrid with a generic error rendered using the `Status` compponent](images/status-generic-error-datagrid.png) + ## Re-Loading Data A DataGrid may have a Reload/Refresh-button in the Header in order to make sure the latest state of the data is displayed and interacted with. diff --git a/docs/ux/error-handling-loading-empty-states.md b/docs/ux/error-handling-loading-empty-states.md index 3db5b0d67d..a9dd14a5d4 100644 --- a/docs/ux/error-handling-loading-empty-states.md +++ b/docs/ux/error-handling-loading-empty-states.md @@ -2,6 +2,9 @@ # Error And Empty/Loading/Busy State Handling Strategies +> [!NOTE] +> For all error, loading, empty, and no-matches states that do not have a dedicated local or component-specific solution, use the [`Status`](https://cloudoperators.github.io/juno/?path=/docs/components-status--docs) component. It covers application-, page-, section-, and DataGrid-level states, ships with sensible defaults for all cases described in this document, and is the required default for any situation not handled by a more specific pattern. Full API and usage examples are in Storybook. + # Error State Handling Patterns and rules for handling errors consistently across our applications. @@ -63,10 +66,6 @@ A single component fails to render and/or does not receive the expected API data **See the [HTTP Error Code Reference](#http-error-code-reference) for specific codes, titles, and messages.** -> **TODO:** Identify components prone to such errors, design and implement loading, empty, error states. (A good and most urgent candidate is DataGrid.) - -> **TODO:** Identify and design error and empty states for affected components. - ## Operation/Action/CRUD errors A user has initiated an action, such as creating, updating, or deleting an item or entity, and the operation fails immediately or with some delay. diff --git a/docs/ux/images/status-generic-error-datagrid.png b/docs/ux/images/status-generic-error-datagrid.png new file mode 100644 index 0000000000..86b8edf628 Binary files /dev/null and b/docs/ux/images/status-generic-error-datagrid.png differ diff --git a/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx b/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx index 2d294fd619..da5d2db935 100644 --- a/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx +++ b/packages/ui-components/src/components/DataGrid/DataGrid.component.tsx @@ -46,6 +46,7 @@ const gridTemplate = ( interface DataGridContextType { cellVerticalAlignment?: CellVerticalAlignmentType + isDataGrid?: boolean } const DataGridContext = createContext({}) @@ -72,6 +73,10 @@ export const DataGrid = ({ }: DataGridProps): ReactNode => { const dataGridConf = { cellVerticalAlignment: cellVerticalAlignment, + // allows consumer components like Status to detect the DataGrid context. + // Note: the context always exists as {} by default, so checking for context presence alone is not sufficient — + // isDataGrid: true is the only reliable signal that a DataGrid provider is actually in the tree. + isDataGrid: true, // selectable: selectable } return ( diff --git a/packages/ui-components/src/components/Status/Status.component.tsx b/packages/ui-components/src/components/Status/Status.component.tsx new file mode 100644 index 0000000000..cdad3d12ff --- /dev/null +++ b/packages/ui-components/src/components/Status/Status.component.tsx @@ -0,0 +1,156 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactNode } from "react" +import { Spinner } from "../Spinner" +import { useDataGridContext } from "../DataGrid/DataGrid.component" + +const HTTP_ERRORS: Record = { + 400: { title: "Bad Request", body: "The request could not be processed due to invalid syntax. Try again." }, + 401: { title: "Authentication Required", body: "Authentication failed. Verify your credentials and try again." }, + 403: { title: "Access Denied", body: "You do not have the required permissions to access this resource." }, + 404: { title: "Page Not Found", body: "The requested URL does not exist or may have moved." }, + 408: { + title: "Request Timeout", + body: "The request did not return a complete result in time. Check your connection and try again.", + }, + 409: { title: "Conflict", body: "" }, + 429: { title: "Too Many Requests", body: "" }, + 500: { title: "Internal Server Error", body: "An internal error occurred. Try again." }, + 502: { title: "Bad Gateway", body: "The server returned an invalid response. Try again." }, + 503: { title: "Service Unavailable", body: "The service is temporarily unavailable. Try again later." }, + 504: { title: "Gateway Timeout", body: "A server did not respond in time. Check your connection and try again." }, +} + +const STATUS_DEFAULTS: Record = { + progress: { title: "Loading…", body: "" }, + error: { title: "Something went wrong", body: "An error occurred. Try again." }, + empty: { title: "No items", body: "There are no items to display." }, + "no-matches": { title: "", body: "No items match the current filters. Adjust or clear filters." }, +} + +export interface StatusProps extends React.HTMLAttributes { + /** The status to display. Determines the default copy. Defaults to `"error"`. */ + status?: "progress" | "error" | "empty" | "no-matches" + /** Optional title. Overrides the per-status default title when set. */ + title?: string + /** Optional body text. Overrides the per-status default body text when set. */ + body?: string + /** Renders a `Spinner`. Defaults to `true` when `status="progress"`, `false` otherwise. */ + spinner?: boolean + /** Displayed large and prominently above the title. Intended for HTTP error codes such as 404 or 500. */ + code?: number | string + /** Rendered in a `
` block using monospaced font. Intended for stack traces and server responses. Scrolls vertically if content exceeds the maximum height. */
+  details?: string
+  /** Optional action area rendered below the content. Typically a `Button` or a button-styled anchor element. */
+  action?: ReactNode
+  /** Add custom CSS classes to the root element. */
+  className?: string
+}
+
+const statusStyles = `
+  jn:flex
+  jn:flex-col
+  jn:items-center
+  jn:text-center
+`
+
+const statusDataGridStyles = `
+  jn:min-h-[12.5rem]
+  jn:max-h-[18.1875rem]
+  jn:justify-center
+  jn:my-2
+`
+
+const detailsDataGridStyles = `
+  jn:min-h-0
+  jn:overflow-y-auto
+`
+
+const codeStyles = `
+  jn:text-[12.5rem]
+  jn:font-bold
+  jn:leading-none
+  jn:text-theme-status-code
+`
+
+const titleStyles = `
+  jn:text-lg
+  jn:leading-[1.5]
+  jn:max-w-[50rem]
+`
+
+const bodyStyles = `
+  jn:leading-[1.5]
+  jn:max-w-[50rem]
+`
+
+const detailsStyles = `
+  jn:text-left
+  jn:text-xs
+  jn:bg-theme-status-details
+  jn:text-theme-status-details
+  jn:border
+  jn:border-theme-status-details
+  jn:py-0.5
+  jn:px-1
+  jn:mt-4
+  jn:w-full
+  jn:max-w-[50rem]
+  jn:max-h-[30rem]
+  jn:overflow-x-auto
+  jn:overflow-y-auto
+`
+
+export const Status = ({
+  status = "error",
+  title,
+  body,
+  spinner,
+  code,
+  details,
+  action,
+  className = "",
+  ...props
+}: StatusProps) => {
+  const { isDataGrid } = useDataGridContext()
+
+  const numericCode = typeof code === "string" ? parseInt(code, 10) : code
+  const httpDefaults = numericCode ? HTTP_ERRORS[numericCode] : undefined
+  const statusDefaults = status ? STATUS_DEFAULTS[status] : undefined
+
+  const resolvedTitle = title ?? httpDefaults?.title ?? statusDefaults?.title
+  const resolvedBody = body ?? httpDefaults?.body ?? statusDefaults?.body
+
+  const resolvedSpinner = spinner ?? status === "progress"
+
+  const resolvedTopMargin = isDataGrid ? "" : code != null ? "jn:mt-4" : resolvedSpinner ? "jn:mt-20" : "jn:mt-24"
+
+  const role = status === "error" ? "alert" : "status"
+
+  return (
+    
+ {/* suppress rendering HTTP error codes inside a DataGrid: */} + {/* explicit null check instead of truthiness — 0 is falsy but a valid (if unusual/non-sensical) value, but still… */} + {code != null && !isDataGrid &&
{code}
} + {resolvedSpinner && } + {resolvedTitle && {resolvedTitle}} + {resolvedBody &&
{resolvedBody}
} + {details && ( +
+          {details}
+        
+ )} + {action &&
{action}
} +
+ ) +} diff --git a/packages/ui-components/src/components/Status/Status.stories.tsx b/packages/ui-components/src/components/Status/Status.stories.tsx new file mode 100644 index 0000000000..cbb69e2638 --- /dev/null +++ b/packages/ui-components/src/components/Status/Status.stories.tsx @@ -0,0 +1,345 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import type { Meta, StoryObj } from "@storybook/react-vite" +import { Status } from "./index" +import { Button } from "../Button/Button.component" +import { DataGrid } from "../DataGrid/DataGrid.component" +import { DataGridRow } from "../DataGridRow/DataGridRow.component" +import { DataGridCell } from "../DataGridCell/DataGridCell.component" +import { DataGridHeadCell } from "../DataGridHeadCell/DataGridHeadCell.component" +import { DataGridToolbar } from "../DataGridToolbar/DataGridToolbar.component" +import { SearchInput } from "../SearchInput/SearchInput.component" +import { Stack } from "../Stack/Stack.component" +import { PageHeader } from "../PageHeader/PageHeader.component" + +const meta: Meta = { + title: "Components/Status", + component: Status, + argTypes: { + status: { + control: "select", + options: ["progress", "error", "empty", "no-matches"], + }, + code: { + control: { type: "number" }, + }, + action: { control: false }, + }, + parameters: { + docs: { + description: { + component: + "`Status` is a general-purpose component for communicating non-data states: in progress, error, empty, and no matches. Use it as the default drop-in whenever a component, view, or data container has no local or specific way to handle these states — it covers application-, page-, and section-level states as well as error boundary fallbacks.\n\nWhen used inside a `DataGrid`, wrap `Status` in a `DataGridRow` and `DataGridCell` with the appropriate `colSpan` — `Status` renders a `
` only and has no table markup of its own. Inside a `DataGrid`, `Status` handles its own sizing and positioning automatically.\n\nOutside of a `DataGrid`, `Status` automatically applies a top margin based on what it renders — larger when neither a code nor a spinner is present, smaller for spinner states, minimal when an HTTP error code is shown. These can be overriden using the `className` when needed.", + }, + }, + }, +} + +export default meta +type Story = StoryObj + +const MockPageContextDecorator = (Story: React.ComponentType) => ( +
+ + +
+) + +const longStackTrace = `Error: Failed to fetch resource + at fetchData (api.ts:42) + at async loadServices (services.ts:17) + at async ServiceList.componentDidMount (ServiceList.tsx:88) + at async Promise.all (index 0) + at async fetchAll (dataLoader.ts:130) + at async DataLoader.load (dataLoader.ts:98) + at async DataLoader.reload (dataLoader.ts:112) + at async AppBootstrap.init (AppBootstrap.ts:54) + at async AppBootstrap.run (AppBootstrap.ts:67) + at async main (index.ts:12) +Caused by: NetworkError: net::ERR_CONNECTION_REFUSED + at XMLHttpRequest.onload (http.ts:23) + at XMLHttpRequest.dispatchEvent (xhr-polyfill.js:14) + at EventTarget.dispatchEvent (event-target.js:88) + at XMLHttpRequest.send (xhr.ts:201) + at HttpClient.request (http-client.ts:77) + at HttpClient.get (http-client.ts:92) + at fetchData (api.ts:38) + at retryWithBackoff (retry.ts:14) + at retryWithBackoff (retry.ts:22) + at retryWithBackoff (retry.ts:22) + at async fetchWithRetry (fetchWithRetry.ts:9) + at async ResourceStore.fetch (ResourceStore.ts:61) + at async ResourceStore.refresh (ResourceStore.ts:74) + at async ResourceStore.initialize (ResourceStore.ts:88) + at async App.bootstrap (App.tsx:33) + at async App.componentDidMount (App.tsx:44) + at async renderWithHooks (react-dom.development.js:14985) + at async commitLifeCycles (react-dom.development.js:20663)` + +export const Default: Story = { + args: {}, +} + +export const HttpError: Story = { + args: { status: "error", code: 404 }, + parameters: { + docs: { + description: { + story: + "Page-level 404 error. Both `title` and `body` will render the correct error title and body for known Http errors by default. These can be overridden when needed.", + }, + }, + }, +} + +export const WithDetails: Story = { + args: { + status: "error", + details: longStackTrace, + }, + parameters: { + docs: { + description: { + story: + "Pass a stack trace or server response via `details` to render it in a scrollable `
` block below the message. Useful for technical users who need the full error context.",
+      },
+    },
+  },
+}
+
+export const WithAction: Story = {
+  args: { status: "error" },
+  render: (args) => Retry} />,
+  parameters: {
+    docs: {
+      description: {
+        story: "Pass any element via the `action` slot — typically a `Button` or a button-styled anchor.",
+      },
+    },
+  },
+}
+
+export const Loading: Story = {
+  args: { status: "progress" },
+  parameters: {
+    docs: {
+      description: {
+        story: 'Use `status="progress"` at page or section level while data is being fetched.',
+      },
+    },
+  },
+}
+
+export const InPageContext: Story = {
+  decorators: [MockPageContextDecorator],
+  args: {},
+  parameters: {
+    docs: {
+      description: {
+        story:
+          "Default error state in a page context. `Status` applies a top margin automatically based on its content.",
+      },
+    },
+  },
+}
+
+export const WithCodeInPageContext: Story = {
+  decorators: [MockPageContextDecorator],
+  args: { code: 404 },
+  parameters: {
+    docs: {
+      description: {
+        story:
+          "HTTP error with a large code display. The top margin is reduced as the code block provides sufficient visual weight.",
+      },
+    },
+  },
+}
+
+export const WithStatusInPageContext: Story = {
+  decorators: [MockPageContextDecorator],
+  args: { status: "progress" },
+  parameters: {
+    docs: {
+      description: {
+        story: "Progress/loading state in a page context. The top margin is sized to account for the spinner.",
+      },
+    },
+  },
+}
+
+export const DataGridLoading: Story = {
+  args: { status: "progress", title: "Loading Servers …" },
+  render: (args) => (
+    
+      
+        
+          
+        
+      
+      
+        
+          Name
+          Region
+          Status
+          Last Updated
+        
+        
+          
+            
+          
+        
+      
+    
+  ),
+  parameters: {
+    docs: {
+      description: {
+        story:
+          'Use `status="progress"` inside a `DataGridRow` spanning all columns while data is being fetched. Use the title `title` prop to further qualify the kind of items currently being loaded if possible, otherwise the default title "Loading …" will be rendered.',
+      },
+    },
+  },
+}
+
+export const DataGridError: Story = {
+  args: { status: "error" },
+  render: (args) => (
+    
+      
+        
+          
+        
+      
+      
+        
+          Name
+          Region
+          Status
+          Last Updated
+        
+        
+          
+            
+          
+        
+      
+    
+  ),
+  parameters: {
+    docs: {
+      description: {
+        story:
+          'Use `status="error"` inside a `DataGridRow` spanning all columns when a data fetch or operation has failed.',
+      },
+    },
+  },
+}
+
+export const DataGridEmpty: Story = {
+  args: { status: "empty" },
+  render: (args) => (
+    
+      
+        
+          
+        
+      
+      
+        
+          Name
+          Region
+          Status
+          Last Updated
+        
+        
+          
+            
+          
+        
+      
+    
+  ),
+  parameters: {
+    docs: {
+      description: {
+        story:
+          'Use `status="empty"` inside a `DataGridRow` spanning all columns when the data source contains no items.',
+      },
+    },
+  },
+}
+
+export const DataGridNoMatches: Story = {
+  args: { status: "no-matches" },
+  render: (args) => (
+    
+      
+        
+          
+        
+      
+      
+        
+          Name
+          Region
+          Status
+          Last Updated
+        
+        
+          
+            
+          
+        
+      
+    
+  ),
+  parameters: {
+    docs: {
+      description: {
+        story:
+          'Use `status="no-matches"` inside a `DataGridRow` spanning all columns when items exist but none match the currently applied filters.',
+      },
+    },
+  },
+}
+
+export const DataGridErrorWithDetails: Story = {
+  args: {
+    status: "error",
+    details: longStackTrace,
+  },
+  render: (args) => (
+    
+      
+        
+          
+        
+      
+      
+        
+          Name
+          Region
+          Status
+          Last Updated
+        
+        
+          
+            
+          
+        
+      
+    
+  ),
+  parameters: {
+    docs: {
+      description: {
+        story: "DataGrid error state with a stack trace passed via `details`.",
+      },
+    },
+  },
+}
diff --git a/packages/ui-components/src/components/Status/Status.test.tsx b/packages/ui-components/src/components/Status/Status.test.tsx
new file mode 100644
index 0000000000..d4e75723db
--- /dev/null
+++ b/packages/ui-components/src/components/Status/Status.test.tsx
@@ -0,0 +1,143 @@
+/*
+ * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import { Status } from "./Status.component"
+import { DataGrid } from "../DataGrid/DataGrid.component"
+import { DataGridRow } from "../DataGridRow/DataGridRow.component"
+import { DataGridCell } from "../DataGridCell/DataGridCell.component"
+
+describe("Status", () => {
+  it("renders a Status", () => {
+    render()
+    expect(screen.getByRole("alert")).toBeInTheDocument()
+  })
+
+  it("renders with role alert for error status", () => {
+    render()
+    expect(screen.getByRole("alert")).toBeInTheDocument()
+  })
+
+  it("renders with role status for non-error statuses", () => {
+    render()
+    expect(screen.getByRole("status")).toBeInTheDocument()
+  })
+
+  it("renders a spinner when status is progress", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toBeInTheDocument()
+    expect(screen.getByRole("progressbar")).toHaveClass("juno-spinner")
+  })
+
+  it("does not render a spinner by default for non-progress statuses", () => {
+    render()
+    expect(screen.queryByRole("progressbar")).not.toBeInTheDocument()
+  })
+
+  it("renders a spinner when spinner prop is true", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toBeInTheDocument()
+    expect(screen.getByRole("progressbar")).toHaveClass("juno-spinner")
+  })
+
+  it("does not render a spinner when spinner prop is false even for progress status", () => {
+    render()
+    expect(screen.queryByRole("progressbar")).not.toBeInTheDocument()
+  })
+
+  it("renders the spinner with the primary variant", () => {
+    render()
+    expect(screen.getByRole("progressbar")).toHaveClass("jn:text-theme-accent")
+  })
+
+  it("renders the default error title with no props", () => {
+    render()
+    expect(screen.getByRole("alert")).toHaveTextContent("Something went wrong")
+  })
+
+  it("renders the default title for the progress status", () => {
+    render()
+    expect(screen.getByRole("status")).toHaveTextContent("Loading…")
+  })
+
+  it("renders the default title for the empty status", () => {
+    render()
+    expect(screen.getByRole("status")).toHaveTextContent("No items")
+  })
+
+  it("renders title and body from HTTP error code", () => {
+    render()
+    expect(screen.getByText("Page Not Found")).toBeInTheDocument()
+    expect(screen.getByText("The requested URL does not exist or may have moved.")).toBeInTheDocument()
+  })
+
+  it("overrides default title when title prop is passed", () => {
+    render()
+    expect(screen.getByText("Custom title")).toBeInTheDocument()
+  })
+
+  it("renders body text", () => {
+    render()
+    expect(screen.getByText("Please try again.")).toBeInTheDocument()
+  })
+
+  it("renders details in a pre element", () => {
+    render()
+    expect(screen.getByLabelText("Error details")).toBeInTheDocument()
+    expect(screen.getByText("Error: something failed")).toBeInTheDocument()
+  })
+
+  it("does not render details when not passed", () => {
+    render()
+    expect(screen.queryByLabelText("Error details")).not.toBeInTheDocument()
+  })
+
+  it("renders code outside a DataGrid", () => {
+    render()
+    expect(screen.getByText("500")).toBeInTheDocument()
+  })
+
+  it("renders the action slot", () => {
+    render(Retry} />)
+    expect(screen.getByRole("button", { name: "Retry" })).toBeInTheDocument()
+  })
+
+  it("renders a custom className", () => {
+    render()
+    expect(screen.getByRole("alert")).toHaveClass("my-custom-class")
+  })
+
+  it("passes arbitrary props to the root element", () => {
+    render()
+    expect(screen.getByTestId("my-status")).toBeInTheDocument()
+  })
+
+  it("does not render a code inside a DataGrid", () => {
+    render(
+      
+        
+          
+            
+          
+        
+      
+    )
+    expect(screen.queryByText("404")).not.toBeInTheDocument()
+  })
+
+  it("adds a datagrid identifier class when rendered inside a DataGrid", () => {
+    render(
+      
+        
+          
+            
+          
+        
+      
+    )
+    expect(screen.getByRole("alert")).toHaveClass("juno-status-datagrid")
+  })
+})
diff --git a/packages/ui-components/src/components/Status/index.ts b/packages/ui-components/src/components/Status/index.ts
new file mode 100644
index 0000000000..57cb34beca
--- /dev/null
+++ b/packages/ui-components/src/components/Status/index.ts
@@ -0,0 +1,6 @@
+/*
+ * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { Status, type StatusProps } from "./Status.component"
diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css
index 33d800a027..e425fe5035 100644
--- a/packages/ui-components/src/global.css
+++ b/packages/ui-components/src/global.css
@@ -381,6 +381,7 @@
   --background-color-theme-required: var(--color-required-bg);
   --background-color-theme-select: var(--color-select-bg);
   --background-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active-bg);
+  --background-color-theme-status-details: var(--color-status-details-bg);
   --background-color-theme-switch-handle: var(--color-switch-handle-bg);
   --background-color-theme-switch-handle-checked: var(--color-switch-handle-checked-bg);
   --background-color-theme-tab-navigation-top: var(--color-tabnavigation-top-bg);
@@ -424,6 +425,8 @@
   --text-color-theme-pageheader-appname-default: var(--color-pageheader-appname-text-default);
   --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover);
   --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active);
+  --text-color-theme-status-details: var(--color-status-details-text);
+  --text-color-theme-status-code: var(--color-status-code);
 
   /* Component Border Colors: */
   --border-color-theme-default: var(--color-border-default);
@@ -467,6 +470,7 @@
   --border-color-theme-textinput-default: var(--color-textinput-default-border);
 
   --border-color-theme-sidenav: var(--color-sidenav-border);
+  --border-color-theme-status-details: var(--color-status-details-border);
 
   --padding-xs: 0.25rem;
   --padding-sm: 0.5rem;
@@ -726,6 +730,11 @@
   --color-codeblock-bg: var(--color-background-lvl-2);
   --color-codeblock-bar-border: var(--color-background-lvl-4);
   --color-codeblock-footer-border: var(--color-background-lvl-4);
+  /* LT Status */
+  --color-status-details-bg: var(--color-background-lvl-2);
+  --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* LT Panel */
   --color-panel-bg: rgb(252 252 252 / 0.8);
   /* LT PageHeader */
@@ -986,6 +995,11 @@
   --color-codeblock-bg: var(--color-background-lvl-2);
   --color-codeblock-bar-border: var(--color-background-lvl-4);
   --color-codeblock-footer-border: var(--color-background-lvl-4);
+  /* DT Status */
+  --color-status-details-bg: var(--color-background-lvl-2);
+  --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* DT Panel */
   --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75);
   /* DT PageHeader */
diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css
index f8484c08e1..d5308e0dd3 100644
--- a/packages/ui-components/src/theme.css
+++ b/packages/ui-components/src/theme.css
@@ -349,6 +349,7 @@
   --background-color-theme-checkbox: var(--color-checkbox-bg); /* -> .jn:bg-theme-checkbox */
 
   --background-color-theme-code-block: var(--color-codeblock-bg);
+  --background-color-theme-status-details: var(--color-status-details-bg);
 
   --background-color-theme-content-area-bg: var(--color-content-area-bg);
 
@@ -407,6 +408,8 @@
   --text-color-theme-pageheader-appname-default: var(--color-pageheader-appname-text-default);
   --text-color-theme-pageheader-appname-hover: var(--color-pageheader-appname-text-hover);
   --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active);
+  --text-color-theme-status-details: var(--color-status-details-text);
+  --text-color-theme-status-code: var(--color-status-code);
 
   /* Component Border Colors: */
   --border-color-theme-default: var(--color-border-default);
@@ -455,6 +458,7 @@
   --border-color-theme-textinput-default: var(--color-textinput-default-border);
 
   --border-color-theme-sidenav: var(--color-sidenav-border);
+  --border-color-theme-status-details: var(--color-status-details-border);
 
   --padding-xs: 0.25rem;
   --padding-sm: 0.5rem;
@@ -701,6 +705,11 @@
   --color-codeblock-bg: var(--color-background-lvl-2);
   --color-codeblock-bar-border: var(--color-background-lvl-4);
   --color-codeblock-footer-border: var(--color-background-lvl-4);
+  /* LT Status */
+  --color-status-details-bg: var(--color-background-lvl-2);
+  --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* LT Panel */
   --color-panel-bg: rgb(252 252 252 / 0.8);
   /* LT PageHeader */
@@ -975,6 +984,11 @@
   --color-codeblock-bg: var(--color-background-lvl-2);
   --color-codeblock-bar-border: var(--color-background-lvl-4);
   --color-codeblock-footer-border: var(--color-background-lvl-4);
+  /* DT Status */
+  --color-status-details-bg: var(--color-background-lvl-2);
+  --color-status-details-text: var(--color-text-default);
+  --color-status-details-border: var(--color-border-default);
+  --color-status-code: var(--color-text-disabled);
   /* DT Panel */
   --color-panel-bg: rgb(from var(--color-juno-grey-blue-11) r g b / 0.75);
   /* DT PageHeader */