mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[CL-499][PM-14020] compact mode (#11796)
This commit is contained in:
@@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
class:
|
||||
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 tw-px-3",
|
||||
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 bit-compact:tw-py-3 tw-px-3 bit-compact:tw-px-2",
|
||||
},
|
||||
})
|
||||
export class CardComponent {}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
@HostBinding("class")
|
||||
get classList() {
|
||||
return ["tw-block"]
|
||||
.concat(this.disableMargin ? [] : ["tw-mb-4"])
|
||||
.concat(this.disableMargin ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
|
||||
.concat(this.readOnly ? [] : "tw-pt-2");
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export * from "./radio-button";
|
||||
export * from "./search";
|
||||
export * from "./section";
|
||||
export * from "./select";
|
||||
export * from "./shared/compact-mode.service";
|
||||
export * from "./table";
|
||||
export * from "./tabs";
|
||||
export * from "./toast";
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
export * from "./item.module";
|
||||
|
||||
export { BitItemHeight, BitItemHeightClass } from "./item.component";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TypographyModule } from "../typography";
|
||||
templateUrl: `item-content.component.html`,
|
||||
host: {
|
||||
class:
|
||||
"fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
|
||||
"fvw-target tw-outline-none tw-text-main hover:tw-text-main tw-no-underline hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 bit-compact:tw-py-1.5 bit-compact:tw-px-2 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
<div
|
||||
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg tw-mb-1.5"
|
||||
[ngClass]="
|
||||
focusVisibleWithin()
|
||||
? 'tw-z-10 tw-rounded tw-outline-none tw-ring-2 tw-ring-primary-600 tw-border-transparent'
|
||||
: 'tw-border-b-shadow'
|
||||
"
|
||||
>
|
||||
<bit-item-action class="item-main-content tw-block tw-flex-1 tw-overflow-hidden">
|
||||
<ng-content></ng-content>
|
||||
</bit-item-action>
|
||||
<bit-item-action class="item-main-content tw-flex tw-flex-1 tw-overflow-hidden">
|
||||
<ng-content></ng-content>
|
||||
</bit-item-action>
|
||||
|
||||
<div
|
||||
#endSlot
|
||||
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
|
||||
[hidden]="endSlot.childElementCount === 0"
|
||||
>
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
<div
|
||||
#endSlot
|
||||
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
|
||||
[hidden]="endSlot.childElementCount === 0"
|
||||
>
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
|
||||
import { A11yRowDirective } from "../a11y/a11y-row.directive";
|
||||
|
||||
import { ItemActionComponent } from "./item-action.component";
|
||||
|
||||
/**
|
||||
* The class used to set the height of a bit item's inner content.
|
||||
*/
|
||||
export const BitItemHeightClass = `tw-h-[52px]`;
|
||||
|
||||
/**
|
||||
* The height of a bit item in pixels. Includes any margin, padding, or border. Used by the virtual scroll
|
||||
* to estimate how many items can be displayed at once and how large the virtual container should be.
|
||||
* Needs to be updated if the item height or spacing changes.
|
||||
*
|
||||
* 52px + 6px bottom margin + 1px border = 59px
|
||||
*/
|
||||
export const BitItemHeight = 59;
|
||||
|
||||
@Component({
|
||||
selector: "bit-item",
|
||||
standalone: true,
|
||||
@@ -26,6 +18,10 @@ export const BitItemHeight = 59;
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "item.component.html",
|
||||
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
|
||||
host: {
|
||||
class:
|
||||
"tw-block tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0",
|
||||
},
|
||||
})
|
||||
export class ItemComponent extends A11yRowDirective {
|
||||
/**
|
||||
@@ -40,4 +36,14 @@ export class ItemComponent extends A11yRowDirective {
|
||||
onFocusOut() {
|
||||
this.focusVisibleWithin.set(false);
|
||||
}
|
||||
|
||||
@HostBinding("class") get classList(): string[] {
|
||||
return [
|
||||
this.focusVisibleWithin()
|
||||
? "tw-z-10 tw-rounded tw-outline-none tw-ring-2 bit-compact:tw-ring-inset tw-ring-primary-600 tw-border-transparent".split(
|
||||
" ",
|
||||
)
|
||||
: "tw-border-b-shadow",
|
||||
].flat();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
import { ItemActionComponent } from "./item-action.component";
|
||||
import { ItemContentComponent } from "./item-content.component";
|
||||
import { ItemGroupComponent } from "./item-group.component";
|
||||
import { ItemComponent, BitItemHeight, BitItemHeightClass } from "./item.component";
|
||||
import { ItemComponent } from "./item.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Item",
|
||||
@@ -152,127 +152,129 @@ export const TextOverflow: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
const multipleActionListTemplate = /*html*/ `
|
||||
<bit-item-group aria-label="Multiple Action List">
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
`;
|
||||
|
||||
export const MultipleActionList: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-item-group aria-label="Multiple Action List">
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
<bit-item>
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
Foo
|
||||
<span slot="secondary">Bar</span>
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-clone" size="small"></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
`,
|
||||
template: multipleActionListTemplate,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -346,18 +348,27 @@ export const SingleActionWithBadge: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const CompactMode: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<div class="tw-bit-compact">
|
||||
${multipleActionListTemplate}
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const VirtualScrolling: Story = {
|
||||
render: (_args) => ({
|
||||
props: {
|
||||
data: Array.from(Array(100000).keys()),
|
||||
itemSize: BitItemHeight,
|
||||
itemClass: BitItemHeightClass,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<cdk-virtual-scroll-viewport [itemSize]="itemSize" class="tw-h-[500px]">
|
||||
<cdk-virtual-scroll-viewport [itemSize]="59" class="tw-h-[500px]">
|
||||
<bit-item-group aria-label="Virtual Scrolling">
|
||||
<bit-item *cdkVirtualFor="let item of data">
|
||||
<button bit-item-content [ngClass]="itemClass">
|
||||
<button bit-item-content>
|
||||
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||
{{ item }}
|
||||
</button>
|
||||
|
||||
@@ -9,7 +9,8 @@ import { Component, Input } from "@angular/core";
|
||||
template: `
|
||||
<section
|
||||
[ngClass]="{
|
||||
'tw-mb-6 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12': !disableMargin,
|
||||
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
|
||||
!disableMargin,
|
||||
}"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
|
||||
11
libs/components/src/shared/compact-mode.service.ts
Normal file
11
libs/components/src/shared/compact-mode.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
/** Global config for the Bitwarden Design System */
|
||||
export abstract class CompactModeService {
|
||||
/**
|
||||
* When true, enables "compact mode".
|
||||
*
|
||||
* Component authors can also hook into compact mode with the `bit-compact:` Tailwind variant.
|
||||
**/
|
||||
enabled$: Observable<boolean>;
|
||||
}
|
||||
45
libs/components/src/stories/compact-mode.mdx
Normal file
45
libs/components/src/stories/compact-mode.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import * as itemStories from "../item/item.stories";
|
||||
import * as popupLayoutStories from "../../../../apps/browser/src/platform/popup/layout/popup-layout.stories";
|
||||
|
||||
<Meta title="Documentation/Compact Mode" />
|
||||
|
||||
# Compact Mode
|
||||
|
||||
The Bitwarden browser extension has a global compact mode setting for users who prefer a more
|
||||
information-dense UI.
|
||||
|
||||
## Tailwind
|
||||
|
||||
Component authors can hook into this setting with the `bit-compact` Tailwind variant. In the
|
||||
following example, the paragraph's padding is reduced when compact mode is enabled.
|
||||
|
||||
```html
|
||||
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
|
||||
|
||||
<div class="tw-bit-compact">
|
||||
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
|
||||
|
||||
<div class="tw-bit-compact">
|
||||
<p class="tw-bg-primary-100 tw-p-4 bit-compact:tw-p-0.5">Lorem impsum doggo dolor...</p>
|
||||
</div>
|
||||
|
||||
## Service
|
||||
|
||||
To get/set compact mode in TypeScript, the `CompactModeService` exposes a `enabled$` observable.
|
||||
However, styling with the Tailwind variant should be used when possible as it is more performant.
|
||||
|
||||
## Examples
|
||||
|
||||
### [Popup Layout](?path=/story/browser-popup-layout--compact-mode)
|
||||
|
||||
<Story autoplay={true} of={popupLayoutStories.CompactMode} />
|
||||
|
||||
### [Item](?path=/story/component-library-item--compact-mode)
|
||||
|
||||
<Story of={itemStories.CompactMode} />
|
||||
Reference in New Issue
Block a user