1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

[CL-1022] Update Berry Styles (#18799)

* created 'berry' component

* added 'bit-berry' to 'popup-tab-navigation'

* simplified - removed null checks

* changed 'effectiveSize' to 'computedSize'

* fixed 'accentPrimary' color

* updated to not render berry if 'count' is 0 or negative number

* simplified checking count undefined

* updated computed padding

* switched from `[ngClass]` to `[class]`

* updated 'popup-tab-navigation' berry to use 'danger' variant

* fixed berry positioning in popup-tab-navigation

* updated content logic

* cleanup unused 'ngClass'

* updated conditional rendering of berry

* updated story 'Usage'

* updates with adding berry 'type'

* added type "status" to popup-tab-navigation

* fixed type error

* updated 'Count Behavior' description
This commit is contained in:
Leslie Xiong
2026-02-11 12:21:33 -05:00
committed by GitHub
parent a9ccb421c4
commit ea55aaaede
9 changed files with 305 additions and 4 deletions

View File

@@ -27,8 +27,8 @@
{{ button.label | i18n }}
</span>
</button>
<div *ngIf="button.showBerry" class="tw-absolute tw-top-1.5 tw-left-[calc(50%+5px)]">
<div class="tw-bg-notification-600 tw-size-2.5 tw-rounded-full"></div>
<div *ngIf="button.showBerry" class="tw-absolute tw-top-0 tw-left-[calc(50%+5px)]">
<bit-berry type="status" variant="danger"></bit-berry>
</div>
</li>
</ul>

View File

@@ -5,7 +5,7 @@ import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BitSvg } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SvgModule, LinkModule } from "@bitwarden/components";
import { SvgModule, LinkModule, BerryComponent } from "@bitwarden/components";
export type NavButton = {
label: string;
@@ -20,7 +20,7 @@ export type NavButton = {
@Component({
selector: "popup-tab-navigation",
templateUrl: "popup-tab-navigation.component.html",
imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule],
imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule, BerryComponent],
host: {
class: "tw-block tw-size-full tw-flex tw-flex-col",
},

View File

@@ -0,0 +1,3 @@
@if (type() === "status" || content()) {
<span [class]="containerClasses()">{{ content() }}</span>
}

View File

@@ -0,0 +1,80 @@
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
export type BerryVariant =
| "primary"
| "subtle"
| "success"
| "warning"
| "danger"
| "accentPrimary"
| "contrast";
/**
* The berry component is a compact visual indicator used to display short,
* supplemental status information about another element,
* like a navigation item, button, or icon button.
* They draw users attention to status changes or new notifications.
*
* > `NOTE:` The maximum displayed value is 999. If the value is over 999, a “+” character is appended to indicate more.
*/
@Component({
selector: "bit-berry",
templateUrl: "berry.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BerryComponent {
protected readonly variant = input<BerryVariant>("primary");
protected readonly value = input<number>();
protected readonly type = input<"status" | "count">("count");
protected readonly content = computed(() => {
const value = this.value();
const type = this.type();
if (type === "status" || !value || value < 0) {
return undefined;
}
return value > 999 ? "999+" : `${value}`;
});
protected readonly textColor = computed(() => {
return this.variant() === "contrast" ? "tw-text-fg-dark" : "tw-text-fg-white";
});
protected readonly padding = computed(() => {
return (this.value()?.toString().length ?? 0) > 2 ? "tw-px-1.5 tw-py-0.5" : "";
});
protected readonly containerClasses = computed(() => {
const baseClasses = [
"tw-inline-flex",
"tw-items-center",
"tw-justify-center",
"tw-align-middle",
"tw-text-xxs",
"tw-rounded-full",
];
const typeClasses = {
status: ["tw-h-2", "tw-w-2"],
count: ["tw-h-4", "tw-min-w-4", this.padding()],
};
const variantClass = {
primary: "tw-bg-bg-brand",
subtle: "tw-bg-bg-contrast",
success: "tw-bg-bg-success",
warning: "tw-bg-bg-warning",
danger: "tw-bg-bg-danger",
accentPrimary: "tw-bg-fg-accent-primary-strong",
contrast: "tw-bg-bg-white",
};
return [
...baseClasses,
...typeClasses[this.type()],
variantClass[this.variant()],
this.textColor(),
].join(" ");
});
}

View File

@@ -0,0 +1,48 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./berry.stories";
<Meta of={stories} />
```ts
import { BerryComponent } from "@bitwarden/components";
```
<Title />
<Description />
<Primary />
<Controls />
## Usage
### Status
- Use a status berry to indicate a new notification of a status change that is not related to a
specific count.
<Canvas of={stories.statusType} />
### Count
- Use a count berry with text to indicate item count information for multiple new notifications.
<Canvas of={stories.countType} />
### All Variants
<Canvas of={stories.AllVariants} />
## Count Behavior
- Counts of **1-99**: Display in a compact circular shape
- Counts of **100-999**: Display in a pill shape with padding
- Counts **over 999**: Display as "999+" to prevent overflow
## Accessibility
- Use berries as **supplemental visual indicators** alongside descriptive text
- Ensure sufficient color contrast with surrounding elements
- For screen readers, provide appropriate labels on parent elements that describe the berry's
meaning
- Berries are decorative; important information should not rely solely on the berry color

View File

@@ -0,0 +1,167 @@
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { BerryComponent } from "./berry.component";
export default {
title: "Component Library/Berry",
component: BerryComponent,
decorators: [
moduleMetadata({
imports: [BerryComponent],
}),
],
args: {
type: "count",
variant: "primary",
value: 5,
},
argTypes: {
type: {
control: "select",
options: ["status", "count"],
description: "The type of the berry, which determines its size and content",
table: {
category: "Inputs",
type: { summary: '"status" | "count"' },
defaultValue: { summary: '"count"' },
},
},
variant: {
control: "select",
options: ["primary", "subtle", "success", "warning", "danger", "accentPrimary", "contrast"],
description: "The visual style variant of the berry",
table: {
category: "Inputs",
type: { summary: "BerryVariant" },
defaultValue: { summary: "primary" },
},
},
value: {
control: "number",
description:
"Optional value to display for berries with type 'count'. Maximum displayed is 999, values above show '999+'. If undefined, a small small berry is shown. If 0 or negative, the berry is hidden.",
table: {
category: "Inputs",
type: { summary: "number | undefined" },
defaultValue: { summary: "undefined" },
},
},
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/branch/rKUVGKb7Kw3d6YGoQl6Ho7/Tailwind-Component-Library?node-id=38367-199458&p=f&m=dev",
},
},
} as Meta<BerryComponent>;
type Story = StoryObj<BerryComponent>;
export const Primary: Story = {
render: (args) => ({
props: args,
template: `<bit-berry [type]="type" [variant]="variant" [value]="value"></bit-berry>`,
}),
};
export const statusType: Story = {
render: (args) => ({
props: args,
template: `
<div class="tw-flex tw-items-center tw-gap-4">
<bit-berry [type]="'status'" variant="primary"></bit-berry>
<bit-berry [type]="'status'" variant="subtle"></bit-berry>
<bit-berry [type]="'status'" variant="success"></bit-berry>
<bit-berry [type]="'status'" variant="warning"></bit-berry>
<bit-berry [type]="'status'" variant="danger"></bit-berry>
<bit-berry [type]="'status'" variant="accentPrimary"></bit-berry>
<bit-berry [type]="'status'" variant="contrast"></bit-berry>
</div>
`,
}),
};
export const countType: Story = {
render: (args) => ({
props: args,
template: `
<div class="tw-flex tw-items-center tw-gap-4">
<bit-berry [value]="5"></bit-berry>
<bit-berry [value]="50"></bit-berry>
<bit-berry [value]="500"></bit-berry>
<bit-berry [value]="5000"></bit-berry>
</div>
`,
}),
};
export const AllVariants: Story = {
render: () => ({
template: `
<div class="tw-flex tw-flex-col tw-gap-4">
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Primary:</span>
<bit-berry type="status" variant="primary"></bit-berry>
<bit-berry variant="primary" [value]="5"></bit-berry>
<bit-berry variant="primary" [value]="50"></bit-berry>
<bit-berry variant="primary" [value]="500"></bit-berry>
<bit-berry variant="primary" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Subtle:</span>
<bit-berry type="status"variant="subtle"></bit-berry>
<bit-berry variant="subtle" [value]="5"></bit-berry>
<bit-berry variant="subtle" [value]="50"></bit-berry>
<bit-berry variant="subtle" [value]="500"></bit-berry>
<bit-berry variant="subtle" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Success:</span>
<bit-berry type="status" variant="success"></bit-berry>
<bit-berry variant="success" [value]="5"></bit-berry>
<bit-berry variant="success" [value]="50"></bit-berry>
<bit-berry variant="success" [value]="500"></bit-berry>
<bit-berry variant="success" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Warning:</span>
<bit-berry type="status" variant="warning"></bit-berry>
<bit-berry variant="warning" [value]="5"></bit-berry>
<bit-berry variant="warning" [value]="50"></bit-berry>
<bit-berry variant="warning" [value]="500"></bit-berry>
<bit-berry variant="warning" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Danger:</span>
<bit-berry type="status" variant="danger"></bit-berry>
<bit-berry variant="danger" [value]="5"></bit-berry>
<bit-berry variant="danger" [value]="50"></bit-berry>
<bit-berry variant="danger" [value]="500"></bit-berry>
<bit-berry variant="danger" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4">
<span class="tw-w-20">Accent primary:</span>
<bit-berry type="status" variant="accentPrimary"></bit-berry>
<bit-berry variant="accentPrimary" [value]="5"></bit-berry>
<bit-berry variant="accentPrimary" [value]="50"></bit-berry>
<bit-berry variant="accentPrimary" [value]="500"></bit-berry>
<bit-berry variant="accentPrimary" [value]="5000"></bit-berry>
</div>
<div class="tw-flex tw-items-center tw-gap-4 tw-bg-bg-dark">
<span class="tw-w-20 tw-text-fg-white">Contrast:</span>
<bit-berry type="status" variant="contrast"></bit-berry>
<bit-berry variant="contrast" [value]="5"></bit-berry>
<bit-berry variant="contrast" [value]="50"></bit-berry>
<bit-berry variant="contrast" [value]="500"></bit-berry>
<bit-berry variant="contrast" [value]="5000"></bit-berry>
</div>
</div>
`,
}),
};

View File

@@ -0,0 +1 @@
export * from "./berry.component";

View File

@@ -7,6 +7,7 @@ export * from "./avatar";
export * from "./badge-list";
export * from "./badge";
export * from "./banner";
export * from "./berry";
export * from "./breadcrumbs";
export * from "./button";
export * from "./callout";

View File

@@ -317,6 +317,7 @@ module.exports = {
base: ["1rem", "150%"],
sm: ["0.875rem", "150%"],
xs: [".75rem", "150%"],
xxs: [".5rem", "150%"],
},
container: {
"@5xl": "1100px",