Compare commits

..
6 Commits
20 changed files with 351 additions and 96 deletions

No files matched your search

+4
View File
@@ -1,3 +1,7 @@
:host {
min-height: 100dvh;
}
main:focus-visible { main:focus-visible {
outline: none; outline: none;
} }
+2 -1
View File
@@ -57,6 +57,7 @@ export class App {
this.contentElement()!.nativeElement.tabIndex = 0; this.contentElement()!.nativeElement.tabIndex = 0;
this.contentElement()!.nativeElement.focus(); this.contentElement()!.nativeElement.focus();
this.contentElement()!.nativeElement.tabIndex = -1; // @ts-ignore
this.contentElement()!.nativeElement.tabIndex = undefined;
} }
} }
+28 -12
View File
@@ -28,6 +28,7 @@ export class BrandAccent implements OnDestroy {
drawGrid = input<boolean>(true); drawGrid = input<boolean>(true);
private bgRendererScale = 1;
private canvasNativeSize: [number, number] = [0, 0]; private canvasNativeSize: [number, number] = [0, 0];
private bgGradient!: CanvasGradient; private bgGradient!: CanvasGradient;
private gridGradient!: CanvasGradient; private gridGradient!: CanvasGradient;
@@ -123,8 +124,8 @@ export class BrandAccent implements OnDestroy {
canvas.height = height * dpr; canvas.height = height * dpr;
if (this.offscreenCanvas) { if (this.offscreenCanvas) {
this.offscreenCanvas.width = width; this.offscreenCanvas.width = width * this.bgRendererScale;
this.offscreenCanvas.height = height; this.offscreenCanvas.height = height * this.bgRendererScale;
} }
this.canvasNativeSize = [width, height]; this.canvasNativeSize = [width, height];
@@ -153,7 +154,12 @@ export class BrandAccent implements OnDestroy {
if (w === 0 || h === 0) return; if (w === 0 || h === 0) return;
if (!isDark) { if (!isDark) {
this.bgGradient = this.ctx.createLinearGradient(0, 0, w, h); this.bgGradient = this.ctx.createLinearGradient(
0,
0,
w * this.bgRendererScale,
h * this.bgRendererScale,
);
this.bgGradient.addColorStop(0, 'oklch(1 0 0 / 0.6)'); this.bgGradient.addColorStop(0, 'oklch(1 0 0 / 0.6)');
this.bgGradient.addColorStop(1, 'oklch(0.7938 0.1292 47.77 / 0.6)'); this.bgGradient.addColorStop(1, 'oklch(0.7938 0.1292 47.77 / 0.6)');
} }
@@ -177,15 +183,15 @@ export class BrandAccent implements OnDestroy {
const h = this.canvasNativeSize[1]; const h = this.canvasNativeSize[1];
if (w === 0 || h === 0) return; if (w === 0 || h === 0) return;
this.offscreenCtx.clearRect(0, 0, w, h); this.offscreenCtx.clearRect(0, 0, w * this.bgRendererScale, h * this.bgRendererScale);
this.offscreenCtx.globalCompositeOperation = 'source-over'; this.offscreenCtx.globalCompositeOperation = 'source-over';
if (!isDark) { if (!isDark) {
this.offscreenCtx.fillStyle = this.bgGradient; this.offscreenCtx.fillStyle = this.bgGradient;
this.offscreenCtx.fillRect(0, 0, w, h); this.offscreenCtx.fillRect(0, 0, w * this.bgRendererScale, h * this.bgRendererScale);
} else { } else {
this.offscreenCtx.fillStyle = 'oklch(0.1776 0 0)'; this.offscreenCtx.fillStyle = 'oklch(0.1776 0 0)';
this.offscreenCtx.fillRect(0, 0, w, h); this.offscreenCtx.fillRect(0, 0, w * this.bgRendererScale, h * this.bgRendererScale);
const ellipseDimension = [Math.max(100, w * 0.9421875), Math.max(200 / 3, h * (203 / 120))]; const ellipseDimension = [Math.max(100, w * 0.9421875), Math.max(200 / 3, h * (203 / 120))];
@@ -196,13 +202,17 @@ export class BrandAccent implements OnDestroy {
Math.min(780, Math.min(ellipseDimension[0], ellipseDimension[1]) * 0.6) / Math.min(780, Math.min(ellipseDimension[0], ellipseDimension[1]) * 0.6) /
this.rendererScale; this.rendererScale;
this.offscreenCtx.filter = `blur(${blurScale}px)`; this.offscreenCtx.filter = `blur(${blurScale * this.bgRendererScale}px)`;
this.offscreenCtx.beginPath(); this.offscreenCtx.beginPath();
this.offscreenCtx.ellipse( this.offscreenCtx.ellipse(
ellipseX, ellipseY, ellipseX * this.bgRendererScale,
ellipseDimension[0] / 2, ellipseDimension[1] / 2, ellipseY * this.bgRendererScale,
0, 0, 2 * Math.PI, (ellipseDimension[0] / 2) * this.bgRendererScale,
(ellipseDimension[1] / 2) * this.bgRendererScale,
0,
0,
2 * Math.PI,
); );
this.offscreenCtx.fillStyle = 'oklch(0.2547 0.0913 29.02)'; this.offscreenCtx.fillStyle = 'oklch(0.2547 0.0913 29.02)';
this.offscreenCtx.fill(); this.offscreenCtx.fill();
@@ -234,7 +244,13 @@ export class BrandAccent implements OnDestroy {
this.ctx.globalCompositeOperation = 'source-over'; this.ctx.globalCompositeOperation = 'source-over';
this.ctx.imageSmoothingEnabled = false; this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(this.offscreenCanvas, 0, 0); this.ctx.drawImage(
this.offscreenCanvas,
0,
0,
this.canvasNativeSize[0],
this.canvasNativeSize[1],
);
// this.ctx.drawImage(this.offscreenCanvas, 0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]); // this.ctx.drawImage(this.offscreenCanvas, 0, 0, this.canvasNativeSize[0], this.canvasNativeSize[1]);
} }
@@ -261,7 +277,7 @@ export class BrandAccent implements OnDestroy {
(this.canvasNativeSize[1] - this.animation.gridDimension[1] * this.animation.gridSquareSize) / (this.canvasNativeSize[1] - this.animation.gridDimension[1] * this.animation.gridSquareSize) /
2; 2;
const movingOffset = -(this.animation.gridSquareSize * this.animation.progress); const movingOffset = this.settingsService.reduceAnimation() ? 0 : -(this.animation.gridSquareSize * this.animation.progress);
for (let y = 0; y < this.animation.gridDimension[1]; y++) { for (let y = 0; y < this.animation.gridDimension[1]; y++) {
this.ctx.moveTo( this.ctx.moveTo(
+1 -1
View File
@@ -121,7 +121,7 @@ $default_animation_duration: 0.2s;
background-color: light-dark(oklch(1 0 0), oklch(0 0 0)); background-color: light-dark(oklch(1 0 0), oklch(0 0 0));
border-radius: $border_radius; border-radius: $border_radius;
z-index: 1000; z-index: 1000;
max-height: 240px; max-height: 200px;
overflow-y: hidden; overflow-y: hidden;
box-shadow: 0 4px 12px oklch(0 0 0 / 10%); // rgba(0, 0, 0, 0.1) box-shadow: 0 4px 12px oklch(0 0 0 / 10%); // rgba(0, 0, 0, 0.1)
animation: slideIn var(--animation-duration) $animation_timing; animation: slideIn var(--animation-duration) $animation_timing;
@@ -8,7 +8,8 @@ $default_animation_duration: 0.2s;
} }
:host { :host {
width: 400px; width: 100%;
max-width: 400px;
height: 160px; height: 160px;
display: flex; display: flex;
+1 -1
View File
@@ -37,7 +37,7 @@ $default_animation_duration: 0.2s;
animation: popup-enter var(--animation-duration) $animation_timing; animation: popup-enter var(--animation-duration) $animation_timing;
outline: light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%)) 2px solid; outline: light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%)) 2px solid;
gap: 16px; gap: 12px;
&.variant-glass { &.variant-glass {
background-color: light-dark(oklch(1 0 0 / 60%), oklch(0 0 0 / 60%)); background-color: light-dark(oklch(1 0 0 / 60%), oklch(0 0 0 / 60%));
+12 -1
View File
@@ -28,6 +28,11 @@ $default_animation_duration: 0.4s;
&:focus-within { &:focus-within {
top: -40px; top: -40px;
outline: 2px solid var(--selection-outline-color); outline: 2px solid var(--selection-outline-color);
::ng-deep * {
// 내부에서 탭 이동 시에는 지연 없이 즉시 전환되도록 합니다.
transition: none;
}
} }
::ng-deep * { ::ng-deep * {
@@ -48,9 +53,15 @@ $default_animation_duration: 0.4s;
opacity: 0; opacity: 0;
overflow: hidden; overflow: hidden;
// Component에서 포커스가 나갈 때(애니메이션 중) 마지막 포커스된 요소가 유지되도록 지연 시간을 둡니다.
transition: opacity 0s var(--animation-duration), width 0s var(--animation-duration);
interpolate-size: allow-keywords;
&:focus { &:focus {
width: unset; width: auto;
opacity: 1; opacity: 1;
transition: none;
} }
} }
} }
+11 -3
View File
@@ -1,21 +1,29 @@
<div class="menu-left"> <div #menuLeft class="menu-left">
<a class="brand" [routerLink]="['/']"> <a class="brand" [routerLink]="['/']">
<img [src]="settings.darkMode() ? '/logo.svg' : '/logo_inverted.svg'" alt="logo" height="20" width="59.52" /> <img [src]="settings.darkMode() ? '/logo.svg' : '/logo_inverted.svg'" alt="logo" height="20" width="59.52" />
<span #brandText [class.hidden]="!hasExtraSpace()"> <span #brandText [class.hidden]="expandStage() < 2">
<strong>ADOFAI Editor Forum</strong> <strong>ADOFAI Editor Forum</strong>
</span> </span>
</a> </a>
<div #leftMenuItems [class.hidden]="expandStage() < 1" [attr.inert]="expandStage() < 1 ? '' : null" class="menu-items">
<a class="menu-item nav-link" [routerLink]="['/levels']">{{ translation.menu.levels }}</a> <a class="menu-item nav-link" [routerLink]="['/levels']">{{ translation.menu.levels }}</a>
<span class="menu-item dummy-link">{{ translation.menu.community }}</span> <span class="menu-item dummy-link">{{ translation.menu.community }}</span>
<span class="menu-item dummy-link">{{ translation.menu.leaderboard }}</span> <span class="menu-item dummy-link">{{ translation.menu.leaderboard }}</span>
<span class="menu-item dummy-link">{{ translation.menu.about }}</span> <span class="menu-item dummy-link">{{ translation.menu.about }}</span>
</div>
</div> </div>
<div class="menu-right" #menuRight> <div #menuRight class="menu-right">
<div #rightMenuItems [class.hidden]="expandStage() < 1" [attr.inert]="expandStage() < 1 ? '' : null" class="menu-items">
<a class="menu-item nav-link" [routerLink]="['/known-issues']">{{ translation.menu.knownIssues }}</a> <a class="menu-item nav-link" [routerLink]="['/known-issues']">{{ translation.menu.knownIssues }}</a>
<span class="menu-item dummy-link"><strong>{{ translation.menu.login }}</strong></span> <span class="menu-item dummy-link"><strong>{{ translation.menu.login }}</strong></span>
<span class="menu-item dummy-link">{{ translation.menu.signup }}</span> <span class="menu-item dummy-link">{{ translation.menu.signup }}</span>
</div>
<button type="button" [class.hidden]="expandStage() > 0" [attr.inert]="expandStage() > 0 ? '' : null" class="material-symbols-outlined icon-40 menu" (click)="onHamburgerClick()" [attr.aria-label]="translation.menu.menu">
menu
</button>
<button type="button" class="material-symbols-outlined icon-40 settings" (click)="onSettingsClick()" [attr.aria-label]="translation.settings.title"> <button type="button" class="material-symbols-outlined icon-40 settings" (click)="onSettingsClick()" [attr.aria-label]="translation.settings.title">
settings settings
+40 -7
View File
@@ -4,11 +4,12 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: start; justify-content: space-between;
gap: 0; gap: 0;
width: fit-content; width: fit-content;
min-width: 100%; min-width: 100%;
height: 64px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 2px 0 light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%)); box-shadow: 0 2px 0 light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%));
@@ -26,11 +27,24 @@
align-items: center; align-items: center;
gap: 48px; gap: 48px;
.menu-items {
display: flex;
flex-direction: row;
align-items: center;
gap: 48px;
&.hidden {
position: absolute;
top: -100000px;
pointer-events: none;
}
}
& > * { & > * {
user-select: none; user-select: none;
} }
.menu-item, a:not(.settings) { .menu-item, a {
text-decoration: none; text-decoration: none;
font-size: 18px; font-size: 18px;
height: 40px; height: 40px;
@@ -42,6 +56,7 @@
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 4px;
&.nav-link { &.nav-link {
font-weight: 700; font-weight: 700;
@@ -62,7 +77,7 @@
} }
} }
.settings { .settings, .menu {
border: none; border: none;
border-radius: 100%; border-radius: 100%;
background-color: inherit; background-color: inherit;
@@ -71,6 +86,9 @@
position: relative; position: relative;
z-index: 10; z-index: 10;
cursor: pointer; cursor: pointer;
}
.settings {
transition: transform 0.3s ease; transition: transform 0.3s ease;
&:hover { &:hover {
@@ -78,6 +96,14 @@
} }
} }
.menu {
&.hidden {
position: absolute;
top: -100000px;
pointer-events: none;
}
}
/* 구체적인 아이콘 예외 처리 */ /* 구체적인 아이콘 예외 처리 */
.icon-40 { .icon-40 {
height: auto; height: auto;
@@ -85,6 +111,9 @@
} }
.menu-left { .menu-left {
position: absolute;
left: 0;
justify-content: start; justify-content: start;
padding: 12px; padding: 12px;
@@ -104,7 +133,7 @@
img { img {
display: inline-block; display: inline-block;
height: 20px; height: 20px;
padding: 10px 10px 10px 0; padding: 10px 0;
} }
strong { strong {
@@ -113,9 +142,11 @@
span { span {
display: inline-block; display: inline-block;
padding-left: 10px;
&.hidden { &.hidden {
display: none; position: absolute;
top: -100000px;
pointer-events: none; pointer-events: none;
} }
} }
@@ -123,8 +154,10 @@
} }
.menu-right { .menu-right {
flex-grow: 1; position: absolute;
right: 0;
justify-content: flex-end; justify-content: flex-end;
padding: 12px 20px; padding: 12px 20px 12px 12px;
} }
} }
+102 -61
View File
@@ -2,6 +2,7 @@ import {
afterNextRender, afterNextRender,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
effect,
ElementRef, ElementRef,
inject, inject,
NgZone, NgZone,
@@ -12,6 +13,7 @@ import {
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { PopupService } from '../../services/popup.service'; import { PopupService } from '../../services/popup.service';
import { SettingsPopup } from '../settings/settings'; import { SettingsPopup } from '../settings/settings';
import { NavigationMenu } from './navigation-menu';
import { TranslationService } from '../../services/translation.service'; import { TranslationService } from '../../services/translation.service';
import { SettingsService } from '../../services/settings.service'; import { SettingsService } from '../../services/settings.service';
@@ -21,101 +23,140 @@ import { SettingsService } from '../../services/settings.service';
templateUrl: './header-menu.html', templateUrl: './header-menu.html',
styleUrl: './header-menu.scss', styleUrl: './header-menu.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[class.folded-menu]': 'expandStage() < 1',
},
}) })
export class HeaderMenu implements OnDestroy { export class HeaderMenu implements OnDestroy {
private popupService = inject(PopupService); private popupService = inject(PopupService);
protected translation = inject(TranslationService).translation; protected translation = inject(TranslationService).translation;
protected settings = inject(SettingsService); protected settings = inject(SettingsService);
menuRight = viewChild<ElementRef<HTMLElement>>('menuRight'); private static readonly LOGICAL_GAP = 24;
brandText = viewChild<ElementRef<HTMLElement>>('brandText');
hasExtraSpace = signal(true); private resizeObserver?: ResizeObserver;
private brandTextObserver?: ResizeObserver;
private menuItemsObserver?: ResizeObserver;
private menuWidth: number = 99999;
private menuResizeObserver: ResizeObserver | null = null; private menuLeft = viewChild<ElementRef<HTMLElement>>('menuLeft');
private textResizeObserver: ResizeObserver | null = null; private leftMenuItems = viewChild<ElementRef<HTMLElement>>('leftMenuItems');
private menuLeftWidth: number = 0;
private leftMenuItemsWidth: number = 0;
private childWidths = new Map<Element, number>(); private menuRight = viewChild<ElementRef<HTMLElement>>('menuRight');
private cachedTextWidth = 0; private rightMenuItems = viewChild<ElementRef<HTMLElement>>('rightMenuItems');
private cachedRightMenuContentWidth = 0; private menuRightWidth: number = 0;
private rightMenuItemsWidth: number = 0;
private brandText = viewChild<ElementRef<HTMLElement>>('brandText');
private brandTextWidth: number = 0;
expandStage = signal(2);
constructor(
private element: ElementRef,
private ngZone: NgZone,
) {
effect(() => {
if (this.expandStage() > 0) {
this.popupService.close(NavigationMenu);
}
});
constructor(private ngZone: NgZone) {
afterNextRender(() => { afterNextRender(() => {
const rightElement = this.menuRight()?.nativeElement;
const textElement = this.brandText()?.nativeElement;
if (!rightElement || !textElement) return;
this.ngZone.runOutsideAngular(() => { this.ngZone.runOutsideAngular(() => {
// 1. Right Menu & Children Observer this.resizeObserver = new ResizeObserver((entries) => {
this.menuResizeObserver = new ResizeObserver((entries) => { for (let entry of entries) {
let containerWidth = -1;
let contentChanged = false;
for (const entry of entries) {
if (entry.target === rightElement) {
containerWidth = entry.borderBoxSize[0].inlineSize;
continue;
}
const width = entry.borderBoxSize[0].inlineSize; const width = entry.borderBoxSize[0].inlineSize;
if (this.childWidths.get(entry.target) !== width) { if (width > 0)
this.childWidths.set(entry.target, width); switch (entry.target) {
contentChanged = true; case element.nativeElement:
this.menuWidth = width;
break;
case this.menuLeft()!.nativeElement:
this.menuLeftWidth = width;
break;
case this.menuRight()!.nativeElement:
this.menuRightWidth = width;
break;
} }
} }
if (contentChanged) this.updateCachedRightMenuContentWidth(); this.updateExpandStatus();
if (containerWidth !== -1) this.resize(containerWidth);
}); });
this.menuResizeObserver.observe(rightElement); this.menuItemsObserver = new ResizeObserver((entries) => {
Array.from(rightElement.children).forEach((child) => { for (let entry of entries) {
this.childWidths.set(child, (child as HTMLElement).offsetWidth); const width = entry.borderBoxSize[0].inlineSize;
this.menuResizeObserver?.observe(child); if (width > 0)
switch (entry.target) {
case this.leftMenuItems()!.nativeElement:
this.leftMenuItemsWidth = width;
break;
case this.rightMenuItems()!.nativeElement:
this.rightMenuItemsWidth = width;
break;
}
}
this.updateExpandStatus();
}); });
// 2. Brand Text Observer this.resizeObserver.observe(element.nativeElement);
this.textResizeObserver = new ResizeObserver((entries) => { this.resizeObserver.observe(this.menuLeft()!.nativeElement);
this.resizeObserver.observe(this.menuRight()!.nativeElement);
this.menuItemsObserver.observe(this.leftMenuItems()!.nativeElement);
this.menuItemsObserver.observe(this.rightMenuItems()!.nativeElement);
this.brandTextObserver = new ResizeObserver((entries) => {
const width = entries[0].borderBoxSize[0].inlineSize; const width = entries[0].borderBoxSize[0].inlineSize;
if (width > 0) this.cachedTextWidth = width; if (width > 0) this.brandTextWidth = width;
this.updateExpandStatus();
}); });
this.textResizeObserver.observe(textElement); this.brandTextObserver.observe(this.brandText()!.nativeElement);
// Initial calculation
this.cachedTextWidth = textElement.offsetWidth;
this.updateCachedRightMenuContentWidth();
this.resize(rightElement.offsetWidth);
}); });
}); });
} }
ngOnDestroy() { ngOnDestroy() {
this.menuResizeObserver?.disconnect(); this.resizeObserver?.disconnect();
this.textResizeObserver?.disconnect(); this.menuItemsObserver?.disconnect();
this.childWidths.clear(); this.brandTextObserver?.disconnect();
} }
private updateCachedRightMenuContentWidth() { private updateExpandStatus() {
const gap = 48; let menuContentWidth = this.menuLeftWidth + this.menuRightWidth;
const widths = Array.from(this.childWidths.values()); let enoughSpace = this.menuWidth > menuContentWidth + HeaderMenu.LOGICAL_GAP;
if (widths.length === 0) { let initialExpandStage = this.expandStage();
this.cachedRightMenuContentWidth = 0;
return; if (enoughSpace) {
let extraSpaceToExpand = Number.MAX_VALUE;
switch (initialExpandStage) {
case 1:
extraSpaceToExpand = this.brandTextWidth;
break;
case 0:
extraSpaceToExpand = this.leftMenuItemsWidth + this.rightMenuItemsWidth + 48 - 40;
break;
} }
this.cachedRightMenuContentWidth = if (
widths.reduce((acc, w) => acc + w, 0) + (widths.length - 1) * gap; // Add a gap of 2px to prevent flickering
this.menuWidth >= menuContentWidth + HeaderMenu.LOGICAL_GAP + extraSpaceToExpand + 2
) {
this.expandStage.set(initialExpandStage + 1);
}
} else if (initialExpandStage > 0) this.expandStage.set(initialExpandStage - 1);
} }
private resize(availableWidth: number) { onHamburgerClick(): void {
const hadSpace = this.hasExtraSpace(); this.popupService.open(NavigationMenu, {
position: { top: '84px', right: '20px' },
const buffer = 20 * 2 + 24; // Internal Padding + Logical Gap });
const threshold = hadSpace ? 0 : this.cachedTextWidth;
const hasSpace = availableWidth > this.cachedRightMenuContentWidth + threshold + buffer;
if (hadSpace !== hasSpace) this.ngZone.run(() => this.hasExtraSpace.set(hasSpace));
} }
onSettingsClick() { onSettingsClick() {
@@ -0,0 +1,23 @@
<app-popup [position]="position()" (closeRequest)="close()">
<div header-left class="menu-header-title">
<span class="material-symbols-outlined">menu</span>
<span>{{ translation.menu.menu }}</span>
</div>
<div class="menu-content">
<div class="menu-section">
<a class="menu-item nav-link" [routerLink]="['/levels']" (click)="close()">{{ translation.menu.levels }}</a>
<span class="menu-item dummy-link">{{ translation.menu.community }}</span>
<span class="menu-item dummy-link">{{ translation.menu.leaderboard }}</span>
<span class="menu-item dummy-link">{{ translation.menu.about }}</span>
</div>
<hr class="menu-divider" />
<div class="menu-section">
<a class="menu-item nav-link" [routerLink]="['/known-issues']" (click)="close()">{{ translation.menu.knownIssues }}</a>
<span class="menu-item dummy-link"><strong>{{ translation.menu.login }}</strong></span>
<span class="menu-item dummy-link">{{ translation.menu.signup }}</span>
</div>
</div>
</app-popup>
@@ -0,0 +1,73 @@
:host {
display: block;
user-select: none;
}
.menu-header-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
.material-symbols-outlined {
font-size: 24px;
}
}
.menu-content {
display: flex;
flex-direction: column;
min-width: 276px;
padding-bottom: 12px;
gap: 12px;
}
.menu-section {
display: flex;
flex-direction: column;
}
.menu-divider {
height: 1px;
border: none;
background-color: light-dark(oklch(0 0 0 / 20%), oklch(1 0 0 / 20%));
margin: 0 6px;
}
.menu-item {
display: flex;
align-items: center;
align-content: center;
height: 40px;
padding-left: 12px;
text-decoration: none;
color: inherit;
font-size: 18px;
cursor: pointer;
vertical-align: middle;
transition: background-color 0.2s;
border-radius: 4px;
.material-symbols-outlined {
font-size: 24px;
color: light-dark(oklch(0.4 0 0), oklch(0.8 0 0));
}
strong {
font-weight: 700;
}
&.nav-link {
font-weight: 700;
color: oklch(0.6 0.15 250);
transition: opacity 0.3s;
&:hover {
opacity: 0.7;
}
}
&.dummy-link {
cursor: not-allowed;
}
}
@@ -0,0 +1,26 @@
import { Component, inject, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PopupComponent, PopupPosition } from '../../components/popup/popup';
import { PopupService } from '../../services/popup.service';
import { TranslationService } from '../../services/translation.service';
@Component({
selector: 'navigation-menu',
standalone: true,
imports: [PopupComponent, RouterLink],
templateUrl: './navigation-menu.html',
styleUrl: './navigation-menu.scss',
})
export class NavigationMenu {
private popupService = inject(PopupService);
protected translation = inject(TranslationService).translation;
/**
* 팝업의 위치
*/
position = input<PopupPosition | 'center'>('center');
close() {
this.popupService.close(NavigationMenu);
}
}
+1
View File
@@ -23,6 +23,7 @@
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
padding-top: 4px;
padding-bottom: 12px; padding-bottom: 12px;
} }
+1
View File
@@ -16,6 +16,7 @@ export const EN = {
knownIssues: 'Known Issues', knownIssues: 'Known Issues',
login: 'Login', login: 'Login',
signup: 'Signup', signup: 'Signup',
menu: 'Menu',
}, },
common: { common: {
selectOption: 'Select Option...', selectOption: 'Select Option...',
+1
View File
@@ -18,6 +18,7 @@ export const JP: Translation = {
knownIssues: '既知の問題', knownIssues: '既知の問題',
login: 'ログイン', login: 'ログイン',
signup: '新規登録', signup: '新規登録',
menu: 'メニュー',
}, },
common: { common: {
selectOption: 'オプションを選択...', selectOption: 'オプションを選択...',
+1
View File
@@ -18,6 +18,7 @@ export const KR: Translation = {
knownIssues: '알려진 문제', knownIssues: '알려진 문제',
login: '로그인', login: '로그인',
signup: '회원가입', signup: '회원가입',
menu: '메뉴',
}, },
common: { common: {
selectOption: '옵션 선택...', selectOption: '옵션 선택...',
+2 -2
View File
@@ -1,6 +1,6 @@
<div class="container"> <brand-accent></brand-accent>
<brand-accent></brand-accent>
<div class="container">
<h1>Component Test</h1> <h1>Component Test</h1>
<section id="switch-test" class="feature-block"> <section id="switch-test" class="feature-block">
+18 -6
View File
@@ -1,6 +1,16 @@
:host {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
width: 100%;
}
:host > .container { :host > .container {
display: flex; display: flex;
width: 600px; width: 100%;
max-width: 600px;
box-sizing: border-box;
flex-direction: column; flex-direction: column;
gap: 40px; gap: 40px;
@@ -8,7 +18,8 @@
section#dropdown-test { section#dropdown-test {
dropdown { dropdown {
width: 324px; width: 100%;
max-width: 324px;
} }
} }
@@ -35,11 +46,12 @@ div.test-container {
brand-accent { brand-accent {
position: absolute; position: absolute;
width: 100%; top: -64px;
height: 100%;
top: 0;
left: 0; left: 0;
width: 100%;
min-width: 100dvw;
height: calc(100% + 64px);
min-height: 100dvh;
z-index: -999999; z-index: -999999;
} }
+2
View File
@@ -39,6 +39,8 @@ body {
margin: 0; margin: 0;
overscroll-behavior-x: none; overscroll-behavior-x: none;
background-color: white; background-color: white;
min-height: 100dvh;
} }
.icon-24 { .icon-24 {