diff --git a/webui/src/components/primitives/index.ts b/webui/src/components/primitives/index.ts
index 849c5bcc..6b63ff8a 100644
--- a/webui/src/components/primitives/index.ts
+++ b/webui/src/components/primitives/index.ts
@@ -1 +1 @@
-export { Show } from './show';
+export * from './primitives';
diff --git a/webui/src/components/primitives/primitives.module.css b/webui/src/components/primitives/primitives.module.css
new file mode 100644
index 00000000..23914559
--- /dev/null
+++ b/webui/src/components/primitives/primitives.module.css
@@ -0,0 +1,45 @@
+.notice {
+ box-sizing: border-box;
+ display: block;
+ width: 100%;
+ margin: 0 0 12px;
+ padding: 10px 14px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.04);
+ color: rgba(255, 255, 255, 0.82);
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 1.4;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
+}
+
+.notice[data-tone='neutral'] {
+ border-color: rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+ color: rgba(255, 255, 255, 0.82);
+}
+
+.notice[data-tone='info'] {
+ border-color: rgba(var(--accent-light-rgb), 0.24);
+ background: rgba(var(--accent-light-rgb), 0.08);
+ color: rgba(255, 232, 188, 0.94);
+}
+
+.notice[data-tone='success'] {
+ border-color: rgba(110, 220, 150, 0.26);
+ background: rgba(110, 220, 150, 0.08);
+ color: rgba(210, 255, 228, 0.94);
+}
+
+.notice[data-tone='warning'] {
+ border-color: rgba(255, 200, 100, 0.26);
+ background: rgba(255, 200, 100, 0.08);
+ color: rgba(255, 232, 188, 0.94);
+}
+
+.notice[data-tone='danger'] {
+ border-color: rgba(255, 120, 120, 0.26);
+ background: rgba(255, 120, 120, 0.08);
+ color: rgba(255, 214, 214, 0.94);
+}
diff --git a/webui/src/components/primitives/show.test.tsx b/webui/src/components/primitives/primitives.test.tsx
similarity index 59%
rename from webui/src/components/primitives/show.test.tsx
rename to webui/src/components/primitives/primitives.test.tsx
index ec090d2c..add08aeb 100644
--- a/webui/src/components/primitives/show.test.tsx
+++ b/webui/src/components/primitives/primitives.test.tsx
@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
-import { Show } from './show';
+import { Notice, Show } from './primitives';
describe('Show', () => {
it('renders children when the condition is true', () => {
@@ -31,3 +31,22 @@ describe('Show', () => {
expect(screen.getByText('Ada')).toBeInTheDocument();
});
});
+
+describe('Notice', () => {
+ it('renders as a note by default', () => {
+ render(Fallback message);
+
+ expect(screen.getByText('Fallback message')).toHaveAttribute('role', 'note');
+ expect(screen.getByText('Fallback message')).toHaveAttribute('data-tone', 'info');
+ });
+
+ it('supports tone overrides', () => {
+ render(
+
+ Provider fallback
+ ,
+ );
+
+ expect(screen.getByRole('note')).toHaveAttribute('data-tone', 'warning');
+ });
+});
diff --git a/webui/src/components/primitives/primitives.tsx b/webui/src/components/primitives/primitives.tsx
new file mode 100644
index 00000000..1fec52b9
--- /dev/null
+++ b/webui/src/components/primitives/primitives.tsx
@@ -0,0 +1,46 @@
+import clsx from 'clsx';
+import { forwardRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
+
+import styles from './primitives.module.css';
+
+type ShowChildren = ReactNode | ((value: NonNullable) => ReactNode);
+
+export interface ShowProps {
+ children: ShowChildren;
+ fallback?: ReactNode;
+ when: T;
+}
+
+export function Show({ fallback = null, children, when }: ShowProps) {
+ if (!when) {
+ return <>{fallback}>;
+ }
+
+ if (typeof children === 'function') {
+ return <>{(children as (value: NonNullable) => ReactNode)(when as NonNullable)}>;
+ }
+
+ return <>{children}>;
+}
+
+export type NoticeTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
+
+export type NoticeProps = Omit, 'className'> & {
+ className?: string;
+ tone?: NoticeTone;
+};
+
+export const Notice = forwardRef(function Notice(
+ { className, tone = 'info', role = 'note', ...props },
+ ref,
+) {
+ return (
+
+ );
+});
diff --git a/webui/src/components/primitives/show.tsx b/webui/src/components/primitives/show.tsx
deleted file mode 100644
index 2265131a..00000000
--- a/webui/src/components/primitives/show.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { ReactNode } from 'react';
-
-type ShowChildren = ReactNode | ((value: NonNullable) => ReactNode);
-
-export function Show({
- fallback = null,
- children,
- when,
-}: {
- children: ShowChildren;
- fallback?: ReactNode;
- when: T;
-}) {
- if (!when) {
- return <>{fallback}>;
- }
-
- if (typeof children === 'function') {
- return <>{(children as (value: NonNullable) => ReactNode)(when as NonNullable)}>;
- }
-
- return <>{children}>;
-}