Add Settings Popup

This commit is contained in:
copecone committed 2026-03-26 00:14:43 +09:00
1 parent d946ee4026
commit 010771b22a
15 files changed
+327 -42

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+6
View File
@@ -1,2 +1,8 @@
<header-menu></header-menu>
<router-outlet />
<div id="global-popup-root" class="popup-outlet">
@for (popup of popupService.activePopups(); track popup) {
<ng-container *ngComponentOutlet="popup" />
}
</div>
+7 -4
View File
@@ -1,12 +1,15 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HeaderMenu } from './features/header-menu/header-menu';
import { PopupService } from './services/popup.service';
import { NgComponentOutlet } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, HeaderMenu],
imports: [RouterOutlet, HeaderMenu, NgComponentOutlet],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {}
export class App {
protected popupService = inject(PopupService);
}
+1 -1
View File
@@ -36,7 +36,7 @@ $dropdown_animation_duration: 0.2s;
.dropdown-box {
display: flex;
align-items: center;
padding: 0 12px;
padding: 0 10px;
height: $dropdown_height;
box-sizing: border-box;
gap: 8px;
+5 -1
View File
@@ -1,13 +1,17 @@
<div class="popup-backdrop" (click)="onClose()">
<div class="popup-container" (click)="$event.stopPropagation()">
<header class="popup-header">
<div class="header-left">
<ng-content select="[header-left]"></ng-content>
</div>
<div class="header-right">
<button class="material-symbols-outlined icon-24 chevron" (click)="onClose()">close</button>
<button class="material-symbols-outlined icon-24 close-btn" (click)="onClose()">close</button>
</div>
</header>
<div class="popup-body">
<ng-content></ng-content>
</div>
</div>
</div>
+78
View File
@@ -0,0 +1,78 @@
:host {
display: block;
}
.popup-backdrop {
position: fixed;
inset: 0;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
z-index: 100; /* .popup-outlet 내부에서의 상대적 순서 */
}
.popup-container {
background: white;
border-radius: 12px;
box-shadow: 0 0 40px oklch(0 0 0 / 15%);
min-width: 40px;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: visible;
animation: popup-enter 0.2s cubic-bezier(0.16, 1, 0.3, 1);
border: oklch(0 0 0 / 20%) 2px solid;
gap: 15px;
}
@keyframes popup-enter {
from {
transform: scale(0.95) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
height: 40px;
border-bottom: 1px solid oklch(0 0 0 / 5%);
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-right {
.close-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
border-radius: 50%;
color: oklch(0.4 0 0);
transition: background-color 0.2s, color 0.2s;
&:hover {
background-color: oklch(0 0 0 / 5%);
color: oklch(0 0 0);
}
}
}
}
.popup-body {
padding: 0 12px;
overflow-y: visible;
flex-grow: 1;
}
+8 -6
View File
@@ -1,16 +1,18 @@
import { Component, input, model, output } from '@angular/core';
import { Dropdown } from '../../components/dropdown/dropdown';
import { Switch } from '../../components/switch/switch';
import { Component, output } from '@angular/core';
@Component({
selector: 'popup',
selector: 'app-popup',
standalone: true,
templateUrl: './popup.html',
styleUrl: './popup.scss',
})
export class PopupComponent {
close = output<void>();
/**
* 팝업 닫기 요청 (배경 클릭 또는 닫기 버튼)
*/
closeRequest = output<void>();
onClose() {
this.close.emit()
this.closeRequest.emit();
}
}
@@ -1,6 +1,6 @@
<div class="menu-left">
<div class="brand" [routerLink]="['/']">
<img src="/logo_inverted.svg" alt="logo"/>
<img ngSrc="/logo_inverted.svg" alt="logo" height="20" width="59.52" />
<span><strong>ADOFAI Editor Forum</strong></span>
</div>
@@ -15,5 +15,5 @@
<span class="dummy-link"><strong>Login</strong></span>
<span class="dummy-link">Signup</span>
<span class="material-symbols-outlined icon-40 settings">settings</span>
<span class="material-symbols-outlined icon-40 settings" (click)="onSettingsClick()">settings</span>
</div>
@@ -81,5 +81,16 @@
flex-grow: 1;
justify-content: flex-end;
padding: 12px 20px;
.settings {
position: relative;
z-index: 10;
cursor: pointer;
transition: transform 0.3s ease;
&:hover {
transform: rotate(30deg);
}
}
}
}
+13 -3
View File
@@ -1,10 +1,20 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { NgOptimizedImage } from '@angular/common';
import { PopupService } from '../../services/popup.service';
import { SettingsPopup } from '../settings/settings';
@Component({
selector: 'header-menu',
imports: [RouterLink],
imports: [RouterLink, NgOptimizedImage],
templateUrl: './header-menu.html',
styleUrl: './header-menu.scss',
})
export class HeaderMenu {}
export class HeaderMenu {
private popupService = inject(PopupService);
onSettingsClick() {
console.log('HeaderMenu: Settings clicked');
this.popupService.open(SettingsPopup);
}
}
+30 -9
View File
@@ -1,16 +1,37 @@
<!--<popup (close)="handleClose()">
<app-popup (closeRequest)="close()">
<div header-left class="profile-section">
<img [src]="userProfileImg" alt="profile" class="avatar">
<img ngSrc="/guest_profile.png" alt="profile" class="avatar" width="24" height="24">
<span>{{ userName }}</span>
</div>
<div class="settings-content">
<label>Language</label>
<section class="settings-category">
<label class="category-header">Language</label>
<div class="settings-container">
<dropdown [options]="languages"
placeholder="Choose Language"></dropdown>
<switch></switch>
<label>Appearance</label>
placeholder="Choose Language"
icon="language"></dropdown>
</div>
</popup>-->
<div class="settings-container">
<div class="settings-name">
<span class="material-symbols-outlined icon-24 settings-icon">match_case</span>
<span>Romanisation</span>
</div>
<switch></switch>
</div>
</section>
<section class="settings-category">
<label class="category-header">Appearance</label>
<div class="settings-container">
<div class="settings-name">
<span class="material-symbols-outlined icon-24 settings-icon">dark_mode</span>
<span>Dark Mode</span>
</div>
<switch></switch>
</div>
</section>
</div>
</app-popup>
+79
View File
@@ -0,0 +1,79 @@
:host {
display: block;
}
.profile-section {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 500;
.avatar {
border-radius: 50%;
border: 1px solid oklch(0 0 0 / 20%);
object-fit: cover;
}
}
.settings-content {
display: flex;
flex-direction: column;
gap: 16px;
padding-bottom: 8px;
section {
display: flex;
flex-direction: column;
gap: 6px;
align-items: flex-start;
justify-content: flex-start;
margin: 0; padding: 0;
border: none;
label {
display: block;
font-weight: 500;
font-size: 14px;
color: #999999;
}
.settings-container {
width: 100%;
height: 40px;
display: flex;
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;
}
}
}
+26 -3
View File
@@ -1,9 +1,32 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';
import { PopupComponent } from '../../components/popup/popup';
import { Dropdown, DropdownOption } from '../../components/dropdown/dropdown';
import { Switch } from '../../components/switch/switch';
import { NgOptimizedImage } from '@angular/common';
import { PopupService } from '../../services/popup.service';
@Component({
selector: 'settings-popup',
imports: [],
standalone: true,
imports: [PopupComponent, Dropdown, Switch, NgOptimizedImage],
templateUrl: './popup.html',
styleUrl: './popup.scss',
})
export class SettingsPopup {}
export class SettingsPopup {
private popupService = inject(PopupService);
languages: DropdownOption[] = [
{ label: 'English', value: 'EN' },
{ label: '한국어', value: 'KR' },
{ label: '日本語', value: 'JP' },
];
userName: string = "Guest";
/**
* 팝업을 닫을 때, 자기 자신(클래스)을 명확하게 지정하여 닫습니다.
*/
close() {
this.popupService.close(SettingsPopup);
}
}
+42
View File
@@ -0,0 +1,42 @@
import { Injectable, signal, Type } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class PopupService {
/**
* 현재 활성화된 팝업 컴포넌트들의 리스트 (Signal)
*/
activePopups = signal<Type<any>[]>([]);
/**
* 팝업을 엽니다. 이미 열려있는 동일한 컴포넌트가 있다면 중복해서 열지 않습니다.
*/
open<T>(component: Type<T>) {
this.activePopups.update((popups) => {
if (popups.includes(component)) return popups;
return [...popups, component];
});
}
/**
* 특정 컴포넌트를 닫습니다.
*/
close(component: Type<any>) {
this.activePopups.update((popups) => popups.filter((p) => p !== component));
}
/**
* 가장 최근에 열린(마지막) 팝업을 닫습니다.
*/
closeLast() {
this.activePopups.update((popups) => popups.slice(0, -1));
}
/**
* 모든 팝업을 닫습니다.
*/
closeAll() {
this.activePopups.set([]);
}
}
+19 -13
View File
@@ -6,29 +6,18 @@ body {
font-family: 'NanumSquareNeo', 'Wanted Sans Variable', 'Pretendard JP', sans-serif;
font-weight: 500;
margin: 0;
overscroll-behavior-x: none;
background-color: white;
}
.icon-24 {
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24;
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
font-size: 24px;
line-height: 24px;
}
.icon-40 {
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 40;
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40;
font-size: 40px;
line-height: 40px;
}
@@ -60,6 +49,18 @@ p {
margin: 0 auto;
}
.popup-outlet {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10000; /* 최상위 레이어 */
}
#global-popup-root > * {
pointer-events: auto;
display: block;
}
section {
margin-bottom: 40px;
padding: 20px;
@@ -67,3 +68,8 @@ section {
border-radius: 8px;
}
.container {
padding: 40px;
max-width: 600px;
margin: 0 auto;
}