diff --git a/.gitignore b/.gitignore index 75a9c68..a0d9202 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ node_modules dist/ build/ *.log -cache \ No newline at end of file +cache +DESIGN.md +*.png \ No newline at end of file diff --git a/public/app.js b/public/app.js index a3c4e30..8cd0849 100644 --- a/public/app.js +++ b/public/app.js @@ -12,6 +12,25 @@ const elTimeRange = document.getElementById('timeRange') const elSentinel = document.getElementById('sentinel') const elBackTop = document.getElementById('backTop') + const elSearchHint = document.getElementById('searchHint') + const listContainer = document.querySelector('.list-container') + + // Mobile Sidebar Logic + const toggleSidebar = document.getElementById('toggleSidebar') + const sidebar = document.getElementById('sidebar') + const sidebarOverlay = document.getElementById('sidebarOverlay') + + if (toggleSidebar && sidebar && sidebarOverlay) { + const closeSidebar = () => { + sidebar.classList.remove('open') + sidebarOverlay.classList.remove('open') + } + toggleSidebar.addEventListener('click', () => { + sidebar.classList.toggle('open') + sidebarOverlay.classList.toggle('open') + }) + sidebarOverlay.addEventListener('click', closeSidebar) + } /** * 格式化字节大小为易读文本 @@ -61,25 +80,20 @@ document.body.appendChild(t) } t.textContent = msg - t.style.background = warn ? 'rgba(255,96,96,0.9)' : 'rgba(108,140,255,0.9)' + t.style.background = warn ? 'rgba(255,59,48,0.9)' : 'rgba(0,122,255,0.9)' t.classList.add('show') clearTimeout(toastTimer) toastTimer = setTimeout(() => t.classList.remove('show'), 1800) } /** - * 渲染缓存条目,仅展示完整URL与操作 + * 渲染缓存条目 * @param {Array<{type:string,url:string,size:number,mtime:number}>} items 列表 */ - /** - * 渲染缓存条目,仅展示完整URL与操作 - * - 维持最大 DOM 节点数量,超量时自动移除顶部旧节点(轻量虚拟化) - * @param {Array<{type:string,url:string,size:number,mtime:number,name?:string,version?:string}>} items 列表 - */ const renderItems = items => { elList.innerHTML = '' if (!items.length) { - elList.innerHTML = '
暂无数据
' + elList.innerHTML = '
暂无内容
' return } const frag = document.createDocumentFragment() @@ -96,8 +110,8 @@ ${fmtTime(it.mtime)}
- - 打开 + + 打开
` card.querySelector('[data-act="copy-url"]').addEventListener('click', () => copy(it.url)) @@ -106,45 +120,67 @@ elList.appendChild(frag) } - /** - * 加载缓存列表数据并更新视图 - * @returns {Promise} - */ /** * 加载缓存列表数据并更新视图(分页 + 过滤 + 排序) - * - 支持增量加载,当页码递增时附加到列表 - * @param {boolean} reset 是否重置列表 - * @returns {Promise} */ let loading = false let page = 1 let pageSize = Number(elPageSize.value || 30) let hasMore = true let itemsBuf = [] + const load = async (reset = false) => { if (loading) return if (reset) { page = 1; itemsBuf = []; elList.innerHTML = ''; hasMore = true } if (!hasMore && !reset) return + loading = true elStats.textContent = '加载中...' - const u = new URL('/api/list-cache', location.origin) - if (state.type) u.searchParams.set('type', state.type) - if (state.q) u.searchParams.set('q', state.q) - const now = Date.now() - const hours = Number(elTimeRange.value || 0) - if (hours > 0) u.searchParams.set('updatedFrom', String(now - hours * 3600 * 1000)) - u.searchParams.set('sortBy', elSortBy.value) - u.searchParams.set('order', elOrder.value) - u.searchParams.set('page', String(page)) - u.searchParams.set('pageSize', String(pageSize)) - const r = await fetch(u) - const data = await r.json() - itemsBuf = reset ? (data.items || []) : itemsBuf.concat(data.items || []) - hasMore = !!data.hasMore - elStats.textContent = `共 ${data.total} 条,已加载 ${itemsBuf.length}${hasMore ? '(继续下拉加载)' : ''}` - renderItems(itemsBuf) - page += 1 - loading = false + + // Manage spinner visibility + const elSpinner = elSentinel.querySelector('.spinner') + if (elSpinner) elSpinner.style.display = 'block' + + try { + const u = new URL('/api/list-cache', location.origin) + if (state.type) u.searchParams.set('type', state.type) + if (state.q) u.searchParams.set('q', state.q) + const now = Date.now() + const hours = Number(elTimeRange.value || 0) + if (hours > 0) u.searchParams.set('updatedFrom', String(now - hours * 3600 * 1000)) + + u.searchParams.set('sortBy', elSortBy.value) + u.searchParams.set('order', elOrder.value) + u.searchParams.set('page', String(page)) + u.searchParams.set('pageSize', String(pageSize)) + + const r = await fetch(u) + const data = await r.json() + + itemsBuf = reset ? (data.items || []) : itemsBuf.concat(data.items || []) + hasMore = !!data.hasMore + elStats.textContent = `共 ${data.total} 条,已加载 ${itemsBuf.length}` + renderItems(itemsBuf) + + // Manage search hint visibility + if (elSearchHint) { + // Show hint if there is more data on server (hasMore) or simply if list is not empty (as requested) + // Requirement: "Add explicit search function hint, guiding user to search to get more content" + // If hasMore is true, it means we only showed a subset. + // If itemsBuf.length < data.total, we are showing a subset. + const isPartial = itemsBuf.length < data.total + elSearchHint.style.display = (isPartial && !loading) ? 'block' : 'none' + } + + page += 1 + } catch (e) { + console.error(e) + elStats.textContent = '加载失败' + toast('加载失败', true) + } finally { + loading = false + if (elSpinner) elSpinner.style.display = 'none' + } } // 事件绑定 @@ -156,39 +192,51 @@ load(true) }) }) + + // 防抖搜索 + let searchTimer elSearch.addEventListener('input', () => { - state.q = elSearch.value.trim() - load(true) + clearTimeout(searchTimer) + searchTimer = setTimeout(() => { + state.q = elSearch.value.trim() + load(true) + }, 300) }) + elSortBy.addEventListener('change', () => load(true)) elOrder.addEventListener('change', () => load(true)) elPageSize.addEventListener('change', () => { pageSize = Number(elPageSize.value || 30); load(true) }) elTimeRange.addEventListener('change', () => load(true)) + elRefresh.addEventListener('click', async () => { try { - const r = await fetch('/api/seed') - const j = await r.json() - toast(`Seed 完成:${j.count} 项`) - } catch {} + toast('刷新列表中...') + // const r = await fetch('/api/seed') + // const j = await r.json() + // toast(`Seed 完成:${j.count} 项`) + } catch { + toast('请求失败', true) + } load(true) }) - // 轻量虚拟滚动:靠近底部即加载下一页;大量节点时隐藏返回顶部按钮控制 + // 虚拟滚动/无限加载 const io = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) load(false) }) - }) + }, { root: listContainer, rootMargin: '100px' }) + io.observe(elSentinel) // 返回顶部按钮展示与交互 const onScroll = () => { - const show = (document.documentElement.scrollTop || document.body.scrollTop) > 400 + const show = listContainer.scrollTop > 400 elBackTop.classList.toggle('show', show) } - window.addEventListener('scroll', onScroll) + listContainer.addEventListener('scroll', onScroll) elBackTop.addEventListener('click', () => { - window.scrollTo({ top: 0, behavior: 'smooth' }) + listContainer.scrollTo({ top: 0, behavior: 'smooth' }) }) // 首次加载 diff --git a/public/index.html b/public/index.html index 02214bd..c5d84ed 100644 --- a/public/index.html +++ b/public/index.html @@ -2,130 +2,148 @@ - - Asset Cache - 前端首页 + + Asset Cache - - + + + + + + + -
-
-

Asset Cache

-

内部资源缓存与静态分发

-

本网站收录的开源库均仅支持内部使用。

-
- 触发一次内置 Seed 抓取 - 查看健康状态 +
+ +
+
+
网站LOGO
+
+

Asset Cache

+ Online +
-
-
+
+ +
+ -
-
-

使用说明

-

- 为保证外部依赖的稳定性,请优先选择成熟公共 CDN 服务。以下为常用公共库加速服务的入口与地址: -

-
+
+ + -
-
-

已缓存资源

-
-
-
+ +
+ +
+
+
- - +
+ +
-
- - - - + +
+
+ + +
+
+ + + + + +
+
+
加载中...
+
+ + +
+
+ +
+
-
-
加载中...
-
-
加载更多...
- -
+ + + +
+
+ + + + - -
- -
-
- © 2025 Asset Cache & © Photo8 Tools Team - -
-
- + - + diff --git a/public/styles.css b/public/styles.css index 1b71bdf..5d1b2c4 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,138 +1,611 @@ :root { - --bg: #0b0f1a; - --fg: #e6e9ef; - --muted: #a9b1bd; - --primary: #6c8cff; - --primary-2: #8ea8ff; - --card: #121725; - --border: #1f2538; - --good: #5be49b; - --warn: #ff9966; + --bg-app: #f5f5f7; + --bg-card: #ffffff; + --bg-sidebar: #ffffff; /* Or slightly different */ + --text-primary: #1d1d1f; + --text-secondary: #86868b; + --accent: #007aff; + --accent-hover: #0062cc; + --border: #d2d2d7; + --border-light: rgba(0, 0, 0, 0.05); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --radius-lg: 12px; + --radius-md: 8px; + --radius-sm: 4px; + --header-height: 60px; + --sidebar-width: 300px; } -* { box-sizing: border-box; } -html, body { height: 100%; } -body { +* { + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +html, body { margin: 0; - font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; - background: radial-gradient(1200px 600px at 10% -20%, #132042 0%, #0b0f1a 60%), var(--bg); - background-attachment: fixed; - background-repeat: no-repeat; - background-size: cover; - background-position: center center; - color: var(--fg); + padding: 0; + height: 100%; + width: 100%; + overflow: hidden; /* No global scroll */ + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", Arial, sans-serif; + background-color: #f5f5f7; /* IE11 Fallback */ + background-color: var(--bg-app); + color: #1d1d1f; /* IE11 Fallback */ + color: var(--text-primary); + font-size: 14px; + line-height: 1.5; } -.container { - max-width: 1100px; +/* --- Layout --- */ +.app-layout { + display: flex; + flex-direction: column; + height: 100vh; +} + +.app-header { + height: var(--header-height); + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: space-between; padding: 0 24px; - margin: 0 auto; + position: relative; + z-index: 100; + flex-shrink: 0; } -.hero { - padding: 80px 0 48px; - background: linear-gradient(180deg, rgba(108,140,255,0.10), rgba(108,140,255,0.0)); - border-bottom: 1px solid var(--border); - margin-bottom: 36px; +.app-main { + flex: 1; + display: flex; + overflow: hidden; + position: relative; } -.hero h1 { font-size: 44px; margin: 0; letter-spacing: 0.5px; } -.hero .subtitle { font-weight: 700; color: var(--primary-2); margin: 12px 0 8px; } -.hero .desc { color: var(--muted); margin: 0 0 16px; } -.cta-group { display: flex; gap: 12px; flex-wrap: wrap; } +/* --- Sidebar --- */ +.app-sidebar { + width: 300px; /* IE11 Fallback */ + width: var(--sidebar-width); + background: #ffffff; /* IE11 Fallback */ + background: var(--bg-sidebar); + border-right: 1px solid #d2d2d7; /* IE11 Fallback */ + border-right: 1px solid var(--border-light); + display: flex; + flex-direction: column; + overflow-y: auto; + z-index: 90; +} + +.sidebar-content { + padding: 24px; +} + +.sidebar-section { + margin-bottom: 32px; +} + +.sidebar-section h2 { + font-size: 13px; + text-transform: uppercase; + color: var(--text-secondary); + letter-spacing: 0.5px; + margin: 0 0 16px 0; + font-weight: 600; +} + +.desc { + color: var(--text-secondary); + font-size: 13px; + margin-bottom: 16px; +} + +.sidebar-footer { + margin-top: auto; + padding-top: 24px; + border-top: 1px solid var(--border-light); + color: var(--text-secondary); + font-size: 12px; +} + +.sidebar-footer p { + margin: 2px 0; +} + +/* --- Content Area --- */ +.app-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background: #f5f5f7; /* IE11 Fallback */ + background: var(--bg-app); + position: relative; +} + +/* --- Toolbar --- */ +.toolbar { + background: rgba(245, 245, 247, 0.9); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-light); + padding: 16px 24px; + display: flex; + flex-direction: column; + gap: 12px; + z-index: 80; + flex-shrink: 0; +} + +.toolbar-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.toolbar-row.secondary { + justify-content: flex-start; +} + +.stats-bar { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +/* --- List --- */ +.list-container { + flex: 1; + overflow-y: auto; + padding: 16px 24px; + /* Scrollbar Styling */ + scrollbar-width: thin; + scrollbar-color: #c1c1c1 transparent; +} + +.list-container::-webkit-scrollbar { + width: 8px; +} +.list-container::-webkit-scrollbar-track { + background: transparent; +} +.list-container::-webkit-scrollbar-thumb { + background-color: #c1c1c1; + border-radius: 4px; + border: 2px solid transparent; + background-clip: content-box; +} + +/* --- Components --- */ +/* Header */ +.header-left { + display: flex; + align-items: center; + gap: 12px; +} +.logo-icon { + width: 32px; + height: 32px; + background: var(--text-primary); + color: white; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 14px; +} +.header-text h1 { + margin: 0; + font-size: 16px; + font-weight: 600; +} +.status-badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; + background: #e1f5ea; + color: #1a7f47; +} + +/* Buttons */ .btn { display: inline-flex; align-items: center; - padding: 10px 14px; - border-radius: 10px; - background: var(--primary); - color: #fff; + justify-content: center; + padding: 10px 16px; + border-radius: var(--radius-md); text-decoration: none; - border: 1px solid rgba(255,255,255,0.1); - transition: transform .12s ease, box-shadow .12s ease; + font-weight: 500; + font-size: 13px; + transition: all 0.2s ease-in-out; + cursor: pointer; + border: none; + gap: 8px; } -.btn:hover { transform: translateY(-1px); box-shadow: 0 8px 30px rgba(108,140,255,0.25); } -.btn.btn-outline { background: transparent; color: var(--primary-2); border-color: var(--primary-2); } - -main { padding: 40px 0; } -.notice h2 { margin: 0 0 12px; } -.notice p { color: var(--muted); } - -.grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - margin-top: 20px; +.btn.primary { + background: var(--accent); + color: white; } - -.card { - background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)); +.btn.primary:hover { + background: var(--accent-hover); +} +.btn.outline { + background: transparent; border: 1px solid var(--border); - border-radius: 14px; + color: var(--text-primary); +} +.btn.outline:hover { + background: rgba(0,0,0,0.02); + border-color: var(--text-secondary); +} +.btn.full-width { + width: 100%; +} +.icon-btn { + background: transparent; + border: none; + padding: 8px; + border-radius: 50%; + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; +} +.icon-btn:hover { + background: rgba(0,0,0,0.05); +} + +/* Segmented Control */ +.seg-control { + background: #e5e5ea; + padding: 2px; + border-radius: 8px; + display: inline-flex; +} +.seg-btn { + border: none; + background: transparent; + padding: 6px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; +} +.seg-btn.is-active { + background: white; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +/* Inputs & Filters */ +.search-wrapper { + position: relative; + flex: 1; + max-width: 400px; +} +.search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + pointer-events: none; +} +.search-wrapper input { + width: 100%; + padding: 8px 12px 8px 36px; + border: 1px solid var(--border); + border-radius: 8px; + background: white; + font-size: 13px; + outline: none; + transition: all 0.2s ease-in-out; +} +.search-wrapper input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(0,122,255,0.1); +} + +.filters { + display: flex; + gap: 12px; + align-items: center; +} + +.select-clean { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-color: white; + border: 1px solid var(--border); + border-radius: 4px; + padding: 6px 32px 6px 12px; + font-size: 14px; + font-family: inherit; + color: var(--text-primary); + line-height: 1.5; + cursor: pointer; + transition: all 0.2s ease-in-out; + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%2386868B' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + outline: none; +} +.select-clean:hover { + border-color: var(--text-secondary); +} +.select-clean:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(0,122,255,0.1); +} + +/* Nav Links */ +.nav-links { + display: flex; + flex-direction: column; + gap: 8px; +} +.nav-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: var(--bg-card); + border-radius: 8px; + text-decoration: none; + border: 1px solid var(--border-light); + transition: transform 0.2s, box-shadow 0.2s; +} +.nav-item:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} +.nav-title { + font-weight: 500; + color: var(--text-primary); +} +.nav-url { + font-size: 12px; + color: var(--text-secondary); +} + +/* List Item (Flat List) */ +.resource-list { + display: flex; + flex-direction: column; + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); /* Unified 2px blur shadow */ +} + +.item { + background: white; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 8px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); /* 20% opacity separator (approx) */ + transition: background-color 0.2s ease-in-out; +} + +.item:last-child { + border-bottom: none; +} + +.item:hover { + background-color: #f5f5f7; +} + +.item .row { + display: flex; + align-items: center; + gap: 12px; + overflow: hidden; +} + +.item .badge { + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + flex-shrink: 0; +} +.badge.css { background: #e3f2fd; color: #1976d2; } +.badge.js { background: #fff3e0; color: #ef6c00; } + +.item .url { + font-family: monospace; + font-size: 13px; /* Slightly larger for readability */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-primary); + flex: 1; +} + +.item .meta { + display: flex; + align-items: center; + gap: 16px; + font-size: 12px; + color: var(--text-secondary); +} + +.item .actions { + display: flex; + gap: 8px; + align-items: center; +} + +.btn.small { + padding: 4px 10px; + font-size: 12px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Ensure icon size consistency */ +.item svg, .btn .icon { + width: 16px; + height: 16px; +} + +/* Loading/Empty */ +.spinner { + width: 24px; + height: 24px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 20px auto; + display: none; /* Hidden by default, toggled by JS */ +} +@keyframes spin { to { transform: rotate(360deg); } } +.loading-sentinel { text-align: center; padding: 20px; } +.empty { text-align: center; color: var(--text-secondary); padding: 40px; background: var(--bg-card); border-radius: 12px; } + +/* Search Hint */ +.search-hint { + text-align: center; padding: 16px; + color: var(--text-secondary); + font-size: 13px; + background: rgba(255,255,255,0.5); + margin-top: 1px; + border-radius: 0 0 12px 12px; } -.card h3 { margin: 0 0 8px; font-size: 18px; } -.card p { margin: 0 0 12px; color: var(--muted); } -.link { color: var(--primary-2); text-decoration: none; } -.link:hover { text-decoration: underline; } -.footer-note { margin-top: 24px; color: var(--muted); } - -.footer { - border-top: 1px solid var(--border); - padding: 24px 0; +/* Back Top */ +.fab-btn { + position: absolute; + bottom: 24px; + right: 24px; + width: 44px; + height: 44px; + border-radius: 50%; + background: white; + color: var(--accent); + border: none; + box-shadow: var(--shadow-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + cursor: pointer; + opacity: 0; + transform: translateY(20px); + transition: all 0.3s; + pointer-events: none; + z-index: 99; } -.footer .container { display: flex; justify-content: space-between; align-items: center; } -.footer nav { display: flex; gap: 12px; } -.footer nav a { color: var(--muted); text-decoration: none; } -.footer nav a:hover { color: var(--fg); } - -@media (max-width: 900px) { .grid { grid-template-columns: 1fr 1fr; } } -@media (max-width: 600px) { .grid { grid-template-columns: 1fr; } .hero { padding: 64px 0 36px; margin-bottom: 30px; } } - -/* 缓存列表 */ -.cache { margin-top: 36px; } -.cache-header { display: flex; flex-direction: column; gap: 16px; } -.cache-header h2 { margin: 0; font-size: 22px; letter-spacing: 0.3px; } -.toolbar { display: grid; gap: 12px; } -.toolbar-top, .toolbar-bottom { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } -.seg { display: inline-flex; gap: 4px; background: #0e1424; border: 1px solid var(--border); border-radius: 10px; padding: 4px; } -.seg-btn { background: transparent; color: var(--muted); border: 0; padding: 10px 14px; border-radius: 10px; cursor: pointer; min-height: 40px; } -.seg-btn.is-active { background: rgba(108,140,255,0.16); color: var(--primary-2); } -.search { background: #0e1424; border: 1px solid var(--border); color: var(--fg); padding: 12px 14px; border-radius: 12px; width: 320px; min-height: 40px; } -.select { background: #0e1424; border: 1px solid var(--border); color: var(--fg); padding: 12px 14px; border-radius: 12px; min-height: 40px; } -.stats { margin-top: 10px; color: var(--muted); } -.list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-top: 18px; } -.item { background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); border: 1px solid var(--border); border-radius: 16px; padding: 16px; transition: transform .12s ease, box-shadow .18s ease, border-color .18s ease; } -.item:hover { transform: translateY(-1px); box-shadow: 0 10px 30px rgba(108,140,255,0.18); border-color: rgba(108,140,255,0.35); } -.item .row { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: flex-start; margin-bottom: 6px; } -.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; font-size: 12px; border: 1px solid var(--border); letter-spacing: 0.2px; } -.badge.css { color: #76e5ff; border-color: rgba(118,229,255,0.3); } -.badge.js { color: #ffd98e; border-color: rgba(255,217,142,0.25); } -.url { display: block; padding: 12px 14px; background: #0e1424; border: 1px solid var(--border); border-radius: 12px; color: var(--primary-2); line-height: 1.5; letter-spacing: 0.2px; white-space: normal; word-break: break-all; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.meta { display: flex; gap: 12px; color: var(--muted); margin-top: 8px; } -.actions { display: flex; gap: 12px; margin-top: 12px; flex-wrap: wrap; } -.btn.small { padding: 10px 12px; border-radius: 10px; font-size: 14px; min-height: 40px; } -.empty { padding: 18px; text-align: center; color: var(--muted); border: 1px dashed var(--border); border-radius: 12px; } - -.toast { position: fixed; left: 50%; transform: translateX(-50%); bottom: 22px; color: #0b0f1a; background: rgba(108,140,255,0.9); padding: 10px 14px; border-radius: 999px; box-shadow: 0 10px 30px rgba(0,0,0,0.35); opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease; } -.toast.show { opacity: 1; } - -.sentinel { text-align: center; color: var(--muted); padding: 12px; } -.back-top { position: fixed; right: 22px; bottom: 24px; background: #0e1424; border: 1px solid var(--border); color: var(--fg); padding: 10px 12px; border-radius: 12px; box-shadow: 0 10px 24px rgba(0,0,0,0.25); opacity: 0; pointer-events: none; transition: opacity .18s ease, transform .18s ease; } -.back-top.show { opacity: 1; pointer-events: auto; } - -@media (max-width: 1100px) { - .list { grid-template-columns: 1fr 1fr; } - .search { width: 280px; } +.fab-btn.show { + opacity: 1; + transform: translateY(0); + pointer-events: auto; } -@media (max-width: 900px) { - .toolbar-top, .toolbar-bottom { gap: 8px; } - .search { width: 220px; } + +/* Toast */ +.toast { + position: fixed; + top: 24px; + left: 50%; + transform: translateX(-50%) translateY(-20px); + background: rgba(0,0,0,0.8); + color: white; + padding: 10px 20px; + border-radius: 20px; + font-size: 13px; + opacity: 0; + transition: all 0.3s; + pointer-events: none; + z-index: 2000; + backdrop-filter: blur(10px); } -@media (max-width: 760px) { - .list { grid-template-columns: 1fr; } - .search { width: 100%; } - .item { padding: 14px; border-radius: 14px; } - .toolbar-top, .toolbar-bottom { flex-direction: column; align-items: stretch; } +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* --- Responsive --- */ +.mobile-only { display: none; } +.sidebar-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.4); + z-index: 85; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; +} + +/* Tablet (769px - 1024px) */ +@media (max-width: 1024px) { + :root { --sidebar-width: 240px; } +} + +/* Mobile (< 768px) */ +@media (max-width: 768px) { + .mobile-only { display: flex; } + + .app-sidebar { + position: fixed; + top: var(--header-height); + left: 0; + bottom: 0; + width: 80%; + max-width: 300px; + transform: translateX(-100%); + transition: transform 0.3s ease-in-out; + background: white; + z-index: 90; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + .app-sidebar.open { + transform: translateX(0); + } + + .sidebar-overlay.open { + opacity: 1; + pointer-events: auto; + } + + .toolbar { + padding: 12px 16px; + } + .toolbar-row { + flex-wrap: wrap; + } + .search-wrapper { + max-width: 100%; + order: 1; + width: 100%; + } + .filters { + order: 2; + width: 100%; + display: flex; + justify-content: space-between; + } + + .app-header { + padding: 0 16px; + } + + .list-container { + padding: 12px 16px; + } }