1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Jacob Fink
2022-12-20 12:59:34 -05:00
63 changed files with 585 additions and 350 deletions

View File

@@ -15,26 +15,9 @@ defaults:
shell: bash shell: bash
jobs: jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
steps:
- name: Checkout repo
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Branch check
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then
echo "==================================="
echo "[!] Can only increase rollout from the 'rc' or 'hotfix-rc-desktop' branches"
echo "==================================="
exit 1
fi
rollout: rollout:
name: Update Rollout Percentage name: Update Rollout Percentage
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: setup
steps: steps:
- name: Login to Azure - name: Login to Azure
uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2022.12.0", "version": "2022.12.1",
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack",

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2022.12.0", "version": "2022.12.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2022.12.0", "version": "2022.12.1",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -64,6 +64,8 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { EnvironmentComponent } from "./accounts/environment.component"; import { EnvironmentComponent } from "./accounts/environment.component";
import { HintComponent } from "./accounts/hint.component"; import { HintComponent } from "./accounts/hint.component";
@@ -202,6 +204,8 @@ registerLocaleData(localeZhTw, "zh-TW");
CipherRowComponent, CipherRowComponent,
VaultItemsComponent, VaultItemsComponent,
CollectionsComponent, CollectionsComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
CurrentTabComponent, CurrentTabComponent,
EnvironmentComponent, EnvironmentComponent,
ExcludedDomainsComponent, ExcludedDomainsComponent,

View File

@@ -193,7 +193,7 @@ p.lead {
font-size: 8px; font-size: 8px;
@include themify($themes) { @include themify($themes) {
color: themed("mutedColor") !important; color: themed("passwordCountText") !important;
} }
} }

View File

@@ -109,6 +109,7 @@ $themes: (
logoSuffix: "dark", logoSuffix: "dark",
passwordNumberColor: #007fde, passwordNumberColor: #007fde,
passwordSpecialColor: #c40800, passwordSpecialColor: #c40800,
passwordCountText: #212529,
calloutBorderColor: $border-color-dark, calloutBorderColor: $border-color-dark,
calloutBackgroundColor: $box-background-color, calloutBackgroundColor: $box-background-color,
toastTextColor: #ffffff, toastTextColor: #ffffff,
@@ -170,6 +171,7 @@ $themes: (
logoSuffix: "white", logoSuffix: "white",
passwordNumberColor: #6f9df1, passwordNumberColor: #6f9df1,
passwordSpecialColor: #ff8d85, passwordSpecialColor: #ff8d85,
passwordCountText: #ffffff,
calloutBorderColor: #4c525f, calloutBorderColor: #4c525f,
calloutBackgroundColor: #3c424e, calloutBackgroundColor: #3c424e,
toastTextColor: #1f242e, toastTextColor: #1f242e,
@@ -230,6 +232,7 @@ $themes: (
logoSuffix: "white", logoSuffix: "white",
passwordNumberColor: $nord8, passwordNumberColor: $nord8,
passwordSpecialColor: $nord12, passwordSpecialColor: $nord12,
passwordCountText: $nord5,
calloutBorderColor: $nord0, calloutBorderColor: $nord0,
calloutBackgroundColor: $nord2, calloutBackgroundColor: $nord2,
toastTextColor: #ffffff, toastTextColor: #ffffff,
@@ -290,6 +293,7 @@ $themes: (
logoSuffix: "white", logoSuffix: "white",
passwordNumberColor: $solarizedDarkCyan, passwordNumberColor: $solarizedDarkCyan,
passwordSpecialColor: $solarizedDarkYellow, passwordSpecialColor: $solarizedDarkYellow,
passwordCountText: $solarizedDarkBase2,
calloutBorderColor: $solarizedDarkBase03, calloutBorderColor: $solarizedDarkBase03,
calloutBackgroundColor: $solarizedDarkBase01, calloutBackgroundColor: $solarizedDarkBase01,
toastTextColor: #ffffff, toastTextColor: #ffffff,

View File

@@ -13,9 +13,10 @@
<div class="box-content-row box-content-row-flex" *ngFor="let h of history"> <div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main"> <div class="row-main">
<div class="row-main-content"> <div class="row-main-content">
<span class="text monospaced no-ellipsis"> <span
{{ h.password }} class="text monospaced no-ellipsis"
</span> [innerHTML]="h.password | colorPassword"
></span>
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span> <span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
</div> </div>
</div> </div>

View File

@@ -58,6 +58,9 @@ import localeZhCn from "@angular/common/locales/zh-Hans";
import localeZhTw from "@angular/common/locales/zh-Hant"; import localeZhTw from "@angular/common/locales/zh-Hant";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component"; import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component";
import { DeleteAccountComponent } from "./accounts/delete-account.component"; import { DeleteAccountComponent } from "./accounts/delete-account.component";
import { EnvironmentComponent } from "./accounts/environment.component"; import { EnvironmentComponent } from "./accounts/environment.component";
@@ -170,6 +173,8 @@ registerLocaleData(localeZhTw, "zh-TW");
AttachmentsComponent, AttachmentsComponent,
VaultItemsComponent, VaultItemsComponent,
CollectionsComponent, CollectionsComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
DeleteAccountComponent, DeleteAccountComponent,
EnvironmentComponent, EnvironmentComponent,
ExportComponent, ExportComponent,

View File

@@ -10,7 +10,7 @@
<div class="box-content-row box-content-row-flex" *ngFor="let h of history"> <div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main"> <div class="row-main">
<div <div
class="generated-wrapper monospaced" class="password-wrapper monospaced"
appSelectCopy appSelectCopy
[innerHTML]="h.password | colorPassword" [innerHTML]="h.password | colorPassword"
></div> ></div>

View File

@@ -9,9 +9,7 @@
<div class="box-content condensed"> <div class="box-content condensed">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history"> <div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main"> <div class="row-main">
<span class="text monospaced"> <span class="text monospaced" [innerHTML]="h.password | colorPassword"></span>
{{ h.password }}
</span>
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span> <span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">

View File

@@ -17,12 +17,16 @@
{{ field.value || "&nbsp;" }} {{ field.value || "&nbsp;" }}
</div> </div>
<div *ngIf="field.type === fieldType.Hidden"> <div *ngIf="field.type === fieldType.Hidden">
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
<span <span
*ngIf="field.showValue" *ngIf="field.showValue && !field.showCount"
class="monospaced show-whitespace" class="monospaced show-whitespace"
[innerHTML]="field.value | colorPassword" [innerHTML]="field.value | colorPassword"
></span> ></span>
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span> <span
*ngIf="field.showValue && field.showCount"
[innerHTML]="field.value | colorPasswordCount"
></span>
</div> </div>
<div *ngIf="field.type === fieldType.Boolean"> <div *ngIf="field.type === fieldType.Boolean">
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i> <i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
@@ -41,7 +45,18 @@
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span> <span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
</div> </div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons action-buttons-fixed">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
(click)="toggleFieldCount(field)"
[attr.aria-pressed]="field.showCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button <button
type="button" type="button"
class="row-btn" class="row-btn"

View File

@@ -50,11 +50,15 @@
{{ cipher.login.maskedPassword }} {{ cipher.login.maskedPassword }}
</div> </div>
<div <div
*ngIf="showPassword" *ngIf="showPassword && !showPasswordCount"
class="monospaced generated-wrapper" class="monospaced password-wrapper"
appSelectCopy appSelectCopy
[innerHTML]="cipher.login.password | colorPassword" [innerHTML]="cipher.login.password | colorPassword"
></div> ></div>
<div
*ngIf="showPassword && showPasswordCount"
[innerHTML]="cipher.login.password | colorPasswordCount"
></div>
</div> </div>
<div class="action-buttons" *ngIf="cipher.viewPassword"> <div class="action-buttons" *ngIf="cipher.viewPassword">
<button <button
@@ -77,6 +81,18 @@
aria-hidden="true" aria-hidden="true"
></i> ></i>
</button> </button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
(click)="togglePasswordCount()"
*ngIf="showPassword"
[attr.aria-pressed]="showPasswordCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button <button
type="button" type="button"
class="row-btn" class="row-btn"

View File

@@ -2057,5 +2057,9 @@
}, },
"logInWithAnotherDevice": { "logInWithAnotherDevice": {
"message": "Log in with another device" "message": "Log in with another device"
},
"toggleCharacterCount": {
"message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password."
} }
} }

View File

@@ -366,6 +366,11 @@
display: flex; display: flex;
margin-left: 5px; margin-left: 5px;
&.action-buttons-fixed {
align-self: start;
margin-top: 2px;
}
.row-btn { .row-btn {
@extend .icon-btn; @extend .icon-btn;
} }

View File

@@ -215,8 +215,8 @@ p.lead {
} }
} }
.generated-wrapper { .password-wrapper {
word-break: break-all; overflow-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
min-width: 0; min-width: 0;
} }
@@ -233,6 +233,30 @@ p.lead {
} }
} }
.password-character {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 30px;
height: 36px;
font-weight: 600;
&:nth-child(odd) {
@include themify($themes) {
background-color: themed("backgroundColorAlt2");
}
}
}
.password-count {
white-space: nowrap;
font-size: 8px;
@include themify($themes) {
color: themed("passwordCountText") !important;
}
}
#duo-frame { #duo-frame {
background: url("../images/loading.svg") 0 0 no-repeat; background: url("../images/loading.svg") 0 0 no-repeat;
height: 330px; height: 330px;

View File

@@ -88,6 +88,7 @@ $themes: (
logoSuffix: "dark", logoSuffix: "dark",
passwordNumberColor: #007fde, passwordNumberColor: #007fde,
passwordSpecialColor: #c40800, passwordSpecialColor: #c40800,
passwordCountText: #212529,
calloutBorderColor: $border-color-dark, calloutBorderColor: $border-color-dark,
calloutBackgroundColor: $background-color, calloutBackgroundColor: $background-color,
accountSwitcherBackgroundColor: $background-color, accountSwitcherBackgroundColor: $background-color,
@@ -142,6 +143,7 @@ $themes: (
logoSuffix: "white", logoSuffix: "white",
passwordNumberColor: #52bdfb, passwordNumberColor: #52bdfb,
passwordSpecialColor: #ff7c70, passwordSpecialColor: #ff7c70,
passwordCountText: #ffffff,
calloutBorderColor: #2f2f2f, calloutBorderColor: #2f2f2f,
calloutBackgroundColor: #363636, calloutBackgroundColor: #363636,
accountSwitcherBackgroundColor: #2f2f2f, accountSwitcherBackgroundColor: #2f2f2f,
@@ -196,6 +198,7 @@ $themes: (
logoSuffix: "white", logoSuffix: "white",
passwordNumberColor: $nord8, passwordNumberColor: $nord8,
passwordSpecialColor: $nord12, passwordSpecialColor: $nord12,
passwordCountText: $nord5,
calloutBorderColor: $nord1, calloutBorderColor: $nord1,
calloutBackgroundColor: $nord2, calloutBackgroundColor: $nord2,
accountSwitcherBackgroundColor: $nord0, accountSwitcherBackgroundColor: $nord0,

View File

@@ -77,18 +77,12 @@
<bit-label>{{ "masterPass" | i18n }}</bit-label> <bit-label>{{ "masterPass" | i18n }}</bit-label>
<input <input
id="login_input_master-password" id="login_input_master-password"
type="password"
bitInput bitInput
[type]="showPassword ? 'text' : 'password'"
formControlName="masterPassword" formControlName="masterPassword"
appAutofocus appAutofocus
/> />
<button type="button" bitSuffix bitButton (click)="togglePassword()"> <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</bit-form-field> </bit-form-field>
<a class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" (click)="setFormValues()">{{ <a class="-tw-mt-2" routerLink="/hint" (mousedown)="goToHint()" (click)="setFormValues()">{{
"getMasterPasswordHint" | i18n "getMasterPasswordHint" | i18n

View File

@@ -34,16 +34,16 @@
<input <input
id="register-form_input_master-password" id="register-form_input_master-password"
bitInput bitInput
[type]="showPassword ? 'text' : 'password'" type="password"
formControlName="masterPassword" formControlName="masterPassword"
/> />
<button type="button" bitSuffix bitButton (click)="togglePassword()"> <button
<i type="button"
aria-hidden="true" bitSuffix
class="bwi bwi-lg bwi-eye" bitIconButton
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }" bitPasswordInputToggle
></i> [(toggled)]="showPassword"
</button> ></button>
<bit-hint> <bit-hint>
<span class="tw-font-semibold">Important:</span> <span class="tw-font-semibold">Important:</span>
{{ "masterPassImportant" | i18n }} {{ "masterPassImportant" | i18n }}
@@ -65,16 +65,16 @@
<input <input
id="register-form_input_confirm-master-password" id="register-form_input_confirm-master-password"
bitInput bitInput
[type]="showPassword ? 'text' : 'password'" type="password"
formControlName="confirmMasterPassword" formControlName="confirmMasterPassword"
/> />
<button type="button" bitSuffix bitButton (click)="togglePassword()"> <button
<i type="button"
aria-hidden="true" bitSuffix
class="bwi bwi-lg bwi-eye" bitIconButton
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }" bitPasswordInputToggle
></i> [(toggled)]="showPassword"
</button> ></button>
</bit-form-field> </bit-form-field>
</div> </div>

View File

@@ -310,7 +310,7 @@ export class PeopleComponent
if ( if (
!user && !user &&
this.organization.planProductType === ProductType.Free && this.organization.planProductType === ProductType.Free &&
this.users.length === this.organization.seats this.allUsers.length === this.organization.seats
) { ) {
// Show org upgrade modal // Show org upgrade modal

View File

@@ -25,6 +25,7 @@ import {
TableModule, TableModule,
TabsModule, TabsModule,
ToggleGroupModule, ToggleGroupModule,
ColorPasswordModule,
} from "@bitwarden/components"; } from "@bitwarden/components";
// Register the locales for the application // Register the locales for the application
@@ -66,6 +67,8 @@ import "./locales";
TableModule, TableModule,
TabsModule, TabsModule,
ToggleGroupModule, ToggleGroupModule,
LinkModule,
ColorPasswordModule,
// Web specific // Web specific
], ],
@@ -97,6 +100,8 @@ import "./locales";
TableModule, TableModule,
TabsModule, TabsModule,
ToggleGroupModule, ToggleGroupModule,
LinkModule,
ColorPasswordModule,
// Web specific // Web specific
], ],

View File

@@ -6,18 +6,10 @@
</app-callout> </app-callout>
<div class="card card-generated bg-light my-4"> <div class="card card-generated bg-light my-4">
<div class="card-body"> <div class="card-body">
<div <bit-color-password
*ngIf="type === 'password'" [password]="type === 'password' ? password : username"
class="generated-wrapper"
[innerHTML]="password | colorPassword"
appSelectCopy appSelectCopy
></div> ></bit-color-password>
<div
*ngIf="type === 'username'"
class="generated-wrapper"
[innerHTML]="username | colorPassword"
appSelectCopy
></div>
</div> </div>
</div> </div>
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading"> <div class="form-group" role="radiogroup" aria-labelledby="typeHeading">

View File

@@ -84,73 +84,41 @@
<br /> <br />
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted"> <ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
<div class="input-group"> <bit-form-field>
<bit-form-field class="tw-w-full">
<bit-label>{{ "filePassword" | i18n }}</bit-label> <bit-label>{{ "filePassword" | i18n }}</bit-label>
<input <input
bitInput bitInput
[type]="showFilePassword ? 'text' : 'password'" type="password"
id="filePassword" id="filePassword"
formControlName="filePassword" formControlName="filePassword"
name="password" name="password"
/> />
<div class="input-group-append">
<button <button
bitSuffix
bitButton
buttonType="secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showFilePassword"
(click)="toggleFilePassword()"
type="button" type="button"
> bitSuffix
<i bitIconButton
class="bwi bwi-lg" bitPasswordInputToggle
aria-hidden="true" [(toggled)]="showFilePassword"
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }" ></button>
></i> <bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</button>
</div>
</bit-form-field> </bit-form-field>
<div class="small text-muted"> <bit-form-field>
{{ "exportPasswordDescription" | i18n }}
</div>
</div>
<div class="input-group tw-mt-4">
<bit-form-field class="tw-w-full">
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label> <bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input <input
bitInput bitInput
[type]="showConfirmFilePassword ? 'text' : 'password'" type="password"
id="confirmFilePassword" id="confirmFilePassword"
formControlName="confirmFilePassword" formControlName="confirmFilePassword"
name="confirmFilePassword" name="confirmFilePassword"
/> />
<div class="input-group-append">
<button <button
bitSuffix
bitButton
buttonType="secondary"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showConfirmFilePassword"
(click)="toggleConfirmFilePassword()"
type="button" type="button"
> bitSuffix
<i bitIconButton
class="bwi bwi-lg" bitPasswordInputToggle
aria-hidden="true" [(toggled)]="showFilePassword"
[ngClass]="{ ></button>
'bwi-eye': !showConfirmFilePassword,
'bwi-eye-slash': showConfirmFilePassword
}"
></i>
</button>
</div>
</bit-form-field> </bit-form-field>
</div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -23,6 +23,7 @@ import { UserVerificationPromptComponent } from "../../components/user-verificat
export class ExportComponent extends BaseExportComponent { export class ExportComponent extends BaseExportComponent {
organizationId: string; organizationId: string;
encryptedExportType = EncryptedExportType; encryptedExportType = EncryptedExportType;
protected showFilePassword: boolean;
constructor( constructor(
cryptoService: CryptoService, cryptoService: CryptoService,

View File

@@ -14,32 +14,17 @@
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5" class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5"
> >
{{ "confirmVaultImportDesc" | i18n }} {{ "confirmVaultImportDesc" | i18n }}
<bit-form-field class="tw-w-full tw-pt-3.5"> <bit-form-field class="tw-pt-3.5">
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label> <bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input <input
bitInput bitInput
required type="password"
[type]="showFilePassword ? 'text' : 'password'"
name="filePassword" name="filePassword"
[formControl]="filePassword" [formControl]="filePassword"
appAutofocus appAutofocus
appInputVerbatim appInputVerbatim
/> />
<button <button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
bitSuffix
bitButton
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showFilePassword"
(click)="toggleFilePassword()"
type="button"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showFilePassword, 'bwi-eye-slash': showFilePassword }"
></i>
</button>
</bit-form-field> </bit-form-field>
</div> </div>
<div <div

View File

@@ -7,15 +7,10 @@ import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
templateUrl: "file-password-prompt.component.html", templateUrl: "file-password-prompt.component.html",
}) })
export class FilePasswordPromptComponent { export class FilePasswordPromptComponent {
showFilePassword: boolean;
filePassword = new FormControl("", Validators.required); filePassword = new FormControl("", Validators.required);
constructor(private modalRef: ModalRef) {} constructor(private modalRef: ModalRef) {}
toggleFilePassword() {
this.showFilePassword = !this.showFilePassword;
}
submit() { submit() {
this.filePassword.markAsTouched(); this.filePassword.markAsTouched();
if (!this.filePassword.valid) { if (!this.filePassword.valid) {

View File

@@ -288,6 +288,10 @@
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the
CSV file. CSV file.
</ng-container> </ng-container>
<ng-container *ngIf="format === 'passkyjson'">
Log in to "https://vault.passky.org" &rarr; "Import & Export" &rarr; "Export" in the Passky
section. ("Backup" is unsupported as it is encrypted).
</ng-container>
</app-callout> </app-callout>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">

View File

@@ -15,12 +15,12 @@
<div class="modal-body" *ngIf="history.length"> <div class="modal-body" *ngIf="history.length">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item d-flex" *ngFor="let h of history"> <li class="list-group-item d-flex" *ngFor="let h of history">
<div class="password-row"> <div class="tw-min-w-0">
<div <bit-color-password
class="text-monospace generated-wrapper" [password]="h.password"
[innerHTML]="h.password | colorPassword" class="tw-block tw-font-mono"
appSelectCopy appSelectCopy
></div> ></bit-color-password>
<small class="text-muted">{{ h.date | date: "medium" }}</small> <small class="text-muted">{{ h.date | date: "medium" }}</small>
</div> </div>
<div class="ml-auto"> <div class="ml-auto">

View File

@@ -128,6 +128,15 @@
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
></i> ></i>
</a> </a>
<a
href="#"
class="d-block bwi-icon-above-input"
appStopClick
[appA11yTitle]="'toggleCharacterCount' | i18n"
(click)="togglePasswordCount()"
>
<i class="bwi bwi-lg bwi-fw bwi-numbered-list" aria-hidden="true"></i>
</a>
</div> </div>
</div> </div>
<div class="input-group"> <div class="input-group">
@@ -169,6 +178,18 @@
</div> </div>
</div> </div>
</div> </div>
<div *ngIf="showPasswordCount" class="tw-mb-4">
<label>{{ "passwordCharacterCount" | i18n }}</label>
<div class="tw-flex tw-justify-between">
<bit-color-password
[password]="cipher.login.password"
[showCount]="true"
></bit-color-password>
<button type="button" bitLink (click)="togglePasswordCount()">
{{ "hide" | i18n }}
</button>
</div>
</div>
<div class="tw-flex tw-flex-row"> <div class="tw-flex tw-flex-row">
<div class="tw-mb-4 tw-w-1/2"> <div class="tw-mb-4 tw-w-1/2">
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label> <label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
@@ -870,7 +891,7 @@
<div class="ml-3" *ngIf="viewingPasswordHistory"> <div class="ml-3" *ngIf="viewingPasswordHistory">
<div *ngFor="let ph of cipher.passwordHistory"> <div *ngFor="let ph of cipher.passwordHistory">
{{ ph.lastUsedDate | date: "short" }} - {{ ph.lastUsedDate | date: "short" }} -
<span class="generated-wrapper text-monospace ml-2">{{ ph.password }}</span> <bit-color-password [password]="ph.password"></bit-color-password>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -35,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
hasPasswordHistory = false; hasPasswordHistory = false;
viewingPasswordHistory = false; viewingPasswordHistory = false;
viewOnly = false; viewOnly = false;
showPasswordCount = false;
protected totpInterval: number; protected totpInterval: number;
protected override componentName = "app-vault-add-edit"; protected override componentName = "app-vault-add-edit";
@@ -104,6 +105,26 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.cipher.favorite = !this.cipher.favorite; this.cipher.favorite = !this.cipher.favorite;
} }
togglePassword() {
super.togglePassword();
// Hide password count when password is hidden to be safe
if (!this.showPassword && this.showPasswordCount) {
this.togglePasswordCount();
}
}
togglePasswordCount() {
this.showPasswordCount = !this.showPasswordCount;
if (this.editMode && this.showPasswordCount) {
this.eventCollectionService.collect(
EventType.Cipher_ClientToggledPasswordVisible,
this.cipherId
);
}
}
launch(uri: LoginUriView) { launch(uri: LoginUriView) {
if (!uri.canLaunch) { if (!uri.canLaunch) {
return; return;

View File

@@ -5579,6 +5579,17 @@
"multiSelectClearAll": { "multiSelectClearAll": {
"message": "Clear all" "message": "Clear all"
}, },
"toggleCharacterCount": {
"message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password."
},
"passwordCharacterCount": {
"message": "Password character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password."
},
"hide": {
"message": "Hide"
},
"projects":{ "projects":{
"message": "Projects" "message": "Projects"
}, },

View File

@@ -1,31 +1,3 @@
.generated-wrapper {
min-width: 0;
white-space: pre-wrap;
word-break: break-all;
}
.password-row {
min-width: 0;
}
.password-letter {
@include themify($themes) {
color: themed("pwLetter");
}
}
.password-number {
@include themify($themes) {
color: themed("pwNumber");
}
}
.password-special {
@include themify($themes) {
color: themed("pwSpecial");
}
}
app-generator { app-generator {
#lengthRange { #lengthRange {
width: 100%; width: 100%;

View File

@@ -201,9 +201,6 @@ $themes: (
navBackgroundAlt: $secondary-alt, navBackgroundAlt: $secondary-alt,
navOrgBackgroundColor: #fbfbfb, navOrgBackgroundColor: #fbfbfb,
navWeight: 600, navWeight: 600,
pwLetter: $body-color,
pwNumber: #007fde,
pwSpecial: #c40800,
pwStrengthBackground: #e9ecef, pwStrengthBackground: #e9ecef,
separator: $secondary, separator: $secondary,
separatorHr: rgb(0, 0, 0, 0.1), separatorHr: rgb(0, 0, 0, 0.1),
@@ -313,9 +310,6 @@ $themes: (
navBackgroundAlt: $darkDarkBlue1, navBackgroundAlt: $darkDarkBlue1,
navOrgBackgroundColor: #161c26, navOrgBackgroundColor: #161c26,
navWeight: 400, navWeight: 400,
pwLetter: $white,
pwNumber: #52bdfb,
pwSpecial: #ff7c70,
pwStrengthBackground: $darkBlue2, pwStrengthBackground: $darkBlue2,
separator: $darkBlue1, separator: $darkBlue1,
separatorHr: $darkBlue1, separatorHr: $darkBlue1,

View File

@@ -42,12 +42,10 @@
<button <button
type="button" type="button"
bitSuffix bitSuffix
bitButton bitIconButton="bwi-clone"
(click)="copyScimUrl()" (click)="copyScimUrl()"
[appA11yTitle]="'copyScimUrl' | i18n" [appA11yTitle]="'copyScimUrl' | i18n"
> ></button>
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field *ngIf="showScimSettings"> <bit-form-field *ngIf="showScimSettings">
@@ -59,40 +57,33 @@
id="clientSecret" id="clientSecret"
/> />
<ng-container> <ng-container>
<button type="button" bitSuffix bitButton (click)="toggleScimKey()"> <button
<i type="button"
aria-hidden="true" bitSuffix
class="bwi bwi-lg bwi-eye" [disabled]="$any(rotateButton).loading"
[ngClass]="{ 'bwi-eye': !showScimKey, 'bwi-eye-slash': showScimKey }" [bitIconButton]="showScimKey ? 'bwi-eye-slash' : 'bwi-eye'"
(click)="toggleScimKey()"
[appA11yTitle]="'toggleVisibility' | i18n" [appA11yTitle]="'toggleVisibility' | i18n"
></i> ></button>
</button>
</ng-container> </ng-container>
<ng-container #rotateButton [appApiAction]="rotatePromise"> <ng-container #rotateButton [appApiAction]="rotatePromise">
<!-- TODO: Convert to async actions -->
<button <button
[disabled]="$any(rotateButton).loading" [loading]="$any(rotateButton).loading"
type="button" type="button"
bitSuffix bitSuffix
bitButton bitIconButton="bwi-generate"
(click)="rotateScimKey()" (click)="rotateScimKey()"
[appA11yTitle]="'rotateScimKey' | i18n" [appA11yTitle]="'rotateScimKey' | i18n"
> ></button>
<i
aria-hidden="true"
class="bwi bwi-lg bwi-generate"
[ngClass]="{ 'bwi-spin': $any(rotateButton).loading }"
></i>
</button>
</ng-container> </ng-container>
<button <button
type="button" type="button"
bitSuffix bitSuffix
bitButton bitIconButton="bwi-clone"
(click)="copyScimKey()" (click)="copyScimKey()"
[appA11yTitle]="'copyScimKey' | i18n" [appA11yTitle]="'copyScimKey' | i18n"
> ></button>
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
</button>
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint> <bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
</bit-form-field> </bit-form-field>

View File

@@ -151,28 +151,24 @@
<bit-label>{{ "callbackPath" | i18n }}</bit-label> <bit-label>{{ "callbackPath" | i18n }}</bit-label>
<input bitInput disabled [value]="callbackPath" /> <input bitInput disabled [value]="callbackPath" />
<button <button
bitButton bitIconButton="bwi-clone"
bitSuffix bitSuffix
type="button" type="button"
[appCopyClick]="callbackPath" [appCopyClick]="callbackPath"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
> ></button>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>
<bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label> <bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label>
<input bitInput disabled [value]="signedOutCallbackPath" /> <input bitInput disabled [value]="signedOutCallbackPath" />
<button <button
bitButton bitIconButton="bwi-clone"
bitSuffix bitSuffix
type="button" type="button"
[appCopyClick]="signedOutCallbackPath" [appCopyClick]="signedOutCallbackPath"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
> ></button>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>
@@ -292,14 +288,12 @@
<bit-label>{{ "spEntityId" | i18n }}</bit-label> <bit-label>{{ "spEntityId" | i18n }}</bit-label>
<input bitInput disabled [value]="spEntityId" /> <input bitInput disabled [value]="spEntityId" />
<button <button
bitButton bitIconButton="bwi-clone"
bitSuffix bitSuffix
type="button" type="button"
[appCopyClick]="spEntityId" [appCopyClick]="spEntityId"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
> ></button>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>
@@ -315,28 +309,24 @@
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i> <i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button> </button>
<button <button
bitButton bitIconButton="bwi-clone"
bitSuffix bitSuffix
type="button" type="button"
[appCopyClick]="spMetadataUrl" [appCopyClick]="spMetadataUrl"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
> ></button>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>
<bit-label>{{ "spAcsUrl" | i18n }}</bit-label> <bit-label>{{ "spAcsUrl" | i18n }}</bit-label>
<input bitInput disabled [value]="spAcsUrl" /> <input bitInput disabled [value]="spAcsUrl" />
<button <button
bitButton bitIconButton="bwi-clone"
bitSuffix bitSuffix
type="button" type="button"
[appCopyClick]="spAcsUrl" [appCopyClick]="spAcsUrl"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
> ></button>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field> </bit-form-field>
<bit-form-field> <bit-form-field>

View File

@@ -2,7 +2,7 @@
<bit-icon [icon]="logo" class="tw-w-full tw-text-alt2"></bit-icon> <bit-icon [icon]="logo" class="tw-w-full tw-text-alt2"></bit-icon>
</a> </a>
<org-switcher></org-switcher> <org-switcher [filter]="orgFilter"></org-switcher>
<bit-nav-item icon="bwi-collection" text="Projects" route="projects"></bit-nav-item> <bit-nav-item icon="bwi-collection" text="Projects" route="projects"></bit-nav-item>
<bit-nav-item icon="bwi-key" text="Secrets" route="secrets"></bit-nav-item> <bit-nav-item icon="bwi-key" text="Secrets" route="secrets"></bit-nav-item>
<bit-nav-item icon="bwi-wrench" text="Service Accounts" route="service-accounts"></bit-nav-item> <bit-nav-item icon="bwi-wrench" text="Service Accounts" route="service-accounts"></bit-nav-item>

View File

@@ -1,5 +1,7 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { SecretsManagerLogo } from "./secrets-manager-logo"; import { SecretsManagerLogo } from "./secrets-manager-logo";
@Component({ @Component({
@@ -8,4 +10,6 @@ import { SecretsManagerLogo } from "./secrets-manager-logo";
}) })
export class NavigationComponent { export class NavigationComponent {
protected readonly logo = SecretsManagerLogo; protected readonly logo = SecretsManagerLogo;
protected orgFilter = (org: Organization) => org.canAccessSecretsManager;
} }

View File

@@ -12,13 +12,22 @@ import type { Organization } from "@bitwarden/common/models/domain/organization"
export class OrgSwitcherComponent { export class OrgSwitcherComponent {
protected organizations$: Observable<Organization[]> = protected organizations$: Observable<Organization[]> =
this.organizationService.organizations$.pipe( this.organizationService.organizations$.pipe(
map((orgs) => orgs.sort((a, b) => a.name.localeCompare(b.name))) map((orgs) => orgs.filter(this.filter).sort((a, b) => a.name.localeCompare(b.name)))
); );
protected activeOrganization$: Observable<Organization> = combineLatest([ protected activeOrganization$: Observable<Organization> = combineLatest([
this.route.paramMap, this.route.paramMap,
this.organizationService.organizations$, this.organizations$,
]).pipe(map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId")))); ]).pipe(map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId"))));
/**
* Filter function for displayed organizations in the `org-switcher`
* @example
* const smFilter = (org: Organization) => org.canAccessSecretsManager
* // <org-switcher [filter]="smFilter">
*/
@Input()
filter: (org: Organization) => boolean = () => true;
/** /**
* Is `true` if the expanded content is visible * Is `true` if the expanded content is visible
*/ */

View File

@@ -21,8 +21,6 @@ export class ExportComponent implements OnInit, OnDestroy {
formPromise: Promise<string>; formPromise: Promise<string>;
disabledByPolicy = false; disabledByPolicy = false;
showFilePassword: boolean;
showConfirmFilePassword: boolean;
exportForm = this.formBuilder.group({ exportForm = this.formBuilder.group({
format: ["json"], format: ["json"],
@@ -199,16 +197,6 @@ export class ExportComponent implements OnInit, OnDestroy {
return this.exportForm.get("fileEncryptionType").value; return this.exportForm.get("fileEncryptionType").value;
} }
toggleFilePassword() {
this.showFilePassword = !this.showFilePassword;
document.getElementById("filePassword").focus();
}
toggleConfirmFilePassword() {
this.showConfirmFilePassword = !this.showConfirmFilePassword;
document.getElementById("confirmFilePassword").focus();
}
adjustValidators() { adjustValidators() {
this.exportForm.get("confirmFilePassword").reset(); this.exportForm.get("confirmFilePassword").reset();
this.exportForm.get("filePassword").reset(); this.exportForm.get("filePassword").reset();

View File

@@ -21,8 +21,6 @@ import { SelectCopyDirective } from "./directives/select-copy.directive";
import { StopClickDirective } from "./directives/stop-click.directive"; import { StopClickDirective } from "./directives/stop-click.directive";
import { StopPropDirective } from "./directives/stop-prop.directive"; import { StopPropDirective } from "./directives/stop-prop.directive";
import { TrueFalseValueDirective } from "./directives/true-false-value.directive"; import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
import { ColorPasswordCountPipe } from "./pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "./pipes/color-password.pipe";
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe"; import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
import { EllipsisPipe } from "./pipes/ellipsis.pipe"; import { EllipsisPipe } from "./pipes/ellipsis.pipe";
import { I18nPipe } from "./pipes/i18n.pipe"; import { I18nPipe } from "./pipes/i18n.pipe";
@@ -50,8 +48,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
AutofocusDirective, AutofocusDirective,
BoxRowDirective, BoxRowDirective,
CalloutComponent, CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe, CreditCardNumberPipe,
EllipsisPipe, EllipsisPipe,
ExportScopeCalloutComponent, ExportScopeCalloutComponent,
@@ -81,8 +77,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
BitwardenToastModule, BitwardenToastModule,
BoxRowDirective, BoxRowDirective,
CalloutComponent, CalloutComponent,
ColorPasswordCountPipe,
ColorPasswordPipe,
CreditCardNumberPipe, CreditCardNumberPipe,
EllipsisPipe, EllipsisPipe,
ExportScopeCalloutComponent, ExportScopeCalloutComponent,

View File

@@ -0,0 +1,34 @@
import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer";
import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json";
import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json";
describe("Passky Json Importer", () => {
let importer: Importer;
beforeEach(() => {
importer = new Importer();
});
it("should not import encrypted backups", async () => {
const testDataJson = JSON.stringify(EncryptedData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
expect(result.success).toBe(false);
expect(result.errorMessage).toBe("Unable to import an encrypted passky backup.");
});
it("should parse login data", async () => {
const testDataJson = JSON.stringify(UnencryptedData);
const result = await importer.parse(testDataJson);
expect(result != null).toBe(true);
const cipher = result.ciphers.shift();
expect(cipher.name).toEqual("https://bitwarden.com/");
expect(cipher.login.username).toEqual("testUser");
expect(cipher.login.password).toEqual("testPassword");
expect(cipher.login.uris.length).toEqual(1);
const uriView = cipher.login.uris.shift();
expect(uriView.uri).toEqual("https://bitwarden.com/");
expect(cipher.notes).toEqual("my notes");
});
});

View File

@@ -0,0 +1,15 @@
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
export const testData: PasskyJsonExport = {
encrypted: true,
passwords: [
{
website:
"w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp",
username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==",
password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+",
message:
"w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==",
},
],
};

View File

@@ -0,0 +1,13 @@
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
export const testData: PasskyJsonExport = {
encrypted: false,
passwords: [
{
website: "https://bitwarden.com/",
username: "testUser",
password: "testPassword",
message: "my notes",
},
],
};

View File

@@ -67,6 +67,7 @@ export const regularImportOptions = [
{ id: "encryptrcsv", name: "Encryptr (csv)" }, { id: "encryptrcsv", name: "Encryptr (csv)" },
{ id: "yoticsv", name: "Yoti (csv)" }, { id: "yoticsv", name: "Yoti (csv)" },
{ id: "nordpasscsv", name: "Nordpass (csv)" }, { id: "nordpasscsv", name: "Nordpass (csv)" },
{ id: "passkyjson", name: "Passky (json)" },
] as const; ] as const;
export type ImportType = export type ImportType =

View File

@@ -0,0 +1,43 @@
import { ImportResult } from "../../models/domain/import-result";
import { BaseImporter } from "../base-importer";
import { Importer } from "../importer";
import { PasskyJsonExport } from "./passky-json-types";
export class PasskyJsonImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const passkyExport: PasskyJsonExport = JSON.parse(data);
if (
passkyExport == null ||
passkyExport.passwords == null ||
passkyExport.passwords.length === 0
) {
result.success = false;
return Promise.resolve(result);
}
if (passkyExport.encrypted == true) {
result.success = false;
result.errorMessage = "Unable to import an encrypted passky backup.";
return Promise.resolve(result);
}
passkyExport.passwords.forEach((record) => {
const cipher = this.initLoginCipher();
cipher.name = record.website;
cipher.login.username = record.username;
cipher.login.password = record.password;
cipher.login.uris = this.makeUriArray(record.website);
cipher.notes = record.message;
this.convertToNoteIfNeeded(cipher);
this.cleanupCipher(cipher);
result.ciphers.push(cipher);
});
result.success = true;
return Promise.resolve(result);
}
}

View File

@@ -0,0 +1,11 @@
export interface PasskyJsonExport {
encrypted: boolean;
passwords: LoginEntry[];
}
export interface LoginEntry {
website: string;
username: string;
password: string;
message: string;
}

View File

@@ -51,6 +51,7 @@ import { OnePasswordMacCsvImporter } from "../importers/onepassword/onepassword-
import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer"; import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer";
import { PadlockCsvImporter } from "../importers/padlock-csv-importer"; import { PadlockCsvImporter } from "../importers/padlock-csv-importer";
import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer"; import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer";
import { PasskyJsonImporter } from "../importers/passky/passky-json-importer";
import { PassmanJsonImporter } from "../importers/passman-json-importer"; import { PassmanJsonImporter } from "../importers/passman-json-importer";
import { PasspackCsvImporter } from "../importers/passpack-csv-importer"; import { PasspackCsvImporter } from "../importers/passpack-csv-importer";
import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer"; import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer";
@@ -279,6 +280,8 @@ export class ImportService implements ImportServiceAbstraction {
return new YotiCsvImporter(); return new YotiCsvImporter();
case "nordpasscsv": case "nordpasscsv":
return new NordPassCsvImporter(); return new NordPassCsvImporter();
case "passkyjson":
return new PasskyJsonImporter();
default: default:
return null; return null;
} }

View File

@@ -27,6 +27,7 @@ const template = `
<bit-form-field> <bit-form-field>
<bit-label>Email</bit-label> <bit-label>Email</bit-label>
<input bitInput formControlName="email" /> <input bitInput formControlName="email" />
<button type="button" bitSuffix bitIconButton="bwi-refresh" bitFormButton [bitAction]="refresh"></button>
</bit-form-field> </bit-form-field>
<button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button> <button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
@@ -47,6 +48,12 @@ class PromiseExampleComponent {
constructor(private formBuilder: FormBuilder) {} constructor(private formBuilder: FormBuilder) {}
refresh = async () => {
await new Promise<void>((resolve, reject) => {
setTimeout(resolve, 2000);
});
};
submit = async () => { submit = async () => {
this.formObj.markAllAsTouched(); this.formObj.markAllAsTouched();
@@ -78,6 +85,10 @@ class ObservableExampleComponent {
constructor(private formBuilder: FormBuilder) {} constructor(private formBuilder: FormBuilder) {}
refresh = () => {
return of("fake observable").pipe(delay(2000));
};
submit = () => { submit = () => {
this.formObj.markAllAsTouched(); this.formObj.markAllAsTouched();

View File

@@ -1,6 +1,5 @@
<span class="tw-relative"> <span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': loading }"> <span [ngClass]="{ 'tw-invisible': loading }">
<i class="bwi bwi-lg" [ngClass]="iconClass" aria-hidden="true" *ngIf="icon"></i>
<ng-content></ng-content> <ng-content></ng-content>
</span> </span>
<span <span

View File

@@ -41,6 +41,19 @@ describe("Button", () => {
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
testAppComponent.buttonType = "unstyled";
fixture.detectChanges();
expect(
Array.from(buttonDebugElement.nativeElement.classList).some((klass: string) =>
klass.startsWith("tw-bg")
)
).toBe(false);
expect(
Array.from(linkDebugElement.nativeElement.classList).some((klass: string) =>
klass.startsWith("tw-bg")
)
).toBe(false);
testAppComponent.buttonType = null; testAppComponent.buttonType = null;
fixture.detectChanges(); fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true); expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);

View File

@@ -1,10 +1,15 @@
import { Input, HostBinding, Component } from "@angular/core"; import { Input, HostBinding, Component } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
export type ButtonTypes = "primary" | "secondary" | "danger"; const focusRing = [
"focus-visible:tw-ring",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
];
const buttonStyles: Record<ButtonTypes, string[]> = { const buttonStyles: Record<ButtonType, string[]> = {
primary: [ primary: [
"tw-border-primary-500", "tw-border-primary-500",
"tw-bg-primary-500", "tw-bg-primary-500",
@@ -15,6 +20,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"disabled:tw-border-primary-500/60", "disabled:tw-border-primary-500/60",
"disabled:!tw-text-contrast/60", "disabled:!tw-text-contrast/60",
"disabled:tw-bg-clip-padding", "disabled:tw-bg-clip-padding",
...focusRing,
], ],
secondary: [ secondary: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -26,6 +32,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"disabled:tw-bg-transparent", "disabled:tw-bg-transparent",
"disabled:tw-border-text-muted/60", "disabled:tw-border-text-muted/60",
"disabled:!tw-text-muted/60", "disabled:!tw-text-muted/60",
...focusRing,
], ],
danger: [ danger: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -37,7 +44,9 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"disabled:tw-bg-transparent", "disabled:tw-bg-transparent",
"disabled:tw-border-danger-500/60", "disabled:tw-border-danger-500/60",
"disabled:!tw-text-danger/60", "disabled:!tw-text-danger/60",
...focusRing,
], ],
unstyled: [],
}; };
@Component({ @Component({
@@ -58,10 +67,6 @@ export class ButtonComponent implements ButtonLikeAbstraction {
"tw-text-center", "tw-text-center",
"hover:tw-no-underline", "hover:tw-no-underline",
"focus:tw-outline-none", "focus:tw-outline-none",
"focus-visible:tw-ring",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
] ]
.concat( .concat(
this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"] this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"]
@@ -75,17 +80,14 @@ export class ButtonComponent implements ButtonLikeAbstraction {
return disabled || this.loading ? true : null; return disabled || this.loading ? true : null;
} }
@Input() buttonType: ButtonTypes = null; @Input() buttonType: ButtonType;
@Input() block?: boolean; @Input() block?: boolean;
@Input() loading = false; @Input() loading = false;
@Input() disabled = false; @Input() disabled = false;
@Input("bitIconButton") icon: string; setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
this.buttonType = value;
get iconClass() {
return [this.icon, "!tw-m-0"];
} }
} }

View File

@@ -101,17 +101,3 @@ export const Block = BlockTemplate.bind({});
Block.args = { Block.args = {
block: true, block: true,
}; };
const IconTemplate: Story = (args) => ({
props: args,
template: `
<button bitButton [bitIconButton]="icon" buttonType="primary" class="tw-mr-2"></button>
<button bitButton [bitIconButton]="icon"buttonType="secondary" class="tw-mr-2"></button>
<button bitButton [bitIconButton]="icon" buttonType="danger" class="tw-mr-2"></button>
`,
});
export const Icon = IconTemplate.bind({});
Icon.args = {
icon: "bwi-eye",
};

View File

@@ -48,12 +48,12 @@ export class ColorPasswordComponent {
if (this.showCount) { if (this.showCount) {
return charClass.concat([ return charClass.concat([
"tw-inline-flex",
"tw-flex-col", "tw-flex-col",
"tw-items-center", "tw-items-center",
"tw-w-7", "tw-w-7",
"tw-py-1", "tw-py-1",
"odd:tw-bg-secondary-100", "odd:tw-bg-secondary-100",
"even:tw-bg-background",
]); ]);
} }

View File

@@ -11,8 +11,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { AsyncActionsModule } from "../async-actions";
import { ButtonModule } from "../button"; import { ButtonModule } from "../button";
import { CheckboxModule } from "../checkbox"; import { CheckboxModule } from "../checkbox";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module"; import { InputModule } from "../input/input.module";
import { RadioButtonModule } from "../radio-button"; import { RadioButtonModule } from "../radio-button";
import { I18nMockService } from "../utils/i18n-mock.service"; import { I18nMockService } from "../utils/i18n-mock.service";
@@ -31,6 +33,8 @@ export default {
FormFieldModule, FormFieldModule,
InputModule, InputModule,
ButtonModule, ButtonModule,
IconButtonModule,
AsyncActionsModule,
CheckboxModule, CheckboxModule,
RadioButtonModule, RadioButtonModule,
], ],
@@ -177,10 +181,13 @@ const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldCom
props: args, props: args,
template: ` template: `
<bit-form-field> <bit-form-field>
<bit-label>Label</bit-label> <button bitPrefix bitIconButton="bwi-star"></button>
<input bitInput placeholder="Placeholder" type="password" /> <input bitInput placeholder="Placeholder" />
<button bitSuffix bitButton bitIconButton="bwi-eye"></button> <button bitSuffix bitIconButton="bwi-eye"></button>
<button bitSuffix bitButton bitIconButton="bwi-clone"></button> <button bitSuffix bitIconButton="bwi-clone"></button>
<button bitSuffix bitButton>
Apply
</button>
</bit-form-field> </bit-form-field>
`, `,
}); });
@@ -195,9 +202,13 @@ const DisabledButtonInputGroupTemplate: Story<BitFormFieldComponent> = (
template: ` template: `
<bit-form-field> <bit-form-field>
<bit-label>Label</bit-label> <bit-label>Label</bit-label>
<button bitPrefix bitIconButton="bwi-star" disabled></button>
<input bitInput placeholder="Placeholder" disabled /> <input bitInput placeholder="Placeholder" disabled />
<button bitSuffix bitButton bitIconButton="bwi-eye" disabled></button> <button bitSuffix bitIconButton="bwi-eye" disabled></button>
<button bitSuffix bitButton bitIconButton="bwi-clone"></button> <button bitSuffix bitIconButton="bwi-clone" disabled></button>
<button bitSuffix bitButton disabled>
Apply
</button>
</bit-form-field> </bit-form-field>
`, `,
}); });

View File

@@ -3,13 +3,16 @@ import {
Directive, Directive,
EventEmitter, EventEmitter,
Host, Host,
HostBinding,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
Output, Output,
} from "@angular/core"; } from "@angular/core";
import { ButtonComponent } from "../button"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
import { BitFormFieldComponent } from "./form-field.component"; import { BitFormFieldComponent } from "./form-field.component";
@@ -17,9 +20,18 @@ import { BitFormFieldComponent } from "./form-field.component";
selector: "[bitPasswordInputToggle]", selector: "[bitPasswordInputToggle]",
}) })
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges { export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
@Input() toggled = false; /**
* Whether the input is toggled to show the password.
*/
@HostBinding("attr.aria-pressed") @Input() toggled = false;
@Output() toggledChange = new EventEmitter<boolean>(); @Output() toggledChange = new EventEmitter<boolean>();
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
@HostBinding("attr.aria-label") label = this.i18nService.t("toggleVisibility");
/**
* Click handler to toggle the state of the input type.
*/
@HostListener("click") onClick() { @HostListener("click") onClick() {
this.toggled = !this.toggled; this.toggled = !this.toggled;
this.toggledChange.emit(this.toggled); this.toggledChange.emit(this.toggled);
@@ -29,7 +41,11 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
this.formField.input?.focus(); this.formField.input?.focus();
} }
constructor(@Host() private button: ButtonComponent, private formField: BitFormFieldComponent) {} constructor(
@Host() private button: BitIconButtonComponent,
private formField: BitFormFieldComponent,
private i18nService: I18nService
) {}
get icon() { get icon() {
return this.toggled ? "bwi-eye-slash" : "bwi-eye"; return this.toggled ? "bwi-eye-slash" : "bwi-eye";

View File

@@ -2,8 +2,12 @@ import { Component, DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { ButtonComponent, ButtonModule } from "../button"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
import { InputModule } from "../input/input.module"; import { InputModule } from "../input/input.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { BitFormFieldControl } from "./form-field-control"; import { BitFormFieldControl } from "./form-field-control";
import { BitFormFieldComponent } from "./form-field.component"; import { BitFormFieldComponent } from "./form-field.component";
@@ -17,7 +21,7 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi
<bit-form-field> <bit-form-field>
<bit-label>Password</bit-label> <bit-label>Password</bit-label>
<input bitInput type="password" /> <input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button> <button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field> </bit-form-field>
</form> </form>
`, `,
@@ -26,21 +30,22 @@ class TestFormFieldComponent {}
describe("PasswordInputToggle", () => { describe("PasswordInputToggle", () => {
let fixture: ComponentFixture<TestFormFieldComponent>; let fixture: ComponentFixture<TestFormFieldComponent>;
let button: ButtonComponent; let button: BitIconButtonComponent;
let input: BitFormFieldControl; let input: BitFormFieldControl;
let toggle: DebugElement; let toggle: DebugElement;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [FormFieldModule, ButtonModule, InputModule], imports: [FormFieldModule, IconButtonModule, InputModule],
declarations: [TestFormFieldComponent], declarations: [TestFormFieldComponent],
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(TestFormFieldComponent); fixture = TestBed.createComponent(TestFormFieldComponent);
fixture.detectChanges(); fixture.detectChanges();
toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective));
const buttonEl = fixture.debugElement.query(By.directive(ButtonComponent)); const buttonEl = fixture.debugElement.query(By.directive(BitIconButtonComponent));
button = buttonEl.componentInstance; button = buttonEl.componentInstance;
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent)); const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
const formField: BitFormFieldComponent = formFieldEl.componentInstance; const formField: BitFormFieldComponent = formFieldEl.componentInstance;

View File

@@ -1,8 +1,11 @@
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module"; import { InputModule } from "../input/input.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { FormFieldModule } from "./form-field.module"; import { FormFieldModule } from "./form-field.module";
import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive"; import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive";
@@ -12,7 +15,13 @@ export default {
component: BitPasswordInputToggleDirective, component: BitPasswordInputToggleDirective,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule], imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, IconButtonModule],
providers: [
{
provide: I18nService,
useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }),
},
],
}), }),
], ],
parameters: { parameters: {
@@ -40,7 +49,7 @@ const Template: Story<BitPasswordInputToggleDirective> = (
<bit-form-field> <bit-form-field>
<bit-label>Password</bit-label> <bit-label>Password</bit-label>
<input bitInput type="password" /> <input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button> <button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field> </bit-form-field>
</form> </form>
`, `,
@@ -60,7 +69,7 @@ const TemplateBinding: Story<BitPasswordInputToggleDirective> = (
<bit-form-field> <bit-form-field>
<bit-label>Password</bit-label> <bit-label>Password</bit-label>
<input bitInput type="password" /> <input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button> <button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
</bit-form-field> </bit-form-field>
<label class="tw-text-main"> <label class="tw-text-main">

View File

@@ -1,24 +1,51 @@
import { Directive, HostBinding, Input } from "@angular/core"; import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
export const PrefixClasses = [ export const PrefixClasses = [
"tw-block",
"tw-px-3",
"tw-py-1.5",
"tw-bg-background-alt", "tw-bg-background-alt",
"tw-border", "tw-border",
"tw-border-solid", "tw-border-solid",
"tw-border-secondary-500", "tw-border-secondary-500",
"tw-text-muted", "tw-text-muted",
"tw-rounded-none", "tw-rounded-none",
"disabled:!tw-text-muted",
"disabled:tw-border-secondary-500",
]; ];
export const PrefixButtonClasses = [
"hover:tw-bg-text-muted",
"hover:tw-text-contrast",
"disabled:tw-opacity-100",
"disabled:tw-bg-secondary-100",
"disabled:hover:tw-bg-secondary-100",
"disabled:hover:tw-text-muted",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-border-primary-700",
"focus-visible:tw-ring-1",
"focus-visible:tw-ring-inset",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
];
export const PrefixStaticContentClasses = ["tw-block", "tw-px-3", "tw-py-1.5"];
@Directive({ @Directive({
selector: "[bitPrefix]", selector: "[bitPrefix]",
}) })
export class BitPrefixDirective { export class BitPrefixDirective implements OnInit {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() { @HostBinding("class") @Input() get classList() {
return PrefixClasses.concat(["tw-border-r-0", "first:tw-rounded-l"]); return PrefixClasses.concat([
"tw-border-r-0",
"first:tw-rounded-l",
"focus-visible:tw-border-r",
"focus-visible:tw-mr-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
} }
} }

View File

@@ -1,12 +1,26 @@
import { Directive, HostBinding, Input } from "@angular/core"; import { Directive, HostBinding, Input, Optional } from "@angular/core";
import { PrefixClasses } from "./prefix.directive"; import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
@Directive({ @Directive({
selector: "[bitSuffix]", selector: "[bitSuffix]",
}) })
export class BitSuffixDirective { export class BitSuffixDirective {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() { @HostBinding("class") @Input() get classList() {
return PrefixClasses.concat(["tw-border-l-0", "last:tw-rounded-r"]); return PrefixClasses.concat([
"tw-border-l-0",
"last:tw-rounded-r",
"focus-visible:tw-border-l",
"focus-visible:tw-ml-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
} }
} }

View File

@@ -1,8 +1,26 @@
import { Component, HostBinding, Input } from "@angular/core"; import { Component, HostBinding, Input } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
export type IconButtonType = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger"; export type IconButtonType = ButtonType | "contrast" | "main" | "muted";
const focusRing = [
// Workaround for box-shadow with transparent offset issue:
// https://github.com/tailwindlabs/tailwindcss/issues/3595
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
"tw-relative",
"before:tw-content-['']",
"before:tw-block",
"before:tw-absolute",
"before:-tw-inset-[3px]",
"before:tw-rounded-md",
"before:tw-transition",
"before:tw-ring",
"before:tw-ring-transparent",
"focus-visible:tw-z-10",
];
const styles: Record<IconButtonType, string[]> = { const styles: Record<IconButtonType, string[]> = {
contrast: [ contrast: [
@@ -12,8 +30,10 @@ const styles: Record<IconButtonType, string[]> = {
"hover:tw-bg-transparent-hover", "hover:tw-bg-transparent-hover",
"hover:tw-border-text-contrast", "hover:tw-border-text-contrast",
"focus-visible:before:tw-ring-text-contrast", "focus-visible:before:tw-ring-text-contrast",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-transparent", "disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
...focusRing,
], ],
main: [ main: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -22,8 +42,10 @@ const styles: Record<IconButtonType, string[]> = {
"hover:tw-bg-transparent-hover", "hover:tw-bg-transparent-hover",
"hover:tw-border-text-main", "hover:tw-border-text-main",
"focus-visible:before:tw-ring-text-main", "focus-visible:before:tw-ring-text-main",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-transparent", "disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
...focusRing,
], ],
muted: [ muted: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -32,8 +54,10 @@ const styles: Record<IconButtonType, string[]> = {
"hover:tw-bg-transparent-hover", "hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700", "hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700", "focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-transparent", "disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
...focusRing,
], ],
primary: [ primary: [
"tw-bg-primary-500", "tw-bg-primary-500",
@@ -42,8 +66,10 @@ const styles: Record<IconButtonType, string[]> = {
"hover:tw-bg-primary-700", "hover:tw-bg-primary-700",
"hover:tw-border-primary-700", "hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700", "focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-primary-500", "disabled:hover:tw-border-primary-500",
"disabled:hover:tw-bg-primary-500", "disabled:hover:tw-bg-primary-500",
...focusRing,
], ],
secondary: [ secondary: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -52,10 +78,12 @@ const styles: Record<IconButtonType, string[]> = {
"hover:!tw-text-contrast", "hover:!tw-text-contrast",
"hover:tw-bg-text-muted", "hover:tw-bg-text-muted",
"focus-visible:before:tw-ring-primary-700", "focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-text-muted", "disabled:hover:tw-border-text-muted",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-muted", "disabled:hover:!tw-text-muted",
"disabled:hover:tw-border-text-muted", "disabled:hover:tw-border-text-muted",
...focusRing,
], ],
danger: [ danger: [
"tw-bg-transparent", "tw-bg-transparent",
@@ -64,11 +92,14 @@ const styles: Record<IconButtonType, string[]> = {
"hover:!tw-text-contrast", "hover:!tw-text-contrast",
"hover:tw-bg-danger-500", "hover:tw-bg-danger-500",
"focus-visible:before:tw-ring-primary-700", "focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-danger-500", "disabled:hover:tw-border-danger-500",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-danger", "disabled:hover:!tw-text-danger",
"disabled:hover:tw-border-danger-500", "disabled:hover:tw-border-danger-500",
...focusRing,
], ],
unstyled: [],
}; };
export type IconButtonSize = "default" | "small"; export type IconButtonSize = "default" | "small";
@@ -86,7 +117,7 @@ const sizes: Record<IconButtonSize, string[]> = {
export class BitIconButtonComponent implements ButtonLikeAbstraction { export class BitIconButtonComponent implements ButtonLikeAbstraction {
@Input("bitIconButton") icon: string; @Input("bitIconButton") icon: string;
@Input() buttonType: IconButtonType = "main"; @Input() buttonType: IconButtonType;
@Input() size: IconButtonSize = "default"; @Input() size: IconButtonSize = "default";
@@ -98,27 +129,9 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
"tw-rounded", "tw-rounded",
"tw-transition", "tw-transition",
"hover:tw-no-underline", "hover:tw-no-underline",
"disabled:tw-opacity-60",
"focus:tw-outline-none", "focus:tw-outline-none",
// Workaround for box-shadow with transparent offset issue:
// https://github.com/tailwindlabs/tailwindcss/issues/3595
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
"tw-relative",
"before:tw-content-['']",
"before:tw-block",
"before:tw-absolute",
"before:-tw-inset-[3px]",
"before:tw-rounded-md",
"before:tw-transition",
"before:tw-ring",
"before:tw-ring-transparent",
"focus-visible:before:tw-ring-text-contrast",
"focus-visible:tw-z-10",
] ]
.concat(styles[this.buttonType]) .concat(styles[this.buttonType ?? "main"])
.concat(sizes[this.size]); .concat(sizes[this.size]);
} }
@@ -134,4 +147,8 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
@Input() loading = false; @Input() loading = false;
@Input() disabled = false; @Input() disabled = false;
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
this.buttonType = value;
}
} }

View File

@@ -31,6 +31,7 @@ export class BitInputDirective implements BitFormFieldControl {
"focus:tw-outline-none", "focus:tw-outline-none",
"focus:tw-border-primary-700", "focus:tw-border-primary-700",
"focus:tw-ring-1", "focus:tw-ring-1",
"focus:tw-ring-inset",
"focus:tw-ring-primary-700", "focus:tw-ring-primary-700",
"focus:tw-z-10", "focus:tw-z-10",
"disabled:tw-bg-secondary-100", "disabled:tw-bg-secondary-100",

View File

@@ -1,4 +1,7 @@
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
export abstract class ButtonLikeAbstraction { export abstract class ButtonLikeAbstraction {
loading: boolean; loading: boolean;
disabled: boolean; disabled: boolean;
setButtonType: (value: ButtonType) => void;
} }

2
package-lock.json generated
View File

@@ -177,7 +177,7 @@
}, },
"apps/browser": { "apps/browser": {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2022.12.0" "version": "2022.12.1"
}, },
"apps/cli": { "apps/cli": {
"name": "@bitwarden/cli", "name": "@bitwarden/cli",