add secondary filters

This commit is contained in:
Dan Percic 2021-04-25 14:31:15 +03:00
parent 885269c7d1
commit 9bbdf522bb
15 changed files with 133 additions and 138 deletions

View File

@ -11,7 +11,8 @@
[chevron]="true"
[filterTemplate]="annotationFilterTemplate"
[actionsTemplate]="annotationFilterActionTemplate"
[filters]="annotationFilters"
[primaryFilters]="primaryFilters"
[secondaryFilters]="secondaryFilters"
></redaction-filter>
</div>
</div>

View File

@ -6,7 +6,6 @@ import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import { debounce } from '../../../../utils/debounce';
import { FileDataModel } from '../../../../models/file/file-data.model';
import { PermissionsService } from '../../../../services/permissions.service';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -29,7 +28,8 @@ export class FileWorkloadComponent {
@Input() activeViewerPage: number;
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
@Input() dialogRef: MatDialogRef<any>;
@Input() annotationFilters: FilterModel[];
@Input() primaryFilters: FilterModel[];
@Input() secondaryFilters: FilterModel[];
@Input() fileData: FileDataModel;
@Input() hideSkipped: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>;
@ -45,7 +45,6 @@ export class FileWorkloadComponent {
public quickScrollLastEnabled = false;
public displayedPages: number[] = [];
public pagesPanelActive = true;
public filterOnlyWithComments = false;
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
@ -106,8 +105,8 @@ export class FileWorkloadComponent {
}
@debounce(0)
public filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters);
public filtersChanged(filters: { primary: FilterModel[]; secondary?: FilterModel[] }) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters.primary, filters.secondary);
this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
this.computeQuickNavButtonsState();
this._changeDetectorRef.markForCheck();

View File

@ -36,4 +36,6 @@
<redaction-annotation-icon *ngIf="filter.key === 'updated'" type="square" label="U" [color]="dictionaryColor"></redaction-annotation-icon>
<redaction-annotation-icon *ngIf="filter.key === 'image'" type="square" label="I" [color]="dictionaryColor"></redaction-annotation-icon>
<mat-icon *ngIf="filter.key.startsWith('red:')" [svgIcon]="filter.key"></mat-icon>
{{ filter.label | translate }}

View File

@ -6,4 +6,11 @@
redaction-annotation-icon {
margin-right: 8px;
}
mat-icon {
width: 16px;
height: 16px;
margin-right: 8px;
opacity: 50%;
}
}

View File

@ -233,7 +233,8 @@
[activeViewerPage]="activeViewerPage"
[(shouldDeselectAnnotationsOnPageChange)]="shouldDeselectAnnotationsOnPageChange"
[dialogRef]="dialogRef"
[annotationFilters]="annotationFilters"
[primaryFilters]="primaryFilters"
[secondaryFilters]="secondaryFilters"
[fileData]="fileData"
[hideSkipped]="hideSkipped"
[annotationActionsTemplate]="annotationActionsTemplate"

View File

@ -51,7 +51,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
annotationData: AnnotationData;
selectedAnnotations: AnnotationWrapper[];
viewReady = false;
annotationFilters: FilterModel[];
primaryFilters: FilterModel[];
secondaryFilters: FilterModel[];
loadingMessage: string;
canPerformAnnotationActions: boolean;
filesAutoUpdateTimer: Subscription;
@ -217,8 +218,12 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
this.userPreferenceService.areDevFeaturesEnabled
);
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations);
this.annotationFilters = processFilters(this.annotationFilters, annotationFilters);
this._workloadComponent.filtersChanged(this.annotationFilters);
this.primaryFilters = processFilters(this.primaryFilters, annotationFilters);
this.secondaryFilters = processFilters(this.secondaryFilters, AnnotationProcessingService.secondaryAnnotationFilters);
this._workloadComponent.filtersChanged({
primary: this.primaryFilters,
secondary: this.secondaryFilters
});
console.log('[REDACTION] Process time: ' + (new Date().getTime() - processStartTime) + 'ms');
console.log(
'[REDACTION] Annotation Redraw and filter rebuild time: ' +
@ -522,12 +527,12 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
}
private _handleDeltaAnnotationFilters(currentPageAnnotations: AnnotationWrapper[], newPageAnnotations: AnnotationWrapper[]) {
const hasAnyFilterSet = this.annotationFilters.find((f) => f.checked || f.indeterminate);
const hasAnyFilterSet = this.primaryFilters.find((f) => f.checked || f.indeterminate);
if (hasAnyFilterSet) {
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters);
this._workloadComponent.filtersChanged(this.annotationFilters);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.primaryFilters);
this._workloadComponent.filtersChanged({ primary: this.primaryFilters });
}
}

View File

@ -6,14 +6,14 @@
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[filters]="statusFilters"
[primaryFilters]="statusFilters"
[icon]="'red:status'"
></redaction-filter>
<redaction-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.people'"
[filters]="peopleFilters"
[primaryFilters]="peopleFilters"
[icon]="'red:user'"
></redaction-filter>
<redaction-filter
@ -21,7 +21,7 @@
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[filters]="needsWorkFilters"
[primaryFilters]="needsWorkFilters"
[icon]="'red:needs-work'"
></redaction-filter>
<redaction-filter
@ -29,7 +29,7 @@
#ruleSetFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.rulesets'"
[filters]="ruleSetFilters"
[primaryFilters]="ruleSetFilters"
[icon]="'red:template'"
></redaction-filter>
<redaction-search-input [form]="searchForm" [placeholder]="'project-listing.search'"></redaction-search-input>

View File

@ -6,14 +6,14 @@
#statusFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.status'"
[filters]="statusFilters"
[primaryFilters]="statusFilters"
[icon]="'red:status'"
></redaction-filter>
<redaction-filter
#peopleFilter
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.assigned-people'"
[filters]="peopleFilters"
[primaryFilters]="peopleFilters"
[icon]="'red:user'"
></redaction-filter>
<redaction-filter
@ -21,7 +21,7 @@
(filtersChanged)="filtersChanged()"
[filterLabel]="'filters.needs-work'"
[filterTemplate]="needsWorkTemplate"
[filters]="needsWorkFilters"
[primaryFilters]="needsWorkFilters"
[icon]="'red:needs-work'"
></redaction-filter>

View File

@ -1,17 +1,14 @@
import { Injectable } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { FilterModel, FilterTypes } from '../../shared/components/filter/model/filter.model';
import { FilterModel } from '../../shared/components/filter/model/filter.model';
import { handleCheckedValue } from '../../shared/components/filter/utils/filter-utils';
import { SuperTypeSorter } from '../../../utils/sorters/super-type-sorter';
@Injectable()
export class AnnotationProcessingService {
constructor(private readonly _appStateService: AppStateService) {}
getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] {
const filterMap = new Map<string, FilterModel>();
let filters: FilterModel[] = [];
const filters: FilterModel[] = [];
annotations?.forEach((a) => {
const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation';
@ -30,7 +27,6 @@ export class AnnotationProcessingService {
}
const childFilter = {
key: a.dictionary,
type: FilterTypes.primary,
checked: false,
filters: [],
matches: 1
@ -52,16 +48,12 @@ export class AnnotationProcessingService {
}
}
filters = filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]);
filters.push(...AnnotationProcessingService._secondaryFilters);
return filters;
return filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]);
}
private _createParentFilter(key: string, filterMap: Map<string, FilterModel>, filters: FilterModel[], type?: FilterTypes) {
private _createParentFilter(key: string, filterMap: Map<string, FilterModel>, filters: FilterModel[]) {
const filter: FilterModel = {
key: key,
type: type || FilterTypes.primary,
topLevelFilter: true,
matches: 1,
label: 'annotation-type.' + key,
@ -72,48 +64,27 @@ export class AnnotationProcessingService {
return filter;
}
filterAndGroupAnnotations(annotations: AnnotationWrapper[], filters: FilterModel[]): { [key: number]: { annotations: AnnotationWrapper[] } } {
filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
primaryFilters: FilterModel[],
secondaryFilters?: FilterModel[]
): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {};
const hasActiveFilters = this._hasActiveFilters(filters);
const flatFilters: FilterModel[] = [];
filters.forEach((filter) => {
flatFilters.push(filter);
flatFilters.push(...filter?.filters);
});
const primaryFilters = flatFilters.filter((f) => f.type === FilterTypes.primary && f.checked);
const secondaryFilters = flatFilters.filter((f) => f.type === FilterTypes.secondary && f.checked && f.action);
console.log(secondaryFilters);
const primaryFlatFilters = this._getFlatFilters(primaryFilters, (f) => f.checked);
const secondaryFlatFilters = this._getFlatFilters(secondaryFilters, (f) => f.checked && !!f.checker);
for (const annotation of annotations) {
const pageNumber = annotation.pageNumber;
const type = annotation.superType;
if (hasActiveFilters) {
let found = false;
for (const filter of primaryFilters) {
if (
(filter.key === annotation.dictionary &&
(annotation.superType === 'hint' || annotation.superType === 'redaction' || annotation.superType === 'recommendation')) ||
filter.key === annotation.superType
) {
let secondaryFiltersNotMatched = false;
for (const secondaryFilter of secondaryFilters) {
if (!secondaryFilter.action(annotation)) {
secondaryFiltersNotMatched = true;
}
}
found = !secondaryFiltersNotMatched;
break;
}
}
if (!found) {
continue;
}
if (!this._matchesOne(primaryFlatFilters, (f) => this._checkByFilterKey(f, annotation))) {
continue;
}
if (!this._matchesAll(secondaryFlatFilters, (f) => f.checker(annotation))) {
continue;
}
const pageNumber = annotation.pageNumber;
if (!obj[pageNumber]) {
obj[pageNumber] = {
annotations: [],
@ -125,7 +96,7 @@ export class AnnotationProcessingService {
}
obj[pageNumber].annotations.push(annotation);
obj[pageNumber][type]++;
obj[pageNumber][annotation.superType]++;
}
Object.keys(obj).map((page) => {
@ -135,6 +106,44 @@ export class AnnotationProcessingService {
return obj;
}
private _getFlatFilters(filters: FilterModel[], filterBy?: (f: FilterModel) => boolean) {
const flatFilters: FilterModel[] = [];
filters.forEach((filter) => {
flatFilters.push(filter);
flatFilters.push(...filter.filters);
});
return !!filterBy ? flatFilters.filter((f) => filterBy(f)) : flatFilters;
}
private _matchesOne = (filters: FilterModel[], condition: (filter: FilterModel) => boolean): boolean => {
if (filters.length === 0) return true;
for (const filter of filters) {
if (condition(filter)) return true;
}
return false;
};
private _matchesAll = (filters: FilterModel[], condition: (filter: FilterModel) => boolean): boolean => {
if (filters.length === 0) return true;
for (const filter of filters) {
if (!condition(filter)) return false;
}
return true;
};
private _checkByFilterKey = (filter: FilterModel, annotation: AnnotationWrapper) => {
const superType = annotation.superType;
const isNotTopLevelFilter = superType === 'hint' || superType === 'redaction' || superType === 'recommendation';
return filter.key === superType || (filter.key === annotation.dictionary && isNotTopLevelFilter);
};
private _sortAnnotations(annotations: AnnotationWrapper[]): AnnotationWrapper[] {
return annotations.sort((ann1, ann2) => {
if (ann1.pageNumber === ann2.pageNumber) {
@ -148,22 +157,15 @@ export class AnnotationProcessingService {
});
}
private _hasActiveFilters(filters: FilterModel[]): boolean {
return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false);
}
private static get _secondaryFilters(): FilterModel[] {
static get secondaryAnnotationFilters(): FilterModel[] {
return [
{
key: 'with-comments',
key: 'red:comment',
label: 'filter-menu.with-comments',
checked: false,
topLevelFilter: true,
type: FilterTypes.secondary,
filters: [],
action: (obj) => {
return obj?.comments?.length > 0;
}
checker: (annotation: AnnotationWrapper) => annotation?.comments?.length > 0
}
];
}

View File

@ -9,7 +9,7 @@
<redaction-chevron-button *ngIf="chevron" [text]="filterLabel" [matMenuTriggerFor]="filterMenu" [showDot]="hasActiveFilters"></redaction-chevron-button>
<mat-menu #filterMenu="matMenu" xPosition="before" (closed)="applyFilters()">
<div (mouseleave)="filterMouseLeave()" (mouseenter)="filterMouseEnter()">
<div (mouseleave)="filterMouseLeave()" (mouseenter)="filterMouseEnter()" [class.pb-24]="secondaryFilters?.length === 0">
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
<div class="actions">
@ -22,16 +22,10 @@
*ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneFilterIsExpandable }"
></ng-template>
</div>
<div class="filter-options" *ngIf="secondaryFilters?.length">
<div class="filter-options" *ngIf="secondaryFilters?.length > 0">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
<!-- <div class="mat-menu-item flex">-->
<!-- <mat-checkbox class="filter-menu-checkbox" (click)="filterOptionsCheckboxClicked($event)" [checked]="filterOnlyWithComments">-->
<!-- <mat-icon svgIcon="red:comment"></mat-icon>-->
<!-- {{ 'filter-menu.with-comments' | translate }}-->
<!-- </mat-checkbox>-->
<!-- </div>-->
<div *ngFor="let filter of secondaryFilters">
<ng-template
*ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable }"
@ -42,21 +36,21 @@
</mat-menu>
<ng-template #defaultFilterLabelTemplate let-filter="filter">
{{ filter?.label }}
{{ _(filter)?.label }}
</ng-template>
<ng-template #defaultFilterTemplate let-filter="filter" let-atLeastOneIsExpandable="atLeastOneIsExpandable">
<div class="mat-menu-item flex" (click)="toggleFilterExpanded($event, filter)">
<div class="arrow-wrapper" *ngIf="isExpandable(filter)">
<mat-icon *ngIf="filter.expanded" svgIcon="red:arrow-down" color="accent"></mat-icon>
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
<mat-icon *ngIf="_(filter).expanded" svgIcon="red:arrow-down" color="accent"></mat-icon>
<mat-icon *ngIf="!_(filter).expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
</div>
<div class="arrow-wrapper spacer" *ngIf="atLeastOneIsExpandable && !isExpandable(filter)">
&nbsp;
</div>
<mat-checkbox
[checked]="filter.checked"
[indeterminate]="filter.indeterminate"
[checked]="_(filter).checked"
[indeterminate]="_(filter).indeterminate"
(click)="filterCheckboxClicked($event, filter)"
class="filter-menu-checkbox"
>
@ -64,8 +58,8 @@
</mat-checkbox>
<ng-template *ngTemplateOutlet="actionsTemplate ? actionsTemplate : null; context: { filter: filter }"></ng-template>
</div>
<div *ngIf="filter.filters?.length && filter.expanded">
<div *ngFor="let subFilter of filter.filters" class="padding-left mat-menu-item" (click)="$event.stopPropagation()">
<div *ngIf="_(filter).filters?.length && _(filter).expanded">
<div *ngFor="let subFilter of _(filter).filters" class="padding-left mat-menu-item" (click)="$event.stopPropagation()">
<mat-checkbox [checked]="subFilter.checked" (click)="filterCheckboxClicked($event, subFilter, filter)">
<ng-template *ngTemplateOutlet="filterTemplate ? filterTemplate : defaultFilterLabelTemplate; context: { filter: subFilter }"></ng-template>
</mat-checkbox>

View File

@ -24,12 +24,6 @@
.filter-options {
background-color: $grey-2;
padding-bottom: 8px;
mat-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
}
::ng-deep .mat-menu-panel .mat-menu-content:not(:empty) {

View File

@ -1,6 +1,5 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core';
import { AppStateService } from '../../../../state/app-state.service';
import { FilterModel, FilterTypes } from './model/filter.model';
import { FilterModel } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
@ -19,10 +18,11 @@ import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
]
})
export class FilterComponent implements OnChanges {
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Output() filtersChanged = new EventEmitter<{ primary: FilterModel[]; secondary?: FilterModel[] }>();
@Input() filterTemplate: TemplateRef<any>;
@Input() actionsTemplate: TemplateRef<any>;
@Input() filters: FilterModel[] = [];
@Input() primaryFilters: FilterModel[] = [];
@Input() secondaryFilters: FilterModel[] = [];
@Input() filterLabel = 'filter-menu.label';
@Input() icon: string;
@Input() chevron = false;
@ -33,15 +33,16 @@ export class FilterComponent implements OnChanges {
atLeastOneFilterIsExpandable = false;
atLeastOneSecondaryFilterIsExpandable = false;
constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {}
constructor(private readonly _changeDetectorRef: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void {
this.atLeastOneFilterIsExpandable = false;
this.atLeastOneSecondaryFilterIsExpandable = false;
this.filters?.forEach((f) => {
if (f.type === FilterTypes.primary) this.atLeastOneFilterIsExpandable = this.atLeastOneFilterIsExpandable || this.isExpandable(f);
if (f.type === FilterTypes.secondary)
this.atLeastOneSecondaryFilterIsExpandable = this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f);
this.primaryFilters?.forEach((f) => {
this.atLeastOneFilterIsExpandable = this.atLeastOneFilterIsExpandable || this.isExpandable(f);
});
this.secondaryFilters?.forEach((f) => {
this.atLeastOneSecondaryFilterIsExpandable = this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f);
});
}
@ -71,7 +72,7 @@ export class FilterComponent implements OnChanges {
}
get hasActiveFilters(): boolean {
for (const filter of this.filters ? this.filters : []) {
for (const filter of this.primaryFilters ? this.primaryFilters : []) {
if (filter.checked || filter.indeterminate) {
return true;
}
@ -79,16 +80,8 @@ export class FilterComponent implements OnChanges {
return false;
}
get primaryFilters() {
return this.filters?.filter((f) => f.type === FilterTypes.primary || f.type === undefined);
}
get secondaryFilters() {
return this.filters?.filter((f) => f.type === FilterTypes.secondary);
}
applyFilters() {
this.filtersChanged.emit(this.filters);
this.filtersChanged.emit({ primary: this.primaryFilters, secondary: this.secondaryFilters });
}
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {
@ -97,7 +90,7 @@ export class FilterComponent implements OnChanges {
}
private _setAllFilters(value: boolean) {
this.filters?.forEach((f) => {
this.primaryFilters?.forEach((f) => {
f.checked = value;
f.indeterminate = false;
f.filters?.forEach((ff) => {
@ -123,4 +116,8 @@ export class FilterComponent implements OnChanges {
isExpandable(filter: FilterModel) {
return filter.filters && filter.filters.length > 0;
}
_(obj): FilterModel {
return obj as FilterModel;
}
}

View File

@ -1,11 +1,5 @@
export enum FilterTypes {
primary = 'primary',
secondary = 'secondary'
}
export interface FilterModel {
key: string;
type: FilterTypes;
label?: string;
checked?: boolean;
indeterminate?: boolean;
@ -13,5 +7,5 @@ export interface FilterModel {
topLevelFilter?: boolean;
matches?: number;
filters?: FilterModel[];
action?: (obj?) => boolean;
checker?: (obj?) => boolean;
}

View File

@ -1,13 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>status</title>
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Styleguide-Actions" transform="translate(-607.000000, -818.000000)" fill="#283241" fill-rule="nonzero">
<g id="Group-6" transform="translate(598.000000, 809.000000)">
<g id="status" transform="translate(9.000000, 9.000000)">
<path d="M8,0 C4,0 0.8,3.2 0.8,7.2 C0.8,8.88 1.36,10.4 2.4,11.68 L2.4,16 L7.04,14.32 C7.44,14.4 7.68,14.4 8,14.4 C12,14.4 15.2,11.2 15.2,7.2 C15.2,3.2 12,0 8,0 Z M8,12.8 C7.76,12.8 7.44,12.8 7.12,12.72 L6.88,12.72 L4,13.76 L4,11.12 L3.76,10.88 C2.88,9.84 2.4,8.56 2.4,7.2 C2.4,4.08 4.88,1.6 8,1.6 C11.12,1.6 13.6,4.08 13.6,7.2 C13.6,10.32 11.12,12.8 8,12.8 Z" id="Shape"></path>
</g>
</g>
</g>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="comments" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path
d="M50,0 C25,0 5,20 5,45 C5,55.5 8.5,65 15,73 L15,100 L44,89.5 C46.5,90 48,90 50,90 C75,90 95,70 95,45 C95,20 75,0 50,0 Z M50,80 C48.5,80 46.5,80 44.5,79.5 L43,79.5 L25,86 L25,69.5 L23.5,68 C18,61.5 15,53.5 15,45 C15,25.5 30.5,10 50,10 C69.5,10 85,25.5 85,45 C85,64.5 69.5,80 50,80 Z"
id="Shape" fill="currentColor" fill-rule="nonzero"></path>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 628 B

View File

@ -229,6 +229,10 @@ body {
margin-top: 20px;
}
.pb-24 {
padding-bottom: 24px;
}
.pb-32 {
padding-bottom: 32px;
}