diff --git a/AGENTS.md b/AGENTS.md index 0043680..c5ddf45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 39eb981..59f91f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index d56e0f7..6e443f7 100644 --- a/README.md +++ b/README.md @@ -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'; Retry} + title="Delete this organization" + action={} > - The service returned an unexpected response. + Permanently removes the organization and all of its projects. ``` +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. diff --git a/docs/evidence/issue-22-alert-emphasis-ember.jpg b/docs/evidence/issue-22-alert-emphasis-ember.jpg new file mode 100644 index 0000000..18603b6 Binary files /dev/null and b/docs/evidence/issue-22-alert-emphasis-ember.jpg differ diff --git a/docs/evidence/issue-22-alert-emphasis-ocean.jpg b/docs/evidence/issue-22-alert-emphasis-ocean.jpg new file mode 100644 index 0000000..bed4eaf Binary files /dev/null and b/docs/evidence/issue-22-alert-emphasis-ocean.jpg differ diff --git a/docs/evidence/issue-22-alert-narrow-ember.jpg b/docs/evidence/issue-22-alert-narrow-ember.jpg new file mode 100644 index 0000000..f0031fe Binary files /dev/null and b/docs/evidence/issue-22-alert-narrow-ember.jpg differ diff --git a/docs/evidence/issue-22-alert-theme-override-ember.jpg b/docs/evidence/issue-22-alert-theme-override-ember.jpg new file mode 100644 index 0000000..1dfd399 Binary files /dev/null and b/docs/evidence/issue-22-alert-theme-override-ember.jpg differ diff --git a/src/components/Alert.stories.tsx b/src/components/Alert.stories.tsx index aca75a5..69822a9 100644 --- a/src/components/Alert.stories.tsx +++ b/src/components/Alert.stories.tsx @@ -1,6 +1,12 @@ 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', @@ -8,11 +14,13 @@ const meta = { 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 }, }, @@ -42,6 +50,60 @@ export const Tones: Story = { ), }; +export const SubtleTones: Story = { + args: { variant: 'subtle' }, + render: () => ( +
+ + Version 4.8 can be enabled from workspace settings. + + + 86% of this month's execution allowance has been used. + + + The key remains available until you leave this page. + + + Destructive organization controls are grouped below. + +
+ ), +}; + +export const EmphasisComparison: Story = { + render: () => ( +
+ + No changes were made. Check your connection and try again. + + Delete organization} + > + Permanently removes the organization and all of its projects. + +
+ ), +}; + +export const ThemeOverride: Story = { + args: { variant: 'subtle', tone: 'danger' }, + render: () => ( +
+ Delete organization} + > + Permanently removes the organization and all of its projects. + +
+ ), +}; + export const WithRecoveryAction: Story = { render: () => ( (
- View usage}> - Some new executions may be held if the remaining allowance is consumed. + Delete organization}> + Permanently removes the organization and all of its projects.
), diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx index 365466b..a591b45 100644 --- a/src/components/Alert.tsx +++ b/src/components/Alert.tsx @@ -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 = { info: 'i', @@ -13,6 +14,7 @@ const alertIcons: Record = { export type AlertProps = Omit, 'title'> & { tone?: AlertTone; + variant?: AlertVariant; title: ReactNode; icon?: ReactNode; action?: ReactNode; @@ -22,6 +24,7 @@ export const Alert = forwardRef( ( { tone = 'info', + variant = 'default', title, icon, action, @@ -31,22 +34,33 @@ export const Alert = forwardRef( ...props }, ref, - ) => ( -
- -
-
-
{title}
- {children === undefined ? null :
{children}
} + ) => { + const defaultMarker = variant === 'subtle' ? null : alertIcons[tone]; + + return ( +
+ {icon === null ? null : ( + + )} +
+
+
{title}
+ {children === undefined ? null :
{children}
} +
+ {action === undefined ? null :
{action}
}
- {action === undefined ? null :
{action}
}
-
- ), + ); + }, ); Alert.displayName = 'Alert'; diff --git a/src/components/components.test.tsx b/src/components/components.test.tsx index c08698f..14902a7 100644 --- a/src/components/components.test.tsx +++ b/src/components/components.test.tsx @@ -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(Destructive controls follow.); + + 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(Message.); + 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)'); diff --git a/src/entries/alert.ts b/src/entries/alert.ts index 91c7943..8b90e7a 100644 --- a/src/entries/alert.ts +++ b/src/entries/alert.ts @@ -1,2 +1,2 @@ export { Alert } from '../components/Alert'; -export type { AlertProps, AlertTone } from '../components/Alert'; +export type { AlertProps, AlertTone, AlertVariant } from '../components/Alert'; diff --git a/src/index.ts b/src/index.ts index db20e86..3ffdc3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/stories/Theme.mdx b/src/stories/Theme.mdx index a2f4b6c..982b815 100644 --- a/src/stories/Theme.mdx +++ b/src/stories/Theme.mdx @@ -130,6 +130,31 @@ a stable selector or application state value: +## 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. + + + + + + + + + +
VariablePurposeFallback
--bits-alert-surfaceAlert background fill--bits-raised, or a tone-derived wash for subtle alerts
--bits-alert-borderAlert border colorThe alert tone, or a weakened tone mix for subtle alerts
+ +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. diff --git a/src/styles.css b/src/styles.css index 57b8661..52b8028 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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; } @@ -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%; }