Add Settings Container Component

This commit is contained in:
copecone committed 2026-04-23 14:13:33 +09:00
1 parent 96f62b0e7c
commit 1885ed3926
9 files changed
+152 -94

No files matched your search

+3 -3
View File
@@ -7,7 +7,7 @@
</ng-template> </ng-template>
<div class="dropdown-box"> <div class="dropdown-box">
@if (icon()) { @if (icon()?.trim()?.length ?? 0 > 0) {
<span class="material-symbols-outlined icon-24 prefix-icon" aria-hidden="true">{{ icon() }}</span> <span class="material-symbols-outlined icon-24 prefix-icon" aria-hidden="true">{{ icon() }}</span>
} }
@@ -22,13 +22,13 @@
@if (isOpen()) { @if (isOpen()) {
<div class="dropdown-container" tabindex="-1"> <div class="dropdown-container" tabindex="-1">
<ul class="dropdown-list" role="listbox" [id]="dropdownId + '-listbox'" tabindex="-1"> <ul class="dropdown-list" role="listbox" [id]="dropdownID + '-listbox'" tabindex="-1">
@for (option of filteredOptions(); track option.value; let i = $index) { @for (option of filteredOptions(); track option.value; let i = $index) {
<li <li
#optionItem #optionItem
class="dropdown-item" class="dropdown-item"
role="option" role="option"
[id]="dropdownId + '-option-' + i" [id]="dropdownID + '-option-' + i"
[class.selected]="option.value === value()" [class.selected]="option.value === value()"
[class.focused]="i === focusedIndex()" [class.focused]="i === focusedIndex()"
[attr.aria-selected]="option.value === value()" [attr.aria-selected]="option.value === value()"
+4 -4
View File
@@ -33,7 +33,7 @@ export interface DropdownOption {
role: 'combobox', role: 'combobox',
'aria-haspopup': 'listbox', 'aria-haspopup': 'listbox',
'[attr.aria-expanded]': 'isOpen()', '[attr.aria-expanded]': 'isOpen()',
'[attr.aria-controls]': "isOpen() ? dropdownId + '-listbox' : null", '[attr.aria-controls]': "isOpen() ? dropdownID + '-listbox' : null",
'[attr.aria-activedescendant]': 'ariaActiveDescendant()', '[attr.aria-activedescendant]': 'ariaActiveDescendant()',
'[attr.aria-label]': 'ariaLabel()', '[attr.aria-label]': 'ariaLabel()',
'[class.open]': 'isOpen()', '[class.open]': 'isOpen()',
@@ -71,13 +71,13 @@ export class Dropdown implements OnInit {
ariaActiveDescendant = computed(() => { ariaActiveDescendant = computed(() => {
return this.isOpen() && this.focusedIndex() >= 0 return this.isOpen() && this.focusedIndex() >= 0
? this.dropdownId + '-option-' + this.focusedIndex() ? this.dropdownID + '-option-' + this.focusedIndex()
: null; : null;
}); });
private static nextId = 0; private static nextID = 0;
private isLatestMouseDownInsideInput = false; private isLatestMouseDownInsideInput = false;
dropdownId = `dropdown-${Dropdown.nextId++}`; dropdownID = `dropdown-${Dropdown.nextID++}`;
private isLatestMouseDownInsideContainer = false; private isLatestMouseDownInsideContainer = false;
constructor(private element: ElementRef) { constructor(private element: ElementRef) {
+8
View File
@@ -7,3 +7,11 @@
margin: 0; padding: 0; margin: 0; padding: 0;
border: none; border: none;
} }
label {
display: block;
font-weight: 500;
font-size: 14px;
height: 15px;
// color: oklch(0.6830 0 0); // #999999
}
+17
View File
@@ -0,0 +1,17 @@
@if (settingsType() == SettingsType.Switch) {
<div class="settings-name">
@if (labelIcon()?.trim()?.length ?? 0 > 0) {
<span class="material-symbols-outlined icon-24 settings-icon" aria-hidden="true">{{ labelIcon() }}</span>
}
<label [id]="labelID">{{ labelText() }}</label>
</div>
<switch [isActive]="value()" (toggle)="onChange.emit($event)" [aria-labelledby]="labelID"></switch>
} @else if (settingsType() == SettingsType.Dropdown) {
<label [id]="labelID" class="hidden-label">{{ labelText() }}</label>
<dropdown [options]="options()"
[value]="value()"
(selectionChange)="onChange.emit($event)"
[aria-labelledby]="labelID"
[placeholder]="placeholder()?.trim() ?? ''"
[icon]="labelIcon()"></dropdown>
}
+55
View File
@@ -0,0 +1,55 @@
:host {
width: 100%;
display: flex;
&:has(> switch) {
height: 32px;
&:nth-child(1 of settings-container) {
margin-top: 4px;
}
}
&:has(> dropdown) {
height: 40px;
}
flex-direction: row;
align-items: center;
justify-content: space-between;
.settings-name {
display: flex;
flex-direction: row;
align-items: center;
padding-left: 10px;
gap: 8px;
span, label {
display: inline-block;
font-size: 18px;
&.settings-icon {
display: inline-block;
width: 24px;
height: 24px;
font-size: 24px;
/*
line-height: 24px;
*/
}
}
}
dropdown {
width: 324px;
}
.hidden-label {
width: 0;
height: 0;
overflow: hidden;
}
}
+26 -28
View File
@@ -1,43 +1,41 @@
<app-popup (closeRequest)="close()" [position]="position()"> <app-popup (closeRequest)="close()" [position]="position()">
<div header-left class="profile-section"> <div header-left class="profile-section">
<img ngSrc="/guest_profile.png" alt="profile" class="avatar" width="24" height="24"> <img ngSrc="/guest_profile.png" alt="profile" class="avatar" width="24" height="24">
<span>{{ settingsService.userName() }}</span> <span>{{ settingsService.isGuest() ? translation.settings.guest : settingsService.userName() }}</span>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<settings-category [headerLabel]="translation.settings.language"> <settings-category [headerLabel]="translation.settings.language">
<div class="settings-container"> <settings-container
<dropdown [options]="languages" [settingsType]="SettingsType.Dropdown"
[value]="settingsService.language()" [options]="languages"
(selectionChange)="onLanguageChange($event)" [value]="settingsService.language()"
icon="language"></dropdown> [labelText]="translation.settings.language" labelIcon="language"
</div> (onChange)="onLanguageChange($event)">
</settings-container>
<div class="settings-container"> <settings-container
<div class="settings-name"> [settingsType]="SettingsType.Switch"
<span class="material-symbols-outlined icon-24 settings-icon" aria-hidden="true">match_case</span> [value]="settingsService.romanisation()"
<span>{{ translation.settings.romanisation }}</span> [labelText]="translation.settings.romanisation" labelIcon="match_case"
</div> (onChange)="onRomanisationChange($event)">
<switch [isActive]="settingsService.romanisation()" (toggle)="onRomanisationChange($event)"></switch> </settings-container>
</div>
</settings-category> </settings-category>
<settings-category [headerLabel]="translation.settings.appearance"> <settings-category [headerLabel]="translation.settings.appearance">
<div class="settings-container"> <settings-container
<div class="settings-name"> [settingsType]="SettingsType.Switch"
<span class="material-symbols-outlined icon-24 settings-icon" aria-hidden="true">dark_mode</span> [value]="settingsService.darkMode()"
<span>{{ translation.settings.darkMode }}</span> [labelText]="translation.settings.darkMode" labelIcon="dark_mode"
</div> (onChange)="onDarkModeChange($event)">
<switch [isActive]="settingsService.darkMode()" (toggle)="onDarkModeChange($event)"></switch> </settings-container>
</div>
<div class="settings-container"> <settings-container
<div class="settings-name"> [settingsType]="SettingsType.Switch"
<span class="material-symbols-outlined icon-24 settings-icon" aria-hidden="true">motion_photos_paused</span> [value]="settingsService.reduceAnimation()"
<span>{{ translation.settings.reduceAnimation }}</span> [labelText]="translation.settings.reduceAnimation" labelIcon="motion_photos_paused"
</div> (onChange)="onReduceAnimationChange($event)">
<switch [isActive]="settingsService.reduceAnimation()" (toggle)="onReduceAnimationChange($event)"></switch> </settings-container>
</div>
</settings-category> </settings-category>
</div> </div>
+1 -57
View File
@@ -24,61 +24,5 @@
gap: 16px; gap: 16px;
padding-bottom: 12px; padding-bottom: 12px;
settings-category {
label {
display: block;
font-weight: 500;
font-size: 14px;
height: 15px;
// color: oklch(0.6830 0 0); // #999999
}
& > .settings-container {
width: 100%;
display: flex;
&:has(> switch) {
height: 32px;
&:nth-child(1 of .settings-container) {
margin-top: 4px;
}
}
&:has(> dropdown) {
height: 40px;
}
flex-direction: row;
align-items: center;
justify-content: space-between;
.settings-name {
display: flex;
flex-direction: row;
align-items: center;
padding-left: 10px;
gap: 8px;
span {
display: inline-block;
font-size: 18px;
&.settings-icon {
display: inline-block;
font-size: 24px;
width: 24px;
height: 24px;
line-height: 24px;
}
}
}
}
dropdown {
width: 324px;
}
}
} }
+37 -2
View File
@@ -1,4 +1,4 @@
import { Component, inject, input } from '@angular/core'; import { Component, inject, input, output } from '@angular/core';
import { PopupComponent, PopupPosition } from '../../components/popup/popup'; import { PopupComponent, PopupPosition } from '../../components/popup/popup';
import { Dropdown, DropdownOption } from '../../components/dropdown/dropdown'; import { Dropdown, DropdownOption } from '../../components/dropdown/dropdown';
import { Switch } from '../../components/switch/switch'; import { Switch } from '../../components/switch/switch';
@@ -7,6 +7,10 @@ import { PopupService } from '../../services/popup.service';
import { SettingsService, Language } from '../../services/settings.service'; import { SettingsService, Language } from '../../services/settings.service';
import { TranslationService } from '../../services/translation.service'; import { TranslationService } from '../../services/translation.service';
export enum SettingsType {
Switch = 'switch', Dropdown = 'dropdown',
}
@Component({ @Component({
selector: 'settings-category', selector: 'settings-category',
standalone: true, standalone: true,
@@ -17,10 +21,39 @@ export class SettingsCategory {
headerLabel = input<string>(); headerLabel = input<string>();
} }
@Component({
selector: 'settings-container',
standalone: true,
templateUrl: './container.html',
styleUrl: './container.scss',
imports: [Switch, Dropdown],
})
export class SettingsContainer {
settingsType = input.required<SettingsType>();
protected readonly SettingsType = SettingsType;
labelIcon = input<string>();
labelText = input.required<string>();
value = input.required<any>();
onChange = output<any>();
placeholder = input<string>();
options = input<any>();
private static nextID = 0;
labelID = `settings-${SettingsContainer.nextID++}-label`;
}
@Component({ @Component({
selector: 'settings-popup', selector: 'settings-popup',
standalone: true, standalone: true,
imports: [PopupComponent, Dropdown, Switch, NgOptimizedImage, SettingsCategory], imports: [
PopupComponent,
NgOptimizedImage,
SettingsCategory,
SettingsContainer,
],
templateUrl: './settings.html', templateUrl: './settings.html',
styleUrl: './settings.scss', styleUrl: './settings.scss',
}) })
@@ -62,4 +95,6 @@ export class SettingsPopup {
close() { close() {
this.popupService.close(SettingsPopup); this.popupService.close(SettingsPopup);
} }
protected readonly SettingsType = SettingsType;
} }
+1
View File
@@ -23,6 +23,7 @@ export class SettingsService {
romanisation = computed(() => this._romanisation()); romanisation = computed(() => this._romanisation());
userName = computed(() => this._userName()); userName = computed(() => this._userName());
reduceAnimation = computed(() => this._reduceAnimation()); reduceAnimation = computed(() => this._reduceAnimation());
isGuest = computed(() => true);
constructor() { constructor() {
this.request = inject(REQUEST); this.request = inject(REQUEST);