Compare commits

5 Commits

Author SHA1 Message Date
wypark
091f97968b .
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m9s
2026-06-06 23:22:46 +09:00
박원엽
f1668f08f2 .
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m7s
2026-06-02 16:58:36 +09:00
박원엽
3fe1b0e6d5 .
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m9s
2026-06-02 16:40:39 +09:00
박원엽
49fc7b5331 .
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m10s
2026-06-02 15:34:30 +09:00
박원엽
d1a0340eb0 . 2026-06-02 15:26:54 +09:00
3 changed files with 141 additions and 37 deletions

15
LOG.md
View File

@@ -1,5 +1,12 @@
# Work Log # Work Log
## 2026-06-06
- Fixed the desktop Dock so route changes from Dock menu links no longer leave focus-held open state behind when the Dock is not pinned.
- Kept the saved `dock-pinned-v2` preference unchanged during navigation and only released transient Dock focus/hover state after menu clicks and pathname changes.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3111/` confirmed that after unpinning the Dock, clicking the Dock archive menu navigates to `/archive`, the pin button remains `aria-pressed=false`, and the Dock collapses after the pointer leaves.
- Recommended next task: do one visual pass on the Dock at narrow desktop widths to confirm the collapse animation still feels smooth around the sidebar offset.
## 2026-06-02 ## 2026-06-02
- Refined the public layout after desktop/mobile review: the shell now guards horizontal overflow, uses route-aware bottom padding, hides the top menubar on mobile, and opens the mobile sidebar/search panel from the Dock. - Refined the public layout after desktop/mobile review: the shell now guards horizontal overflow, uses route-aware bottom padding, hides the top menubar on mobile, and opens the mobile sidebar/search panel from the Dock.
@@ -7,6 +14,10 @@
- Smoothed the desktop Dock hide/show interaction by separating hover-open, closing, and fully-collapsed states; the Dock now keeps the expanded hit area during the closing animation so re-entering mid-collapse reverses smoothly. - Smoothed the desktop Dock hide/show interaction by separating hover-open, closing, and fully-collapsed states; the Dock now keeps the expanded hit area during the closing animation so re-entering mid-collapse reverses smoothly.
- Changed the desktop Dock's default state to pinned/open and added distinct soft pastel tones to Dock buttons, including stronger visual treatment for pin/unpin and auth actions. - Changed the desktop Dock's default state to pinned/open and added distinct soft pastel tones to Dock buttons, including stronger visual treatment for pin/unpin and auth actions.
- Softened the Dock button palette to quieter pastel tones and made the light-mode Dock bar read as a cleaner white glass surface. - Softened the Dock button palette to quieter pastel tones and made the light-mode Dock bar read as a cleaner white glass surface.
- Fixed the Dock pin toggle so mouse-based unpinning no longer leaves button focus holding the Dock open, and changed the Dock Home button to a white neutral tone.
- Reworked the Dock material away from cute pastel tiles into a more macOS-like neutral frosted glass texture with translucent white glass buttons.
- Corrected the Dock button direction so only Home stays white, while the other actions keep subtle pastel tints with a translucent macOS-style glass material.
- Increased Dock button translucency contrast so the buttons remain glassy while sitting slightly more opaque than the Dock background.
- Rebalanced the home page around latest posts with popular posts as a supporting panel, converted the post detail page into a quieter reader surface, and hardened Markdown/code/table wrapping against mobile overflow. - Rebalanced the home page around latest posts with popular posts as a supporting panel, converted the post detail page into a quieter reader surface, and hardened Markdown/code/table wrapping against mobile overflow.
- Added a client-side archive explorer with year, month, category, and compact/comfort density controls over the existing server-fetched post list. - Added a client-side archive explorer with year, month, category, and compact/comfort density controls over the existing server-fetched post list.
- Rebuilt the chess page with safer viewport-aware board sizing, quieter puzzle panels, and enough layout clearance for the Dock. - Rebuilt the chess page with safer viewport-aware board sizing, quieter puzzle panels, and enough layout clearance for the Dock.
@@ -14,6 +25,10 @@
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on a local `next start` server confirmed the desktop Dock starts as a small bottom tab, uses 560ms transitions for tab/panel geometry, and expands into the larger Dock hit area when opened. - Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on a local `next start` server confirmed the desktop Dock starts as a small bottom tab, uses 560ms transitions for tab/panel geometry, and expands into the larger Dock hit area when opened.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on a local `next start` server confirmed the Dock opens by default and Home/Archive/Chess/Login/Signup/Pin buttons render distinct pastel colors. - Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on a local `next start` server confirmed the Dock opens by default and Home/Archive/Chess/Login/Signup/Pin buttons render distinct pastel colors.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3106/` confirmed the light-mode Dock bar renders as a cleaner white glass surface and the Dock button colors use quieter pastel tones. - Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3106/` confirmed the light-mode Dock bar renders as a cleaner white glass surface and the Dock button colors use quieter pastel tones.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3107/` confirmed the Dock Home button renders white and mouse-based unpinning collapses the Dock after the pointer leaves.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3108/` confirmed the Dock uses neutral translucent glass material, with non-home buttons rendering as subtle white glass rather than pastel color tiles.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3109/` confirmed Home stays white while the other Dock buttons keep pastel-tinted translucent glass backgrounds with button-level blur.
- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3110/` confirmed Dock buttons remain translucent but render slightly more opaque than the Dock background for clearer contrast.
- Recommended next task: run a browser pass against a reachable backend API, especially one real post detail route and the populated chess puzzle board, then tune any remaining content-specific long-title or board-height edge cases. - Recommended next task: run a browser pass against a reachable backend API, especially one real post detail route and the populated chess puzzle board, then tune any remaining content-specific long-title or board-height edge cases.
## 2026-05-29 ## 2026-05-29

View File

@@ -42,8 +42,8 @@
--control-border: rgba(39, 54, 82, 0.12); --control-border: rgba(39, 54, 82, 0.12);
--menubar-bg: rgba(255, 255, 255, 0.5); --menubar-bg: rgba(255, 255, 255, 0.5);
--menubar-border: rgba(255, 255, 255, 0.7); --menubar-border: rgba(255, 255, 255, 0.7);
--dock-bg: rgba(255, 255, 255, 0.86); --dock-bg: rgba(248, 250, 252, 0.46);
--dock-border: rgba(255, 255, 255, 0.92); --dock-border: rgba(255, 255, 255, 0.62);
--sidebar-bg: rgba(255, 255, 255, 0.48); --sidebar-bg: rgba(255, 255, 255, 0.48);
--sidebar-border: rgba(255, 255, 255, 0.6); --sidebar-border: rgba(255, 255, 255, 0.6);
--color-surface: var(--card-bg); --color-surface: var(--card-bg);
@@ -62,7 +62,7 @@
--shadow-menubar: 0 16px 40px rgba(70, 92, 130, 0.12), 0 1px 0 rgba(255, 255, 255, 0.58) inset; --shadow-menubar: 0 16px 40px rgba(70, 92, 130, 0.12), 0 1px 0 rgba(255, 255, 255, 0.58) inset;
--shadow-panel: var(--shadow-sidebar); --shadow-panel: var(--shadow-sidebar);
--shadow-card: 0 14px 34px rgba(70, 92, 130, 0.09), 0 1px 0 rgba(255, 255, 255, 0.5) inset; --shadow-card: 0 14px 34px rgba(70, 92, 130, 0.09), 0 1px 0 rgba(255, 255, 255, 0.5) inset;
--shadow-dock: 0 22px 58px rgba(70, 92, 130, 0.18), 0 8px 18px rgba(70, 92, 130, 0.1), 0 1px 0 rgba(255, 255, 255, 0.68) inset; --shadow-dock: 0 28px 78px rgba(15, 23, 42, 0.18), 0 12px 32px rgba(15, 23, 42, 0.12), 0 1px 0 rgba(255, 255, 255, 0.68) inset, 0 -1px 0 rgba(15, 23, 42, 0.06) inset;
--shadow-control: 0 12px 26px rgba(70, 92, 130, 0.1), 0 1px 0 rgba(255, 255, 255, 0.46) inset; --shadow-control: 0 12px 26px rgba(70, 92, 130, 0.1), 0 1px 0 rgba(255, 255, 255, 0.46) inset;
--focus-ring: 0 0 0 4px rgba(0, 102, 204, 0.14); --focus-ring: 0 0 0 4px rgba(0, 102, 204, 0.14);
--background: var(--color-page); --background: var(--color-page);
@@ -96,8 +96,8 @@ html[data-theme="dark"] {
--control-border: rgba(255, 255, 255, 0.13); --control-border: rgba(255, 255, 255, 0.13);
--menubar-bg: rgba(24, 27, 38, 0.62); --menubar-bg: rgba(24, 27, 38, 0.62);
--menubar-border: rgba(255, 255, 255, 0.16); --menubar-border: rgba(255, 255, 255, 0.16);
--dock-bg: rgba(27, 30, 42, 0.66); --dock-bg: rgba(18, 20, 28, 0.46);
--dock-border: rgba(255, 255, 255, 0.18); --dock-border: rgba(255, 255, 255, 0.16);
--sidebar-bg: rgba(21, 24, 34, 0.64); --sidebar-bg: rgba(21, 24, 34, 0.64);
--sidebar-border: rgba(255, 255, 255, 0.14); --sidebar-border: rgba(255, 255, 255, 0.14);
--color-surface: var(--card-bg); --color-surface: var(--card-bg);
@@ -116,7 +116,7 @@ html[data-theme="dark"] {
--shadow-menubar: 0 18px 46px rgba(0, 0, 0, 0.32), 0 1px 0 rgba(255, 255, 255, 0.09) inset; --shadow-menubar: 0 18px 46px rgba(0, 0, 0, 0.32), 0 1px 0 rgba(255, 255, 255, 0.09) inset;
--shadow-panel: var(--shadow-sidebar); --shadow-panel: var(--shadow-sidebar);
--shadow-card: 0 16px 34px rgba(0, 0, 0, 0.32), 0 1px 0 rgba(255, 255, 255, 0.08) inset; --shadow-card: 0 16px 34px rgba(0, 0, 0, 0.32), 0 1px 0 rgba(255, 255, 255, 0.08) inset;
--shadow-dock: 0 26px 68px rgba(0, 0, 0, 0.44), 0 10px 24px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.12) inset; --shadow-dock: 0 30px 86px rgba(0, 0, 0, 0.48), 0 12px 34px rgba(0, 0, 0, 0.34), 0 1px 0 rgba(255, 255, 255, 0.14) inset, 0 -1px 0 rgba(0, 0, 0, 0.36) inset;
--shadow-control: 0 14px 30px rgba(0, 0, 0, 0.28), 0 1px 0 rgba(255, 255, 255, 0.08) inset; --shadow-control: 0 14px 30px rgba(0, 0, 0, 0.28), 0 1px 0 rgba(255, 255, 255, 0.08) inset;
--focus-ring: 0 0 0 4px rgba(41, 151, 255, 0.18); --focus-ring: 0 0 0 4px rgba(41, 151, 255, 0.18);
--background: var(--color-page); --background: var(--color-page);

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { type CSSProperties, type FocusEvent, useEffect, useRef, useState } from 'react'; import { type CSSProperties, type FocusEvent, useCallback, useEffect, useRef, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
@@ -76,19 +76,41 @@ const createDockTone = (
'--dock-item-ring': ring, '--dock-item-ring': ring,
}); });
const dockGlassTone = createDockTone(
'rgba(255, 255, 255, 0.24)',
'rgba(255, 255, 255, 0.36)',
'rgba(255, 255, 255, 0.46)',
'rgba(255, 255, 255, 0.2)',
'rgba(255, 255, 255, 0.34)',
'var(--color-text-muted)',
'var(--color-text)',
'rgba(255, 255, 255, 0.34)',
);
const homeDockTone = createDockTone(
'rgba(255, 255, 255, 0.58)',
'rgba(255, 255, 255, 0.68)',
'rgba(255, 255, 255, 0.76)',
'rgba(255, 255, 255, 0.28)',
'rgba(255, 255, 255, 0.46)',
'var(--color-text-muted)',
'var(--color-text)',
'rgba(255, 255, 255, 0.42)',
);
const dockToneStyles: Record<string, DockToneStyle> = { const dockToneStyles: Record<string, DockToneStyle> = {
home: createDockTone('rgba(232, 248, 239, 0.58)', 'rgba(220, 244, 232, 0.74)', 'rgba(211, 239, 224, 0.82)', 'rgba(65, 151, 106, 0.16)', 'rgba(65, 151, 106, 0.24)', '#5b8f72', '#377458', 'rgba(65, 151, 106, 0.16)'), home: homeDockTone,
archive: createDockTone('rgba(239, 235, 255, 0.56)', 'rgba(231, 226, 250, 0.72)', 'rgba(224, 217, 246, 0.8)', 'rgba(111, 92, 176, 0.15)', 'rgba(111, 92, 176, 0.23)', '#7a6d9f', '#5f5289', 'rgba(111, 92, 176, 0.15)'), archive: createDockTone('rgba(222, 216, 255, 0.54)', 'rgba(222, 216, 255, 0.64)', 'rgba(222, 216, 255, 0.72)', 'rgba(255, 255, 255, 0.26)', 'rgba(151, 132, 214, 0.34)', '#6f668f', '#514274', 'rgba(151, 132, 214, 0.2)'),
chess: createDockTone('rgba(255, 247, 222, 0.62)', 'rgba(255, 240, 207, 0.78)', 'rgba(252, 234, 194, 0.84)', 'rgba(164, 122, 36, 0.16)', 'rgba(164, 122, 36, 0.24)', '#8a7a4b', '#6f5d2e', 'rgba(164, 122, 36, 0.15)'), chess: createDockTone('rgba(255, 229, 162, 0.54)', 'rgba(255, 229, 162, 0.64)', 'rgba(255, 229, 162, 0.72)', 'rgba(255, 255, 255, 0.28)', 'rgba(184, 134, 42, 0.34)', '#7b6d4a', '#665125', 'rgba(184, 134, 42, 0.2)'),
admin: createDockTone('rgba(232, 244, 255, 0.6)', 'rgba(220, 238, 252, 0.76)', 'rgba(209, 232, 247, 0.84)', 'rgba(60, 129, 181, 0.16)', 'rgba(60, 129, 181, 0.25)', '#5f86a8', '#3d6f98', 'rgba(60, 129, 181, 0.15)'), admin: createDockTone('rgba(192, 226, 255, 0.54)', 'rgba(192, 226, 255, 0.64)', 'rgba(192, 226, 255, 0.72)', 'rgba(255, 255, 255, 0.27)', 'rgba(81, 145, 195, 0.34)', '#627f96', '#3f6686', 'rgba(81, 145, 195, 0.2)'),
write: createDockTone('rgba(255, 239, 229, 0.62)', 'rgba(255, 230, 215, 0.78)', 'rgba(251, 221, 203, 0.84)', 'rgba(177, 94, 58, 0.16)', 'rgba(177, 94, 58, 0.24)', '#9a755f', '#7e5843', 'rgba(177, 94, 58, 0.15)'), write: createDockTone('rgba(255, 209, 184, 0.54)', 'rgba(255, 209, 184, 0.64)', 'rgba(255, 209, 184, 0.72)', 'rgba(255, 255, 255, 0.28)', 'rgba(187, 106, 70, 0.34)', '#8c705f', '#724d38', 'rgba(187, 106, 70, 0.2)'),
login: createDockTone('rgba(230, 248, 249, 0.58)', 'rgba(218, 241, 244, 0.74)', 'rgba(205, 235, 239, 0.82)', 'rgba(47, 139, 148, 0.16)', 'rgba(47, 139, 148, 0.24)', '#5f9298', '#3d7880', 'rgba(47, 139, 148, 0.15)'), login: createDockTone('rgba(190, 236, 238, 0.54)', 'rgba(190, 236, 238, 0.64)', 'rgba(190, 236, 238, 0.72)', 'rgba(255, 255, 255, 0.27)', 'rgba(68, 150, 158, 0.34)', '#63898c', '#446d72', 'rgba(68, 150, 158, 0.2)'),
signup: createDockTone('rgba(249, 235, 255, 0.58)', 'rgba(243, 225, 251, 0.74)', 'rgba(236, 216, 246, 0.82)', 'rgba(139, 75, 160, 0.16)', 'rgba(139, 75, 160, 0.24)', '#94739f', '#795586', 'rgba(139, 75, 160, 0.15)'), signup: createDockTone('rgba(237, 207, 250, 0.54)', 'rgba(237, 207, 250, 0.64)', 'rgba(237, 207, 250, 0.72)', 'rgba(255, 255, 255, 0.27)', 'rgba(153, 91, 176, 0.34)', '#846c8e', '#684a78', 'rgba(153, 91, 176, 0.2)'),
logout: createDockTone('rgba(255, 237, 240, 0.62)', 'rgba(255, 226, 231, 0.78)', 'rgba(249, 216, 223, 0.84)', 'rgba(173, 70, 86, 0.16)', 'rgba(173, 70, 86, 0.25)', '#9a6870', '#7e4b55', 'rgba(173, 70, 86, 0.15)'), logout: createDockTone('rgba(255, 207, 216, 0.54)', 'rgba(255, 207, 216, 0.64)', 'rgba(255, 207, 216, 0.72)', 'rgba(255, 255, 255, 0.28)', 'rgba(188, 84, 99, 0.34)', '#8f6970', '#714850', 'rgba(188, 84, 99, 0.2)'),
pin: createDockTone('rgba(255, 244, 226, 0.72)', 'rgba(255, 235, 210, 0.86)', 'rgba(250, 227, 197, 0.92)', 'rgba(181, 115, 45, 0.2)', 'rgba(181, 115, 45, 0.32)', '#957049', '#765530', 'rgba(181, 115, 45, 0.18)'), pin: createDockTone('rgba(255, 219, 170, 0.56)', 'rgba(255, 219, 170, 0.66)', 'rgba(255, 219, 170, 0.74)', 'rgba(255, 255, 255, 0.28)', 'rgba(184, 118, 44, 0.36)', '#8a704e', '#6b4f2d', 'rgba(184, 118, 44, 0.22)'),
menu: createDockTone('rgba(238, 247, 232, 0.58)', 'rgba(228, 241, 221, 0.74)', 'rgba(219, 235, 211, 0.82)', 'rgba(91, 140, 75, 0.16)', 'rgba(91, 140, 75, 0.24)', '#728c64', '#58734a', 'rgba(91, 140, 75, 0.15)'), menu: createDockTone('rgba(213, 234, 203, 0.54)', 'rgba(213, 234, 203, 0.64)', 'rgba(213, 234, 203, 0.72)', 'rgba(255, 255, 255, 0.27)', 'rgba(98, 143, 80, 0.34)', '#6f8366', '#526f44', 'rgba(98, 143, 80, 0.2)'),
more: createDockTone('rgba(235, 243, 255, 0.6)', 'rgba(225, 236, 252, 0.76)', 'rgba(214, 228, 246, 0.84)', 'rgba(76, 118, 178, 0.16)', 'rgba(76, 118, 178, 0.24)', '#6e86a8', '#526d91', 'rgba(76, 118, 178, 0.15)'), more: createDockTone('rgba(210, 228, 255, 0.54)', 'rgba(210, 228, 255, 0.64)', 'rgba(210, 228, 255, 0.72)', 'rgba(255, 255, 255, 0.27)', 'rgba(86, 126, 183, 0.34)', '#6d7f98', '#4f688d', 'rgba(86, 126, 183, 0.2)'),
default: createDockTone('rgba(246, 247, 250, 0.58)', 'rgba(237, 240, 246, 0.76)', 'rgba(228, 233, 242, 0.84)', 'rgba(98, 110, 132, 0.14)', 'rgba(98, 110, 132, 0.22)', '#747d8d', '#596578', 'rgba(98, 110, 132, 0.14)'), default: dockGlassTone,
}; };
const getDockToneStyle = (key: string) => dockToneStyles[key] ?? dockToneStyles.default; const getDockToneStyle = (key: string) => dockToneStyles[key] ?? dockToneStyles.default;
@@ -102,14 +124,16 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
const [isDockHovered, setIsDockHovered] = useState(false); const [isDockHovered, setIsDockHovered] = useState(false);
const [isDockFocused, setIsDockFocused] = useState(false); const [isDockFocused, setIsDockFocused] = useState(false);
const [isDockClosing, setIsDockClosing] = useState(false); const [isDockClosing, setIsDockClosing] = useState(false);
const isPinnedRef = useRef(isPinned);
const dockZoneRef = useRef<HTMLDivElement>(null); const dockZoneRef = useRef<HTMLDivElement>(null);
const dockLeaveTimerRef = useRef<number | null>(null); const dockLeaveTimerRef = useRef<number | null>(null);
const dockCollapseTimerRef = useRef<number | null>(null); const dockCollapseTimerRef = useRef<number | null>(null);
const pinToggleStartedByPointerRef = useRef(false);
const isAdmin = _hasHydrated && isLoggedIn && role?.includes('ADMIN'); const isAdmin = _hasHydrated && isLoggedIn && role?.includes('ADMIN');
const isDockOpen = isPinned || isDockHovered || isDockFocused; const isDockOpen = isPinned || isDockHovered || isDockFocused;
const isDockRangeExpanded = isDockOpen || isDockClosing; const isDockRangeExpanded = isDockOpen || isDockClosing;
const clearDockTimers = () => { const clearDockTimers = useCallback(() => {
if (dockLeaveTimerRef.current !== null) { if (dockLeaveTimerRef.current !== null) {
window.clearTimeout(dockLeaveTimerRef.current); window.clearTimeout(dockLeaveTimerRef.current);
dockLeaveTimerRef.current = null; dockLeaveTimerRef.current = null;
@@ -119,7 +143,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
window.clearTimeout(dockCollapseTimerRef.current); window.clearTimeout(dockCollapseTimerRef.current);
dockCollapseTimerRef.current = null; dockCollapseTimerRef.current = null;
} }
}; }, []);
const expandDock = () => { const expandDock = () => {
clearDockTimers(); clearDockTimers();
@@ -127,7 +151,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
setIsDockHovered(true); setIsDockHovered(true);
}; };
const startDockCollapse = () => { const startDockCollapse = useCallback(() => {
clearDockTimers(); clearDockTimers();
setIsDockHovered(false); setIsDockHovered(false);
setIsDockClosing(true); setIsDockClosing(true);
@@ -135,7 +159,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
setIsDockClosing(false); setIsDockClosing(false);
dockCollapseTimerRef.current = null; dockCollapseTimerRef.current = null;
}, DOCK_COLLAPSE_MS); }, DOCK_COLLAPSE_MS);
}; }, [clearDockTimers]);
const scheduleDockCollapse = () => { const scheduleDockCollapse = () => {
if (isPinned || isDockFocused) return; if (isPinned || isDockFocused) return;
@@ -146,10 +170,44 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
}, DOCK_LEAVE_DELAY_MS); }, DOCK_LEAVE_DELAY_MS);
}; };
const blurFocusedDockItem = useCallback(() => {
if (
document.activeElement instanceof HTMLElement &&
dockZoneRef.current?.contains(document.activeElement)
) {
document.activeElement.blur();
}
}, []);
const releaseUnpinnedDockAfterNavigation = useCallback(() => {
if (isPinnedRef.current) return;
const isPointerInsideDock = dockZoneRef.current?.matches(':hover') ?? false;
blurFocusedDockItem();
setIsDockFocused(false);
if (isPointerInsideDock) {
clearDockTimers();
setIsDockClosing(false);
setIsDockHovered(true);
return;
}
startDockCollapse();
}, [blurFocusedDockItem, clearDockTimers, startDockCollapse]);
useEffect(() => {
isPinnedRef.current = isPinned;
}, [isPinned]);
useEffect(() => { useEffect(() => {
const frameId = window.requestAnimationFrame(() => { const frameId = window.requestAnimationFrame(() => {
const savedValue = window.localStorage.getItem(DOCK_PINNED_STORAGE_KEY); const savedValue = window.localStorage.getItem(DOCK_PINNED_STORAGE_KEY);
setIsPinned(savedValue === null ? true : savedValue === 'true'); const nextValue = savedValue === null ? true : savedValue === 'true';
isPinnedRef.current = nextValue;
setIsPinned(nextValue);
}); });
return () => window.cancelAnimationFrame(frameId); return () => window.cancelAnimationFrame(frameId);
@@ -158,24 +216,51 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
useEffect(() => { useEffect(() => {
const frameId = window.requestAnimationFrame(() => { const frameId = window.requestAnimationFrame(() => {
setIsMoreOpen(false); setIsMoreOpen(false);
releaseUnpinnedDockAfterNavigation();
}); });
return () => window.cancelAnimationFrame(frameId); return () => window.cancelAnimationFrame(frameId);
}, [pathname]); }, [pathname, releaseUnpinnedDockAfterNavigation]);
useEffect(() => { useEffect(() => {
return () => clearDockTimers(); return () => clearDockTimers();
}, []); }, [clearDockTimers]);
const handlePinnedChange = () => { const handlePinnedChange = () => {
setIsPinned((previous) => { const nextValue = !isPinned;
const nextValue = !previous; const startedByPointer = pinToggleStartedByPointerRef.current;
window.localStorage.setItem(DOCK_PINNED_STORAGE_KEY, String(nextValue)); const isPointerInsideDock = dockZoneRef.current?.matches(':hover') ?? false;
clearDockTimers();
pinToggleStartedByPointerRef.current = false;
window.localStorage.setItem(DOCK_PINNED_STORAGE_KEY, String(nextValue));
clearDockTimers();
isPinnedRef.current = nextValue;
setIsPinned(nextValue);
if (nextValue) {
setIsDockClosing(false); setIsDockClosing(false);
setIsDockHovered(!nextValue); setIsDockHovered(false);
return nextValue; return;
}); }
if (startedByPointer) {
setIsDockFocused(false);
blurFocusedDockItem();
}
if (isPointerInsideDock) {
setIsDockClosing(false);
setIsDockHovered(true);
return;
}
if (!isDockFocused || startedByPointer) {
startDockCollapse();
return;
}
setIsDockHovered(false);
setIsDockClosing(false);
}; };
const handleDockZoneFocus = () => { const handleDockZoneFocus = () => {
@@ -307,7 +392,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
const active = item.isActive?.(pathname) ?? false; const active = item.isActive?.(pathname) ?? false;
const toneStyle = getDockToneStyle(item.key); const toneStyle = getDockToneStyle(item.key);
const itemClass = clsx( const itemClass = clsx(
'group/item relative flex h-12 w-12 items-center justify-center rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] transition duration-150 hover:-translate-y-1 hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)] focus-visible:-translate-y-1', 'group/item relative flex h-12 w-12 items-center justify-center rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] backdrop-blur-[18px] transition duration-150 hover:-translate-y-1 hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)] focus-visible:-translate-y-1',
active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]', active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]',
); );
@@ -333,6 +418,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
aria-current={active ? 'page' : undefined} aria-current={active ? 'page' : undefined}
className={itemClass} className={itemClass}
style={toneStyle} style={toneStyle}
onClick={releaseUnpinnedDockAfterNavigation}
> >
{content} {content}
</Link> </Link>
@@ -359,7 +445,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
const active = activeOverride ?? item.isActive?.(pathname) ?? false; const active = activeOverride ?? item.isActive?.(pathname) ?? false;
const toneStyle = getDockToneStyle(item.key); const toneStyle = getDockToneStyle(item.key);
const itemClass = clsx( const itemClass = clsx(
'flex h-12 min-w-0 flex-1 flex-col items-center justify-center gap-0.5 rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] px-1 text-[10px] font-semibold leading-none text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] transition-colors', 'flex h-12 min-w-0 flex-1 flex-col items-center justify-center gap-0.5 rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] px-1 text-[10px] font-semibold leading-none text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] backdrop-blur-[18px] transition-colors',
'hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)] focus-visible:bg-[var(--dock-item-bg-active)]', 'hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)] focus-visible:bg-[var(--dock-item-bg-active)]',
active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]', active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]',
); );
@@ -462,9 +548,12 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
title={isPinned ? 'Dock 고정 해제' : 'Dock 고정'} title={isPinned ? 'Dock 고정 해제' : 'Dock 고정'}
aria-label={isPinned ? 'Dock 고정 해제' : 'Dock 고정'} aria-label={isPinned ? 'Dock 고정 해제' : 'Dock 고정'}
aria-pressed={isPinned} aria-pressed={isPinned}
onPointerDown={() => {
pinToggleStartedByPointerRef.current = true;
}}
onClick={handlePinnedChange} onClick={handlePinnedChange}
className={clsx( className={clsx(
'group/item relative ml-1 flex h-12 w-12 items-center justify-center rounded-lg border border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] shadow-[var(--shadow-control)] ring-1 ring-[var(--dock-item-ring)] transition duration-150 hover:-translate-y-1 hover:bg-[var(--dock-item-bg-hover)]', 'group/item relative ml-1 flex h-12 w-12 items-center justify-center rounded-lg border border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] shadow-[var(--shadow-control)] ring-1 ring-[var(--dock-item-ring)] backdrop-blur-[18px] transition duration-150 hover:-translate-y-1 hover:bg-[var(--dock-item-bg-hover)]',
isPinned && 'shadow-[var(--shadow-dock)]', isPinned && 'shadow-[var(--shadow-dock)]',
)} )}
style={getDockToneStyle('pin')} style={getDockToneStyle('pin')}
@@ -493,7 +582,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De
const active = item.isActive?.(pathname) ?? false; const active = item.isActive?.(pathname) ?? false;
const toneStyle = getDockToneStyle(item.key); const toneStyle = getDockToneStyle(item.key);
const className = clsx( const className = clsx(
'inline-flex h-10 min-w-0 items-center justify-center gap-2 rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] px-3 text-sm font-semibold text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] transition-colors hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)]', 'inline-flex h-10 min-w-0 items-center justify-center gap-2 rounded-lg border border-[var(--dock-item-border)] bg-[var(--dock-item-bg)] px-3 text-sm font-semibold text-[var(--dock-item-fg)] shadow-[var(--shadow-control)] backdrop-blur-[18px] transition-colors hover:border-[var(--dock-item-border-hover)] hover:bg-[var(--dock-item-bg-hover)] hover:text-[var(--dock-item-fg-strong)]',
active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]', active && 'border-[var(--dock-item-border-hover)] bg-[var(--dock-item-bg-active)] text-[var(--dock-item-fg-strong)] ring-1 ring-[var(--dock-item-ring)]',
); );