Add Settings Popup
This commit is contained in:
1 parent
d946ee4026
commit
010771b22a
15 files changed
+335
-50
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,2 +1,8 @@
|
|||||||
<header-menu></header-menu>
|
<header-menu></header-menu>
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
|
|
||||||
|
<div id="global-popup-root" class="popup-outlet">
|
||||||
|
@for (popup of popupService.activePopups(); track popup) {
|
||||||
|
<ng-container *ngComponentOutlet="popup" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
+7
-4
@@ -1,12 +1,15 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { HeaderMenu } from './features/header-menu/header-menu';
|
import { HeaderMenu } from './features/header-menu/header-menu';
|
||||||
|
import { PopupService } from './services/popup.service';
|
||||||
|
import { NgComponentOutlet } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
imports: [RouterOutlet, HeaderMenu, NgComponentOutlet],
|
||||||
imports: [RouterOutlet, HeaderMenu],
|
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
})
|
})
|
||||||
export class App {}
|
export class App {
|
||||||
|
protected popupService = inject(PopupService);
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ $dropdown_animation_duration: 0.2s;
|
|||||||
.dropdown-box {
|
.dropdown-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
height: $dropdown_height;
|
height: $dropdown_height;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<header class="popup-header">
|
<div class="popup-backdrop" (click)="onClose()">
|
||||||
<div class="header-left">
|
<div class="popup-container" (click)="$event.stopPropagation()">
|
||||||
<ng-content select="[header-left]"></ng-content>
|
<header class="popup-header">
|
||||||
</div>
|
<div class="header-left">
|
||||||
|
<ng-content select="[header-left]"></ng-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="header-right">
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Component, input, model, output } from '@angular/core';
|
import { Component, output } from '@angular/core';
|
||||||
import { Dropdown } from '../../components/dropdown/dropdown';
|
|
||||||
import { Switch } from '../../components/switch/switch';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'popup',
|
selector: 'app-popup',
|
||||||
|
standalone: true,
|
||||||
templateUrl: './popup.html',
|
templateUrl: './popup.html',
|
||||||
styleUrl: './popup.scss',
|
styleUrl: './popup.scss',
|
||||||
})
|
})
|
||||||
export class PopupComponent {
|
export class PopupComponent {
|
||||||
close = output<void>();
|
/**
|
||||||
|
* 팝업 닫기 요청 (배경 클릭 또는 닫기 버튼)
|
||||||
|
*/
|
||||||
|
closeRequest = output<void>();
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
this.close.emit()
|
this.closeRequest.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="menu-left">
|
<div class="menu-left">
|
||||||
<div class="brand" [routerLink]="['/']">
|
<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>
|
<span><strong>ADOFAI Editor Forum</strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -15,5 +15,5 @@
|
|||||||
<span class="dummy-link"><strong>Login</strong></span>
|
<span class="dummy-link"><strong>Login</strong></span>
|
||||||
<span class="dummy-link">Signup</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>
|
</div>
|
||||||
@@ -81,5 +81,16 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
|
import { PopupService } from '../../services/popup.service';
|
||||||
|
import { SettingsPopup } from '../settings/settings';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header-menu',
|
selector: 'header-menu',
|
||||||
imports: [RouterLink],
|
imports: [RouterLink, NgOptimizedImage],
|
||||||
templateUrl: './header-menu.html',
|
templateUrl: './header-menu.html',
|
||||||
styleUrl: './header-menu.scss',
|
styleUrl: './header-menu.scss',
|
||||||
})
|
})
|
||||||
export class HeaderMenu {}
|
export class HeaderMenu {
|
||||||
|
private popupService = inject(PopupService);
|
||||||
|
|
||||||
|
onSettingsClick() {
|
||||||
|
console.log('HeaderMenu: Settings clicked');
|
||||||
|
this.popupService.open(SettingsPopup);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,37 @@
|
|||||||
<!--<popup (close)="handleClose()">
|
<app-popup (closeRequest)="close()">
|
||||||
<div header-left class="profile-section">
|
<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>
|
<span>{{ userName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<label>Language</label>
|
<section class="settings-category">
|
||||||
<dropdown [options]="languages"
|
<label class="category-header">Language</label>
|
||||||
placeholder="Choose Language"></dropdown>
|
|
||||||
<switch></switch>
|
|
||||||
|
|
||||||
<label>Appearance</label>
|
<div class="settings-container">
|
||||||
|
<dropdown [options]="languages"
|
||||||
|
placeholder="Choose Language"
|
||||||
|
icon="language"></dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</popup>-->
|
</app-popup>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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({
|
@Component({
|
||||||
selector: 'settings-popup',
|
selector: 'settings-popup',
|
||||||
imports: [],
|
standalone: true,
|
||||||
|
imports: [PopupComponent, Dropdown, Switch, NgOptimizedImage],
|
||||||
templateUrl: './popup.html',
|
templateUrl: './popup.html',
|
||||||
styleUrl: './popup.scss',
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -6,29 +6,18 @@ body {
|
|||||||
font-family: 'NanumSquareNeo', 'Wanted Sans Variable', 'Pretendard JP', sans-serif;
|
font-family: 'NanumSquareNeo', 'Wanted Sans Variable', 'Pretendard JP', sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-24 {
|
.icon-24 {
|
||||||
font-variation-settings:
|
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
||||||
'FILL' 0,
|
|
||||||
'wght' 300,
|
|
||||||
'GRAD' 0,
|
|
||||||
'opsz' 24;
|
|
||||||
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-40 {
|
.icon-40 {
|
||||||
font-variation-settings:
|
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40;
|
||||||
'FILL' 0,
|
|
||||||
'wght' 300,
|
|
||||||
'GRAD' 0,
|
|
||||||
'opsz' 40;
|
|
||||||
|
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
@@ -60,6 +49,18 @@ p {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-outlet {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10000; /* 최상위 레이어 */
|
||||||
|
}
|
||||||
|
|
||||||
|
#global-popup-root > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -67,3 +68,8 @@ section {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user