Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,13 @@ Use `Avatar` with a required `name`; sizes are `sm`, `md`, and `lg`. Supply `src

### Alert

Use `Alert` for persistent inline feedback. Tones are `info`, `warning`, `success`, and `danger`.
Use `Alert` for persistent inline feedback. Tones are `info`, `warning`, `success`, and `danger`; variants are `default` and `subtle`.

- Use `danger` for failures that need immediate attention; it uses `role="alert"`.
- Use `subtle` for persistent advisory regions such as danger zones that should communicate meaning without competing with current content. Subtle alerts use `role="status"` unless `role` is set explicitly.
- Use quieter tones for advisory state; they use `role="status"`.
- Theme alert fills and borders with `--bits-alert-surface` and `--bits-alert-border`; do not target private `.bits-alert-*` selectors.
- Pass `icon={null}` when the alert should have no marker. Subtle alerts otherwise use a compact, glyph-free marker by default.
- Provide a concrete recovery control through `action` when the user can resolve the condition.
- Do not use Alert for transient confirmation that is better handled by Toast.
- Do not use Toast as the only record of a blocking error.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- Dialog block content, compact and medium sizes, and disabled/loading confirmation states for short form workflows.
- A subtle Alert variant with derived low-emphasis surfaces, borders, and compact markers.

### Changed

- Dialog focus trapping now includes native select, textarea, editable, and explicitly tabbable controls.
- An open Select consumes Escape before a containing dialog closes.
- Alert surfaces and borders can be themed through `--bits-alert-surface` and `--bits-alert-border`.

## 0.2.0 - 2026-09-02

Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,30 @@ import { Segmented } from '@16-bits-design/ui/segmented';

### Alert

Use `Alert` for persistent inline feedback that must remain available in the page. Danger alerts use `role="alert"`; info, warning, and success alerts use the quieter `role="status"`. Actions remain explicit composable controls.
Use `Alert` for persistent inline feedback that must remain available in the page.
The default variant uses a strong tone border for events that need attention now;
use `variant="subtle"` for persistent advisory regions such as a danger zone.
Subtle alerts use a compact marker and the quieter `role="status"`, including for
the danger tone. Actions remain explicit composable controls.

```tsx
import { Alert } from '@16-bits-design/ui/alert';
import { Button } from '@16-bits-design/ui/button';

<Alert
variant="subtle"
tone="danger"
title="Could not load deployments"
action={<Button size="sm">Retry</Button>}
title="Delete this organization"
action={<Button size="sm" variant="danger">Delete organization</Button>}
>
The service returned an unexpected response.
Permanently removes the organization and all of its projects.
</Alert>
```

The derived subtle colors follow the active tone and theme. A theme can provide
exact authored values through `--bits-alert-surface` and `--bits-alert-border`
without targeting component classes. Pass `icon={null}` to omit the marker.

### Table

Use the composable table primitives for genuinely tabular data. `Table` keeps wide content inside a keyboard-focusable horizontal scroll region; give that region a context-specific `scrollLabel`. End-aligned cells use tabular numerals automatically.
Expand Down
Binary file added docs/evidence/issue-22-alert-emphasis-ember.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/issue-22-alert-emphasis-ocean.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/issue-22-alert-narrow-ember.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 64 additions & 2 deletions src/components/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Alert } from './Alert';
import { Button } from './Button';
import type { ThemeCSSProperties } from '../theme';

const authoredDangerZoneTheme: ThemeCSSProperties = {
'--bits-alert-surface': '#170d0b',
'--bits-alert-border': '#4a231c',
};

const meta = {
title: 'Components/Alert',
component: Alert,
tags: ['autodocs'],
args: {
tone: 'info',
variant: 'default',
title: 'Maintenance window scheduled',
children: 'Requests may be delayed for up to two minutes while the edge nodes restart.',
},
argTypes: {
tone: { control: 'inline-radio', options: ['info', 'warning', 'success', 'danger'] },
variant: { control: 'inline-radio', options: ['default', 'subtle'] },
action: { control: false },
icon: { control: false },
},
Expand Down Expand Up @@ -42,6 +50,60 @@ export const Tones: Story = {
),
};

export const SubtleTones: Story = {
args: { variant: 'subtle' },
render: () => (
<div className="story-stack" style={{ maxWidth: 640 }}>
<Alert variant="subtle" tone="info" title="New runtime available">
Version 4.8 can be enabled from workspace settings.
</Alert>
<Alert variant="subtle" tone="warning" title="Usage is nearing its limit">
86% of this month&apos;s execution allowance has been used.
</Alert>
<Alert variant="subtle" tone="success" title="Access key is ready">
The key remains available until you leave this page.
</Alert>
<Alert variant="subtle" tone="danger" title="Danger zone">
Destructive organization controls are grouped below.
</Alert>
</div>
),
};

export const EmphasisComparison: Story = {
render: () => (
<div className="story-stack" style={{ maxWidth: 720 }}>
<Alert tone="danger" title="Could not delete organization">
No changes were made. Check your connection and try again.
</Alert>
<Alert
variant="subtle"
tone="danger"
title="Delete this organization"
action={<Button size="sm" variant="danger">Delete organization</Button>}
>
Permanently removes the organization and all of its projects.
</Alert>
</div>
),
};

export const ThemeOverride: Story = {
args: { variant: 'subtle', tone: 'danger' },
render: () => (
<div style={{ ...authoredDangerZoneTheme, maxWidth: 720 }}>
<Alert
variant="subtle"
tone="danger"
title="Delete this organization"
action={<Button size="sm" variant="danger">Delete organization</Button>}
>
Permanently removes the organization and all of its projects.
</Alert>
</div>
),
};

export const WithRecoveryAction: Story = {
render: () => (
<Alert
Expand All @@ -58,8 +120,8 @@ export const WithRecoveryAction: Story = {
export const NarrowViewport: Story = {
render: () => (
<div style={{ maxWidth: 320 }}>
<Alert tone="warning" title="Usage is nearing its monthly limit" action={<Button size="sm" variant="secondary">View usage</Button>}>
Some new executions may be held if the remaining allowance is consumed.
<Alert variant="subtle" tone="danger" title="Delete this organization" action={<Button size="sm" variant="danger">Delete organization</Button>}>
Permanently removes the organization and all of its projects.
</Alert>
</div>
),
Expand Down
44 changes: 29 additions & 15 deletions src/components/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { HTMLAttributes, ReactNode } from 'react';
import { cn } from '../lib/cn';

export type AlertTone = 'info' | 'warning' | 'success' | 'danger';
export type AlertVariant = 'default' | 'subtle';

const alertIcons: Record<AlertTone, string> = {
info: 'i',
Expand All @@ -13,6 +14,7 @@ const alertIcons: Record<AlertTone, string> = {

export type AlertProps = Omit<HTMLAttributes<HTMLDivElement>, 'title'> & {
tone?: AlertTone;
variant?: AlertVariant;
title: ReactNode;
icon?: ReactNode;
action?: ReactNode;
Expand All @@ -22,6 +24,7 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
(
{
tone = 'info',
variant = 'default',
title,
icon,
action,
Expand All @@ -31,22 +34,33 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
...props
},
ref,
) => (
<div
{...props}
ref={ref}
className={cn('bits-component bits-alert', `bits-alert--${tone}`, className)}
role={role ?? (tone === 'danger' ? 'alert' : 'status')}
>
<span className="bits-alert__icon" aria-hidden="true">{icon ?? alertIcons[tone]}</span>
<div className="bits-alert__content">
<div className="bits-alert__copy">
<div className="bits-alert__title">{title}</div>
{children === undefined ? null : <div className="bits-alert__message">{children}</div>}
) => {
const defaultMarker = variant === 'subtle' ? null : alertIcons[tone];

return (
<div
{...props}
ref={ref}
className={cn('bits-component bits-alert', `bits-alert--${tone}`, `bits-alert--${variant}`, className)}
role={role ?? (tone === 'danger' && variant === 'default' ? 'alert' : 'status')}
>
{icon === null ? null : (
<span
className={cn('bits-alert__icon', variant === 'subtle' && icon === undefined && 'bits-alert__icon--marker')}
aria-hidden="true"
>
{icon ?? defaultMarker}
</span>
)}
<div className="bits-alert__content">
<div className="bits-alert__copy">
<div className="bits-alert__title">{title}</div>
{children === undefined ? null : <div className="bits-alert__message">{children}</div>}
</div>
{action === undefined ? null : <div className="bits-alert__action">{action}</div>}
</div>
{action === undefined ? null : <div className="bits-alert__action">{action}</div>}
</div>
</div>
),
);
},
);
Alert.displayName = 'Alert';
17 changes: 17 additions & 0 deletions src/components/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@ describe('display components', () => {
expect(screen.getByRole('button', { name: 'Retry request' })).toBeEnabled();
});

it('renders subtle alerts with a compact marker and advisory semantics', () => {
render(<Alert variant="subtle" tone="danger" title="Danger zone">Destructive controls follow.</Alert>);

const alert = screen.getByRole('status');
expect(alert).toHaveClass('bits-alert--subtle', 'bits-alert--danger');
expect(alert.querySelector('.bits-alert__icon--marker')).toBeEmptyDOMElement();
expect(stylesheet).toContain('var(--bits-alert-border, var(--bits-alert-tone))');
expect(stylesheet).toContain('var(--bits-alert-surface, var(--bits-raised))');
expect(stylesheet).toContain('var(--bits-alert-border, color-mix');
expect(stylesheet).toContain('var(--bits-alert-surface, color-mix');
});

it('allows an alert marker to be omitted explicitly', () => {
render(<Alert icon={null} title="No marker">Message.</Alert>);
expect(screen.getByRole('status').querySelector('.bits-alert__icon')).not.toBeInTheDocument();
});

it('exposes only live Ember and Ocean theme tokens', () => {
expect(builtInThemes).toEqual(['ember', 'ocean']);
expect(colors.primary).toBe('var(--bits-primary)');
Expand Down
2 changes: 1 addition & 1 deletion src/entries/alert.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Alert } from '../components/Alert';
export type { AlertProps, AlertTone } from '../components/Alert';
export type { AlertProps, AlertTone, AlertVariant } from '../components/Alert';
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { Alert } from '@16-bits-design/ui/alert';
export type { AlertProps, AlertTone } from '@16-bits-design/ui/alert';
export type { AlertProps, AlertTone, AlertVariant } from '@16-bits-design/ui/alert';

export { Avatar } from '@16-bits-design/ui/avatar';
export type { AvatarProps, AvatarSize } from '@16-bits-design/ui/avatar';
Expand Down
25 changes: 25 additions & 0 deletions src/stories/Theme.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,31 @@ a stable selector or application state value:
</tbody>
</table>

## Component color overrides

Component-level variables are optional. They inherit through a theme container and
let a design tune a component without depending on its private class names.

<table>
<thead>
<tr><th>Variable</th><th>Purpose</th><th>Fallback</th></tr>
</thead>
<tbody>
<tr><td><code>--bits-alert-surface</code></td><td>Alert background fill</td><td><code>--bits-raised</code>, or a tone-derived wash for subtle alerts</td></tr>
<tr><td><code>--bits-alert-border</code></td><td>Alert border color</td><td>The alert tone, or a weakened tone mix for subtle alerts</td></tr>
</tbody>
</table>

For example, a theme with authored danger-zone values can set both variables on a
container. The same Alert API and markup remain unchanged:

```css
.brand-danger-zone {
--bits-alert-surface: #170d0b;
--bits-alert-border: #4a231c;
}
```

Keep text/background and interactive-state combinations accessible. In particular,
check `--bits-ink` on `--bits-primary`, all text levels on surfaces, and focus rings.

Expand Down
4 changes: 3 additions & 1 deletion src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,9 @@
.bits-badge--danger { background: var(--bits-danger); }
.bits-badge--outline { border: var(--bits-border-width) solid currentColor; background: transparent; color: var(--bits-text-muted); }

.bits-alert { --bits-alert-tone: var(--bits-primary); display: flex; align-items: flex-start; gap: var(--bits-space-3); width: 100%; padding: var(--bits-space-3); border: var(--bits-border-width) solid var(--bits-alert-tone); background: var(--bits-raised); color: var(--bits-text); font-family: var(--bits-font-body); }
.bits-alert { --bits-alert-tone: var(--bits-primary); display: flex; align-items: flex-start; gap: var(--bits-space-3); width: 100%; padding: var(--bits-space-3); border: var(--bits-border-width) solid var(--bits-alert-border, var(--bits-alert-tone)); background: var(--bits-alert-surface, var(--bits-raised)); color: var(--bits-text); font-family: var(--bits-font-body); }
.bits-alert__icon { display: flex; align-items: center; justify-content: center; flex: none; width: 24px; height: 24px; background: var(--bits-alert-tone); color: var(--bits-ink); font: 400 10px/1 var(--bits-font-display); }
.bits-alert__icon--marker { width: 12px; height: 12px; margin-top: 2px; }
.bits-alert__content { display: flex; align-items: center; justify-content: space-between; gap: var(--bits-space-4); flex: 1; min-width: 0; }
.bits-alert__copy { display: flex; flex-direction: column; gap: var(--bits-space-1); min-width: 0; }
.bits-alert__title { color: var(--bits-text); font-size: var(--bits-font-size-small); font-weight: 700; line-height: 1.45; }
Expand All @@ -373,6 +374,7 @@
.bits-alert--warning { --bits-alert-tone: var(--bits-amber); }
.bits-alert--success { --bits-alert-tone: var(--bits-success); }
.bits-alert--danger { --bits-alert-tone: var(--bits-danger); }
.bits-alert--subtle { border-color: var(--bits-alert-border, color-mix(in srgb, var(--bits-alert-tone) 30%, var(--bits-surface))); background: var(--bits-alert-surface, color-mix(in srgb, var(--bits-alert-tone) 8%, var(--bits-surface))); }
@media (max-width: 480px) {
.bits-alert__content { align-items: flex-start; flex-direction: column; }
.bits-alert__action { width: 100%; }
Expand Down