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:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
3
libs/components/src/berry/berry.component.html
Normal file
3
libs/components/src/berry/berry.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
@if (type() === "status" || content()) {
|
||||
<span [class]="containerClasses()">{{ content() }}</span>
|
||||
}
|
||||
80
libs/components/src/berry/berry.component.ts
Normal file
80
libs/components/src/berry/berry.component.ts
Normal 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(" ");
|
||||
});
|
||||
}
|
||||
48
libs/components/src/berry/berry.mdx
Normal file
48
libs/components/src/berry/berry.mdx
Normal 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
|
||||
167
libs/components/src/berry/berry.stories.ts
Normal file
167
libs/components/src/berry/berry.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
1
libs/components/src/berry/index.ts
Normal file
1
libs/components/src/berry/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./berry.component";
|
||||
@@ -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";
|
||||
|
||||
@@ -317,6 +317,7 @@ module.exports = {
|
||||
base: ["1rem", "150%"],
|
||||
sm: ["0.875rem", "150%"],
|
||||
xs: [".75rem", "150%"],
|
||||
xxs: [".5rem", "150%"],
|
||||
},
|
||||
container: {
|
||||
"@5xl": "1100px",
|
||||
|
||||
Reference in New Issue
Block a user