Files
xianyan/docs/ctc/preview.html
Developer 016ad3cea1 feat: 新增CTC云端笔记仓库功能
- 新增多语言国际化文案支持笔记仓库模块
- 配置Universal Links与App Links支持ctc.s2ss.com域名跳转
- 实现CTS会话入口与会话时间更新逻辑
- 新增CTC笔记完整服务栈:API客户端、本地存储、同步服务
- 新增笔记编辑、预览、冲突解决、版本对比组件
- 新增二维码扫码/分享功能与路由配置
- 修复UrlAnalyzerService调用参数冗余问题
- 修复ProfileHeader组件样式问题
- 统一macOS部署目标版本为13.0
- 抑制liquid_glass_widgets高频调试日志
2026-06-11 08:46:46 +08:00

1270 lines
83 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>笔记仓库 - 预览方案 v2</title>
<style>
/* ========== Design System Tokens ========== */
:root {
--primary: #6C5CE7;
--primary-light: #A29BFE;
--primary-dark: #5A4BD1;
--secondary: #00CEC9;
--accent: #6C5CE7;
--accent-light: rgba(108, 92, 231, 0.12);
--bg-primary: #F2F2F7;
--bg-secondary: #FFFFFF;
--bg-tertiary: #F2F2F7;
--bg-grouped: #F2F2F7;
--bg-card: #FFFFFF;
--text-primary: #1C1C1E;
--text-secondary: #8E8E93;
--text-tertiary: #AEAEB2;
--text-link: #6C5CE7;
--separator: rgba(60, 60, 67, 0.12);
--border: rgba(60, 60, 67, 0.12);
--destructive: #FF3B30;
--success: #34C759;
--warning: #FF9500;
--overlay: rgba(0, 0, 0, 0.4);
--glass-bg: rgba(255, 255, 255, 0.72);
--glass-blur: 20px;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px;
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-full: 9999px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 30px rgba(0,0,0,0.12);
--shadow-float: 0 12px 40px rgba(0,0,0,0.15);
--font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
--font-title: 600 17px var(--font-family);
--font-large: 500 22px var(--font-family);
--font-body: 400 15px var(--font-family);
--font-caption: 400 13px var(--font-family);
--font-micro: 400 11px var(--font-family);
--transition-fast: 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
--transition-normal: 0.35s cubic-bezier(0.25, 0.1, 0.25, 1);
--transition-spring: 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
[data-theme="dark"] {
--primary: #A29BFE; --primary-light: rgba(162,155,254,0.15); --primary-dark: #6C5CE7;
--bg-primary: #000000; --bg-secondary: #1C1C1E; --bg-tertiary: #2C2C2E;
--bg-grouped: #000000; --bg-card: #1C1C1E;
--text-primary: #FFFFFF; --text-secondary: #98989D; --text-tertiary: #636366; --text-link: #A29BFE;
--separator: rgba(84,84,88,0.65); --border: rgba(84,84,88,0.65);
--overlay: rgba(0,0,0,0.6); --glass-bg: rgba(28,28,30,0.78);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3); --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 30px rgba(0,0,0,0.5); --shadow-float: 0 12px 40px rgba(0,0,0,0.6);
}
[data-theme="amoled"] {
--primary: #A29BFE; --primary-light: rgba(162,155,254,0.12); --primary-dark: #6C5CE7;
--bg-primary: #000000; --bg-secondary: #0A0A0A; --bg-tertiary: #141414;
--bg-grouped: #000000; --bg-card: #0A0A0A;
--text-primary: #F5F5F5; --text-secondary: #8A8A8A; --text-tertiary: #555555; --text-link: #A29BFE;
--separator: rgba(255,255,255,0.06); --border: rgba(255,255,255,0.06);
--overlay: rgba(0,0,0,0.7); --glass-bg: rgba(10,10,10,0.85);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.5); --shadow-md: 0 4px 10px rgba(0,0,0,0.6);
--shadow-lg: 0 8px 25px rgba(0,0,0,0.7); --shadow-float: 0 12px 35px rgba(0,0,0,0.8);
}
[data-accent="coral"] { --primary: #FF6B6B; --primary-light: rgba(255,107,107,0.12); --primary-dark: #E55555; --text-link: #FF6B6B; }
[data-accent="teal"] { --primary: #00B894; --primary-light: rgba(0,184,148,0.12); --primary-dark: #00A383; --text-link: #00B894; }
[data-accent="blue"] { --primary: #0984E3; --primary-light: rgba(9,132,227,0.12); --primary-dark: #0770C4; --text-link: #0984E3; }
[data-accent="orange"] { --primary: #E17055; --primary-light: rgba(225,112,85,0.12); --primary-dark: #C95A3F; --text-link: #E17055; }
[data-accent="pink"] { --primary: #FD79A8; --primary-light: rgba(253,121,168,0.12); --primary-dark: #E4608F; --text-link: #FD79A8; }
[data-accent="green"] { --primary: #00B894; --primary-light: rgba(0,184,148,0.12); --primary-dark: #009B7D; --text-link: #00B894; }
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html, body { height:100%; font-family:var(--font-family); background:var(--bg-primary); color:var(--text-primary); overflow:hidden; transition: background var(--transition-normal), color var(--transition-normal); }
/* ========== App Shell ========== */
.app-shell { display:flex; flex-direction:column; height:100vh; height:100dvh; max-width:430px; margin:0 auto; position:relative; overflow:hidden; background:var(--bg-primary); }
@media (min-width:768px) { .app-shell { max-width:430px; border-left:1px solid var(--separator); border-right:1px solid var(--separator); } }
/* ========== Nav Bar ========== */
.nav-bar { position:sticky; top:0; z-index:100; background:var(--glass-bg); backdrop-filter:blur(var(--glass-blur)); -webkit-backdrop-filter:blur(var(--glass-blur)); border-bottom:0.5px solid var(--separator); transition:background var(--transition-normal); }
.nav-bar-content { display:flex; align-items:center; justify-content:space-between; padding:var(--space-2) var(--space-4); min-height:44px; }
.nav-bar-left { display:flex; align-items:center; gap:var(--space-2); }
.nav-bar-title { font:var(--font-title); color:var(--text-primary); letter-spacing:-0.2px; }
.nav-bar-right { display:flex; align-items:center; gap:var(--space-1); }
.nav-btn { width:36px; height:36px; border-radius:var(--radius-full); border:none; background:var(--accent-light); color:var(--primary); display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all var(--transition-fast); font-size:18px; }
.nav-btn:active { transform:scale(0.9); background:var(--primary); color:white; }
/* ========== Main Content ========== */
.main-content { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; padding-bottom:34px; position:relative; }
/* ========== Pull to Refresh ========== */
.pull-indicator { display:flex; align-items:center; justify-content:center; padding:12px 0; gap:6px; font:var(--font-caption); color:var(--text-tertiary); transition: all 0.3s; }
.pull-indicator .pull-icon { transition: transform 0.3s; }
.pull-indicator.pulling .pull-icon { transform: rotate(180deg); }
.pull-indicator.refreshing .pull-icon { animation: spin 0.8s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* ========== Red Dot (变更指示) ========== */
.red-dot { width:8px; height:8px; border-radius:50%; background:var(--destructive); position:absolute; top:8px; right:8px; animation: redPulse 2s infinite; }
.red-dot-sm { width:6px; height:6px; border-radius:50%; background:var(--destructive); display:inline-block; animation: redPulse 2s infinite; }
@keyframes redPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:0.7; transform:scale(0.8); } }
/* ========== Search Bar ========== */
.search-bar { padding:var(--space-2) var(--space-4); }
.search-input-wrapper { display:flex; align-items:center; gap:var(--space-2); background:var(--bg-tertiary); border-radius:var(--radius-sm); padding:var(--space-2) var(--space-3); transition:background var(--transition-normal); }
.search-icon { color:var(--text-tertiary); font-size:15px; }
.search-input { flex:1; border:none; background:transparent; font:var(--font-body); color:var(--text-primary); outline:none; }
.search-input::placeholder { color:var(--text-tertiary); }
/* ========== Section Header ========== */
.section-header { display:flex; align-items:center; justify-content:space-between; padding:var(--space-4) var(--space-4) var(--space-2); }
.section-title { font:var(--font-caption); color:var(--text-secondary); text-transform:uppercase; letter-spacing:0.5px; display:flex; align-items:center; gap:6px; }
.section-count { font:var(--font-micro); color:var(--text-tertiary); background:var(--bg-tertiary); padding:2px 8px; border-radius:var(--radius-full); }
/* ========== Swipeable Container (列表模式) ========== */
.swipe-container { position:relative; overflow:hidden; }
.swipe-actions { position:absolute; top:0; bottom:0; display:flex; align-items:stretch; z-index:1; }
.swipe-actions.left { left:0; }
.swipe-actions.right { right:0; }
.swipe-action { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:0 14px; gap:4px; cursor:pointer; border:none; color:white; font:var(--font-micro); transition: opacity 0.2s; min-width:64px; }
.swipe-action:active { opacity:0.7; }
.swipe-action .action-icon { font-size:20px; }
.swipe-action.star { background:var(--warning); }
.swipe-action.share { background:var(--primary); }
.swipe-action.edit { background:#0984E3; }
.swipe-action.more { background:var(--text-secondary); }
.swipe-action.delete { background:var(--destructive); }
.swipe-action.pin { background:var(--success); }
.swipe-content { position:relative; z-index:2; background:var(--bg-card); transition:transform 0.3s cubic-bezier(0.25,0.1,0.25,1); }
/* ========== Note Card (Grid Mode) ========== */
.notes-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:var(--space-3); padding:0 var(--space-4); }
.note-card { background:var(--bg-card); border-radius:var(--radius-md); padding:var(--space-4); box-shadow:var(--shadow-sm); cursor:pointer; transition:all var(--transition-fast); border:1px solid var(--separator); position:relative; overflow:hidden; }
.note-card:active { transform:scale(0.97); box-shadow:var(--shadow-md); }
.note-card-key { font:600 15px var(--font-family); color:var(--primary); margin-bottom:var(--space-2); display:flex; align-items:center; gap:var(--space-2); }
.note-card-key .emoji { font-size:18px; }
.note-card-preview { font:var(--font-caption); color:var(--text-secondary); line-height:1.5; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; word-break:break-all; }
.note-card-meta { display:flex; align-items:center; justify-content:space-between; margin-top:var(--space-3); padding-top:var(--space-2); border-top:0.5px solid var(--separator); }
.note-card-time { font:var(--font-micro); color:var(--text-tertiary); }
.note-card-badge { font:var(--font-micro); padding:2px 6px; border-radius:var(--radius-full); background:var(--accent-light); color:var(--primary); }
.note-card-star { position:absolute; top:var(--space-2); right:var(--space-2); font-size:14px; opacity:0.6; }
/* ========== Note List Item ========== */
.notes-list { padding:0 var(--space-4); }
.note-list-item { display:flex; align-items:center; gap:var(--space-3); padding:var(--space-3) 0; border-bottom:0.5px solid var(--separator); cursor:pointer; transition:background var(--transition-fast); position:relative; }
.note-list-item:active { background:var(--accent-light); }
.note-list-icon { width:44px; height:44px; border-radius:var(--radius-md); background:var(--accent-light); display:flex; align-items:center; justify-content:center; font-size:22px; flex-shrink:0; position:relative; }
.note-list-content { flex:1; min-width:0; }
.note-list-title { font:500 15px var(--font-family); color:var(--text-primary); margin-bottom:2px; display:flex; align-items:center; gap:6px; }
.note-list-subtitle { font:var(--font-caption); color:var(--text-secondary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.note-list-right { display:flex; flex-direction:column; align-items:flex-end; gap:2px; flex-shrink:0; }
.note-list-time { font:var(--font-micro); color:var(--text-tertiary); }
.note-list-badge { font:var(--font-micro); padding:1px 6px; border-radius:var(--radius-full); background:var(--accent-light); color:var(--primary); }
/* ========== Timeline Mode ========== */
.notes-timeline { padding:0 var(--space-4); }
.timeline-date { font:600 13px var(--font-family); color:var(--primary); padding:var(--space-3) 0 var(--space-2); position:relative; display:flex; align-items:center; gap:var(--space-2); }
.timeline-date::before { content:''; width:8px; height:8px; border-radius:50%; background:var(--primary); flex-shrink:0; }
.timeline-line { position:relative; padding-left:20px; margin-left:3.5px; border-left:2px solid var(--separator); }
.timeline-item { background:var(--bg-card); border-radius:var(--radius-md); padding:var(--space-3); margin-bottom:var(--space-3); box-shadow:var(--shadow-sm); border:1px solid var(--separator); cursor:pointer; transition:all var(--transition-fast); position:relative; }
.timeline-item:active { transform:scale(0.98); }
.timeline-item-title { font:500 15px var(--font-family); color:var(--text-primary); margin-bottom:var(--space-1); }
.timeline-item-preview { font:var(--font-caption); color:var(--text-secondary); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.timeline-item-time { font:var(--font-micro); color:var(--text-tertiary); margin-top:var(--space-2); }
/* ========== Modal Overlay ========== */
.modal-overlay { position:fixed; inset:0; background:var(--overlay); z-index:200; display:flex; align-items:flex-end; justify-content:center; opacity:0; pointer-events:none; transition:opacity var(--transition-normal); }
.modal-overlay.active { opacity:1; pointer-events:auto; }
.modal-sheet { width:100%; max-width:430px; background:var(--bg-card); border-radius:var(--radius-xl) var(--radius-xl) 0 0; padding:var(--space-2) var(--space-4) var(--space-8); transform:translateY(100%); transition:transform var(--transition-spring); }
.modal-overlay.active .modal-sheet { transform:translateY(0); }
.modal-handle { width:36px; height:5px; border-radius:var(--radius-full); background:var(--text-tertiary); margin:0 auto var(--space-4); }
.modal-title { font:var(--font-large); color:var(--text-primary); text-align:center; margin-bottom:var(--space-5); }
.url-preview { background:var(--bg-tertiary); border-radius:var(--radius-md); padding:var(--space-3) var(--space-4); margin-bottom:var(--space-4); display:flex; align-items:center; gap:var(--space-2); border:1px solid var(--separator); }
.url-preview-label { font:var(--font-caption); color:var(--text-tertiary); flex-shrink:0; }
.url-preview-base { font:500 15px var(--font-family); color:var(--text-secondary); flex-shrink:0; }
.url-preview-key { font:600 15px var(--font-family); color:var(--primary); min-width:40px; border-bottom:2px solid var(--primary); padding-bottom:1px; }
.url-preview-key.empty { color:var(--text-tertiary); border-bottom-color:var(--text-tertiary); }
.input-group { margin-bottom:var(--space-4); }
.input-label { font:var(--font-caption); color:var(--text-secondary); margin-bottom:var(--space-2); display:block; }
.input-field { width:100%; padding:var(--space-3) var(--space-4); background:var(--bg-tertiary); border:1.5px solid var(--separator); border-radius:var(--radius-md); font:var(--font-body); color:var(--text-primary); outline:none; transition:border-color var(--transition-fast); }
.input-field:focus { border-color:var(--primary); }
.input-field.error { border-color:var(--destructive); }
.input-hint { font:var(--font-micro); color:var(--text-tertiary); margin-top:var(--space-1); }
.input-hint.error { color:var(--destructive); }
.input-counter { font:var(--font-micro); color:var(--text-tertiary); text-align:right; margin-top:var(--space-1); }
.modal-actions { display:flex; gap:var(--space-3); margin-top:var(--space-4); }
.btn { flex:1; padding:var(--space-3) var(--space-4); border-radius:var(--radius-md); font:500 16px var(--font-family); border:none; cursor:pointer; transition:all var(--transition-fast); }
.btn:active { transform:scale(0.97); }
.btn-secondary { background:var(--bg-tertiary); color:var(--text-primary); }
.btn-primary { background:var(--primary); color:white; }
.btn-primary:disabled { opacity:0.4; cursor:not-allowed; }
/* ========== Page Overlay (Settings / Editor) ========== */
.page-overlay { position:fixed; inset:0; z-index:150; background:var(--bg-primary); transform:translateX(100%); transition:transform var(--transition-normal); max-width:430px; margin:0 auto; display:flex; flex-direction:column; }
.page-overlay.active { transform:translateX(0); }
.settings-scroll { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; padding-bottom:34px; }
.settings-group { margin:var(--space-4); background:var(--bg-card); border-radius:var(--radius-md); overflow:hidden; box-shadow:var(--shadow-sm); }
.settings-group-title { font:var(--font-caption); color:var(--text-secondary); padding:var(--space-4) var(--space-4) var(--space-2); text-transform:uppercase; letter-spacing:0.5px; }
.settings-item { display:flex; align-items:center; justify-content:space-between; padding:var(--space-3) var(--space-4); min-height:44px; border-bottom:0.5px solid var(--separator); }
.settings-item:last-child { border-bottom:none; }
.settings-item-left { display:flex; align-items:center; gap:var(--space-3); }
.settings-item-icon { width:30px; height:30px; border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center; font-size:16px; flex-shrink:0; }
.settings-item-icon.blue { background:#0984E3; color:white; }
.settings-item-icon.green { background:#00B894; color:white; }
.settings-item-icon.orange { background:#E17055; color:white; }
.settings-item-icon.purple { background:#6C5CE7; color:white; }
.settings-item-icon.red { background:#FF3B30; color:white; }
.settings-item-icon.pink { background:#FD79A8; color:white; }
.settings-item-icon.teal { background:#00CEC9; color:white; }
.settings-item-icon.gray { background:#8E8E93; color:white; }
.settings-item-text { font:var(--font-body); color:var(--text-primary); }
.settings-item-desc { font:var(--font-micro); color:var(--text-tertiary); margin-top:1px; }
.settings-item-right { display:flex; align-items:center; gap:var(--space-2); flex-shrink:0; }
.settings-item-value { font:var(--font-caption); color:var(--text-tertiary); }
.toggle { width:51px; height:31px; border-radius:var(--radius-full); background:var(--bg-tertiary); position:relative; cursor:pointer; transition:background var(--transition-fast); border:none; flex-shrink:0; }
.toggle.active { background:var(--success); }
.toggle::after { content:''; position:absolute; width:27px; height:27px; border-radius:50%; background:white; top:2px; left:2px; box-shadow:0 1px 3px rgba(0,0,0,0.2); transition:transform var(--transition-fast); }
.toggle.active::after { transform:translateX(20px); }
.segmented { display:flex; background:var(--bg-tertiary); border-radius:var(--radius-sm); padding:2px; gap:2px; }
.segmented-btn { flex:1; padding:var(--space-1) var(--space-2); border:none; border-radius:6px; background:transparent; font:500 13px var(--font-family); color:var(--text-secondary); cursor:pointer; transition:all var(--transition-fast); white-space:nowrap; }
.segmented-btn.active { background:var(--bg-card); color:var(--text-primary); box-shadow:var(--shadow-sm); }
.quota-bar { margin:var(--space-2) var(--space-4) var(--space-1); background:var(--bg-tertiary); border-radius:var(--radius-full); height:6px; overflow:hidden; }
.quota-bar-fill { height:100%; border-radius:var(--radius-full); background:var(--success); transition:width var(--transition-normal); }
.quota-bar-fill.warning { background:var(--warning); }
.quota-bar-fill.danger { background:var(--destructive); }
.quota-text { font:var(--font-micro); color:var(--text-tertiary); text-align:right; padding:0 var(--space-4); }
.notice-section { margin:var(--space-4); padding:var(--space-4); background:var(--accent-light); border-radius:var(--radius-md); border-left:3px solid var(--primary); }
.notice-title { font:500 14px var(--font-family); color:var(--primary); margin-bottom:var(--space-2); display:flex; align-items:center; gap:var(--space-2); }
.notice-list { list-style:none; }
.notice-list li { font:var(--font-caption); color:var(--text-secondary); line-height:1.6; padding:var(--space-1) 0; position:relative; padding-left:14px; }
.notice-list li::before { content:'•'; position:absolute; left:0; color:var(--primary); }
.locked-item { opacity:0.5; pointer-events:none; }
.locked-badge { font:var(--font-micro); color:var(--text-tertiary); background:var(--bg-tertiary); padding:2px 8px; border-radius:var(--radius-full); }
/* ========== Sync Indicator ========== */
.sync-indicator { display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-4); font:var(--font-micro); color:var(--text-tertiary); }
.sync-dot { width:6px; height:6px; border-radius:50%; background:var(--success); }
.sync-dot.syncing { animation:pulse 1.5s infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
/* ========== Editor Page ========== */
.editor-page { position:fixed; inset:0; z-index:160; background:var(--bg-primary); transform:translateX(100%); transition:transform var(--transition-normal); max-width:430px; margin:0 auto; display:flex; flex-direction:column; }
.editor-page.active { transform:translateX(0); }
.editor-toolbar { display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-4); border-bottom:0.5px solid var(--separator); background:var(--glass-bg); backdrop-filter:blur(var(--glass-blur)); -webkit-backdrop-filter:blur(var(--glass-blur)); }
.editor-toolbar-btn { width:32px; height:32px; border-radius:var(--radius-sm); border:none; background:transparent; color:var(--text-secondary); display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:16px; transition:all var(--transition-fast); }
.editor-toolbar-btn:active { background:var(--accent-light); color:var(--primary); }
.editor-toolbar-btn.active { background:var(--accent-light); color:var(--primary); }
.editor-toolbar-divider { width:1px; height:20px; background:var(--separator); margin:0 2px; }
.editor-status { display:flex; align-items:center; justify-content:space-between; padding:var(--space-1) var(--space-4); font:var(--font-micro); color:var(--text-tertiary); border-bottom:0.5px solid var(--separator); background:var(--bg-card); }
.editor-status-left { display:flex; align-items:center; gap:var(--space-2); }
.editor-url-bar { display:flex; align-items:center; gap:var(--space-2); padding:var(--space-2) var(--space-4); background:var(--bg-tertiary); margin:var(--space-2) var(--space-4); border-radius:var(--radius-sm); font:var(--font-caption); }
.editor-url-base { color:var(--text-tertiary); }
.editor-url-key { color:var(--primary); font-weight:600; }
.editor-textarea { flex:1; width:100%; border:none; background:var(--bg-card); color:var(--text-primary); font:400 15px/1.7 var(--font-family); padding:var(--space-4); outline:none; resize:none; -webkit-overflow-scrolling:touch; }
.editor-textarea::placeholder { color:var(--text-tertiary); }
.editor-footer { display:flex; align-items:center; justify-content:space-between; padding:var(--space-2) var(--space-4); border-top:0.5px solid var(--separator); background:var(--glass-bg); backdrop-filter:blur(var(--glass-blur)); -webkit-backdrop-filter:blur(var(--glass-blur)); font:var(--font-micro); color:var(--text-tertiary); }
.editor-footer-left { display:flex; align-items:center; gap:var(--space-3); }
.editor-footer-right { display:flex; align-items:center; gap:var(--space-3); }
/* ========== Theme / Layout Switcher ========== */
.theme-switcher { position:fixed; top:50%; right:8px; transform:translateY(-50%); z-index:300; display:flex; flex-direction:column; gap:6px; }
.theme-dot { width:28px; height:28px; border-radius:50%; border:2px solid rgba(255,255,255,0.3); cursor:pointer; transition:all var(--transition-fast); box-shadow:var(--shadow-md); }
.theme-dot:active { transform:scale(0.9); }
.theme-dot.active { border-color:var(--primary); transform:scale(1.15); }
.layout-switcher { position:fixed; bottom:20px; right:8px; z-index:300; display:flex; flex-direction:column; gap:6px; }
.layout-btn { width:36px; height:36px; border-radius:var(--radius-sm); border:1px solid var(--separator); background:var(--glass-bg); backdrop-filter:blur(10px); color:var(--text-primary); display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:16px; transition:all var(--transition-fast); }
.layout-btn:active { transform:scale(0.9); }
.layout-btn.active { background:var(--primary); color:white; border-color:var(--primary); }
/* ========== Action Sheet ========== */
.action-sheet-overlay { position:fixed; inset:0; background:var(--overlay); z-index:250; display:flex; align-items:flex-end; justify-content:center; opacity:0; pointer-events:none; transition:opacity var(--transition-normal); }
.action-sheet-overlay.active { opacity:1; pointer-events:auto; }
.action-sheet { width:100%; max-width:430px; padding:0 var(--space-4) var(--space-8); transform:translateY(100%); transition:transform var(--transition-spring); }
.action-sheet-overlay.active .action-sheet { transform:translateY(0); }
.action-sheet-group { background:var(--bg-card); border-radius:var(--radius-md); overflow:hidden; margin-bottom:var(--space-2); }
.action-sheet-item { display:flex; align-items:center; justify-content:center; gap:var(--space-2); padding:var(--space-4); font:var(--font-body); color:var(--primary); cursor:pointer; border-bottom:0.5px solid var(--separator); transition:background var(--transition-fast); }
.action-sheet-item:last-child { border-bottom:none; }
.action-sheet-item:active { background:var(--accent-light); }
.action-sheet-item.destructive { color:var(--destructive); }
.action-sheet-cancel { background:var(--bg-card); border-radius:var(--radius-md); padding:var(--space-4); font:600 17px var(--font-family); color:var(--primary); text-align:center; cursor:pointer; }
.action-sheet-cancel:active { background:var(--accent-light); }
/* ========== Animations ========== */
@keyframes fadeInUp { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:translateY(0); } }
.animate-in { animation:fadeInUp 0.4s ease-out both; }
.animate-in:nth-child(1) { animation-delay:0.05s; }
.animate-in:nth-child(2) { animation-delay:0.1s; }
.animate-in:nth-child(3) { animation-delay:0.15s; }
.animate-in:nth-child(4) { animation-delay:0.2s; }
.animate-in:nth-child(5) { animation-delay:0.25s; }
.animate-in:nth-child(6) { animation-delay:0.3s; }
/* ========== Markdown Preview ========== */
.editor-preview { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:var(--space-4); background:var(--bg-card); display:none; }
.editor-preview.active { display:block; }
.editor-preview h1 { font:700 24px var(--font-family); color:var(--text-primary); margin:var(--space-4) 0 var(--space-2); }
.editor-preview h2 { font:600 20px var(--font-family); color:var(--text-primary); margin:var(--space-4) 0 var(--space-2); }
.editor-preview h3 { font:600 17px var(--font-family); color:var(--text-primary); margin:var(--space-3) 0 var(--space-2); }
.editor-preview p { font:var(--font-body); color:var(--text-primary); line-height:1.7; margin:var(--space-2) 0; }
.editor-preview ul, .editor-preview ol { padding-left:var(--space-5); margin:var(--space-2) 0; }
.editor-preview li { font:var(--font-body); color:var(--text-primary); line-height:1.7; }
.editor-preview code { font-family:'SF Mono', Menlo, monospace; background:var(--bg-tertiary); padding:2px 6px; border-radius:4px; font-size:13px; color:var(--primary); }
.editor-preview pre { background:var(--bg-tertiary); padding:var(--space-3); border-radius:var(--radius-md); overflow-x:auto; margin:var(--space-3) 0; }
.editor-preview pre code { background:transparent; padding:0; }
.editor-preview blockquote { border-left:3px solid var(--primary); padding-left:var(--space-3); margin:var(--space-3) 0; color:var(--text-secondary); }
.editor-preview strong { font-weight:600; }
.editor-preview em { font-style:italic; }
.editor-preview hr { border:none; border-top:1px solid var(--separator); margin:var(--space-4) 0; }
.nav-btn.preview-active { background:var(--primary); color:white; }
/* ========== Random Key Button ========== */
.key-input-row { display:flex; gap:var(--space-2); align-items:flex-start; }
.key-input-row .input-field { flex:1; }
.btn-random { width:44px; height:44px; border-radius:var(--radius-md); border:1.5px solid var(--separator); background:var(--bg-tertiary); color:var(--text-primary); font-size:20px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all var(--transition-fast); flex-shrink:0; margin-top:0; }
.btn-random:active { transform:scale(0.92); background:var(--accent-light); border-color:var(--primary); }
/* ========== Empty State ========== */
.empty-state { display:none; flex-direction:column; align-items:center; justify-content:center; padding:var(--space-8) var(--space-4); text-align:center; }
.empty-state.active { display:flex; }
.empty-state-icon { font-size:64px; margin-bottom:var(--space-5); opacity:0.8; }
.empty-state-title { font:600 20px var(--font-family); color:var(--text-primary); margin-bottom:var(--space-2); }
.empty-state-subtitle { font:var(--font-caption); color:var(--text-secondary); line-height:1.5; }
/* ========== Tag Badges ========== */
.note-card-tags { display:flex; flex-wrap:wrap; gap:4px; margin-top:var(--space-2); }
.tag-badge { font:500 10px var(--font-family); padding:2px 8px; border-radius:var(--radius-full); white-space:nowrap; }
.tag-badge.work { background:rgba(9,132,227,0.12); color:#0984E3; }
.tag-badge.diary { background:rgba(253,121,168,0.12); color:#FD79A8; }
.tag-badge.code { background:rgba(108,92,231,0.12); color:#6C5CE7; }
.tag-badge.life { background:rgba(0,184,148,0.12); color:#00B894; }
.tag-badge.idea { background:rgba(225,112,85,0.12); color:#E17055; }
.tag-badge.food { background:rgba(255,150,0,0.12); color:#FF9500; }
/* ========== Skeleton Loading ========== */
.skeleton-container { padding:0 var(--space-4); }
.skeleton-card { background:var(--bg-card); border-radius:var(--radius-md); padding:var(--space-4); border:1px solid var(--separator); margin-bottom:var(--space-3); position:relative; overflow:hidden; }
.skeleton-line { height:14px; border-radius:7px; background:var(--bg-tertiary); margin-bottom:var(--space-2); }
.skeleton-line.short { width:40%; }
.skeleton-line.medium { width:70%; }
.skeleton-line.long { width:90%; }
.skeleton-line.title { height:18px; width:50%; border-radius:9px; margin-bottom:var(--space-3); }
.skeleton-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:var(--space-3); }
.skeleton-card::after { content:''; position:absolute; inset:0; background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.4) 50%, transparent 100%); animation:shimmer 1.5s infinite; }
[data-theme="dark"] .skeleton-card::after, [data-theme="amoled"] .skeleton-card::after { background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%); }
@keyframes shimmer { 0% { transform:translateX(-100%); } 100% { transform:translateX(100%); } }
/* ========== Enhanced Card Animations ========== */
.note-card.animate-in { animation:cardBounceIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; animation-delay:calc(var(--card-index, 0) * 0.08s); }
@keyframes cardBounceIn { 0% { opacity:0; transform:scale(0.85) translateY(16px); } 60% { opacity:1; transform:scale(1.03) translateY(-2px); } 100% { opacity:1; transform:scale(1) translateY(0); } }
/* ========== Page Transition Enhancement ========== */
.app-shell.editor-open { transform:scale(0.96); filter:brightness(0.95); transition:transform 0.4s cubic-bezier(0.25,0.1,0.25,1), filter 0.4s cubic-bezier(0.25,0.1,0.25,1); }
.editor-page.active { box-shadow:-8px 0 30px rgba(0,0,0,0.15); }
/* ========== Button Haptic Feedback ========== */
.nav-btn, .btn, .btn-random, .editor-toolbar-btn, .layout-btn, .action-sheet-item, .action-sheet-cancel { transition:transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), background var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast); }
.nav-btn:active, .btn:active, .btn-random:active, .editor-toolbar-btn:active, .layout-btn:active, .action-sheet-item:active, .action-sheet-cancel:active { transform:scale(0.92); }
/* ========== Pull-to-Refresh Circular Progress ========== */
.pull-progress { width:24px; height:24px; border-radius:50%; border:2.5px solid var(--separator); border-top-color:var(--primary); display:none; }
.pull-indicator.refreshing .pull-progress { display:block; animation:spin 0.8s linear infinite; }
.pull-indicator.refreshing .pull-icon { display:none; }
/* ========== Red Dot Appear Animation ========== */
.red-dot, .red-dot-sm { animation:dotAppear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both, redPulse 2s 0.4s infinite; }
@keyframes dotAppear { from { transform:scale(0); } to { transform:scale(1); } }
</style>
</head>
<body data-theme="light" data-accent="purple">
<!-- ========== 主页面:笔记仓库列表 ========== -->
<div class="app-shell" id="mainPage">
<div class="nav-bar">
<div class="nav-bar-content">
<div class="nav-bar-left">
<button class="nav-btn" onclick="showToast('返回')"></button>
<span class="nav-bar-title">📝 笔记仓库</span>
</div>
<div class="nav-bar-right">
<button class="nav-btn" onclick="openSettings()" title="设置"></button>
<button class="nav-btn" onclick="openAddSheet()" title="添加">+</button>
</div>
</div>
</div>
<div class="sync-indicator">
<div class="sync-dot syncing"></div>
<span>已同步 · 刚刚</span>
</div>
<div class="search-bar">
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input class="search-input" type="text" placeholder="搜索笔记仓库...">
</div>
</div>
<div class="main-content" id="mainContent">
<!-- Pull to Refresh Indicator -->
<div class="pull-indicator" id="pullIndicator">
<span class="pull-icon">🔄</span>
<span id="pullText">下拉刷新</span>
</div>
<!-- Grid View (Default) -->
<div id="gridView">
<div class="section-header">
<span class="section-title">⭐ 主要 <span class="red-dot-sm"></span></span>
<span class="section-count">2</span>
</div>
<div class="notes-grid">
<div class="note-card animate-in" onclick="openEditor('my-diary')">
<div class="red-dot"></div>
<span class="note-card-star"></span>
<div class="note-card-key"><span class="emoji">📓</span> my-diary</div>
<div class="note-card-preview">今天天气很好,适合出去散步。下午去了公园,看到了很多花...</div>
<div class="note-card-meta">
<span class="note-card-time">3分钟前</span>
<span class="note-card-badge">1.2KB</span>
</div>
</div>
<div class="note-card animate-in" onclick="openEditor('work-notes')">
<span class="note-card-star"></span>
<div class="note-card-key"><span class="emoji">💼</span> work-notes</div>
<div class="note-card-preview">项目进度前端开发完成80%,后端接口联调中...</div>
<div class="note-card-meta">
<span class="note-card-time">1小时前</span>
<span class="note-card-badge">856B</span>
</div>
</div>
</div>
<div class="section-header" style="margin-top:8px;">
<span class="section-title">📋 其他</span>
<span class="section-count">4</span>
</div>
<div class="notes-grid">
<div class="note-card animate-in" onclick="openEditor('todo-list')">
<div class="red-dot"></div>
<div class="note-card-key"><span class="emoji"></span> todo-list</div>
<div class="note-card-preview">- 完成周报\n- 提交代码review\n- 准备会议材料</div>
<div class="note-card-meta">
<span class="note-card-time">昨天</span>
<span class="note-card-badge">320B</span>
</div>
</div>
<div class="note-card animate-in" onclick="openEditor('recipes')">
<div class="note-card-key"><span class="emoji">🍳</span> recipes</div>
<div class="note-card-preview">番茄炒蛋鸡蛋3个番茄2个盐适量...</div>
<div class="note-card-meta">
<span class="note-card-time">3天前</span>
<span class="note-card-badge">512B</span>
</div>
</div>
<div class="note-card animate-in" onclick="openEditor('code-snippets')">
<div class="note-card-key"><span class="emoji">💻</span> code-snippets</div>
<div class="note-card-preview">flutter pub run build_runner build --delete-conflicting-outputs</div>
<div class="note-card-meta">
<span class="note-card-time">上周</span>
<span class="note-card-badge">2.1KB</span>
</div>
</div>
<div class="note-card animate-in" onclick="openEditor('shared-ideas')">
<div class="note-card-key"><span class="emoji">💡</span> shared-ideas</div>
<div class="note-card-preview">新功能想法支持Markdown渲染添加标签系统...</div>
<div class="note-card-meta">
<span class="note-card-time">2周前</span>
<span class="note-card-badge">1.5KB</span>
</div>
</div>
</div>
</div>
<!-- List View (with swipe) -->
<div id="listView" style="display:none;">
<div class="section-header">
<span class="section-title">⭐ 主要 <span class="red-dot-sm"></span></span>
<span class="section-count">2</span>
</div>
<div class="notes-list">
<!-- Swipeable Item 1 -->
<div class="swipe-container" data-key="my-diary">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action more" onclick="openActionSheet('my-diary')"><span class="action-icon">•••</span>更多</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('my-diary')">
<div class="note-list-icon">📓<div class="red-dot" style="top:-2px;right:-2px;"></div></div>
<div class="note-list-content">
<div class="note-list-title">my-diary <span class="red-dot-sm"></span></div>
<div class="note-list-subtitle">今天天气很好,适合出去散步。下午去了公园...</div>
</div>
<div class="note-list-right">
<span class="note-list-time">3分钟前</span>
<span class="note-list-badge">⭐ 主要</span>
</div>
</div>
</div>
</div>
<!-- Swipeable Item 2 -->
<div class="swipe-container" data-key="work-notes">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action more" onclick="openActionSheet('work-notes')"><span class="action-icon">•••</span>更多</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('work-notes')">
<div class="note-list-icon">💼</div>
<div class="note-list-content">
<div class="note-list-title">work-notes</div>
<div class="note-list-subtitle">项目进度前端开发完成80%,后端接口联调中...</div>
</div>
<div class="note-list-right">
<span class="note-list-time">1小时前</span>
<span class="note-list-badge">⭐ 主要</span>
</div>
</div>
</div>
</div>
</div>
<div class="section-header">
<span class="section-title">📋 其他</span>
<span class="section-count">4</span>
</div>
<div class="notes-list">
<div class="swipe-container" data-key="todo-list">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action delete" onclick="showToast('已删除')"><span class="action-icon">🗑</span>删除</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('todo-list')">
<div class="note-list-icon"><div class="red-dot" style="top:-2px;right:-2px;"></div></div>
<div class="note-list-content">
<div class="note-list-title">todo-list <span class="red-dot-sm"></span></div>
<div class="note-list-subtitle">- 完成周报 - 提交代码review - 准备会议材料</div>
</div>
<div class="note-list-right">
<span class="note-list-time">昨天</span>
</div>
</div>
</div>
</div>
<div class="swipe-container" data-key="recipes">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action delete" onclick="showToast('已删除')"><span class="action-icon">🗑</span>删除</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('recipes')">
<div class="note-list-icon">🍳</div>
<div class="note-list-content">
<div class="note-list-title">recipes</div>
<div class="note-list-subtitle">番茄炒蛋鸡蛋3个番茄2个盐适量...</div>
</div>
<div class="note-list-right">
<span class="note-list-time">3天前</span>
</div>
</div>
</div>
</div>
<div class="swipe-container" data-key="code-snippets">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action delete" onclick="showToast('已删除')"><span class="action-icon">🗑</span>删除</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('code-snippets')">
<div class="note-list-icon">💻</div>
<div class="note-list-content">
<div class="note-list-title">code-snippets</div>
<div class="note-list-subtitle">flutter pub run build_runner build...</div>
</div>
<div class="note-list-right">
<span class="note-list-time">上周</span>
</div>
</div>
</div>
</div>
<div class="swipe-container" data-key="shared-ideas">
<div class="swipe-actions right">
<button class="swipe-action pin" onclick="showToast('已置顶')"><span class="action-icon">📌</span>置顶</button>
<button class="swipe-action star" onclick="showToast('已标星')"><span class="action-icon"></span>主要</button>
<button class="swipe-action share" onclick="showToast('已复制链接')"><span class="action-icon">🔗</span>分享</button>
<button class="swipe-action delete" onclick="showToast('已删除')"><span class="action-icon">🗑</span>删除</button>
</div>
<div class="swipe-content" ontouchstart="handleTouchStart(event)" ontouchmove="handleTouchMove(event)" ontouchend="handleTouchEnd(event)">
<div class="note-list-item" onclick="openEditor('shared-ideas')">
<div class="note-list-icon">💡</div>
<div class="note-list-content">
<div class="note-list-title">shared-ideas</div>
<div class="note-list-subtitle">新功能想法支持Markdown渲染...</div>
</div>
<div class="note-list-right">
<span class="note-list-time">2周前</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Timeline View -->
<div id="timelineView" style="display:none;">
<div class="notes-timeline">
<div class="timeline-date">今天</div>
<div class="timeline-line">
<div class="timeline-item" onclick="openEditor('my-diary')">
<div class="red-dot" style="top:8px;right:8px;"></div>
<div class="timeline-item-title">📓 my-diary</div>
<div class="timeline-item-preview">今天天气很好,适合出去散步。下午去了公园,看到了很多花...</div>
<div class="timeline-item-time">3分钟前 · 1.2KB · 有变更</div>
</div>
<div class="timeline-item" onclick="openEditor('work-notes')">
<div class="timeline-item-title">💼 work-notes</div>
<div class="timeline-item-preview">项目进度前端开发完成80%,后端接口联调中...</div>
<div class="timeline-item-time">1小时前 · 856B</div>
</div>
</div>
<div class="timeline-date">昨天</div>
<div class="timeline-line">
<div class="timeline-item" onclick="openEditor('todo-list')">
<div class="red-dot" style="top:8px;right:8px;"></div>
<div class="timeline-item-title">✅ todo-list</div>
<div class="timeline-item-preview">- 完成周报 - 提交代码review - 准备会议材料</div>
<div class="timeline-item-time">昨天 · 320B · 有变更</div>
</div>
</div>
<div class="timeline-date">更早</div>
<div class="timeline-line">
<div class="timeline-item" onclick="openEditor('recipes')">
<div class="timeline-item-title">🍳 recipes</div>
<div class="timeline-item-preview">番茄炒蛋鸡蛋3个番茄2个盐适量...</div>
<div class="timeline-item-time">3天前 · 512B</div>
</div>
<div class="timeline-item" onclick="openEditor('code-snippets')">
<div class="timeline-item-title">💻 code-snippets</div>
<div class="timeline-item-preview">flutter pub run build_runner build --delete-conflicting-outputs</div>
<div class="timeline-item-time">上周 · 2.1KB</div>
</div>
<div class="timeline-item" onclick="openEditor('shared-ideas')">
<div class="timeline-item-title">💡 shared-ideas</div>
<div class="timeline-item-preview">新功能想法支持Markdown渲染添加标签系统...</div>
<div class="timeline-item-time">2周前 · 1.5KB</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ========== 添加笔记 ========== -->
<div class="modal-overlay" id="addSheet">
<div class="modal-sheet">
<div class="modal-handle"></div>
<div class="modal-title">📝 新建笔记</div>
<div class="url-preview">
<span class="url-preview-label">预览</span>
<span class="url-preview-base">ctc.s2ss.com/</span>
<span class="url-preview-key empty" id="urlKeyPreview">钥匙</span>
</div>
<div class="input-group">
<label class="input-label">🔑 钥匙(唯一标识)</label>
<input class="input-field" id="keyInput" type="text" placeholder="输入2-16位数字或字符" maxlength="16" autocomplete="off" oninput="validateKey()">
<div class="input-hint" id="keyHint">仅支持数字和字母2-16位</div>
<div class="input-counter"><span id="keyCount">0</span>/16</div>
</div>
<div class="input-group">
<label class="input-label">📄 初始内容(可选)</label>
<textarea class="input-field" id="contentInput" rows="3" placeholder="输入笔记内容..." style="resize:none; min-height:80px;" oninput="updateContentCount()"></textarea>
<div class="input-counter"><span id="contentCount">0</span>/10000</div>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeAddSheet()">取消</button>
<button class="btn btn-primary" id="createBtn" disabled onclick="createNote()">创建</button>
</div>
</div>
</div>
<!-- ========== Action Sheet (更多操作) ========== -->
<div class="action-sheet-overlay" id="actionSheetOverlay" onclick="closeActionSheet()">
<div class="action-sheet" id="actionSheet">
<div class="action-sheet-group">
<div class="action-sheet-item" onclick="showToast('编辑笔记'); closeActionSheet();">✏️ 编辑内容</div>
<div class="action-sheet-item" onclick="showToast('重命名钥匙'); closeActionSheet();">🔑 重命名</div>
<div class="action-sheet-item" onclick="showToast('查看历史'); closeActionSheet();">📜 查看历史</div>
<div class="action-sheet-item" onclick="showToast('导出笔记'); closeActionSheet();">📤 导出</div>
<div class="action-sheet-item" onclick="showToast('显示二维码'); closeActionSheet();">📱 二维码</div>
</div>
<div class="action-sheet-group">
<div class="action-sheet-item destructive" onclick="showToast('已删除'); closeActionSheet();">🗑 删除笔记</div>
</div>
<div class="action-sheet-cancel" onclick="closeActionSheet()">取消</div>
</div>
</div>
<!-- ========== 设置页面 ========== -->
<div class="page-overlay" id="settingsPage">
<div class="nav-bar">
<div class="nav-bar-content">
<div class="nav-bar-left">
<button class="nav-btn" onclick="closeSettings()"></button>
<span class="nav-bar-title">⚙️ 笔记仓库设置</span>
</div>
</div>
</div>
<div class="settings-scroll">
<div class="settings-group-title" style="padding-left:16px;">🔄 同步设置</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-icon blue">🔆</div>
<div><div class="settings-item-text">屏幕常亮</div><div class="settings-item-desc">编辑笔记时保持屏幕不熄灭</div></div>
</div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-icon green">🔄</div>
<div><div class="settings-item-text">自动同步</div><div class="settings-item-desc">每天限制 5000 次(含上传/下载/同步)</div></div>
</div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
</div>
<div class="quota-bar"><div class="quota-bar-fill" style="width:12%;"></div></div>
<div class="quota-text">今日剩余 4,380 次</div>
<div class="settings-group-title" style="padding-left:16px;">⚡ 同步选项</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon teal">⬇️</div><div class="settings-item-text">拉取同步</div></div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon orange">⬆️</div><div class="settings-item-text">推送同步</div></div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon purple">🔀</div><div><div class="settings-item-text">合并策略</div><div class="settings-item-desc">冲突时自动合并超过1万字符截断</div></div></div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon blue"></div><div class="settings-item-text">同步频率</div></div>
<div class="settings-item-right"><div class="segmented"><button class="segmented-btn active" onclick="selectSegment(this)">5秒</button><button class="segmented-btn" onclick="selectSegment(this)">10秒</button></div></div>
</div>
</div>
<div class="settings-group-title" style="padding-left:16px;">📜 历史记录</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon green">📋</div><div class="settings-item-text">开启历史记录</div></div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item" onclick="showToast('查看历史记录')">
<div class="settings-item-left"><div class="settings-item-icon gray">🕐</div><div class="settings-item-text">显示历史记录</div></div>
<div class="settings-item-right"><span class="settings-item-value">3 条 </span></div>
</div>
</div>
<div class="settings-group-title" style="padding-left:16px;">🎨 排版样式</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon purple">🖼</div><div class="settings-item-text">列表样式</div></div>
<div class="settings-item-right"><div class="segmented"><button class="segmented-btn active" onclick="switchLayout('grid',this)">网格</button><button class="segmented-btn" onclick="switchLayout('list',this)">列表</button><button class="segmented-btn" onclick="switchLayout('timeline',this)">时间线</button></div></div>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon pink"></div><div><div class="settings-item-text">设为主要</div><div class="settings-item-desc">主要笔记长存在顶部</div></div></div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
</div>
<div class="settings-group-title" style="padding-left:16px;">📎 元数据</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon orange">🕐</div><div class="settings-item-text">添加时间</div></div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon green">📍</div><div class="settings-item-text">添加地点</div></div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon blue">📱</div><div class="settings-item-text">添加机型</div></div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
</div>
<div class="settings-group-title" style="padding-left:16px;">🔒 安全</div>
<div class="settings-group">
<div class="settings-item locked-item">
<div class="settings-item-left"><div class="settings-item-icon red">🔐</div><div><div class="settings-item-text">加密锁定</div><div class="settings-item-desc">后续开发</div></div></div>
<span class="locked-badge">即将推出</span>
</div>
</div>
<div class="settings-group-title" style="padding-left:16px;">👁 显示</div>
<div class="settings-group">
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon teal">#️⃣</div><div class="settings-item-text">显示行列</div></div>
<button class="toggle active" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item">
<div class="settings-item-left"><div class="settings-item-icon purple">📱</div><div class="settings-item-text">显示二维码</div></div>
<button class="toggle" onclick="this.classList.toggle('active')"></button>
</div>
<div class="settings-item locked-item">
<div class="settings-item-left"><div class="settings-item-icon gray">🔍</div><div><div class="settings-item-text">搜索仓库内容</div><div class="settings-item-desc">所在用户组无权限</div></div></div>
<span class="locked-badge">🔒 无权限</span>
</div>
</div>
<!-- 使用须知 - 10条 -->
<div class="notice-section">
<div class="notice-title">⚠️ 使用须知</div>
<ul class="notice-list">
<li>简单钥匙(如 123、abc可能被他人修改建议使用复杂钥匙</li>
<li>部分违规内容会被系统自动清空</li>
<li>超过1年未修改的笔记将被自动清空</li>
<li>单条笔记大小限制 1MB</li>
<li>请勿存储敏感信息(密码、密钥等)</li>
<li>数据通过 HTTPS 传输,但存储无加密</li>
<li>同一钥匙可被任何人访问和修改</li>
<li>每日API调用限额 5000 次,超出后暂停同步</li>
<li>笔记内容不做备份,删除后无法恢复</li>
<li>服务仅供临时记录,不保证数据持久性和完整性</li>
</ul>
</div>
</div>
</div>
<!-- ========== 笔记编辑页面 ========== -->
<div class="editor-page" id="editorPage">
<div class="nav-bar">
<div class="nav-bar-content">
<div class="nav-bar-left">
<button class="nav-btn" onclick="closeEditor()"></button>
<span class="nav-bar-title" id="editorTitle">📓 my-diary</span>
</div>
<div class="nav-bar-right">
<button class="nav-btn" onclick="showToast('更多操作')" title="更多">•••</button>
</div>
</div>
</div>
<!-- URL Bar -->
<div class="editor-url-bar">
<span class="editor-url-base">ctc.s2ss.com/</span>
<span class="editor-url-key" id="editorUrlKey">my-diary</span>
</div>
<!-- Toolbar -->
<div class="editor-toolbar">
<button class="editor-toolbar-btn" onclick="showToast('撤销')" title="撤销">↩️</button>
<button class="editor-toolbar-btn" onclick="showToast('重做')" title="重做">↪️</button>
<div class="editor-toolbar-divider"></div>
<button class="editor-toolbar-btn" onclick="showToast('剪切')" title="剪切">✂️</button>
<button class="editor-toolbar-btn" onclick="showToast('复制')" title="复制">📋</button>
<button class="editor-toolbar-btn" onclick="showToast('粘贴')" title="粘贴">📄</button>
<div class="editor-toolbar-divider"></div>
<button class="editor-toolbar-btn" onclick="showToast('分享')" title="分享">🔗</button>
<button class="editor-toolbar-btn" onclick="showToast('二维码')" title="二维码">📱</button>
<div class="editor-toolbar-divider"></div>
<button class="editor-toolbar-btn" onclick="showToast('全屏')" title="全屏"></button>
</div>
<!-- Status Bar -->
<div class="editor-status">
<div class="editor-status-left">
<span>行 1, 列 1</span>
</div>
<div class="editor-status-left">
<span id="editorCharCount">128 字符</span>
<span>·</span>
<span id="editorLineCount">5 行</span>
</div>
</div>
<!-- Editor Area -->
<textarea class="editor-textarea" id="editorTextarea" placeholder="开始输入..." oninput="updateEditorStats()">今天天气很好,适合出去散步。
下午去了公园,看到了很多花。
樱花开了,粉色的花瓣飘落在小径上。
明天计划:
- 上午写代码
- 下午去图书馆
- 晚上看电影</textarea>
<!-- Footer -->
<div class="editor-footer">
<div class="editor-footer-left">
<span>📍 北京</span>
<span>·</span>
<span>📱 iPhone 16 Pro</span>
</div>
<div class="editor-footer-right">
<div class="sync-dot syncing"></div>
<span>自动同步中</span>
</div>
</div>
</div>
<!-- ========== Theme Switcher ========== -->
<div class="theme-switcher">
<div class="theme-dot active" style="background:#F2F2F7;" onclick="setTheme('light')" title="浅色"></div>
<div class="theme-dot" style="background:#1C1C1E;" onclick="setTheme('dark')" title="深色"></div>
<div class="theme-dot" style="background:#000000;" onclick="setTheme('amoled')" title="AMOLED"></div>
<div style="height:8px;"></div>
<div class="theme-dot" style="background:#6C5CE7;" onclick="setAccent('purple')" title="薰衣草"></div>
<div class="theme-dot" style="background:#FF6B6B;" onclick="setAccent('coral')" title="珊瑚红"></div>
<div class="theme-dot" style="background:#00B894;" onclick="setAccent('teal')" title="薄荷绿"></div>
<div class="theme-dot" style="background:#0984E3;" onclick="setAccent('blue')" title="天空蓝"></div>
<div class="theme-dot" style="background:#E17055;" onclick="setAccent('orange')" title="暖阳橙"></div>
<div class="theme-dot" style="background:#FD79A8;" onclick="setAccent('pink')" title="樱花粉"></div>
</div>
<!-- ========== Layout Switcher ========== -->
<div class="layout-switcher">
<button class="layout-btn active" onclick="switchLayout('grid',this)" title="网格"></button>
<button class="layout-btn" onclick="switchLayout('list',this)" title="列表"></button>
<button class="layout-btn" onclick="switchLayout('timeline',this)" title="时间线"></button>
</div>
<!-- ========== Toast ========== -->
<div id="toast" style="position:fixed; bottom:80px; left:50%; transform:translateX(-50%) translateY(20px); background:var(--glass-bg); backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px); color:var(--text-primary); padding:10px 20px; border-radius:20px; font:500 14px var(--font-family); box-shadow:var(--shadow-lg); opacity:0; transition:all 0.3s ease; z-index:999; pointer-events:none; border:0.5px solid var(--separator);"></div>
<script>
// ========== CTC API ==========
const API_BASE = 'https://ctc.s2ss.com';
// 本地笔记列表存储在localStorage
let noteList = JSON.parse(localStorage.getItem('ctc_notes') || '[]');
// 笔记变更记录mtime快照
let noteMtimes = JSON.parse(localStorage.getItem('ctc_mtimes') || '{}');
function saveNoteList() { localStorage.setItem('ctc_notes', JSON.stringify(noteList)); }
function saveMtimes() { localStorage.setItem('ctc_mtimes', JSON.stringify(noteMtimes)); }
// API: 读取笔记
async function apiRead(key) {
const resp = await fetch(`${API_BASE}/${key}?raw`);
if (!resp.ok) return null;
return await resp.text();
}
// API: 写入笔记
async function apiWrite(key, text) {
const resp = await fetch(`${API_BASE}/${key}?json`, { method: 'POST', body: `text=${encodeURIComponent(text)}`, headers: {'Content-Type':'application/x-www-form-urlencoded'} });
return await resp.json();
}
// API: 获取笔记信息
async function apiInfo(key) {
const resp = await fetch(`${API_BASE}/?info&note=${key}`);
if (!resp.ok) return null;
return await resp.json();
}
// API: 批量检查变更
async function apiCheck(keys) {
if (!keys.length) return {};
const resp = await fetch(`${API_BASE}/?check&keys=${keys.join(',')}`);
return await resp.json();
}
// API: 删除笔记
async function apiDelete(key) {
const resp = await fetch(`${API_BASE}/?delete&note=${key}`);
return await resp.json();
}
// API: 新建随机笔记
async function apiNew(text) {
const resp = await fetch(`${API_BASE}/?new&json&text=${encodeURIComponent(text)}`);
return await resp.json();
}
// ========== Theme ==========
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-switcher .theme-dot').forEach((d, i) => {
d.classList.toggle('active', i === ['light','dark','amoled'].indexOf(theme));
});
}
function setAccent(accent) { document.body.setAttribute('data-accent', accent); }
// ========== Layout Switch ==========
function switchLayout(mode, btn) {
document.getElementById('gridView').style.display = mode === 'grid' ? '' : 'none';
document.getElementById('listView').style.display = mode === 'list' ? '' : 'none';
document.getElementById('timelineView').style.display = mode === 'timeline' ? '' : 'none';
document.querySelectorAll('.layout-switcher .layout-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
}
// ========== Pull to Refresh (接入真实API) ==========
let pullStartY = 0, isPulling = false, isRefreshing = false;
const mainContent = document.getElementById('mainContent');
const pullIndicator = document.getElementById('pullIndicator');
mainContent.addEventListener('touchstart', function(e) {
if (mainContent.scrollTop === 0 && !isRefreshing) {
pullStartY = e.touches[0].clientY;
isPulling = true;
}
});
mainContent.addEventListener('touchmove', function(e) {
if (!isPulling || isRefreshing) return;
const diff = e.touches[0].clientY - pullStartY;
if (diff > 0 && mainContent.scrollTop === 0) {
pullIndicator.style.padding = Math.min(diff / 2, 60) + 'px 0';
pullIndicator.style.opacity = Math.min(diff / 80, 1);
if (diff > 60) {
pullIndicator.classList.add('pulling');
document.getElementById('pullText').textContent = '释放刷新';
} else {
pullIndicator.classList.remove('pulling');
document.getElementById('pullText').textContent = '下拉刷新';
}
}
});
mainContent.addEventListener('touchend', function() {
if (!isPulling) return;
isPulling = false;
if (pullIndicator.classList.contains('pulling')) {
isRefreshing = true;
pullIndicator.classList.add('refreshing');
pullIndicator.classList.remove('pulling');
document.getElementById('pullText').textContent = '刷新中...';
doRefresh();
} else {
pullIndicator.style.padding = '12px 0';
pullIndicator.style.opacity = '0.5';
}
});
async function doRefresh() {
try {
const keys = noteList.map(n => n.key);
if (keys.length === 0) {
finishRefresh('暂无笔记');
return;
}
const result = await apiCheck(keys);
let changed = 0;
if (result.code === 1 && result.data) {
for (const [key, info] of Object.entries(result.data)) {
if (info && noteMtimes[key] && info.mtime > noteMtimes[key]) {
changed++;
}
if (info) noteMtimes[key] = info.mtime;
}
saveMtimes();
}
finishRefresh(changed > 0 ? `检测到 ${changed} 条变更` : '已刷新,无变更');
} catch(e) {
finishRefresh('刷新失败: ' + e.message);
}
}
function finishRefresh(msg) {
pullIndicator.classList.remove('refreshing');
document.getElementById('pullText').textContent = '下拉刷新';
pullIndicator.style.padding = '12px 0';
pullIndicator.style.opacity = '0.5';
isRefreshing = false;
showToast(msg);
}
// ========== Swipe Actions (列表模式) ==========
let swipeStartX = 0, swipeCurrentX = 0, currentSwipeEl = null;
function handleTouchStart(e) {
const container = e.currentTarget.closest('.swipe-container');
if (currentSwipeEl && currentSwipeEl !== container) { resetSwipe(currentSwipeEl); }
currentSwipeEl = container;
swipeStartX = e.touches[0].clientX;
swipeCurrentX = 0;
}
function handleTouchMove(e) {
if (!currentSwipeEl) return;
const diff = swipeStartX - e.touches[0].clientX;
swipeCurrentX = Math.max(0, Math.min(diff, 260));
const content = currentSwipeEl.querySelector('.swipe-content');
content.style.transform = 'translateX(-' + swipeCurrentX + 'px)';
content.style.transition = 'none';
}
function handleTouchEnd(e) {
if (!currentSwipeEl) return;
const content = currentSwipeEl.querySelector('.swipe-content');
content.style.transition = '';
if (swipeCurrentX > 80) { content.style.transform = 'translateX(-260px)'; }
else { content.style.transform = 'translateX(0)'; currentSwipeEl = null; }
}
function resetSwipe(container) {
const content = container.querySelector('.swipe-content');
content.style.transition = '';
content.style.transform = 'translateX(0)';
}
document.addEventListener('click', function(e) {
if (currentSwipeEl && !e.target.closest('.swipe-container')) { resetSwipe(currentSwipeEl); currentSwipeEl = null; }
});
// ========== Action Sheet ==========
function openActionSheet(key) { document.getElementById('actionSheetOverlay').classList.add('active'); }
function closeActionSheet() { document.getElementById('actionSheetOverlay').classList.remove('active'); }
// ========== Add Sheet (接入API) ==========
function openAddSheet() {
document.getElementById('addSheet').classList.add('active');
setTimeout(() => document.getElementById('keyInput').focus(), 400);
}
function closeAddSheet() {
document.getElementById('addSheet').classList.remove('active');
document.getElementById('keyInput').value = '';
document.getElementById('contentInput').value = '';
document.getElementById('keyCount').textContent = '0';
document.getElementById('contentCount').textContent = '0';
document.getElementById('urlKeyPreview').textContent = '钥匙';
document.getElementById('urlKeyPreview').classList.add('empty');
document.getElementById('createBtn').disabled = true;
document.getElementById('keyHint').textContent = '仅支持数字和字母2-16位';
document.getElementById('keyHint').classList.remove('error');
document.getElementById('keyInput').classList.remove('error');
}
function validateKey() {
const input = document.getElementById('keyInput');
const hint = document.getElementById('keyHint');
const preview = document.getElementById('urlKeyPreview');
const counter = document.getElementById('keyCount');
const btn = document.getElementById('createBtn');
let val = input.value.replace(/[^a-zA-Z0-9]/g, '');
if (val !== input.value) input.value = val;
counter.textContent = val.length;
preview.textContent = val || '钥匙';
preview.classList.toggle('empty', !val);
if (val.length > 0 && val.length < 2) {
hint.textContent = '钥匙至少需要2个字符'; hint.classList.add('error'); input.classList.add('error'); btn.disabled = true;
} else if (val.length >= 2) {
hint.textContent = '仅支持数字和字母2-16位'; hint.classList.remove('error'); input.classList.remove('error'); btn.disabled = false;
} else {
hint.textContent = '仅支持数字和字母2-16位'; hint.classList.remove('error'); input.classList.remove('error'); btn.disabled = true;
}
}
function updateContentCount() { document.getElementById('contentCount').textContent = document.getElementById('contentInput').value.length; }
async function createNote() {
const key = document.getElementById('keyInput').value;
const content = document.getElementById('contentInput').value;
const btn = document.getElementById('createBtn');
btn.disabled = true;
btn.textContent = '创建中...';
try {
const result = await apiWrite(key, content);
if (result.code === 1) {
// 添加到本地列表
noteList.push({ key, star: false, addedAt: Date.now() });
if (result.data) noteMtimes[key] = result.data.mtime;
saveNoteList();
saveMtimes();
showToast('创建成功: ctc.s2ss.com/' + key);
closeAddSheet();
} else {
showToast('创建失败: ' + (result.msg || '未知错误'));
btn.disabled = false;
btn.textContent = '创建';
}
} catch(e) {
showToast('网络错误: ' + e.message);
btn.disabled = false;
btn.textContent = '创建';
}
}
// ========== Settings ==========
function openSettings() { document.getElementById('settingsPage').classList.add('active'); }
function closeSettings() { document.getElementById('settingsPage').classList.remove('active'); }
function selectSegment(btn) { btn.parentElement.querySelectorAll('.segmented-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }
// ========== Editor (接入API) ==========
let editorKey = '';
let editorSaveTimer = null;
async function openEditor(key) {
editorKey = key;
document.getElementById('editorTitle').textContent = '📓 ' + key;
document.getElementById('editorUrlKey').textContent = key;
document.getElementById('editorPage').classList.add('active');
// 从API加载内容
const ta = document.getElementById('editorTextarea');
ta.value = '加载中...';
ta.disabled = true;
try {
const content = await apiRead(key);
ta.value = content || '';
} catch(e) {
ta.value = '加载失败: ' + e.message;
}
ta.disabled = false;
updateEditorStats();
}
function closeEditor() {
document.getElementById('editorPage').classList.remove('active');
if (editorSaveTimer) { clearTimeout(editorSaveTimer); editorSaveTimer = null; }
editorKey = '';
}
function updateEditorStats() {
const ta = document.getElementById('editorTextarea');
const text = ta.value;
const chars = text.length;
const lines = text.split('\n').length;
document.getElementById('editorCharCount').textContent = chars + ' 字符';
document.getElementById('editorLineCount').textContent = lines + ' 行';
}
// 自动保存2秒防抖
document.getElementById('editorTextarea').addEventListener('input', function() {
updateEditorStats();
if (editorSaveTimer) clearTimeout(editorSaveTimer);
editorSaveTimer = setTimeout(async () => {
if (!editorKey) return;
const text = document.getElementById('editorTextarea').value;
try {
const result = await apiWrite(editorKey, text);
if (result.code === 1 && result.data) {
noteMtimes[editorKey] = result.data.mtime;
saveMtimes();
}
} catch(e) {
showToast('自动保存失败');
}
}, 2000);
});
// ========== Toast ==========
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.style.opacity = '1';
toast.style.transform = 'translateX(-50%) translateY(0)';
clearTimeout(toast._timer);
toast._timer = setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(-50%) translateY(20px)';
}, 2000);
}
// Close modal on overlay click
document.getElementById('addSheet').addEventListener('click', function(e) { if (e.target === this) closeAddSheet(); });
</script>
</body>
</html>