-
Notifications
You must be signed in to change notification settings - Fork 467
feat(ui): add the Mosaic Banner component #9581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
949c800
feat(ui): add the Mosaic Banner component
alexcarpenter 7718b65
adjust styles
alexcarpenter e3d5a50
tweaks
alexcarpenter 9997067
fix type
alexcarpenter c5aeee4
docs(swingset): omit className and style from the Banner prop table
alexcarpenter 7c9b89d
fix height
alexcarpenter 0a842ba
Merge branch 'main' into carp/mosaic-banner-component
alexcarpenter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@clerk/ui': minor | ||
| --- | ||
|
|
||
| Add the Mosaic `Banner` component: a tinted surface that annotates the content around it with a status message. Compose it from `Banner.Root`, `Banner.Label`, and `Banner.Description`. `Banner.Root` takes a `color` of `neutral`, `warning`, or `negative`, and renders the icon for that color itself. It sets no ARIA role, so pass `role='status'` (or `role='alert'`) when the banner appears in response to something the user did. | ||
|
|
||
| Also adds an `info-circle` glyph to the Mosaic icon set. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import * as BannerStories from './banner.stories'; | ||
|
|
||
| # Banner | ||
|
|
||
| Banner annotates the surface it sits on with a status message. It is a compound component: `Banner.Root` sets the semantic `color` and renders the matching icon itself, and `Banner.Label` and `Banner.Description` carry the copy. It ships no ARIA role, so a banner that appears in response to something the user did needs `role='status'` (or `role='alert'` for an error) to be announced. | ||
|
|
||
| ## Playground | ||
|
|
||
| <Preview | ||
| name='Default' | ||
| storyModule={BannerStories} | ||
| /> | ||
|
|
||
| ## Props | ||
|
|
||
| <PropTable | ||
| meta={BannerStories.meta} | ||
| styleProps={false} | ||
| /> | ||
|
|
||
| ## Usage | ||
|
|
||
| ```tsx | ||
| import { Banner } from '@clerk/ui/mosaic/components/banner'; | ||
|
|
||
| <Banner.Root color='negative'> | ||
| <Banner.Label>Error banner</Banner.Label> | ||
| <Banner.Description>Renew now to avoid service interruption.</Banner.Description> | ||
| </Banner.Root>; | ||
| ``` | ||
|
|
||
| `color` lives on `Banner.Root` only; `Banner.Label` and `Banner.Description` read it from context, so they never need it passed again. The description is optional — a `Banner.Root` with just a label renders as a single line. | ||
|
|
||
| ## Parts | ||
|
|
||
| | Part | Stable slot class | Description | | ||
| | -------------------- | ------------------------ | --------------------------------------------------------------- | | ||
| | `Banner.Root` | `.cl-banner-root` | Tinted `div` surface. Owns `color` and renders the icon for it. | | ||
| | `Banner.Label` | `.cl-banner-label` | The headline `span`, in the root's color at medium weight. | | ||
| | `Banner.Description` | `.cl-banner-description` | Supporting `p` beneath the label. | | ||
|
|
||
| `Banner.Root` also wraps its children in a `.cl-banner-content` column so the copy aligns past the icon. Every part accepts `render` for polymorphism and forwards its ref. | ||
|
|
||
| ## Styling | ||
|
|
||
| Each part carries its stable slot class alongside the generated StyleX atoms and reflects the active color as `data-color`. Override a `.cl-banner-*` class from a CSS layer that wins over `@clerk/ui/styles.css`: | ||
|
|
||
| ```css | ||
| @import '@clerk/ui/styles.css' layer(components); | ||
|
|
||
| @layer overrides { | ||
| .cl-banner-root[data-color='negative'] { | ||
| border-radius: 0; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| | Part | `data-color` | | ||
| | -------------------- | ------------------------------------ | | ||
| | `Banner.Root` | `neutral` \| `warning` \| `negative` | | ||
| | `Banner.Label` | `neutral` \| `warning` \| `negative` | | ||
| | `Banner.Description` | `neutral` \| `warning` \| `negative` | | ||
|
|
||
| The fill is a 4% mix of the color's token rather than its `-faded` surface, so a banner tints whatever it sits on instead of painting over it, and it inverts with the token in dark mode. Retheme a color by overriding the token it reads — `--cl-color-negative`, `--cl-color-warning`, or `--cl-color-neutral` (plus `--cl-color-border`, which draws the neutral hairline) — and the fill, border, icon, and copy all move together. | ||
|
|
||
| --- | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Colors | ||
|
|
||
| <Story | ||
| name='Colors' | ||
| storyModule={BannerStories} | ||
| /> | ||
|
|
||
| ### Label only | ||
|
|
||
| <Story | ||
| name='LabelOnly' | ||
| storyModule={BannerStories} | ||
| /> | ||
|
|
||
| ### Announced | ||
|
|
||
| <Story | ||
| name='Announced' | ||
| storyModule={BannerStories} | ||
| /> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import type { BannerRootProps } from '@clerk/ui/mosaic/components/banner'; | ||
| import { Banner } from '@clerk/ui/mosaic/components/banner'; | ||
|
|
||
| import type { StoryMeta } from '@/lib/types'; | ||
|
|
||
| // Exposes this file's own source (via the `?raw` webpack rule) so each `<Story>` example | ||
| // renders a code footer with its function's source. See `StoryModule.__source`. | ||
| export { default as __source } from './banner.stories?raw'; | ||
|
|
||
| // StyleX has no runtime recipe to derive knobs from, so the variant surface is described | ||
| // here to drive the playground + prop table. Keys mirror `BannerRootProps`. | ||
| export const meta: StoryMeta = { | ||
| group: 'Components', | ||
| title: 'Banner', | ||
| source: 'packages/ui/src/mosaic/components/banner/banner.tsx', | ||
| styles: { | ||
| _variants: { | ||
| color: { neutral: {}, warning: {}, negative: {} }, | ||
| }, | ||
| _defaultVariants: { | ||
| color: 'neutral', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| // Story functions accept Record<string,unknown> (knob values) and cast to BannerRootProps. | ||
| // The cast is unavoidable: knobs are dynamically typed; Banner.Root has a strict prop interface. | ||
| function knobsAsProps(props: Record<string, unknown>) { | ||
| return props as unknown as BannerRootProps; | ||
| } | ||
|
|
||
| export function Default(props: Record<string, unknown>) { | ||
| return ( | ||
| <Banner.Root {...knobsAsProps(props)}> | ||
| <Banner.Label>Info banner</Banner.Label> | ||
| <Banner.Description>Here is a tip for how this should work</Banner.Description> | ||
| </Banner.Root> | ||
| ); | ||
| } | ||
|
|
||
| export function Colors() { | ||
| return ( | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> | ||
| <Banner.Root color='negative'> | ||
| <Banner.Label>Error banner</Banner.Label> | ||
| <Banner.Description> | ||
| Renew now to avoid service interruption or upgrade to a paid plan to continue using the service. | ||
| </Banner.Description> | ||
| </Banner.Root> | ||
| <Banner.Root color='warning'> | ||
| <Banner.Label>Warning banner</Banner.Label> | ||
| <Banner.Description> | ||
| Your payment could not be processed. Please check your payment method and try again. | ||
| </Banner.Description> | ||
| </Banner.Root> | ||
| <Banner.Root color='neutral'> | ||
| <Banner.Label>Info banner</Banner.Label> | ||
| <Banner.Description>Here is a tip for how this should work</Banner.Description> | ||
| </Banner.Root> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function LabelOnly() { | ||
| return ( | ||
| <Banner.Root color='warning'> | ||
| <Banner.Label>Your trial ends in 3 days</Banner.Label> | ||
| </Banner.Root> | ||
| ); | ||
| } | ||
|
|
||
| export function Announced() { | ||
| return ( | ||
| <Banner.Root | ||
| color='negative' | ||
| role='alert' | ||
| > | ||
| <Banner.Label>Payment failed</Banner.Label> | ||
| <Banner.Description>We could not charge your card. Update your payment method to continue.</Banner.Description> | ||
| </Banner.Root> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import * as stylex from '@stylexjs/stylex'; | ||
|
|
||
| import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; | ||
|
|
||
| const neutralFill = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; | ||
| const warningFill = `color-mix(in oklab, ${colorVars['--cl-color-warning']} 4%, transparent)`; | ||
| const negativeFill = `color-mix(in oklab, ${colorVars['--cl-color-negative']} 4%, transparent)`; | ||
| const warningBorder = `color-mix(in oklab, ${colorVars['--cl-color-warning']} 20%, transparent)`; | ||
| const negativeBorder = `color-mix(in oklab, ${colorVars['--cl-color-negative']} 20%, transparent)`; | ||
|
|
||
| export const styles = stylex.create({ | ||
| root: { | ||
| borderRadius: radiusVars['--cl-radius-lg'], | ||
| borderStyle: 'solid', | ||
| borderWidth: '1px', | ||
| gap: space['1.5'], | ||
| paddingBlock: space['2'], | ||
| paddingInline: space['3'], | ||
| alignItems: 'flex-start', | ||
| display: 'flex', | ||
| fontFamily: fontFamilyVars['--cl-font-family-sans'], | ||
| fontSize: typeScaleVars['--cl-text-sm-size'], | ||
| lineHeight: typeScaleVars['--cl-text-sm-leading'], | ||
| }, | ||
| icon: { | ||
| flexShrink: 0, | ||
| height: '1lh', | ||
| }, | ||
| content: { | ||
| gap: space['1'], | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| minWidth: 0, | ||
| }, | ||
| label: { | ||
| fontWeight: fontWeightVars['--cl-font-medium'], | ||
| }, | ||
| description: { | ||
| textWrap: 'pretty', | ||
| }, | ||
| }); | ||
|
|
||
| export const rootColors = stylex.create({ | ||
| neutral: { | ||
| borderColor: colorVars['--cl-color-border'], | ||
| backgroundColor: neutralFill, | ||
| color: colorVars['--cl-color-neutral-foreground'], | ||
| }, | ||
| warning: { | ||
| borderColor: warningBorder, | ||
| backgroundColor: warningFill, | ||
| color: colorVars['--cl-color-warning'], | ||
| }, | ||
| negative: { | ||
| borderColor: negativeBorder, | ||
| backgroundColor: negativeFill, | ||
| color: colorVars['--cl-color-negative'], | ||
| }, | ||
| }); | ||
|
|
||
| export const descriptionColors = stylex.create({ | ||
| neutral: { color: colorVars['--cl-color-neutral-faded'] }, | ||
| warning: { color: colorVars['--cl-color-warning'] }, | ||
| negative: { color: colorVars['--cl-color-negative'] }, | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.