Files
web-temp/app/pages/_components/index.vue
2025-11-19 10:02:24 +09:00

1217 lines
32 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
definePageMeta({
layout: false,
})
interface ComponentItem {
name: string
path: string
category: string
description?: string
sampleProps?: Record<string, any>
hasPreview?: boolean
previewNotes?: string
}
const selectedCategory = ref<string>('all')
const searchQuery = ref('')
const selectedComponent = ref<ComponentItem | null>(null)
const showCode = ref(false)
const currentPage = ref(1)
// 컴포넌트 목록 정의
const components: ComponentItem[] = [
// Atoms - Buttons
{
name: 'Button Circle',
path: 'atoms/Button/Circle',
category: 'atoms',
description: '원형 버튼 컴포넌트',
hasPreview: true,
sampleProps: { type: 'button', srOnly: '버튼' },
},
// {
// name: 'Button Play',
// path: 'atoms/Button/Play',
// category: 'atoms',
// description: '플레이 버튼 컴포넌트',
// hasPreview: true,
// sampleProps: { srOnly: '재생' },
// },
// {
// name: 'Button Variant',
// path: 'atoms/Button/Variant',
// category: 'atoms',
// description: '다양한 버튼 변형',
// hasPreview: true,
// sampleProps: { variant: 'filled', disabled: false },
// },
// // Atoms - Icons
// {
// name: 'Icon Arrow Right',
// path: 'atoms/icons/ArrowRightLine',
// category: 'atoms',
// description: '오른쪽 화살표 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Calendar',
// path: 'atoms/icons/CalendarLine',
// category: 'atoms',
// description: '캘린더 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Check',
// path: 'atoms/icons/CheckLine',
// category: 'atoms',
// description: '체크 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#10b981' },
// },
// {
// name: 'Icon Close',
// path: 'atoms/icons/CloseLine',
// category: 'atoms',
// description: '닫기 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#ef4444' },
// },
// {
// name: 'Icon Download',
// path: 'atoms/icons/DownloadLine',
// category: 'atoms',
// description: '다운로드 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Menu',
// path: 'atoms/icons/MenuBoldLine',
// category: 'atoms',
// description: '메뉴 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Search',
// path: 'atoms/icons/SearchLine',
// category: 'atoms',
// description: '검색 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Share',
// path: 'atoms/icons/ShareLine',
// category: 'atoms',
// description: '공유 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#333333' },
// },
// {
// name: 'Icon Star',
// path: 'atoms/icons/StarFill',
// category: 'atoms',
// description: '별 아이콘',
// hasPreview: true,
// sampleProps: { size: 24, color: '#fbbf24' },
// },
// {
// name: 'Logo Stove',
// path: 'atoms/icons/LogoStove',
// category: 'atoms',
// description: '스토브 로고',
// hasPreview: true,
// sampleProps: { size: 32 },
// },
// // Atoms - Form Elements
// {
// name: 'Input',
// path: 'atoms/Input',
// category: 'atoms',
// description: '입력 필드 컴포넌트',
// hasPreview: true,
// sampleProps: { modelValue: '', placeholder: '텍스트를 입력하세요', useClearButton: true },
// },
// {
// name: 'Select',
// path: 'atoms/Select',
// category: 'atoms',
// description: '선택 박스 컴포넌트',
// hasPreview: false,
// previewNotes: 'Select 컴포넌트는 복잡한 옵션 데이터가 필요합니다',
// },
// // Atoms - Media
// {
// name: 'Image',
// path: 'atoms/Img',
// category: 'atoms',
// description: '이미지 컴포넌트',
// hasPreview: false,
// previewNotes: 'Image 컴포넌트는 실제 이미지 경로가 필요합니다',
// },
// {
// name: 'Video',
// path: 'atoms/Video',
// category: 'atoms',
// description: '비디오 컴포넌트',
// hasPreview: false,
// previewNotes: 'Video 컴포넌트는 실제 비디오 경로가 필요합니다',
// },
// // Atoms - Loading
// {
// name: 'Loading Full',
// path: 'atoms/loading/Full',
// category: 'atoms',
// description: '전체 화면 로딩',
// hasPreview: false,
// previewNotes: 'Full 로딩은 전체 화면을 차지하므로 미리보기가 제한됩니다',
// },
// {
// name: 'Loading Local',
// path: 'atoms/loading/Local',
// category: 'atoms',
// description: '로컬 로딩',
// hasPreview: true,
// sampleProps: {},
// },
// // Blocks
// {
// name: 'Card News',
// path: 'blocks/CardNews',
// category: 'blocks',
// description: '카드 뉴스 컴포넌트',
// hasPreview: true,
// sampleProps: {
// title: '샘플 카드 제목',
// description: '카드 뉴스의 설명입니다',
// imgPath: '/images/sample/sample01.jpg',
// url: '#',
// },
// },
// {
// name: 'Date Picker',
// path: 'blocks/DatePicker',
// category: 'blocks',
// description: '날짜 선택기',
// hasPreview: false,
// previewNotes: 'DatePicker는 복잡한 날짜 데이터와 상태 관리가 필요합니다',
// },
// {
// name: 'Language Switcher',
// path: 'blocks/LanguageSwitcher',
// category: 'blocks',
// description: '언어 전환기',
// hasPreview: true,
// sampleProps: {},
// },
// {
// name: 'Pagination',
// path: 'blocks/Pagination',
// category: 'blocks',
// description: '페이지네이션',
// hasPreview: true,
// sampleProps: { totalCount: 100, currentPage: 1, pageSize: 10, pageBlock: 10, useMinMax: true },
// },
// {
// name: 'LNB',
// path: 'blocks/Lnb',
// category: 'blocks',
// description: '로컬 네비게이션 바',
// hasPreview: false,
// previewNotes: 'LNB는 페이지 데이터와 라우팅이 필요합니다',
// },
// // Blocks - Buttons
// {
// name: 'Home Button',
// path: 'blocks/Button/Home',
// category: 'blocks',
// description: '홈 버튼',
// hasPreview: true,
// sampleProps: {},
// },
// {
// name: 'Launcher Button',
// path: 'blocks/Button/Launcher',
// category: 'blocks',
// description: '런처 버튼',
// hasPreview: false,
// previewNotes: 'Launcher 버튼은 게임 데이터가 필요합니다',
// },
// {
// name: 'Scroll Top Button',
// path: 'blocks/Button/ScrollTop',
// category: 'blocks',
// description: '맨 위로 버튼',
// hasPreview: true,
// sampleProps: {},
// },
// {
// name: 'SNS Button',
// path: 'blocks/Button/Sns',
// category: 'blocks',
// description: 'SNS 버튼',
// hasPreview: false,
// previewNotes: 'SNS 버튼은 SNS 데이터 배열이 필요합니다',
// },
// // Blocks - Modals
// {
// name: 'Modal Alert',
// path: 'blocks/modal/Alert',
// category: 'blocks',
// description: '알림 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// {
// name: 'Modal Confirm',
// path: 'blocks/modal/Confirm',
// category: 'blocks',
// description: '확인 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// {
// name: 'Modal Content',
// path: 'blocks/modal/Content',
// category: 'blocks',
// description: '콘텐츠 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// {
// name: 'Modal Layer',
// path: 'blocks/modal/Layer',
// category: 'blocks',
// description: '레이어 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// {
// name: 'Modal Toast',
// path: 'blocks/modal/Toast',
// category: 'blocks',
// description: '토스트 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// {
// name: 'Modal YouTube',
// path: 'blocks/modal/YouTube',
// category: 'blocks',
// description: '유튜브 모달',
// hasPreview: false,
// previewNotes: 'Modal은 Modal Store와 함께 사용됩니다',
// },
// // Blocks - Slides
// {
// name: 'Slide Default',
// path: 'blocks/slide/Default',
// category: 'blocks',
// description: '기본 슬라이드',
// hasPreview: false,
// previewNotes: 'Slide는 슬라이드 데이터 배열이 필요합니다',
// },
// {
// name: 'Slide Center Focus',
// path: 'blocks/slide/CenterFocus',
// category: 'blocks',
// description: '중앙 포커스 슬라이드',
// hasPreview: false,
// previewNotes: 'Slide는 슬라이드 데이터 배열이 필요합니다',
// },
// {
// name: 'Slide Center Highlight',
// path: 'blocks/slide/CenterHighlight',
// category: 'blocks',
// description: '중앙 하이라이트 슬라이드',
// hasPreview: false,
// previewNotes: 'Slide는 슬라이드 데이터 배열이 필요합니다',
// },
// {
// name: 'Slide Fade',
// path: 'blocks/slide/Fade',
// category: 'blocks',
// description: '페이드 슬라이드',
// hasPreview: false,
// previewNotes: 'Slide는 슬라이드 데이터 배열이 필요합니다',
// },
// {
// name: 'Slide Thumbnail',
// path: 'blocks/slide/Thumbnail',
// category: 'blocks',
// description: '썸네일 슬라이드',
// hasPreview: false,
// previewNotes: 'Slide는 슬라이드 데이터 배열이 필요합니다',
// },
// // Layouts
// {
// name: 'Header',
// path: 'layouts/Header',
// category: 'layouts',
// description: '헤더',
// hasPreview: false,
// previewNotes: 'Layout 컴포넌트는 전체 레이아웃 구조가 필요합니다',
// },
// {
// name: 'Footer',
// path: 'layouts/Footer',
// category: 'layouts',
// description: '푸터',
// hasPreview: false,
// previewNotes: 'Layout 컴포넌트는 전체 레이아웃 구조가 필요합니다',
// },
// {
// name: 'Main',
// path: 'layouts/Main',
// category: 'layouts',
// description: '메인 레이아웃',
// hasPreview: false,
// previewNotes: 'Layout 컴포넌트는 전체 레이아웃 구조가 필요합니다',
// },
// {
// name: 'Stove Header',
// path: 'layouts/StoveHeader',
// category: 'layouts',
// description: '스토브 헤더',
// hasPreview: false,
// previewNotes: 'Layout 컴포넌트는 전체 레이아웃 구조가 필요합니다',
// },
// {
// name: 'Event Navigation',
// path: 'layouts/EventNavigation',
// category: 'layouts',
// description: '이벤트 네비게이션',
// hasPreview: false,
// previewNotes: 'Layout 컴포넌트는 전체 레이아웃 구조가 필요합니다',
// },
// // Widgets
// {
// name: 'Background',
// path: 'widgets/Background',
// category: 'widgets',
// description: '배경 위젯',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Button List',
// path: 'widgets/ButtonList',
// category: 'widgets',
// description: '버튼 리스트',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Description',
// path: 'widgets/Description',
// category: 'widgets',
// description: '설명',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Main Title',
// path: 'widgets/MainTitle',
// category: 'widgets',
// description: '메인 타이틀',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Sub Title',
// path: 'widgets/SubTitle',
// category: 'widgets',
// description: '서브 타이틀',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Fix Main Title',
// path: 'widgets/FixMainTitle',
// category: 'widgets',
// description: '고정 메인 타이틀',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Fix Sub Title',
// path: 'widgets/FixSubTitle',
// category: 'widgets',
// description: '고정 서브 타이틀',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
// {
// name: 'Video Play',
// path: 'widgets/VideoPlay',
// category: 'widgets',
// description: '비디오 플레이',
// hasPreview: false,
// previewNotes: 'Widget 컴포넌트는 복잡한 데이터 구조가 필요합니다',
// },
]
const categories = [
{ value: 'all', label: '전체', count: components.length },
{ value: 'atoms', label: 'Atoms', count: components.filter(c => c.category === 'atoms').length },
{ value: 'blocks', label: 'Blocks', count: components.filter(c => c.category === 'blocks').length },
{ value: 'layouts', label: 'Layouts', count: components.filter(c => c.category === 'layouts').length },
{ value: 'widgets', label: 'Widgets', count: components.filter(c => c.category === 'widgets').length },
]
// 필터링된 컴포넌트 목록
const filteredComponents = computed(() => {
let filtered = components
if (selectedCategory.value !== 'all') {
filtered = filtered.filter(c => c.category === selectedCategory.value)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(c =>
c.name.toLowerCase().includes(query) ||
c.description?.toLowerCase().includes(query)
)
}
return filtered
})
// 카테고리별로 그룹화
const groupedComponents = computed(() => {
const groups: Record<string, ComponentItem[]> = {}
filteredComponents.value.forEach(component => {
if (!groups[component.category]) {
groups[component.category] = []
}
groups[component.category].push(component)
})
return groups
})
const selectComponent = (component: ComponentItem) => {
selectedComponent.value = component
showCode.value = false
}
const closeDetail = () => {
selectedComponent.value = null
showCode.value = false
}
</script>
<template>
<div class="component-viewer">
<!-- Header -->
<header class="viewer-header">
<div class="container">
<h1 class="title">
🎨 Component Viewer
</h1>
<p class="subtitle">
프로젝트의 모든 컴포넌트를 한눈에 확인하세요
</p>
</div>
</header>
<div class="container">
<div class="viewer-content">
<!-- Sidebar -->
<aside class="sidebar">
<!-- Search -->
<div class="search-box">
<input
v-model="searchQuery"
type="text"
placeholder="컴포넌트 검색..."
class="search-input"
/>
</div>
<!-- Categories -->
<nav class="categories">
<button
v-for="category in categories"
:key="category.value"
:class="['category-btn', { active: selectedCategory === category.value }]"
@click="selectedCategory = category.value"
>
{{ category.label }}
<span class="count">{{ category.count }}</span>
</button>
</nav>
<!-- Component List -->
<div class="component-list">
<template v-for="(items, category) in groupedComponents" :key="category">
<div class="category-group">
<h3 class="category-title">
{{ category.toUpperCase() }}
</h3>
<button
v-for="component in items"
:key="component.path"
:class="['component-item', { active: selectedComponent?.path === component.path }]"
@click="selectComponent(component)"
>
<span class="component-name">{{ component.name }}</span>
<span v-if="component.description" class="component-desc">
{{ component.description }}
</span>
</button>
</div>
</template>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div v-if="!selectedComponent" class="empty-state">
<div class="empty-icon">📦</div>
<h2 class="empty-title">컴포넌트를 선택해주세요</h2>
<p class="empty-text">
왼쪽 목록에서 컴포넌트를 선택하면<br />
미리보기와 상세 정보를 확인할 있습니다
</p>
</div>
<div v-else class="component-detail">
<!-- Detail Header -->
<div class="detail-header">
<div>
<h2 class="detail-title">{{ selectedComponent.name }}</h2>
<p v-if="selectedComponent.description" class="detail-desc">
{{ selectedComponent.description }}
</p>
<code class="detail-path">{{ selectedComponent.path }}</code>
</div>
<button class="close-btn" @click="closeDetail">
</button>
</div>
<!-- Preview Info -->
<div class="preview-info">
<div class="info-badge">
<span class="badge-label">Category:</span>
<span class="badge-value">{{ selectedComponent.category }}</span>
</div>
<div class="info-badge">
<span class="badge-label">Path:</span>
<span class="badge-value">layers/components/{{ selectedComponent.path }}.vue</span>
</div>
</div>
<!-- Preview Section -->
<div v-if="selectedComponent.hasPreview" class="preview-section">
<h3 class="section-title">👁 미리보기</h3>
<div class="preview-container">
<!-- Button Circle -->
<template v-if="selectedComponent.path === 'atoms/Button/Circle'">
<AtomsButtonCircle v-bind="selectedComponent.sampleProps">
<AtomsIconsStove />
</AtomsButtonCircle>
</template>
<!-- Button Play -->
<template v-else-if="selectedComponent.path === 'atoms/Button/Play'">
<AtomsButtonPlay v-bind="selectedComponent.sampleProps" />
</template>
<!-- Button Variant -->
<template v-else-if="selectedComponent.path === 'atoms/Button/Variant'">
<div class="flex flex-col gap-4">
<AtomsButtonVariant variant="filled">Filled Button</AtomsButtonVariant>
<AtomsButtonVariant variant="outlined">Outlined Button</AtomsButtonVariant>
<AtomsButtonVariant variant="filled" :disabled="true">Disabled Button</AtomsButtonVariant>
</div>
</template>
<!-- Icons -->
<template v-else-if="selectedComponent.path.includes('atoms/icons/')">
<component :is="'Atoms' + selectedComponent.path.replace('atoms/', '').replace(/\//g, '')" v-bind="selectedComponent.sampleProps" />
</template>
<!-- Input -->
<template v-else-if="selectedComponent.path === 'atoms/Input'">
<div class="w-full max-w-md">
<AtomsInput
model-value=""
:placeholder="selectedComponent.sampleProps?.placeholder as string"
:use-clear-button="selectedComponent.sampleProps?.useClearButton as boolean"
/>
</div>
</template>
<!-- Loading Local -->
<template v-else-if="selectedComponent.path === 'atoms/loading/Local'">
<div class="w-20 h-20 relative">
<AtomsLoadingLocal />
</div>
</template>
<!-- Card News -->
<template v-else-if="selectedComponent.path === 'blocks/CardNews'">
<div class="w-full max-w-sm h-64">
<BlocksCardNews
:title="selectedComponent.sampleProps?.title as string"
:description="selectedComponent.sampleProps?.description as string"
:img-path="selectedComponent.sampleProps?.imgPath as string"
:url="selectedComponent.sampleProps?.url as string"
/>
</div>
</template>
<!-- Language Switcher -->
<template v-else-if="selectedComponent.path === 'blocks/LanguageSwitcher'">
<BlocksLanguageSwitcher />
</template>
<!-- Pagination -->
<template v-else-if="selectedComponent.path === 'blocks/Pagination'">
<BlocksPagination
:total-count="selectedComponent.sampleProps?.totalCount as number"
:current-page="currentPage"
:page-size="selectedComponent.sampleProps?.pageSize as number"
:page-block="selectedComponent.sampleProps?.pageBlock as number"
:use-min-max="selectedComponent.sampleProps?.useMinMax as boolean"
@update:page="currentPage = $event"
/>
</template>
<!-- Home Button -->
<template v-else-if="selectedComponent.path === 'blocks/Button/Home'">
<BlocksButtonHome />
</template>
<!-- Scroll Top Button -->
<template v-else-if="selectedComponent.path === 'blocks/Button/ScrollTop'">
<BlocksButtonScrollTop />
</template>
</div>
</div>
<!-- Preview Not Available -->
<div v-else class="preview-section">
<h3 class="section-title">👁 미리보기</h3>
<div class="preview-unavailable">
<div class="unavailable-icon">🚧</div>
<p class="unavailable-text">
{{ selectedComponent.previewNotes || '이 컴포넌트는 미리보기를 제공하지 않습니다' }}
</p>
</div>
</div>
<!-- Props Information -->
<div v-if="selectedComponent.sampleProps && Object.keys(selectedComponent.sampleProps).length > 0" class="props-section">
<h3 class="section-title"> Props 예제</h3>
<div class="props-table">
<div v-for="(value, key) in selectedComponent.sampleProps" :key="key" class="prop-row">
<span class="prop-key">{{ key }}</span>
<span class="prop-value">{{ JSON.stringify(value) }}</span>
</div>
</div>
</div>
<!-- Import Path -->
<div class="usage-section">
<h3 class="section-title">📥 Import</h3>
<div class="code-block">
<pre><code>// Auto-imported (Nuxt layers)
// 자동으로 import 됩니다</code></pre>
</div>
</div>
<!-- Notes -->
<div class="notes-section">
<h3 class="section-title">💡 참고사항</h3>
<ul class="notes-list">
<li> 컴포넌트는 Nuxt layers를 통해 자동으로 import됩니다</li>
<li>실제 사용 필요한 props를 전달해야 합니다</li>
<li>자세한 props 정보는 컴포넌트 파일을 참고하세요</li>
</ul>
</div>
</div>
</main>
</div>
</div>
</div>
</template>
<style scoped>
.component-viewer {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.viewer-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding: 2rem 0;
color: white;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem;
}
.title {
font-size: 2.5rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
}
.subtitle {
font-size: 1.125rem;
opacity: 0.9;
margin: 0;
}
.viewer-content {
display: grid;
grid-template-columns: 320px 1fr;
gap: 2rem;
padding: 2rem 0;
min-height: calc(100vh - 200px);
}
/* Sidebar */
.sidebar {
background: white;
border-radius: 12px;
padding: 1.5rem;
height: fit-content;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: sticky;
top: 2rem;
max-height: calc(100vh - 4rem);
overflow-y: auto;
}
.search-box {
margin-bottom: 1.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 0.875rem;
transition: all 0.2s;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.categories {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid #f3f4f6;
}
.category-btn {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border: none;
background: #f9fafb;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
color: #374151;
}
.category-btn:hover {
background: #f3f4f6;
}
.category-btn.active {
background: #667eea;
color: white;
}
.count {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
background: rgba(0, 0, 0, 0.1);
border-radius: 12px;
}
.component-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.category-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.category-title {
font-size: 0.75rem;
font-weight: 700;
color: #9ca3af;
margin: 0 0 0.5rem 0;
letter-spacing: 0.05em;
}
.component-item {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.75rem;
border: none;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
border: 1px solid #e5e7eb;
}
.component-item:hover {
background: #f9fafb;
border-color: #667eea;
}
.component-item.active {
background: #ede9fe;
border-color: #667eea;
}
.component-name {
font-size: 0.875rem;
font-weight: 500;
color: #111827;
}
.component-desc {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
/* Main Content */
.main-content {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
text-align: center;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-title {
font-size: 1.5rem;
font-weight: 600;
color: #111827;
margin: 0 0 0.5rem 0;
}
.empty-text {
font-size: 1rem;
color: #6b7280;
margin: 0;
line-height: 1.5;
}
/* Component Detail */
.component-detail {
display: flex;
flex-direction: column;
gap: 2rem;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 1.5rem;
border-bottom: 2px solid #f3f4f6;
}
.detail-title {
font-size: 2rem;
font-weight: 700;
color: #111827;
margin: 0 0 0.5rem 0;
}
.detail-desc {
font-size: 1rem;
color: #6b7280;
margin: 0 0 0.75rem 0;
}
.detail-path {
display: inline-block;
padding: 0.5rem 1rem;
background: #f3f4f6;
border-radius: 6px;
font-size: 0.875rem;
color: #667eea;
font-family: 'Courier New', monospace;
}
.close-btn {
padding: 0.5rem;
border: none;
background: #f3f4f6;
border-radius: 6px;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.2s;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background: #e5e7eb;
}
.preview-info {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.info-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.badge-label {
font-size: 0.875rem;
font-weight: 600;
color: #6b7280;
}
.badge-value {
font-size: 0.875rem;
color: #111827;
font-family: 'Courier New', monospace;
}
.usage-section,
.notes-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
color: #111827;
margin: 0;
}
.code-block {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
}
.code-block pre {
margin: 0;
}
.code-block code {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
color: #10b981;
line-height: 1.6;
}
.notes-list {
margin: 0;
padding-left: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.notes-list li {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.6;
}
/* Preview Section */
.preview-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.preview-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 2rem;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12px;
border: 2px dashed #d1d5db;
}
.preview-unavailable {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 150px;
padding: 2rem;
background: #f9fafb;
border-radius: 12px;
border: 2px dashed #e5e7eb;
}
.unavailable-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.unavailable-text {
font-size: 0.875rem;
color: #6b7280;
text-align: center;
margin: 0;
}
/* Props Section */
.props-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.props-table {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: #f9fafb;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e5e7eb;
}
.prop-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: white;
border-radius: 6px;
border: 1px solid #e5e7eb;
gap: 1rem;
}
.prop-key {
font-size: 0.875rem;
font-weight: 600;
color: #667eea;
font-family: 'Courier New', monospace;
}
.prop-value {
font-size: 0.875rem;
color: #374151;
font-family: 'Courier New', monospace;
background: #f3f4f6;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
/* Scrollbar Styling */
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
@media (max-width: 1024px) {
.viewer-content {
grid-template-columns: 1fr;
}
.sidebar {
position: relative;
top: 0;
max-height: none;
}
.prop-row {
flex-direction: column;
align-items: flex-start;
}
}
</style>