diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html
index d620b6a1278..f2fe7c1db9d 100644
--- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html
@@ -32,17 +32,12 @@
}
@for (book of bookOptions; track book) {
-
-
- {{ "canon.book_names." + book.bookId | transloco }}
-
-
+ >
}
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts
index 34e0e2bd219..516f731701d 100644
--- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts
@@ -3,6 +3,7 @@ import { MatChipsModule } from '@angular/material/chips';
import { TranslocoModule } from '@ngneat/transloco';
import { Canon } from '@sillsdev/scripture';
import { UICommonModule } from 'xforge-common/ui-common.module';
+import { ToggleBookComponent } from '../../translate/draft-generation/toggle-book/toggle-book.component';
export interface BookOption {
bookNum: number;
@@ -14,7 +15,7 @@ export interface BookOption {
selector: 'app-book-multi-select',
templateUrl: './book-multi-select.component.html',
standalone: true,
- imports: [UICommonModule, MatChipsModule, TranslocoModule],
+ imports: [UICommonModule, MatChipsModule, TranslocoModule, ToggleBookComponent],
styleUrls: ['./book-multi-select.component.scss']
})
export class BookMultiSelectComponent implements OnChanges {
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.html
new file mode 100644
index 00000000000..51cc617a5c6
--- /dev/null
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.html
@@ -0,0 +1,19 @@
+
+
+
+ {{ bookName(book) }}
+
+
+
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.scss
new file mode 100644
index 00000000000..3812ee60761
--- /dev/null
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.scss
@@ -0,0 +1,51 @@
+:host {
+ --progress-color: hsl(0, 0%, 80%);
+ --progress-hover-color: hsl(0, 0%, 70%);
+ --progress-bg-color: hsl(0, 0%, 90%);
+ --progress-hover-bg-color: hsl(0, 0%, 80%);
+
+ user-select: none;
+}
+
+.wrapper {
+ display: inline-block;
+ border-radius: 1000px;
+}
+
+.wrapper:has(.selected) {
+ background-image: var(--background-image);
+}
+
+.book {
+ display: block;
+ background-color: var(--progress-bg-color);
+ padding: 0 16px;
+ line-height: 32px;
+ border-radius: 1000px;
+ margin: var(--border-width);
+ position: relative;
+
+ background-image: linear-gradient(
+ 90deg,
+ var(--progress-color) var(--progress),
+ var(--progress-bg-color) var(--progress)
+ );
+
+ &.disabled:not(.selected) {
+ opacity: 0.7;
+ }
+
+ &:not(.disabled) {
+ cursor: pointer;
+ }
+
+ &:hover:not(.disabled),
+ &:focus:not(.disabled) {
+ outline: none;
+ background-image: linear-gradient(
+ 90deg,
+ var(--progress-hover-color) var(--progress),
+ var(--progress-hover-bg-color) var(--progress)
+ );
+ }
+}
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.ts
new file mode 100644
index 00000000000..32075e49a3d
--- /dev/null
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.component.ts
@@ -0,0 +1,70 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { MatRippleModule } from '@angular/material/core';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { TranslocoModule } from '@ngneat/transloco';
+import { I18nService } from '../../../../xforge-common/i18n.service';
+
+@Component({
+ selector: 'app-toggle-book',
+ standalone: true,
+ imports: [TranslocoModule, MatTooltipModule, MatRippleModule],
+ templateUrl: './toggle-book.component.html',
+ styleUrl: './toggle-book.component.scss'
+})
+export class ToggleBookComponent {
+ @Output() selectedChanged = new EventEmitter();
+ @Input() selected = false;
+ @Input() disabled = false;
+ @Input() borderWidth = 2;
+ @Input() book!: number;
+ @Input() progress?: number;
+ @Input() hues: number[] = [230];
+
+ constructor(private readonly i18n: I18nService) {}
+
+ bookName(book: number): string {
+ return this.i18n.localizeBook(book);
+ }
+
+ toggleSelected(): void {
+ if (!this.disabled) {
+ this.selected = !this.selected;
+ this.selectedChanged.emit(this.book);
+ }
+ }
+
+ onKeyPress(event: KeyboardEvent): void {
+ if (event.key === 'Enter' || event.key === ' ') {
+ this.toggleSelected();
+ event.preventDefault();
+ }
+ }
+
+ get backgroundCssGradientStripes(): string {
+ const percentPerStripe = 12.5;
+ const colors = this.hues.map(hue => `hsl(${hue}, 80%, 60%)`);
+ let gradient = [];
+ for (const [index, color] of colors.entries()) {
+ const from = index * percentPerStripe;
+ const to = (index + 1) * percentPerStripe;
+ gradient.push(`${color} ${from}%, ${color} ${to}%`);
+ }
+ return `repeating-linear-gradient(135deg, ${gradient.join(', ')})`;
+ }
+
+ get progressCssValue(): string {
+ return `${(this.progress ?? 0) * 100}%`;
+ }
+
+ get borderWidthCssValue(): string {
+ return `${this.borderWidth}px`;
+ }
+
+ get progressDescription(): string {
+ if (this.progress == null) return '';
+
+ // avoid showing 100% when it's not quite there
+ let percent = this.progress > 0.99 && this.progress < 1 ? 99 : Math.round(this.progress * 100);
+ return this.progress != null ? `${Math.round(percent)}% translated` : '';
+ }
+}
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.stories.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.stories.ts
new file mode 100644
index 00000000000..e9dcfb187a7
--- /dev/null
+++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/toggle-book/toggle-book.stories.ts
@@ -0,0 +1,68 @@
+import { CommonModule } from '@angular/common';
+import { TranslocoModule } from '@ngneat/transloco';
+import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import { TranslocoMarkupModule } from 'ngx-transloco-markup';
+import { I18nStoryModule } from '../../../../xforge-common/i18n-story.module';
+import { UICommonModule } from '../../../../xforge-common/ui-common.module';
+import { ToggleBookComponent } from './toggle-book.component';
+
+const meta: Meta = {
+ title: 'Translate/ToggleBook',
+ component: ToggleBookComponent,
+ decorators: [
+ moduleMetadata({
+ imports: [UICommonModule, I18nStoryModule, CommonModule, TranslocoModule, TranslocoMarkupModule]
+ })
+ ],
+ argTypes: {
+ progress: { control: { type: 'range', min: 0, max: 1, step: 0.01 } }
+ }
+};
+
+export default meta;
+
+interface StoryState {
+ book: number;
+ progress?: number;
+ hues: number[];
+ selected: boolean;
+ disabled: boolean;
+}
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ book: 1,
+ progress: 0.37,
+ hues: [0]
+ }
+};
+
+export const Selected: Story = {
+ args: {
+ ...Default.args,
+ selected: true
+ }
+};
+
+export const TwoColor: Story = {
+ args: {
+ ...Selected.args,
+ hues: [0, 240]
+ }
+};
+
+export const ThreeColor: Story = {
+ args: {
+ ...Selected.args,
+ hues: [0, 120, 240]
+ }
+};
+
+export const Disabled: Story = {
+ args: {
+ book: 8,
+ disabled: true
+ }
+};