From 3f9e66690c5a160fab33ba5aef88da45d6218c7c Mon Sep 17 00:00:00 2001 From: dev-wooyeon Date: Wed, 6 May 2026 19:11:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?style(content):=20=EB=AA=A8=EB=B0=94?= =?UTF-8?q?=EC=9D=BC=20=EB=B3=B8=EB=AC=B8=20=EA=B0=80=EB=8F=85=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 본문 전용 line-height 토큰을 추가하고 MDX prose의 본문 크기와 문단 간격을 모바일 긴 글 기준으로 조정했어요. 고대비 환경에서 링크와 인용문, 주요 색상 토큰이 더 분명하게 보이도록 contrast override도 추가했어요. --- src/styles/globals.css | 71 +++++++++++++++++++++++++++----------- src/styles/globals.test.ts | 12 +++++++ src/styles/tokens.css | 29 ++++++++++++++-- 3 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index 7adc8e7..b1e7de9 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -262,7 +262,6 @@ /* ===== Reduced Motion ===== */ @media (prefers-reduced-motion: reduce) { - *, *::before, *::after { @@ -297,14 +296,14 @@ max-width: none; font-family: var(--font-sans-emoji); color: var(--color-text-primary); - font-size: var(--text-base); - line-height: var(--leading-relaxed); + font-size: var(--text-md); + line-height: var(--leading-prose); } .prose p { - margin-bottom: 1.125rem; + margin-bottom: 1.35rem; color: var(--color-grey-700); - line-height: var(--leading-relaxed); + line-height: var(--leading-prose); } .prose h1, @@ -317,8 +316,8 @@ color: var(--color-grey-900); font-weight: var(--font-bold); line-height: var(--leading-tight); - margin-top: 2.5rem; - margin-bottom: 1rem; + margin-top: 3rem; + margin-bottom: 1.125rem; } .prose h1 { @@ -366,7 +365,7 @@ .prose ul, .prose ol { padding-left: 1.5rem; - margin: 1rem 0 1.25rem; + margin: 1.125rem 0 1.5rem; } .prose ul { @@ -378,9 +377,9 @@ } .prose li { - margin-bottom: 0.375rem; + margin-bottom: 0.5rem; color: var(--color-grey-700); - line-height: var(--leading-relaxed); + line-height: var(--leading-prose); } .prose li::marker { @@ -389,8 +388,8 @@ .prose blockquote { border-left: 3px solid var(--color-toss-blue); - padding: 1rem 1.25rem; - margin: 1.5rem 0; + padding: 1.125rem 1.25rem; + margin: 1.75rem 0; background-color: var(--color-grey-50); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; color: var(--color-text-secondary); @@ -425,7 +424,7 @@ padding: 1.25rem 0; border-radius: var(--radius-md); overflow-x: auto; - margin: 1.5rem 0; + margin: 1.75rem 0; font-size: var(--text-sm); line-height: 1.7; background-color: var(--color-code-bg) !important; @@ -444,7 +443,7 @@ } .prose [data-rehype-pretty-code-figure] { - margin: 1.5rem 0; + margin: 1.75rem 0; } .prose [data-rehype-pretty-code-title] { @@ -461,7 +460,7 @@ z-index: 1; } -.prose [data-rehype-pretty-code-title]+pre { +.prose [data-rehype-pretty-code-title] + pre { margin-top: 0; border-top-left-radius: 0; border-top-right-radius: 0; @@ -664,7 +663,7 @@ .prose table { width: 100%; border-collapse: collapse; - margin: 1.5rem 0; + margin: 1.75rem 0; font-size: var(--text-sm); } @@ -688,17 +687,50 @@ .prose hr { border: none; border-top: 1px solid var(--color-grey-200); - margin: 2.5rem 0; + margin: 3rem 0; } .prose img { border-radius: var(--radius-sm); - margin: 1.5rem 0; + margin: 1.75rem 0; +} + +@media (max-width: 767px) { + .prose h1, + .prose h2, + .prose h3, + .prose h4, + .prose h5, + .prose h6 { + margin-top: 2.5rem; + } + + .prose h1 { + font-size: var(--text-2xl); + } + + .prose h2 { + font-size: var(--text-xl); + } + + .prose pre, + .prose table { + font-size: var(--text-sm); + } +} + +@media (prefers-contrast: more) { + .prose a { + text-decoration-thickness: 2px; + } + + .prose blockquote { + border-left-width: 4px; + } } /* ===== Custom Animations ===== */ @keyframes pulse { - 0%, 100% { opacity: 1; @@ -791,7 +823,6 @@ } @keyframes cursor-blink { - 0%, 100% { opacity: 1; diff --git a/src/styles/globals.test.ts b/src/styles/globals.test.ts index 6de2297..1fc0baf 100644 --- a/src/styles/globals.test.ts +++ b/src/styles/globals.test.ts @@ -30,4 +30,16 @@ describe('globals styles', () => { expect(globalsContent).toContain('.prose {'); expect(globalsContent).toContain('font-family: var(--font-sans-emoji);'); }); + + it('uses mobile-readable prose sizing and spacing tokens', () => { + expect(tokensContent).toContain('--leading-prose: 1.72;'); + expect(globalsContent).toContain('font-size: var(--text-md);'); + expect(globalsContent).toContain('line-height: var(--leading-prose);'); + expect(globalsContent).toContain('@media (max-width: 767px)'); + }); + + it('defines high contrast color overrides', () => { + expect(tokensContent).toContain('@media (prefers-contrast: more)'); + expect(globalsContent).toContain('@media (prefers-contrast: more)'); + }); }); diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 054622d..27df197 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -84,9 +84,9 @@ 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; --font-sans-emoji: - 'Pretendard', 'Tossface Safe', -apple-system, BlinkMacSystemFont, 'Segoe UI', - Roboto, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', - sans-serif; + 'Pretendard', 'Tossface Safe', -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Noto Color Emoji', sans-serif; --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; @@ -123,6 +123,7 @@ --leading-normal: 1.5; --leading-relaxed: 1.6; --leading-loose: 1.8; + --leading-prose: 1.72; /* Letter Spacing */ --tracking-tight: -0.02em; @@ -281,3 +282,25 @@ --color-category-life-border: rgba(163, 176, 255, 0.48); --color-category-life-text: #d0d8ff; } + +@media (prefers-contrast: more) { + :root { + --color-text-secondary: #333d4b; + --color-text-tertiary: #4e5968; + --color-border: #b0b8c1; + --color-border-hover: #8b95a1; + --color-divider: #d1d6db; + --color-toss-blue: #1b64da; + --color-toss-blue-dark: #0f4fb8; + } + + .dark { + --color-text-secondary: #f2f4f6; + --color-text-tertiary: #e5e8eb; + --color-border: #6b7684; + --color-border-hover: #b0b8c1; + --color-divider: #6b7684; + --color-toss-blue: #8cc7ff; + --color-toss-blue-dark: #b7d9ff; + } +} From aeea98b35729fd5bb3e6d9f503cd6a3ad1790300 Mon Sep 17 00:00:00 2001 From: dev-wooyeon Date: Wed, 6 May 2026 19:11:54 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(search):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=EC=99=80=20=EC=B6=94=EC=B2=9C=EC=96=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command Palette에 전체, Tech, Life, Resume 범위 chip을 추가하고 섹션 이동 액션을 검색 대상으로 포함했어요. 고정 추천어 대신 실제 포스트 태그 빈도 기반 추천어를 노출하도록 검색 추천 모델과 테스트를 추가했어요. --- .../search/model/get-search-actions.test.ts | 58 ++++++-- .../search/model/get-search-actions.ts | 50 ++++++- .../model/search-recommendations.test.ts | 28 ++++ .../search/model/search-recommendations.ts | 49 +++++++ .../CommandPalette/CommandPalette.module.css | 97 ++++++++++++- .../CommandPalette/CommandPalette.tsx | 129 +++++++++++++++--- .../search/ui/components/KBarProvider.tsx | 2 +- 7 files changed, 371 insertions(+), 42 deletions(-) create mode 100644 src/features/search/model/search-recommendations.test.ts create mode 100644 src/features/search/model/search-recommendations.ts diff --git a/src/features/search/model/get-search-actions.test.ts b/src/features/search/model/get-search-actions.test.ts index 9e6cc5a..31ead52 100644 --- a/src/features/search/model/get-search-actions.test.ts +++ b/src/features/search/model/get-search-actions.test.ts @@ -24,11 +24,23 @@ describe('getSearchActions', () => { }, ]); - expect(actions).toHaveLength(1); - expect(actions[0].id).toBe('redis-basics'); - expect(actions[0].name).toBe('Redis Basics'); - expect(actions[0].section).toBe('블로그 포스트'); - expect(actions[0].keywords).toContain('Redis'); + const postAction = actions.find((action) => action.id === 'redis-basics'); + + expect(postAction).toBeDefined(); + expect(postAction?.name).toBe('Redis Basics'); + expect(postAction?.section).toBe('블로그 포스트'); + expect(postAction?.keywords).toContain('Redis'); + }); + + it('includes section navigation actions for scoped search', () => { + const actions = getSearchActions([]); + + expect(actions.map((action) => action.id)).toEqual([ + 'go-engineering', + 'go-life', + 'go-resume', + ]); + expect(actions[2].keywords).toContain('이력서'); }); it('handles missing tag arrays without crashing', () => { @@ -42,23 +54,39 @@ describe('getSearchActions', () => { }, ]); - expect(actions).toHaveLength(1); - expect(actions[0].keywords).toBe('No Tags Life Tags missing post'); + const postAction = actions.find((action) => action.id === 'no-tags'); + + expect(postAction?.keywords).toBe('No Tags Life Tags missing post'); }); it('keeps post list order as provided', () => { const actions = getSearchActions([ - { slug: 'second', title: 'B', category: 'Tech', tags: ['A'], description: 'B' }, - { slug: 'first', title: 'A', category: 'Tech', tags: ['A'], description: 'A' }, + { + slug: 'second', + title: 'B', + category: 'Tech', + tags: ['A'], + description: 'B', + }, + { + slug: 'first', + title: 'A', + category: 'Tech', + tags: ['A'], + description: 'A', + }, ]); - expect(actions.map((action) => action.id)).toEqual(['second', 'first']); + expect(actions.map((action) => action.id).slice(3)).toEqual([ + 'second', + 'first', + ]); }); it('tracks and navigates when action is performed', () => { mockTrackEvent.mockClear(); - const [action] = getSearchActions([ + const action = getSearchActions([ { slug: 'redis-basics', title: 'Redis Basics', @@ -66,15 +94,15 @@ describe('getSearchActions', () => { tags: ['Redis'], description: 'Redis 기초', }, - ]); + ]).find((candidate) => candidate.id === 'redis-basics'); - action.perform?.(); + action?.perform?.(); expect(mockTrackEvent).toHaveBeenCalledWith('click', { target: 'command_palette_result', post_slug: 'redis-basics', }); - expect(typeof action.perform).toBe('function'); - expect(() => action.perform?.()).not.toThrow(); + expect(typeof action?.perform).toBe('function'); + expect(() => action?.perform?.()).not.toThrow(); }); }); diff --git a/src/features/search/model/get-search-actions.ts b/src/features/search/model/get-search-actions.ts index 95f250a..f740e4b 100644 --- a/src/features/search/model/get-search-actions.ts +++ b/src/features/search/model/get-search-actions.ts @@ -7,6 +7,38 @@ type SearchablePost = Pick< 'slug' | 'title' | 'category' | 'tags' | 'description' >; +interface NavigationActionSource { + id: string; + name: string; + href: string; + keywords: string; + subtitle: string; +} + +const navigationActions: NavigationActionSource[] = [ + { + id: 'go-engineering', + name: 'Tech', + href: '/engineering', + keywords: 'Tech Engineering 기술 글 시리즈', + subtitle: '기술 글과 시리즈 보기', + }, + { + id: 'go-life', + name: 'Life', + href: '/life', + keywords: 'Life 회고 에세이 일상', + subtitle: '회고와 에세이 보기', + }, + { + id: 'go-resume', + name: 'Resume', + href: '/resume', + keywords: 'Resume 이력서 경력 프로젝트', + subtitle: '경력과 프로젝트 보기', + }, +]; + /** * 전역 검색을 위한 액션 초기 데이터 생성 함수 * 블로그 포스트를 검색할 수 있게 액션 객체 배열을 반환합니다. @@ -36,5 +68,21 @@ export const getSearchActions = (posts: SearchablePost[]): Action[] => { subtitle: post.description, })); - return postActions; + const sectionActions = navigationActions.map((action) => ({ + id: action.id, + name: action.name, + shortcut: [], + keywords: action.keywords, + section: '섹션', + perform: () => { + trackEvent(AnalyticsEvents.click, { + target: 'command_palette_section', + destination: action.href, + }); + window.location.assign(action.href); + }, + subtitle: action.subtitle, + })); + + return [...sectionActions, ...postActions]; }; diff --git a/src/features/search/model/search-recommendations.test.ts b/src/features/search/model/search-recommendations.test.ts new file mode 100644 index 0000000..6c0a7d5 --- /dev/null +++ b/src/features/search/model/search-recommendations.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { getRecommendedSearchTerms } from './search-recommendations'; + +describe('getRecommendedSearchTerms', () => { + it('returns popular tags ordered by frequency', () => { + const terms = getRecommendedSearchTerms([ + { tags: ['Redis', '회고'] }, + { tags: ['Redis', 'Flink'] }, + { tags: ['회고'] }, + ]); + + expect(terms.slice(0, 3)).toEqual(['Redis', '회고', 'Flink']); + }); + + it('falls back to section terms when posts do not have tags', () => { + expect(getRecommendedSearchTerms([{ tags: [] }])).toEqual([ + 'Tech', + 'Life', + 'Resume', + ]); + }); + + it('trims empty tags and respects the requested limit', () => { + expect( + getRecommendedSearchTerms([{ tags: [' Redis ', '', 'Flink', '회고'] }], 2) + ).toHaveLength(2); + }); +}); diff --git a/src/features/search/model/search-recommendations.ts b/src/features/search/model/search-recommendations.ts new file mode 100644 index 0000000..cdac057 --- /dev/null +++ b/src/features/search/model/search-recommendations.ts @@ -0,0 +1,49 @@ +import type { FeedData } from '@/domains/post/model/types'; + +type SearchRecommendationSource = Pick; + +const FALLBACK_RECOMMENDATIONS = ['Tech', 'Life', 'Resume']; + +export function getRecommendedSearchTerms( + posts: SearchRecommendationSource[], + limit = 5 +): string[] { + const counts = new Map(); + const firstSeenIndexes = new Map(); + let nextIndex = 0; + + posts.forEach((post) => { + post.tags?.forEach((tag) => { + const term = tag.trim(); + + if (term.length === 0) { + return; + } + + if (!firstSeenIndexes.has(term)) { + firstSeenIndexes.set(term, nextIndex); + nextIndex += 1; + } + + counts.set(term, (counts.get(term) ?? 0) + 1); + }); + }); + + const recommendations = Array.from(counts.entries()) + .sort(([leftTerm, leftCount], [rightTerm, rightCount]) => { + if (leftCount !== rightCount) { + return rightCount - leftCount; + } + + return ( + (firstSeenIndexes.get(leftTerm) ?? 0) - + (firstSeenIndexes.get(rightTerm) ?? 0) + ); + }) + .map(([term]) => term) + .slice(0, limit); + + return recommendations.length > 0 + ? recommendations + : FALLBACK_RECOMMENDATIONS.slice(0, limit); +} diff --git a/src/features/search/ui/components/CommandPalette/CommandPalette.module.css b/src/features/search/ui/components/CommandPalette/CommandPalette.module.css index 5270c10..6edb2dc 100644 --- a/src/features/search/ui/components/CommandPalette/CommandPalette.module.css +++ b/src/features/search/ui/components/CommandPalette/CommandPalette.module.css @@ -60,8 +60,8 @@ } .closeButton { - width: 1.875rem; - height: 1.875rem; + width: 2.75rem; + height: 2.75rem; border-radius: var(--radius-full); background: color-mix( in srgb, @@ -81,7 +81,67 @@ border-color: var(--color-toss-blue); color: var(--color-toss-blue); background: var(--color-bg-primary); - outline: none; + outline: 2px solid var(--color-toss-blue); + outline-offset: 2px; +} + +.scopeBar { + display: flex; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + overflow-x: auto; + border-bottom: 1px solid var(--color-border); +} + +.scopeButton { + min-height: 2.5rem; + padding: 0 var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-full); + background: var(--color-bg-primary); + color: var(--color-text-secondary); + font-size: var(--text-sm); + font-weight: var(--font-medium); + white-space: nowrap; + cursor: pointer; + transition: + background-color var(--duration-150) var(--ease-default), + border-color var(--duration-150) var(--ease-default), + color var(--duration-150) var(--ease-default); +} + +.scopeButton:hover, +.scopeButton:focus-visible { + border-color: var(--color-toss-blue); + color: var(--color-toss-blue); + outline: 2px solid var(--color-toss-blue); + outline-offset: 2px; +} + +.scopeButtonActive { + border-color: var(--color-toss-blue); + background: var(--color-toss-blue); + color: white; +} + +.scopeButtonActive:hover, +.scopeButtonActive:focus-visible { + color: white; +} + +.recommendationPanel { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-bottom: 1px solid var(--color-border); +} + +.recommendationLabel { + flex-shrink: 0; + font-size: var(--text-xs); + font-weight: var(--font-semibold); + color: var(--color-text-tertiary); } .results { @@ -111,6 +171,7 @@ .resultItem { padding: var(--space-3) var(--space-5); + min-height: 3.25rem; display: flex; align-items: center; justify-content: space-between; @@ -181,13 +242,14 @@ } .suggestionButton { + min-height: 2.75rem; border: 1px solid var(--color-border); background: var(--color-bg-secondary); color: var(--color-text-primary); font-size: var(--text-sm); font-weight: var(--font-medium); border-radius: var(--radius-full); - padding: var(--space-1) var(--space-3); + padding: 0 var(--space-4); cursor: pointer; transition: all var(--duration-150) var(--ease-default); } @@ -196,7 +258,8 @@ .suggestionButton:focus-visible { border-color: var(--color-toss-blue); color: var(--color-toss-blue); - outline: none; + outline: 2px solid var(--color-toss-blue); + outline-offset: 2px; } .recoveryActions { @@ -206,9 +269,13 @@ } .recoveryButton { + min-height: 2.75rem; + display: inline-flex; + align-items: center; + justify-content: center; border: 1px solid var(--color-border); border-radius: var(--radius-sm); - padding: var(--space-1) var(--space-3); + padding: 0 var(--space-4); background: transparent; color: var(--color-text-secondary); font-size: var(--text-sm); @@ -221,7 +288,8 @@ .recoveryButton:focus-visible { border-color: var(--color-toss-blue); color: var(--color-toss-blue); - outline: none; + outline: 2px solid var(--color-toss-blue); + outline-offset: 2px; } @media (max-width: 767px) { @@ -243,6 +311,21 @@ padding: var(--space-3); } + .scopeBar { + padding: var(--space-3); + } + + .scopeButton { + min-height: 2.75rem; + } + + .recommendationPanel { + align-items: flex-start; + flex-direction: column; + gap: var(--space-2); + padding: var(--space-3); + } + .search { font-size: var(--text-md); } diff --git a/src/features/search/ui/components/CommandPalette/CommandPalette.tsx b/src/features/search/ui/components/CommandPalette/CommandPalette.tsx index 2cfacb7..3c81330 100644 --- a/src/features/search/ui/components/CommandPalette/CommandPalette.tsx +++ b/src/features/search/ui/components/CommandPalette/CommandPalette.tsx @@ -11,10 +11,47 @@ import { useMatches, } from 'kbar'; import styles from './CommandPalette.module.css'; +import type { FeedData } from '@/domains/post/model/types'; import { AnalyticsEvents, trackEvent } from '@/shared/analytics/lib/analytics'; +import { getRecommendedSearchTerms } from '@/features/search/model/search-recommendations'; -export const CommandPalette = () => { - const { query } = useKBar(); +type SearchScopeId = 'all' | 'tech' | 'life' | 'resume'; + +interface SearchScope { + id: SearchScopeId; + label: string; + query: string; +} + +interface CommandPaletteProps { + posts?: FeedData[]; +} + +const SEARCH_SCOPES: SearchScope[] = [ + { id: 'all', label: '전체', query: '' }, + { id: 'tech', label: 'Tech', query: 'Tech' }, + { id: 'life', label: 'Life', query: 'Life' }, + { id: 'resume', label: 'Resume', query: 'Resume' }, +]; + +function getActiveSearchScope(normalizedQuery: string): SearchScopeId { + const matchedScope = SEARCH_SCOPES.find( + (scope) => scope.query.toLowerCase() === normalizedQuery.toLowerCase() + ); + + return matchedScope?.id ?? 'all'; +} + +export const CommandPalette = ({ posts = [] }: CommandPaletteProps) => { + const { query, searchQuery } = useKBar((state) => ({ + searchQuery: state.searchQuery, + })); + const normalizedQuery = searchQuery.trim(); + const activeScope = getActiveSearchScope(normalizedQuery); + const recommendedTerms = React.useMemo( + () => getRecommendedSearchTerms(posts), + [posts] + ); return ( @@ -23,8 +60,8 @@ export const CommandPalette = () => {
- + query.setSearch(scope.query)} + /> + {normalizedQuery.length === 0 && ( +
+

추천 키워드

+ +
+ )} +
); }; -function RenderResults() { +function SearchScopeBar({ + activeScope, + onSelect, +}: { + activeScope: SearchScopeId; + onSelect: (scope: SearchScope) => void; +}) { + return ( +
+ {SEARCH_SCOPES.map((scope) => { + const isActive = scope.id === activeScope; + + return ( + + ); + })} +
+ ); +} + +function SearchSuggestionButtons({ + terms, + onSelect, +}: { + terms: string[]; + onSelect: (term: string) => void; +}) { + return ( +
+ {terms.map((keyword) => ( + + ))} +
+ ); +} + +function RenderResults({ recommendedTerms }: { recommendedTerms: string[] }) { const { results } = useMatches(); const { query, searchQuery } = useKBar((state) => ({ searchQuery: state.searchQuery, @@ -101,18 +202,10 @@ function RenderResults() {

다른 키워드로 검색하거나 추천 키워드를 선택해 보세요.

-
- {['Redis', 'Flink', '회고'].map((keyword) => ( - - ))} -
+
+ ); + } + + const containerVariants = getContainerVariants(effectiveMotionMode); + const itemVariants = getItemVariants(effectiveMotionMode); + if (layout === 'list') { return ( {posts.map((post) => ( @@ -66,7 +106,7 @@ export default function PostList({ posts, layout = 'grid' }: PostListProps) { variants={containerVariants} initial="hidden" animate="visible" - className="grid gap-6 md:grid-cols-2" + className={layoutClassNames.grid} > {posts.map((post) => ( diff --git a/src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx b/src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx index 5746a36..518177d 100644 --- a/src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx +++ b/src/features/blog/ui/components/ReadingProgress/ReadingProgress.tsx @@ -2,15 +2,25 @@ import { useEffect, useState } from 'react'; import { motion, useScroll, useSpring } from 'framer-motion'; +import { useEffectiveMotionMode } from '@/shared/motion/model/motion-mode'; export default function ReadingProgress() { const [isVisible, setIsVisible] = useState(false); + const effectiveMotionMode = useEffectiveMotionMode(); const { scrollYProgress } = useScroll(); - const scaleX = useSpring(scrollYProgress, { + const smoothScaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001, }); + const scaleX = + effectiveMotionMode === 'full' ? smoothScaleX : scrollYProgress; + const opacityTransitionDuration = + effectiveMotionMode === 'off' + ? 0 + : effectiveMotionMode === 'reduced' + ? 0.08 + : 0.2; useEffect(() => { const handleScroll = () => { @@ -26,7 +36,7 @@ export default function ReadingProgress() { className="fixed top-0 left-0 right-0 h-1 bg-[var(--color-grey-100)] z-[var(--z-sticky)] origin-left" initial={{ opacity: 0 }} animate={{ opacity: isVisible ? 1 : 0 }} - transition={{ duration: 0.2 }} + transition={{ duration: opacityTransitionDuration }} > ( #{tag} diff --git a/src/shared/layout/AppShell/AppShell.tsx b/src/shared/layout/AppShell/AppShell.tsx index b3d09c7..13a1f4c 100644 --- a/src/shared/layout/AppShell/AppShell.tsx +++ b/src/shared/layout/AppShell/AppShell.tsx @@ -160,7 +160,7 @@ function SearchButton() {