mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
[CL-499][PM-14020] compact mode (#11796)
This commit is contained in:
parent
3521c54672
commit
a07b072196
@ -4843,5 +4843,11 @@
|
|||||||
},
|
},
|
||||||
"generatedPassword": {
|
"generatedPassword": {
|
||||||
"message": "Generated password"
|
"message": "Generated password"
|
||||||
|
},
|
||||||
|
"compactMode": {
|
||||||
|
"message": "Compact mode"
|
||||||
|
},
|
||||||
|
"beta": {
|
||||||
|
"message": "Beta"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { inject, Injectable } from "@angular/core";
|
||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { GlobalStateProvider, KeyDefinition, THEMING_DISK } from "@bitwarden/common/platform/state";
|
||||||
|
import { CompactModeService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
const COMPACT_MODE = new KeyDefinition<boolean>(THEMING_DISK, "compactMode", {
|
||||||
|
deserializer: (s) => s,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to persist Compact Mode to state / user settings.
|
||||||
|
**/
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class PopupCompactModeService implements CompactModeService {
|
||||||
|
private state = inject(GlobalStateProvider).get(COMPACT_MODE);
|
||||||
|
|
||||||
|
enabled$: Observable<boolean> = this.state.state$.pipe(map((state) => state ?? false));
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.enabled$.subscribe((enabled) => {
|
||||||
|
enabled
|
||||||
|
? document.body.classList.add("tw-bit-compact")
|
||||||
|
: document.body.classList.remove("tw-bit-compact");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEnabled(enabled: boolean) {
|
||||||
|
await this.state.update(() => enabled);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<footer
|
<footer
|
||||||
class="tw-p-4 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
class="tw-p-3 bit-compact:tw-p-2 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
||||||
>
|
>
|
||||||
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full tw-items-center">
|
||||||
<div class="tw-flex tw-justify-start tw-gap-2">
|
<div class="tw-flex tw-justify-start tw-gap-2">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<header
|
<header
|
||||||
class="tw-px-4 tw-py-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
class="tw-p-3 bit-compact:tw-p-2 tw-pl-4 bit-compact:tw-pl-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-bg-background-alt tw-border-transparent':
|
'tw-bg-background-alt tw-border-transparent':
|
||||||
this.background === 'alt' && !pageContentScrolled(),
|
this.background === 'alt' && !pageContentScrolled(),
|
||||||
@ -10,6 +10,7 @@
|
|||||||
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||||
<button
|
<button
|
||||||
|
class="-tw-ml-1"
|
||||||
bitIconButton="bwi-back"
|
bitIconButton="bwi-back"
|
||||||
type="button"
|
type="button"
|
||||||
*ngIf="showBackButton"
|
*ngIf="showBackButton"
|
||||||
|
@ -117,11 +117,7 @@ class MockCurrentAccountComponent {}
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "mock-search",
|
selector: "mock-search",
|
||||||
template: `
|
template: ` <bit-search placeholder="Search"> </bit-search> `,
|
||||||
<div class="tw-p-4">
|
|
||||||
<bit-search placeholder="Search"> </bit-search>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SearchModule],
|
imports: [SearchModule],
|
||||||
})
|
})
|
||||||
@ -410,6 +406,46 @@ export const PopupPageWithFooter: Story = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CompactMode: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /* HTML */ `
|
||||||
|
<div class="tw-flex tw-gap-6 tw-text-main">
|
||||||
|
<div id="regular-example">
|
||||||
|
<p>Relaxed</p>
|
||||||
|
<p class="example-label"></p>
|
||||||
|
<extension-container>
|
||||||
|
<mock-vault-subpage></mock-vault-subpage>
|
||||||
|
</extension-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="compact-example" class="tw-bit-compact">
|
||||||
|
<p>Compact</p>
|
||||||
|
<p class="example-label"></p>
|
||||||
|
<extension-container>
|
||||||
|
<mock-vault-subpage></mock-vault-subpage>
|
||||||
|
</extension-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
play: async (context) => {
|
||||||
|
const canvasEl = context.canvasElement;
|
||||||
|
const updateLabel = (containerId: string) => {
|
||||||
|
const compact = canvasEl.querySelector(
|
||||||
|
`#${containerId} [data-testid=popup-layout-scroll-region]`,
|
||||||
|
);
|
||||||
|
const label = canvasEl.querySelector(`#${containerId} .example-label`);
|
||||||
|
const percentVisible =
|
||||||
|
100 -
|
||||||
|
Math.round((100 * (compact.scrollHeight - compact.clientHeight)) / compact.scrollHeight);
|
||||||
|
label.textContent = `${percentVisible}% above the fold`;
|
||||||
|
};
|
||||||
|
updateLabel("compact-example");
|
||||||
|
updateLabel("regular-example");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const PoppedOut: Story = {
|
export const PoppedOut: Story = {
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt">
|
<main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt">
|
||||||
<div
|
<div
|
||||||
#nonScrollable
|
#nonScrollable
|
||||||
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid tw-p-3 bit-compact:tw-p-2"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-invisible': loading || nonScrollable.childElementCount === 0,
|
'tw-invisible !tw-p-0': loading || nonScrollable.childElementCount === 0,
|
||||||
'tw-border-secondary-300': scrolled(),
|
'tw-border-secondary-300': scrolled(),
|
||||||
'tw-border-transparent': !scrolled(),
|
'tw-border-transparent': !scrolled(),
|
||||||
}"
|
}"
|
||||||
@ -13,12 +13,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
|
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
|
||||||
|
data-testid="popup-layout-scroll-region"
|
||||||
(scroll)="handleScroll($event)"
|
(scroll)="handleScroll($event)"
|
||||||
[ngClass]="{ 'tw-invisible': loading }"
|
[ngClass]="{ 'tw-invisible': loading }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
|
class="tw-max-w-screen-sm tw-mx-auto tw-flex-1 tw-flex tw-flex-col tw-h-full tw-w-full"
|
||||||
[ngClass]="{ 'tw-p-3': !disablePadding }"
|
[ngClass]="{ 'tw-p-3 bit-compact:tw-p-2': !disablePadding }"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
|
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
|
||||||
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none">
|
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none">
|
||||||
<button
|
<button
|
||||||
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 tw-pt-3 tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
|
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 bit-compact:tw-pb-1 tw-pt-3 bit-compact:tw-pt-2 tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
|
||||||
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
||||||
[title]="button.label"
|
[title]="button.label"
|
||||||
[routerLink]="button.page"
|
[routerLink]="button.page"
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { flagEnabled } from "../platform/flags";
|
import { flagEnabled } from "../platform/flags";
|
||||||
|
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
|
||||||
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
|
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
|
||||||
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
|
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
|
||||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||||
@ -42,6 +43,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
|||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
private viewCacheService = inject(PopupViewCacheService);
|
private viewCacheService = inject(PopupViewCacheService);
|
||||||
|
private compactModeService = inject(PopupCompactModeService);
|
||||||
|
|
||||||
private lastActivity: Date;
|
private lastActivity: Date;
|
||||||
private activeUserId: UserId;
|
private activeUserId: UserId;
|
||||||
@ -96,6 +98,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
initPopupClosedListener();
|
initPopupClosedListener();
|
||||||
await this.viewCacheService.init();
|
await this.viewCacheService.init();
|
||||||
|
|
||||||
|
this.compactModeService.init();
|
||||||
|
|
||||||
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
||||||
// Clear them aggressively to make sure this doesn't occur
|
// Clear them aggressively to make sure this doesn't occur
|
||||||
await this.clearComponentStates();
|
await this.clearComponentStates();
|
||||||
|
@ -103,7 +103,7 @@ import {
|
|||||||
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { CompactModeService, DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
@ -123,6 +123,7 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen
|
|||||||
/* eslint-enable no-restricted-imports */
|
/* eslint-enable no-restricted-imports */
|
||||||
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
|
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
|
||||||
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
||||||
|
import { PopupCompactModeService } from "../../platform/popup/layout/popup-compact-mode.service";
|
||||||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||||
import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service";
|
import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service";
|
||||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||||
@ -581,6 +582,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ExtensionAnonLayoutWrapperDataService,
|
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CompactModeService,
|
||||||
|
useExisting: PopupCompactModeService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<app-current-account></app-current-account>
|
<app-current-account></app-current-account>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</popup-header>
|
</popup-header>
|
||||||
<div slot="above-scroll-area" class="tw-p-4" *ngIf="!(sendsLoading$ | async)">
|
<ng-container slot="above-scroll-area" *ngIf="!(sendsLoading$ | async)">
|
||||||
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
||||||
{{ "sendDisabledWarning" | i18n }}
|
{{ "sendDisabledWarning" | i18n }}
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<tools-send-search></tools-send-search>
|
<tools-send-search></tools-send-search>
|
||||||
<app-send-list-filters></app-send-list-filters>
|
<app-send-list-filters></app-send-list-filters>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</ng-container>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="listState === sendState.Empty"
|
*ngIf="listState === sendState.Empty"
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<bit-item-group>
|
<bit-item-group>
|
||||||
<cdk-virtual-scroll-viewport
|
<cdk-virtual-scroll-viewport
|
||||||
[itemSize]="ItemHeight"
|
[itemSize]="itemHeight$ | async"
|
||||||
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
|
||||||
>
|
>
|
||||||
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
<bit-item *cdkVirtualFor="let cipher of ciphers">
|
||||||
@ -30,7 +30,7 @@
|
|||||||
(click)="onViewCipher(cipher)"
|
(click)="onViewCipher(cipher)"
|
||||||
(dblclick)="launchCipher(cipher)"
|
(dblclick)="launchCipher(cipher)"
|
||||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
class="{{ ItemHeightClass }}"
|
class="{{ itemHeightClass }}"
|
||||||
>
|
>
|
||||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { booleanAttribute, Component, EventEmitter, Input, Output } from "@angular/core";
|
import { booleanAttribute, Component, EventEmitter, inject, Input, Output } from "@angular/core";
|
||||||
import { Router, RouterLink } from "@angular/router";
|
import { Router, RouterLink } from "@angular/router";
|
||||||
|
import { map } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -9,9 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
BitItemHeight,
|
|
||||||
BitItemHeightClass,
|
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
|
CompactModeService,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
@ -49,8 +49,25 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class VaultListItemsContainerComponent {
|
export class VaultListItemsContainerComponent {
|
||||||
protected ItemHeightClass = BitItemHeightClass;
|
private compactModeService = inject(CompactModeService);
|
||||||
protected ItemHeight = BitItemHeight;
|
|
||||||
|
/**
|
||||||
|
* The class used to set the height of a bit item's inner content.
|
||||||
|
*/
|
||||||
|
protected readonly itemHeightClass = `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.
|
||||||
|
*
|
||||||
|
* Default: 52px + 1px border + 6px bottom margin = 59px
|
||||||
|
*
|
||||||
|
* Compact mode: 52px + 1px border = 53px
|
||||||
|
*/
|
||||||
|
protected readonly itemHeight$ = this.compactModeService.enabled$.pipe(
|
||||||
|
map((enabled) => (enabled ? 53 : 59)),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timeout used to add a small delay when selecting a cipher to allow for double click to launch
|
* Timeout used to add a small delay when selecting a cipher to allow for double click to launch
|
||||||
|
@ -23,14 +23,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Show search & filters outside of the scroll area of the page -->
|
<!-- Show search & filters outside of the scroll area of the page -->
|
||||||
<div
|
<ng-container
|
||||||
slot="above-scroll-area"
|
slot="above-scroll-area"
|
||||||
class="tw-p-4"
|
|
||||||
*ngIf="vaultState !== VaultStateEnum.Empty && !(loading$ | async)"
|
*ngIf="vaultState !== VaultStateEnum.Empty && !(loading$ | async)"
|
||||||
>
|
>
|
||||||
<app-vault-v2-search> </app-vault-v2-search>
|
<app-vault-v2-search> </app-vault-v2-search>
|
||||||
<app-vault-list-filters></app-vault-list-filters>
|
<app-vault-list-filters></app-vault-list-filters>
|
||||||
</div>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
||||||
<div
|
<div
|
||||||
@ -61,7 +60,7 @@
|
|||||||
<div
|
<div
|
||||||
*ngIf="vaultState === null"
|
*ngIf="vaultState === null"
|
||||||
cdkVirtualScrollingElement
|
cdkVirtualScrollingElement
|
||||||
class="tw-h-full tw-p-3 tw-styled-scrollbar"
|
class="tw-h-full tw-p-3 bit-compact:tw-p-2 tw-styled-scrollbar"
|
||||||
>
|
>
|
||||||
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
||||||
<app-vault-list-items-container
|
<app-vault-list-items-container
|
||||||
|
@ -18,6 +18,14 @@
|
|||||||
</bit-select>
|
</bit-select>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
|
<bit-form-control>
|
||||||
|
<input bitCheckbox formControlName="enableCompactMode" type="checkbox" />
|
||||||
|
<bit-label
|
||||||
|
>{{ "compactMode" | i18n }}
|
||||||
|
<span bitBadge variant="warning">{{ "beta" | i18n }}</span></bit-label
|
||||||
|
>
|
||||||
|
</bit-form-control>
|
||||||
|
|
||||||
<bit-form-control>
|
<bit-form-control>
|
||||||
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
|
<input bitCheckbox formControlName="enableBadgeCounter" type="checkbox" />
|
||||||
<bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label>
|
<bit-label>{{ "showNumberOfAutofillSuggestions" | i18n }}</bit-label>
|
||||||
|
@ -13,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
|
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
@ -43,10 +44,12 @@ describe("AppearanceV2Component", () => {
|
|||||||
const enableBadgeCounter$ = new BehaviorSubject<boolean>(true);
|
const enableBadgeCounter$ = new BehaviorSubject<boolean>(true);
|
||||||
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
|
const selectedTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Nord);
|
||||||
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
|
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
|
||||||
|
const enableCompactMode$ = new BehaviorSubject<boolean>(false);
|
||||||
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
|
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
|
||||||
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
|
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
|
||||||
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
|
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
|
||||||
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
|
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
setSelectedTheme.mockClear();
|
setSelectedTheme.mockClear();
|
||||||
@ -71,6 +74,10 @@ describe("AppearanceV2Component", () => {
|
|||||||
provide: BadgeSettingsServiceAbstraction,
|
provide: BadgeSettingsServiceAbstraction,
|
||||||
useValue: { enableBadgeCounter$, setEnableBadgeCounter },
|
useValue: { enableBadgeCounter$, setEnableBadgeCounter },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PopupCompactModeService,
|
||||||
|
useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(AppearanceV2Component, {
|
.overrideComponent(AppearanceV2Component, {
|
||||||
@ -94,6 +101,7 @@ describe("AppearanceV2Component", () => {
|
|||||||
enableFavicon: true,
|
enableFavicon: true,
|
||||||
enableBadgeCounter: true,
|
enableBadgeCounter: true,
|
||||||
theme: ThemeType.Nord,
|
theme: ThemeType.Nord,
|
||||||
|
enableCompactMode: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, DestroyRef, OnInit } from "@angular/core";
|
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
@ -12,12 +12,13 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CheckboxModule } from "@bitwarden/components";
|
import { BadgeModule, CheckboxModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
|
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
|
||||||
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
|
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
|
||||||
import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
|
import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
|
||||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
@ -35,14 +36,18 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
SelectModule,
|
SelectModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
|
BadgeModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppearanceV2Component implements OnInit {
|
export class AppearanceV2Component implements OnInit {
|
||||||
|
private compactModeService = inject(PopupCompactModeService);
|
||||||
|
|
||||||
appearanceForm = this.formBuilder.group({
|
appearanceForm = this.formBuilder.group({
|
||||||
enableFavicon: false,
|
enableFavicon: false,
|
||||||
enableBadgeCounter: true,
|
enableBadgeCounter: true,
|
||||||
theme: ThemeType.System,
|
theme: ThemeType.System,
|
||||||
enableAnimations: true,
|
enableAnimations: true,
|
||||||
|
enableCompactMode: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** To avoid flashes of inaccurate values, only show the form after the entire form is populated. */
|
/** To avoid flashes of inaccurate values, only show the form after the entire form is populated. */
|
||||||
@ -75,6 +80,7 @@ export class AppearanceV2Component implements OnInit {
|
|||||||
const enableAnimations = await firstValueFrom(
|
const enableAnimations = await firstValueFrom(
|
||||||
this.animationControlService.enableRoutingAnimation$,
|
this.animationControlService.enableRoutingAnimation$,
|
||||||
);
|
);
|
||||||
|
const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$);
|
||||||
|
|
||||||
// Set initial values for the form
|
// Set initial values for the form
|
||||||
this.appearanceForm.setValue({
|
this.appearanceForm.setValue({
|
||||||
@ -82,6 +88,7 @@ export class AppearanceV2Component implements OnInit {
|
|||||||
enableBadgeCounter,
|
enableBadgeCounter,
|
||||||
theme,
|
theme,
|
||||||
enableAnimations,
|
enableAnimations,
|
||||||
|
enableCompactMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.formLoading = false;
|
this.formLoading = false;
|
||||||
@ -109,6 +116,12 @@ export class AppearanceV2Component implements OnInit {
|
|||||||
.subscribe((enableBadgeCounter) => {
|
.subscribe((enableBadgeCounter) => {
|
||||||
void this.updateAnimations(enableBadgeCounter);
|
void this.updateAnimations(enableBadgeCounter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.appearanceForm.controls.enableCompactMode.valueChanges
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((enableCompactMode) => {
|
||||||
|
void this.updateCompactMode(enableCompactMode);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFavicon(enableFavicon: boolean) {
|
async updateFavicon(enableFavicon: boolean) {
|
||||||
@ -127,4 +140,8 @@ export class AppearanceV2Component implements OnInit {
|
|||||||
async updateAnimations(enableAnimations: boolean) {
|
async updateAnimations(enableAnimations: boolean) {
|
||||||
await this.animationControlService.setEnableRoutingAnimation(enableAnimations);
|
await this.animationControlService.setEnableRoutingAnimation(enableAnimations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCompactMode(enableCompactMode: boolean) {
|
||||||
|
await this.compactModeService.setEnabled(enableCompactMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
host: {
|
host: {
|
||||||
class:
|
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 {}
|
export class CardComponent {}
|
||||||
|
@ -70,7 +70,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
|||||||
@HostBinding("class")
|
@HostBinding("class")
|
||||||
get classList() {
|
get classList() {
|
||||||
return ["tw-block"]
|
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");
|
.concat(this.readOnly ? [] : "tw-pt-2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ export * from "./radio-button";
|
|||||||
export * from "./search";
|
export * from "./search";
|
||||||
export * from "./section";
|
export * from "./section";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
|
export * from "./shared/compact-mode.service";
|
||||||
export * from "./table";
|
export * from "./table";
|
||||||
export * from "./tabs";
|
export * from "./tabs";
|
||||||
export * from "./toast";
|
export * from "./toast";
|
||||||
|
@ -1,3 +1 @@
|
|||||||
export * from "./item.module";
|
export * from "./item.module";
|
||||||
|
|
||||||
export { BitItemHeight, BitItemHeightClass } from "./item.component";
|
|
||||||
|
@ -17,7 +17,7 @@ import { TypographyModule } from "../typography";
|
|||||||
templateUrl: `item-content.component.html`,
|
templateUrl: `item-content.component.html`,
|
||||||
host: {
|
host: {
|
||||||
class:
|
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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
<div
|
<bit-item-action class="item-main-content tw-flex tw-flex-1 tw-overflow-hidden">
|
||||||
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>
|
<ng-content></ng-content>
|
||||||
</bit-item-action>
|
</bit-item-action>
|
||||||
|
|
||||||
@ -17,4 +9,3 @@
|
|||||||
>
|
>
|
||||||
<ng-content select="[slot=end]"></ng-content>
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
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 { A11yRowDirective } from "../a11y/a11y-row.directive";
|
||||||
|
|
||||||
import { ItemActionComponent } from "./item-action.component";
|
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({
|
@Component({
|
||||||
selector: "bit-item",
|
selector: "bit-item",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -26,6 +18,10 @@ export const BitItemHeight = 59;
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
templateUrl: "item.component.html",
|
templateUrl: "item.component.html",
|
||||||
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
|
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 {
|
export class ItemComponent extends A11yRowDirective {
|
||||||
/**
|
/**
|
||||||
@ -40,4 +36,14 @@ export class ItemComponent extends A11yRowDirective {
|
|||||||
onFocusOut() {
|
onFocusOut() {
|
||||||
this.focusVisibleWithin.set(false);
|
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 { ItemActionComponent } from "./item-action.component";
|
||||||
import { ItemContentComponent } from "./item-content.component";
|
import { ItemContentComponent } from "./item-content.component";
|
||||||
import { ItemGroupComponent } from "./item-group.component";
|
import { ItemGroupComponent } from "./item-group.component";
|
||||||
import { ItemComponent, BitItemHeight, BitItemHeightClass } from "./item.component";
|
import { ItemComponent } from "./item.component";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Component Library/Item",
|
title: "Component Library/Item",
|
||||||
@ -152,10 +152,7 @@ export const TextOverflow: Story = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultipleActionList: Story = {
|
const multipleActionListTemplate = /*html*/ `
|
||||||
render: (args) => ({
|
|
||||||
props: args,
|
|
||||||
template: /*html*/ `
|
|
||||||
<bit-item-group aria-label="Multiple Action List">
|
<bit-item-group aria-label="Multiple Action List">
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<button bit-item-content>
|
<button bit-item-content>
|
||||||
@ -272,7 +269,12 @@ export const MultipleActionList: Story = {
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
</bit-item-group>
|
</bit-item-group>
|
||||||
`,
|
`;
|
||||||
|
|
||||||
|
export const MultipleActionList: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
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 = {
|
export const VirtualScrolling: Story = {
|
||||||
render: (_args) => ({
|
render: (_args) => ({
|
||||||
props: {
|
props: {
|
||||||
data: Array.from(Array(100000).keys()),
|
data: Array.from(Array(100000).keys()),
|
||||||
itemSize: BitItemHeight,
|
|
||||||
itemClass: BitItemHeightClass,
|
|
||||||
},
|
},
|
||||||
template: /*html*/ `
|
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-group aria-label="Virtual Scrolling">
|
||||||
<bit-item *cdkVirtualFor="let item of data">
|
<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>
|
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -9,7 +9,8 @@ import { Component, Input } from "@angular/core";
|
|||||||
template: `
|
template: `
|
||||||
<section
|
<section
|
||||||
[ngClass]="{
|
[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>
|
<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} />
|
@ -175,5 +175,9 @@ module.exports = {
|
|||||||
addVariant(state, [`&:${state}`, `&.test-${state}`]);
|
addVariant(state, [`&:${state}`, `&.test-${state}`]);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
/** Plugin for compact mode */
|
||||||
|
plugin(function ({ addVariant }) {
|
||||||
|
addVariant("bit-compact", ".bit-compact &");
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user