歡迎回來!
+準備好今天的語言學習冒險了嗎?
+詞彙學習
+透過科學的間隔重複法,高效記憶新詞彙
+ +情境對話
+在真實場景中練習對話,提升口語表達能力
+ +diff --git a/apps/web-native/assets/css/components.css b/apps/web-native/assets/css/components.css new file mode 100644 index 0000000..9e6a18d --- /dev/null +++ b/apps/web-native/assets/css/components.css @@ -0,0 +1,723 @@ +/* Drama Ling - 組件樣式 */ + +/* ======================================== + 按鈕組件 (Buttons) +======================================== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-6); + border: none; + border-radius: var(--radius-xl); + font-size: var(--text-base); + font-weight: var(--font-weight-semibold); + text-decoration: none; + cursor: pointer; + transition: all var(--transition-fast); + min-height: 2.75rem; + position: relative; + overflow: hidden; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +/* 按鈕尺寸 */ +.btn-sm { + padding: var(--space-2) var(--space-4); + font-size: var(--text-sm); + min-height: 2rem; +} + +.btn-lg { + padding: var(--space-4) var(--space-8); + font-size: var(--text-lg); + min-height: 3.5rem; +} + +/* 按鈕變體 */ +.btn-primary { + background: linear-gradient(135deg, var(--primary-teal), var(--secondary-purple)); + color: var(--text-inverse); + box-shadow: 0 4px 15px 0 var(--shadow-primary); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px 0 var(--shadow-primary); +} + +.btn-secondary { + background-color: var(--background-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-light); +} + +.btn-secondary:hover { + background-color: var(--border-light); + border-color: var(--border-medium); +} + +.btn-outline { + background-color: transparent; + color: var(--primary-teal); + border: 2px solid var(--primary-teal); +} + +.btn-outline:hover { + background-color: var(--primary-teal); + color: var(--text-inverse); +} + +.btn-ghost { + background-color: transparent; + color: var(--text-secondary); +} + +.btn-ghost:hover { + background-color: var(--background-tertiary); + color: var(--text-primary); +} + +.btn-danger { + background-color: var(--text-error); + color: var(--text-inverse); +} + +.btn-danger:hover { + background-color: #DC2626; + transform: translateY(-1px); +} + +/* ======================================== + 卡片組件 (Cards) +======================================== */ +.card { + background-color: var(--background-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-6); + box-shadow: 0 2px 8px 0 var(--shadow-light); + transition: all var(--transition-fast); +} + +.card:hover { + box-shadow: 0 8px 25px 0 var(--shadow-medium); + transform: translateY(-2px); +} + +.card-header { + margin-bottom: var(--space-4); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--border-light); +} + +.card-title { + font-size: var(--text-lg); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + margin: 0; +} + +.card-subtitle { + font-size: var(--text-sm); + color: var(--text-secondary); + margin: var(--space-1) 0 0 0; +} + +.card-body { + color: var(--text-secondary); + line-height: var(--leading-relaxed); +} + +.card-footer { + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 1px solid var(--border-light); + display: flex; + gap: var(--space-3); + justify-content: flex-end; +} + +/* 卡片變體 */ +.card-interactive { + cursor: pointer; + transition: all var(--transition-normal); +} + +.card-interactive:hover { + border-color: var(--primary-teal); + box-shadow: 0 12px 30px 0 var(--shadow-primary); +} + +.card-flat { + box-shadow: none; + border: 1px solid var(--border-light); +} + +.card-elevated { + box-shadow: 0 10px 25px 0 var(--shadow-medium); +} + +/* ======================================== + 表單組件 (Forms) +======================================== */ +.form-group { + margin-bottom: var(--space-4); +} + +.form-label { + display: block; + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + color: var(--text-primary); + margin-bottom: var(--space-2); +} + +.form-label.required::after { + content: '*'; + color: var(--text-error); + margin-left: var(--space-1); +} + +.form-input, +.form-textarea, +.form-select { + width: 100%; + padding: var(--space-3) var(--space-4); + border: 2px solid var(--border-light); + border-radius: var(--radius-lg); + font-size: var(--text-base); + background-color: var(--background-secondary); + color: var(--text-primary); + transition: all var(--transition-fast); +} + +.form-input:focus, +.form-textarea:focus, +.form-select:focus { + outline: none; + border-color: var(--primary-teal); + box-shadow: 0 0 0 3px rgba(0, 229, 204, 0.1); +} + +.form-input.error, +.form-textarea.error, +.form-select.error { + border-color: var(--text-error); +} + +.form-input.error:focus, +.form-textarea.error:focus, +.form-select.error:focus { + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); +} + +.form-textarea { + resize: vertical; + min-height: 6rem; +} + +.form-help { + font-size: var(--text-sm); + color: var(--text-secondary); + margin-top: var(--space-1); +} + +.form-error { + font-size: var(--text-sm); + color: var(--text-error); + margin-top: var(--space-1); + display: flex; + align-items: center; + gap: var(--space-1); +} + +/* 複選框和單選框 */ +.form-checkbox, +.form-radio { + display: flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; +} + +.form-checkbox input, +.form-radio input { + width: 1.25rem; + height: 1.25rem; + margin: 0; +} + +/* ======================================== + 徽章組件 (Badges) +======================================== */ +.badge { + display: inline-flex; + align-items: center; + padding: var(--space-1) var(--space-3); + font-size: var(--text-xs); + font-weight: var(--font-weight-semibold); + border-radius: var(--radius-full); +} + +.badge-primary { + background-color: var(--primary-teal); + color: var(--text-inverse); +} + +.badge-secondary { + background-color: var(--text-secondary); + color: var(--text-inverse); +} + +.badge-success { + background-color: var(--text-success); + color: var(--text-inverse); +} + +.badge-warning { + background-color: var(--text-warning); + color: var(--text-inverse); +} + +.badge-error { + background-color: var(--text-error); + color: var(--text-inverse); +} + +.badge-outline { + background-color: transparent; + border: 1px solid currentColor; +} + +/* ======================================== + 彈窗組件 (Modal) +======================================== */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal-backdrop); + opacity: 0; + visibility: hidden; + transition: all var(--transition-normal); +} + +.modal-overlay.active { + opacity: 1; + visibility: visible; +} + +.modal { + background-color: var(--background-secondary); + border-radius: var(--radius-2xl); + box-shadow: 0 20px 50px 0 var(--shadow-heavy); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + transform: scale(0.95) translateY(20px); + transition: transform var(--transition-normal); +} + +.modal-overlay.active .modal { + transform: scale(1) translateY(0); +} + +.modal-header { + padding: var(--space-6); + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-title { + font-size: var(--text-xl); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + margin: 0; +} + +.modal-close { + width: 2rem; + height: 2rem; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + transition: all var(--transition-fast); +} + +.modal-close:hover { + background-color: var(--background-tertiary); + color: var(--text-primary); +} + +.modal-body { + padding: var(--space-6); +} + +.modal-footer { + padding: var(--space-6); + border-top: 1px solid var(--border-light); + display: flex; + gap: var(--space-3); + justify-content: flex-end; +} + +/* ======================================== + 提示框組件 (Toast) +======================================== */ +.toast-container { + position: fixed; + top: var(--space-4); + right: var(--space-4); + z-index: var(--z-tooltip); + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.toast { + background-color: var(--background-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-lg); + padding: var(--space-4); + box-shadow: 0 8px 25px 0 var(--shadow-medium); + display: flex; + align-items: center; + gap: var(--space-3); + max-width: 400px; + animation: slideInRight var(--transition-normal) ease-out; +} + +.toast.removing { + animation: slideOutRight var(--transition-normal) ease-in; +} + +.toast-icon { + font-size: var(--text-lg); + flex-shrink: 0; +} + +.toast-content { + flex: 1; +} + +.toast-title { + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + margin-bottom: var(--space-1); +} + +.toast-message { + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.toast-close { + color: var(--text-secondary); + cursor: pointer; + padding: var(--space-1); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.toast-close:hover { + background-color: var(--background-tertiary); + color: var(--text-primary); +} + +/* 提示框類型 */ +.toast-success { + border-left: 4px solid var(--text-success); +} + +.toast-success .toast-icon { + color: var(--text-success); +} + +.toast-warning { + border-left: 4px solid var(--text-warning); +} + +.toast-warning .toast-icon { + color: var(--text-warning); +} + +.toast-error { + border-left: 4px solid var(--text-error); +} + +.toast-error .toast-icon { + color: var(--text-error); +} + +.toast-info { + border-left: 4px solid var(--primary-teal); +} + +.toast-info .toast-icon { + color: var(--primary-teal); +} + +/* ======================================== + 載入組件 (Loading) +======================================== */ +.loading { + display: inline-block; + width: 1.5rem; + height: 1.5rem; + border: 2px solid var(--border-light); + border-top-color: var(--primary-teal); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; +} + +.loading-sm { + width: 1rem; + height: 1rem; + border-width: 1px; +} + +.loading-lg { + width: 2rem; + height: 2rem; + border-width: 3px; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 載入覆蓋層 */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: var(--z-modal); +} + +/* ======================================== + 進度條組件 (Progress) +======================================== */ +.progress { + width: 100%; + height: 0.5rem; + background-color: var(--background-tertiary); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--primary-teal), var(--secondary-purple)); + border-radius: var(--radius-full); + transition: width var(--transition-normal); + position: relative; +} + +.progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient(90deg, + rgba(255, 255, 255, 0.2) 0%, + rgba(255, 255, 255, 0.4) 50%, + rgba(255, 255, 255, 0.2) 100%); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +/* 進度條尺寸 */ +.progress-sm { + height: 0.25rem; +} + +.progress-lg { + height: 0.75rem; +} + +/* ======================================== + 標籤頁組件 (Tabs) +======================================== */ +.tabs { + display: flex; + flex-direction: column; +} + +.tab-list { + display: flex; + border-bottom: 2px solid var(--border-light); + margin-bottom: var(--space-6); +} + +.tab-button { + padding: var(--space-3) var(--space-4); + border: none; + background: none; + color: var(--text-secondary); + font-weight: var(--font-weight-medium); + cursor: pointer; + position: relative; + transition: all var(--transition-fast); +} + +.tab-button:hover { + color: var(--text-primary); +} + +.tab-button.active { + color: var(--primary-teal); +} + +.tab-button.active::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background-color: var(--primary-teal); +} + +.tab-panel { + display: none; + animation: fadeInUp var(--transition-normal) ease-out; +} + +.tab-panel.active { + display: block; +} + +/* ======================================== + 下拉選單組件 (Dropdown) +======================================== */ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-toggle { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + transition: background-color var(--transition-fast); +} + +.dropdown-toggle:hover { + background-color: var(--background-tertiary); +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + min-width: 200px; + background-color: var(--background-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-lg); + box-shadow: 0 8px 25px 0 var(--shadow-medium); + padding: var(--space-2); + z-index: var(--z-dropdown); + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all var(--transition-fast); +} + +.dropdown.open .dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.dropdown-item { + display: flex; + align-items: center; + gap: var(--space-3); + width: 100%; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + color: var(--text-secondary); + transition: all var(--transition-fast); + cursor: pointer; +} + +.dropdown-item:hover { + background-color: var(--background-tertiary); + color: var(--text-primary); +} + +.dropdown-divider { + height: 1px; + background-color: var(--border-light); + margin: var(--space-2) 0; +} + +/* 響應式調整 */ +@media (max-width: 767px) { + .modal { + width: 95%; + margin: var(--space-4); + } + + .toast-container { + left: var(--space-4); + right: var(--space-4); + } + + .toast { + max-width: none; + } +} + +/* Toast 動畫 */ +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOutRight { + to { + transform: translateX(100%); + opacity: 0; + } +} \ No newline at end of file diff --git a/apps/web-native/assets/css/layouts.css b/apps/web-native/assets/css/layouts.css new file mode 100644 index 0000000..a55a445 --- /dev/null +++ b/apps/web-native/assets/css/layouts.css @@ -0,0 +1,628 @@ +/* Drama Ling - 佈局樣式 */ + +/* ======================================== + 頁首導航 (Header) +======================================== */ +.app-header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 4rem; + background-color: var(--background-secondary); + border-bottom: 1px solid var(--border-light); + z-index: var(--z-sticky); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.header-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; + padding: 0 var(--space-4); + max-width: 1920px; + margin: 0 auto; +} + +.header-left { + display: flex; + align-items: center; + gap: var(--space-4); +} + +.menu-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-md); + background-color: transparent; + transition: all var(--transition-fast); +} + +.menu-toggle:hover { + background-color: var(--background-tertiary); +} + +.hamburger { + position: relative; + width: 1.25rem; + height: 2px; + background-color: var(--text-primary); + transition: all var(--transition-fast); +} + +.hamburger::before, +.hamburger::after { + content: ''; + position: absolute; + width: 1.25rem; + height: 2px; + background-color: var(--text-primary); + transition: all var(--transition-fast); +} + +.hamburger::before { + top: -6px; +} + +.hamburger::after { + bottom: -6px; +} + +/* 漢堡選單動畫 */ +.menu-toggle.active .hamburger { + background-color: transparent; +} + +.menu-toggle.active .hamburger::before { + transform: rotate(45deg); + top: 0; +} + +.menu-toggle.active .hamburger::after { + transform: rotate(-45deg); + bottom: 0; +} + +.logo { + display: flex; + align-items: center; + gap: var(--space-2); + font-weight: var(--font-weight-bold); + font-size: var(--text-lg); + color: var(--text-primary); +} + +.logo-icon { + width: 2rem; + height: 2rem; +} + +.logo-text { + font-family: var(--font-family-display); +} + +.header-center { + display: none; +} + +.user-stats { + display: flex; + gap: var(--space-4); +} + +.stat-item { + display: flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-2) var(--space-3); + background-color: var(--background-tertiary); + border-radius: var(--radius-xl); + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); +} + +.stat-icon { + font-size: var(--text-base); +} + +.stat-value { + color: var(--text-primary); +} + +.header-right { + display: flex; + align-items: center; +} + +.profile-btn { + width: 2.5rem; + height: 2.5rem; + border-radius: var(--radius-full); + overflow: hidden; + transition: transform var(--transition-fast); +} + +.profile-btn:hover { + transform: scale(1.05); +} + +.avatar { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* ======================================== + 側邊欄 (Sidebar) +======================================== */ +.sidebar { + position: fixed; + top: 0; + left: 0; + width: 320px; + height: 100vh; + background-color: var(--background-secondary); + border-right: 1px solid var(--border-light); + transform: translateX(-100%); + transition: transform var(--transition-normal); + z-index: var(--z-fixed); + overflow-y: auto; +} + +.sidebar.open { + transform: translateX(0); +} + +.sidebar-content { + display: flex; + flex-direction: column; + height: 100%; +} + +.sidebar-header { + padding: var(--space-6) var(--space-4); + border-bottom: 1px solid var(--border-light); +} + +.user-profile { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.user-avatar { + width: 3rem; + height: 3rem; + border-radius: var(--radius-full); + object-fit: cover; +} + +.user-info { + flex: 1; +} + +.user-name { + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + margin-bottom: var(--space-1); +} + +.user-level { + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.sidebar-menu { + flex: 1; + padding: var(--space-4) 0; +} + +.menu-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + color: var(--text-secondary); + transition: all var(--transition-fast); + position: relative; +} + +.menu-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background-color: var(--primary-teal); + transform: scaleY(0); + transition: transform var(--transition-fast); +} + +.menu-item:hover, +.menu-item.active { + color: var(--text-primary); + background-color: var(--background-tertiary); +} + +.menu-item.active::before { + transform: scaleY(1); +} + +.menu-icon { + font-size: var(--text-lg); + width: 1.5rem; + text-align: center; +} + +.menu-text { + font-weight: var(--font-weight-medium); +} + +.sidebar-footer { + padding: var(--space-4); + border-top: 1px solid var(--border-light); +} + +.logout-btn { + display: flex; + align-items: center; + gap: var(--space-3); + width: 100%; + padding: var(--space-3) var(--space-4); + color: var(--text-error); + border-radius: var(--radius-md); + transition: background-color var(--transition-fast); +} + +.logout-btn:hover { + background-color: rgba(239, 68, 68, 0.1); +} + +/* ======================================== + 主內容區域 (Main Content) +======================================== */ +.main-content { + flex: 1; + padding: var(--space-6) var(--space-4); + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +.page-view { + display: none; + animation: fadeInUp var(--transition-normal) ease-out; +} + +.page-view.active { + display: block; +} + +.dashboard-container { + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.welcome-section { + text-align: center; + padding: var(--space-8) 0; +} + +.welcome-title { + font-size: var(--text-3xl); + font-weight: var(--font-weight-bold); + font-family: var(--font-family-display); + color: var(--text-primary); + margin-bottom: var(--space-2); +} + +.welcome-subtitle { + font-size: var(--text-lg); + color: var(--text-secondary); + margin: 0; +} + +.quick-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-4); +} + +.stat-card { + background-color: var(--background-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-6); + display: flex; + align-items: center; + gap: var(--space-4); + transition: all var(--transition-fast); +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px 0 var(--shadow-medium); + border-color: var(--primary-teal); +} + +.stat-card .stat-icon { + font-size: var(--text-2xl); + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--primary-teal), var(--secondary-purple)); + border-radius: var(--radius-xl); + margin: 0; +} + +.stat-card .stat-info { + flex: 1; +} + +.stat-card .stat-value { + font-size: var(--text-2xl); + font-weight: var(--font-weight-bold); + color: var(--text-primary); + display: block; + margin-bottom: var(--space-1); +} + +.stat-card .stat-label { + font-size: var(--text-sm); + color: var(--text-secondary); +} + +.learning-modules { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--space-6); +} + +.module-card { + background-color: var(--background-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-2xl); + padding: var(--space-8); + transition: all var(--transition-normal); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.module-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-teal), var(--secondary-purple)); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--transition-normal); +} + +.module-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 30px 0 var(--shadow-primary); + border-color: var(--primary-teal); +} + +.module-card:hover::before { + transform: scaleX(1); +} + +.module-icon { + font-size: var(--text-4xl); + margin-bottom: var(--space-4); + display: block; +} + +.module-content { + margin-bottom: var(--space-6); +} + +.module-title { + font-size: var(--text-xl); + font-weight: var(--font-weight-bold); + color: var(--text-primary); + margin-bottom: var(--space-2); + font-family: var(--font-family-display); +} + +.module-description { + font-size: var(--text-base); + color: var(--text-secondary); + line-height: var(--leading-relaxed); + margin-bottom: var(--space-4); +} + +.module-btn { + background: linear-gradient(135deg, var(--primary-teal), var(--secondary-purple)); + color: var(--text-inverse); + padding: var(--space-3) var(--space-6); + border-radius: var(--radius-xl); + font-weight: var(--font-weight-semibold); + transition: all var(--transition-fast); +} + +.module-btn:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px 0 var(--shadow-primary); +} + +.module-progress { + display: flex; + align-items: center; + gap: var(--space-3); + margin-top: var(--space-4); +} + +.progress-bar { + flex: 1; + height: 6px; + background-color: var(--background-tertiary); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-teal), var(--secondary-purple)); + border-radius: var(--radius-full); + transition: width var(--transition-normal); +} + +.progress-text { + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + color: var(--text-secondary); +} + +/* ======================================== + 響應式設計 +======================================== */ + +/* 平板及以上:顯示 Header 中央統計 */ +@media (min-width: 768px) { + .header-center { + display: block; + } +} + +/* 平板及以上:調整側邊欄為靜態 */ +@media (min-width: 768px) { + .sidebar { + position: static; + transform: translateX(0); + grid-area: sidebar; + } + + .app-header { + grid-area: header; + position: static; + } + + .main-content { + grid-area: main; + padding: var(--space-8); + } + + .menu-toggle { + display: none; + } +} + +/* 大型桌機:增加 Header 容器最大寬度 */ +@media (min-width: 1200px) { + .header-container { + padding: 0 var(--space-8); + } + + .main-content { + padding: var(--space-12); + } + + .dashboard-container { + gap: var(--space-12); + } + + .learning-modules { + gap: var(--space-8); + } +} + +/* 手機優化 */ +@media (max-width: 767px) { + .app-header { + height: 3.5rem; + } + + .header-container { + padding: 0 var(--space-3); + } + + .main-content { + padding: var(--space-4); + margin-top: 3.5rem; + } + + .welcome-title { + font-size: var(--text-2xl); + } + + .welcome-subtitle { + font-size: var(--text-base); + } + + .quick-stats { + grid-template-columns: 1fr; + gap: var(--space-3); + } + + .stat-card { + padding: var(--space-4); + } + + .learning-modules { + grid-template-columns: 1fr; + gap: var(--space-4); + } + + .module-card { + padding: var(--space-6); + } + + .sidebar { + width: 280px; + } +} + +/* 極小手機優化 */ +@media (max-width: 375px) { + .header-container { + padding: 0 var(--space-2); + } + + .main-content { + padding: var(--space-3); + } + + .dashboard-container { + gap: var(--space-6); + } +} + +/* 側邊欄遮罩層 */ +.sidebar-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + visibility: hidden; + transition: all var(--transition-normal); + z-index: var(--z-modal-backdrop); +} + +.sidebar-overlay.active { + opacity: 1; + visibility: visible; +} + +@media (min-width: 768px) { + .sidebar-overlay { + display: none; + } +} \ No newline at end of file diff --git a/apps/web-native/assets/css/main.css b/apps/web-native/assets/css/main.css new file mode 100644 index 0000000..e051fbe --- /dev/null +++ b/apps/web-native/assets/css/main.css @@ -0,0 +1,476 @@ +/* Drama Ling - 主要樣式框架 */ + +/* CSS 變數系統 - 設計代幣 */ +:root { + /* 色彩系統 */ + --primary-teal: #00E5CC; + --primary-teal-light: #33EBDB; + --primary-teal-dark: #00B8A3; + --secondary-purple: #6366F1; + --secondary-purple-light: #8B87F7; + --secondary-purple-dark: #4F46E5; + + /* 背景色彩 */ + --background-primary: #F7F9FC; + --background-secondary: #FFFFFF; + --background-tertiary: #F1F5F9; + --background-dark: #1A1A1A; + --background-dark-secondary: #2D2D2D; + + /* 文字色彩 */ + --text-primary: #2C3E50; + --text-secondary: #64748B; + --text-tertiary: #94A3B8; + --text-inverse: #FFFFFF; + --text-success: #10B981; + --text-warning: #F59E0B; + --text-error: #EF4444; + + /* 邊框色彩 */ + --border-light: #E2E8F0; + --border-medium: #CBD5E1; + --border-dark: #64748B; + + /* 陰影色彩 */ + --shadow-light: rgba(0, 0, 0, 0.05); + --shadow-medium: rgba(0, 0, 0, 0.1); + --shadow-heavy: rgba(0, 0, 0, 0.15); + --shadow-primary: rgba(0, 229, 204, 0.2); + + /* 間距系統 */ + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + --space-12: 3rem; /* 48px */ + --space-16: 4rem; /* 64px */ + --space-20: 5rem; /* 80px */ + + /* 字體系統 */ + --font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-family-display: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; + --font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; + + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 1.875rem; /* 30px */ + --text-4xl: 2.25rem; /* 36px */ + --text-5xl: 3rem; /* 48px */ + + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* 行高系統 */ + --leading-none: 1; + --leading-tight: 1.25; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; + + /* 圓角系統 */ + --radius-sm: 0.25rem; /* 4px */ + --radius-md: 0.5rem; /* 8px */ + --radius-lg: 0.75rem; /* 12px */ + --radius-xl: 1rem; /* 16px */ + --radius-2xl: 1.5rem; /* 24px */ + --radius-full: 9999px; + + /* 動畫過渡 */ + --transition-fast: 0.15s ease-out; + --transition-normal: 0.3s ease-out; + --transition-slow: 0.5s ease-out; + + /* Z-index 層級 */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; +} + +/* 暗色主題變數 */ +@media (prefers-color-scheme: dark) { + :root { + --background-primary: #0F172A; + --background-secondary: #1E293B; + --background-tertiary: #334155; + --text-primary: #F8FAFC; + --text-secondary: #CBD5E1; + --text-tertiary: #94A3B8; + --border-light: #334155; + --border-medium: #475569; + --border-dark: #64748B; + } +} + +/* 全域重置和基礎樣式 */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-family-base); + font-size: var(--text-base); + font-weight: var(--font-weight-normal); + line-height: var(--leading-normal); + color: var(--text-primary); + background-color: var(--background-primary); + overflow-x: hidden; +} + +/* 可訪問性改善 */ +*:focus { + outline: 2px solid var(--primary-teal); + outline-offset: 2px; +} + +*:focus:not(:focus-visible) { + outline: none; +} + +/* 按鈕重置 */ +button { + margin: 0; + padding: 0; + border: none; + background: none; + font: inherit; + cursor: pointer; +} + +/* 連結重置 */ +a { + text-decoration: none; + color: inherit; +} + +/* 圖片重置 */ +img { + max-width: 100%; + height: auto; +} + +/* 表單元素重置 */ +input, +textarea, +select { + font: inherit; + margin: 0; +} + +/* 清單重置 */ +ul, +ol { + list-style: none; + margin: 0; + padding: 0; +} + +/* 載入畫面 */ +.loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, var(--primary-teal), var(--secondary-purple)); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: var(--z-modal); + transition: opacity var(--transition-slow), visibility var(--transition-slow); +} + +.loading-screen.hidden { + opacity: 0; + visibility: hidden; +} + +.loading-logo { + text-align: center; + margin-bottom: var(--space-8); +} + +.loading-icon { + width: 4rem; + height: 4rem; + margin-bottom: var(--space-4); + animation: pulse 2s ease-in-out infinite; +} + +.loading-text { + font-size: var(--text-2xl); + font-weight: var(--font-weight-bold); + color: var(--text-inverse); + font-family: var(--font-family-display); +} + +.loading-progress { + width: 12rem; + height: 0.25rem; + background-color: rgba(255, 255, 255, 0.2); + border-radius: var(--radius-full); + overflow: hidden; +} + +.loading-bar { + width: 100%; + height: 100%; + background: linear-gradient(90deg, + var(--text-inverse) 0%, + rgba(255, 255, 255, 0.8) 50%, + var(--text-inverse) 100%); + animation: loading 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.05); opacity: 0.8; } +} + +@keyframes loading { + 0% { transform: translateX(-100%); } + 50% { transform: translateX(0%); } + 100% { transform: translateX(100%); } +} + +/* 應用容器 */ +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* 響應式設計斷點 */ +/* 手機 */ +@media (max-width: 767px) { + .app-container { + padding-top: 4rem; /* Header 高度 */ + } +} + +/* 平板 */ +@media (min-width: 768px) and (max-width: 1023px) { + .app-container { + display: grid; + grid-template-columns: 280px 1fr; + grid-template-rows: auto 1fr; + grid-template-areas: + "sidebar header" + "sidebar main"; + } +} + +/* 桌機 */ +@media (min-width: 1024px) { + .app-container { + display: grid; + grid-template-columns: 320px 1fr; + grid-template-rows: auto 1fr; + grid-template-areas: + "sidebar header" + "sidebar main"; + } +} + +/* 工具類別 */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.hidden { display: none !important; } +.invisible { visibility: hidden; } + +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } + +.w-full { width: 100%; } +.h-full { height: 100%; } + +.rounded { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +.shadow { box-shadow: 0 1px 3px 0 var(--shadow-light); } +.shadow-md { box-shadow: 0 4px 6px -1px var(--shadow-medium); } +.shadow-lg { box-shadow: 0 10px 15px -3px var(--shadow-heavy); } + +.transition { transition: all var(--transition-normal); } + +/* 色彩工具類 */ +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-success { color: var(--text-success); } +.text-warning { color: var(--text-warning); } +.text-error { color: var(--text-error); } + +.bg-primary { background-color: var(--primary-teal); } +.bg-secondary { background-color: var(--secondary-purple); } +.bg-white { background-color: var(--background-secondary); } + +/* 間距工具類 */ +.p-1 { padding: var(--space-1); } +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } +.p-6 { padding: var(--space-6); } +.p-8 { padding: var(--space-8); } + +.m-1 { margin: var(--space-1); } +.m-2 { margin: var(--space-2); } +.m-3 { margin: var(--space-3); } +.m-4 { margin: var(--space-4); } +.m-6 { margin: var(--space-6); } +.m-8 { margin: var(--space-8); } + +.mt-1 { margin-top: var(--space-1); } +.mt-2 { margin-top: var(--space-2); } +.mt-4 { margin-top: var(--space-4); } +.mt-6 { margin-top: var(--space-6); } +.mt-8 { margin-top: var(--space-8); } + +.mb-1 { margin-bottom: var(--space-1); } +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-6 { margin-bottom: var(--space-6); } +.mb-8 { margin-bottom: var(--space-8); } + +/* 字體工具類 */ +.text-xs { font-size: var(--text-xs); } +.text-sm { font-size: var(--text-sm); } +.text-base { font-size: var(--text-base); } +.text-lg { font-size: var(--text-lg); } +.text-xl { font-size: var(--text-xl); } +.text-2xl { font-size: var(--text-2xl); } +.text-3xl { font-size: var(--text-3xl); } +.text-4xl { font-size: var(--text-4xl); } + +.font-light { font-weight: var(--font-weight-light); } +.font-normal { font-weight: var(--font-weight-normal); } +.font-medium { font-weight: var(--font-weight-medium); } +.font-semibold { font-weight: var(--font-weight-semibold); } +.font-bold { font-weight: var(--font-weight-bold); } + +/* 互動效果 */ +.hover-lift { + transition: transform var(--transition-fast), box-shadow var(--transition-fast); +} + +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px 0 var(--shadow-medium); +} + +.hover-scale { + transition: transform var(--transition-fast); +} + +.hover-scale:hover { + transform: scale(1.05); +} + +/* 動畫關鍵幀 */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideInLeft { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +.animate-fade-in { + animation: fadeIn var(--transition-normal) ease-out; +} + +.animate-fade-in-up { + animation: fadeInUp var(--transition-normal) ease-out; +} + +.animate-slide-in-right { + animation: slideInRight var(--transition-normal) ease-out; +} + +.animate-slide-in-left { + animation: slideInLeft var(--transition-normal) ease-out; +} + +/* 無障礙設計 - 減少動畫 */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} \ No newline at end of file diff --git a/apps/web-native/assets/js/app.js b/apps/web-native/assets/js/app.js new file mode 100644 index 0000000..b034621 --- /dev/null +++ b/apps/web-native/assets/js/app.js @@ -0,0 +1,528 @@ +/** + * Drama Ling - 主應用程序 + * 原生JavaScript模組化架構 + */ + +import { AppState } from './utils.js'; +import { ApiClient } from './api.js'; +import { AuthManager } from './auth.js'; +import { Sidebar } from './components/sidebar.js'; +import { Navbar } from './components/navbar.js'; +import { Modal } from './components/modal.js'; +import { Toast } from './components/toast.js'; + +class DramaLingApp { + constructor() { + this.state = new AppState(); + this.api = new ApiClient(this.getApiBaseUrl()); + this.auth = new AuthManager(this.api, this.state); + + // UI 組件 + this.sidebar = null; + this.navbar = null; + this.modal = new Modal(); + this.toast = new Toast(); + + // 當前頁面 + this.currentPage = 'home'; + + this.init(); + } + + /** + * 初始化應用程序 + */ + async init() { + try { + // 顯示載入畫面 + this.showLoading(); + + // 檢查認證狀態 + const isAuthenticated = await this.auth.checkAuthStatus(); + + // 初始化 UI 組件 + this.initializeComponents(); + + // 設定事件監聽器 + this.setupEventListeners(); + + // 根據認證狀態決定初始頁面 + if (isAuthenticated) { + await this.loadUserData(); + this.showMainApp(); + } else { + this.showAuthPage(); + } + + // 隱藏載入畫面 + this.hideLoading(); + + } catch (error) { + console.error('應用程序初始化失敗:', error); + this.toast.show('錯誤', '應用程序載入失敗,請刷新頁面重試', 'error'); + this.hideLoading(); + } + } + + /** + * 獲取 API 基礎 URL + */ + getApiBaseUrl() { + return import.meta.env?.VITE_API_BASE_URL || 'http://localhost:3000/api'; + } + + /** + * 初始化 UI 組件 + */ + initializeComponents() { + this.navbar = new Navbar(this); + this.sidebar = new Sidebar(this); + + // 綁定組件到應用狀態 + this.state.addListener('userUpdated', (user) => { + this.navbar.updateUserInfo(user); + this.sidebar.updateUserInfo(user); + }); + + this.state.addListener('statsUpdated', (stats) => { + this.navbar.updateStats(stats); + }); + } + + /** + * 設定事件監聽器 + */ + setupEventListeners() { + // 全域鍵盤快捷鍵 + document.addEventListener('keydown', this.handleKeyboardShortcuts.bind(this)); + + // 路由變化監聽 + window.addEventListener('popstate', this.handlePopState.bind(this)); + + // 網路狀態監聽 + window.addEventListener('online', this.handleOnline.bind(this)); + window.addEventListener('offline', this.handleOffline.bind(this)); + + // 頁面可見性變化 + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + + // 模組卡片點擊 + document.querySelectorAll('.module-card').forEach(card => { + card.addEventListener('click', this.handleModuleClick.bind(this)); + }); + + // 選單項目點擊 + document.addEventListener('click', (e) => { + const menuItem = e.target.closest('.menu-item'); + if (menuItem && menuItem.dataset.page) { + e.preventDefault(); + this.navigateTo(menuItem.dataset.page); + } + }); + } + + /** + * 鍵盤快捷鍵處理 + */ + handleKeyboardShortcuts(e) { + // Ctrl/Cmd + K: 開啟搜尋 + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + this.openSearch(); + } + + // Ctrl/Cmd + /: 開啟快捷鍵說明 + if ((e.ctrlKey || e.metaKey) && e.key === '/') { + e.preventDefault(); + this.showShortcutsHelp(); + } + + // Escape: 關閉彈窗和選單 + if (e.key === 'Escape') { + this.sidebar.close(); + this.modal.close(); + } + + // Alt + 數字鍵: 快速導航 + if (e.altKey && /^[1-5]$/.test(e.key)) { + e.preventDefault(); + const pages = ['home', 'vocabulary', 'dialogue', 'profile', 'settings']; + const pageIndex = parseInt(e.key) - 1; + if (pages[pageIndex]) { + this.navigateTo(pages[pageIndex]); + } + } + } + + /** + * 瀏覽器歷史記錄變化處理 + */ + handlePopState(e) { + const page = e.state?.page || 'home'; + this.navigateTo(page, false); // false = 不更新歷史記錄 + } + + /** + * 網路連線恢復 + */ + handleOnline() { + this.toast.show('網路已連線', '已恢復網路連接', 'success'); + this.syncPendingData(); + } + + /** + * 網路連線中斷 + */ + handleOffline() { + this.toast.show('網路已中斷', '請檢查網路連接,離線模式已啟用', 'warning'); + } + + /** + * 頁面可見性變化 + */ + handleVisibilityChange() { + if (document.hidden) { + // 頁面被隱藏時,保存當前狀態 + this.saveCurrentState(); + } else { + // 頁面重新可見時,檢查是否需要更新數據 + this.checkForUpdates(); + } + } + + /** + * 模組卡片點擊處理 + */ + handleModuleClick(e) { + const moduleCard = e.currentTarget; + const moduleType = moduleCard.dataset.module; + + switch (moduleType) { + case 'vocabulary': + this.navigateTo('vocabulary'); + break; + case 'dialogue': + this.navigateTo('dialogue'); + break; + default: + console.warn(`未知的模組類型: ${moduleType}`); + } + } + + /** + * 頁面導航 + */ + navigateTo(page, updateHistory = true) { + if (this.currentPage === page) return; + + // 隱藏當前頁面 + this.hidePage(this.currentPage); + + // 顯示新頁面 + this.showPage(page); + + // 更新當前頁面 + this.currentPage = page; + + // 更新選單狀態 + this.updateMenuState(page); + + // 更新瀏覽器歷史記錄 + if (updateHistory) { + const url = page === 'home' ? '/' : `/${page}`; + history.pushState({ page }, document.title, url); + } + + // 關閉側邊欄(手機版) + this.sidebar.close(); + + // 載入頁面數據 + this.loadPageData(page); + } + + /** + * 隱藏頁面 + */ + hidePage(page) { + const pageElement = document.getElementById(`${page}-view`); + if (pageElement) { + pageElement.classList.remove('active'); + } + } + + /** + * 顯示頁面 + */ + showPage(page) { + const pageElement = document.getElementById(`${page}-view`); + if (pageElement) { + pageElement.classList.add('active'); + } else { + // 頁面不存在,需要動態載入 + this.loadPageComponent(page); + } + } + + /** + * 更新選單狀態 + */ + updateMenuState(activePage) { + document.querySelectorAll('.menu-item').forEach(item => { + if (item.dataset.page === activePage) { + item.classList.add('active'); + } else { + item.classList.remove('active'); + } + }); + } + + /** + * 載入頁面數據 + */ + async loadPageData(page) { + try { + switch (page) { + case 'home': + await this.loadDashboardData(); + break; + case 'vocabulary': + await this.loadVocabularyData(); + break; + case 'dialogue': + await this.loadDialogueData(); + break; + case 'profile': + await this.loadProfileData(); + break; + default: + console.log(`頁面 ${page} 不需要載入額外數據`); + } + } catch (error) { + console.error(`載入頁面 ${page} 數據失敗:`, error); + this.toast.show('載入失敗', '頁面數據載入失敗', 'error'); + } + } + + /** + * 載入儀表板數據 + */ + async loadDashboardData() { + const stats = await this.api.getUserStats(); + this.state.updateStats(stats); + this.updateDashboardStats(stats); + } + + /** + * 更新儀表板統計 + */ + updateDashboardStats(stats) { + const elements = { + totalWords: document.getElementById('total-words'), + studyTime: document.getElementById('study-time'), + achievements: document.getElementById('achievements') + }; + + if (elements.totalWords) elements.totalWords.textContent = stats.totalWords || 0; + if (elements.studyTime) elements.studyTime.textContent = stats.studyTime || 0; + if (elements.achievements) elements.achievements.textContent = stats.achievements || 0; + } + + /** + * 載入詞彙數據 + */ + async loadVocabularyData() { + // 這將在詞彙模組中實現 + console.log('載入詞彙數據...'); + } + + /** + * 載入對話數據 + */ + async loadDialogueData() { + // 這將在對話模組中實現 + console.log('載入對話數據...'); + } + + /** + * 載入個人檔案數據 + */ + async loadProfileData() { + // 這將在個人檔案模組中實現 + console.log('載入個人檔案數據...'); + } + + /** + * 動態載入頁面組件 + */ + async loadPageComponent(page) { + try { + // 動態導入頁面模組 + const module = await import(`./pages/${page}.js`); + const PageClass = module.default; + + // 創建頁面實例 + const pageInstance = new PageClass(this); + await pageInstance.render(); + + } catch (error) { + console.error(`載入頁面組件 ${page} 失敗:`, error); + this.show404Page(); + } + } + + /** + * 顯示 404 頁面 + */ + show404Page() { + const mainContent = document.getElementById('main-content'); + if (mainContent) { + mainContent.innerHTML = ` +
找不到頁面
+ +