From ea55aaaede0b917e6e582613503a3b5fbd278496 Mon Sep 17 00:00:00 2001 From: Leslie Xiong Date: Wed, 11 Feb 2026 12:21:33 -0500 Subject: [PATCH] [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 --- .../popup-tab-navigation.component.html | 4 +- .../layout/popup-tab-navigation.component.ts | 4 +- .../components/src/berry/berry.component.html | 3 + libs/components/src/berry/berry.component.ts | 80 +++++++++ libs/components/src/berry/berry.mdx | 48 +++++ libs/components/src/berry/berry.stories.ts | 167 ++++++++++++++++++ libs/components/src/berry/index.ts | 1 + libs/components/src/index.ts | 1 + libs/components/tailwind.config.base.js | 1 + 9 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 libs/components/src/berry/berry.component.html create mode 100644 libs/components/src/berry/berry.component.ts create mode 100644 libs/components/src/berry/berry.mdx create mode 100644 libs/components/src/berry/berry.stories.ts create mode 100644 libs/components/src/berry/index.ts diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html index e04d302ea2c..919d01f2d51 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -27,8 +27,8 @@ {{ button.label | i18n }} -
-
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts index 5a40b72daff..f873d25641b 100644 --- a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -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", }, diff --git a/libs/components/src/berry/berry.component.html b/libs/components/src/berry/berry.component.html new file mode 100644 index 00000000000..2a05f534843 --- /dev/null +++ b/libs/components/src/berry/berry.component.html @@ -0,0 +1,3 @@ +@if (type() === "status" || content()) { + {{ content() }} +} diff --git a/libs/components/src/berry/berry.component.ts b/libs/components/src/berry/berry.component.ts new file mode 100644 index 00000000000..8e58b888f39 --- /dev/null +++ b/libs/components/src/berry/berry.component.ts @@ -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("primary"); + protected readonly value = input(); + 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(" "); + }); +} diff --git a/libs/components/src/berry/berry.mdx b/libs/components/src/berry/berry.mdx new file mode 100644 index 00000000000..b79ed35cac8 --- /dev/null +++ b/libs/components/src/berry/berry.mdx @@ -0,0 +1,48 @@ +import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks"; + +import * as stories from "./berry.stories"; + + + +```ts +import { BerryComponent } from "@bitwarden/components"; +``` + + +<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 diff --git a/libs/components/src/berry/berry.stories.ts b/libs/components/src/berry/berry.stories.ts new file mode 100644 index 00000000000..0b71e7259d8 --- /dev/null +++ b/libs/components/src/berry/berry.stories.ts @@ -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> + `, + }), +}; diff --git a/libs/components/src/berry/index.ts b/libs/components/src/berry/index.ts new file mode 100644 index 00000000000..8f85908653e --- /dev/null +++ b/libs/components/src/berry/index.ts @@ -0,0 +1 @@ +export * from "./berry.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index d92e0770e49..d0bb8576095 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -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"; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index d8220c39ff8..5de00fac34f 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -317,6 +317,7 @@ module.exports = { base: ["1rem", "150%"], sm: ["0.875rem", "150%"], xs: [".75rem", "150%"], + xxs: [".5rem", "150%"], }, container: { "@5xl": "1100px",