Add Settings Popup
This commit is contained in:
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 |
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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 { 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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-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;
|
||||
}
|
||||
Reference in new issue
Block a user