You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
808 lines
20 KiB
808 lines
20 KiB
<script setup lang="ts">
|
|
import type { CategoryNode } from '~/components/index/CategoryTreeNode.vue'
|
|
|
|
definePageMeta({
|
|
layout: 'home',
|
|
})
|
|
|
|
const { $toast } = useNuxtApp()
|
|
const isMobile = useMediaQuery('(max-width: 767.98px)')
|
|
const drawerOpen = ref(false)
|
|
const sidebarMode = computed<'rail' | 'drawer'>(() => isMobile.value ? 'drawer' : 'rail')
|
|
|
|
interface CardItem {
|
|
id: number
|
|
type: 'text' | 'image' | 'image-text' | 'portfolio' | 'project'
|
|
image?: string
|
|
images?: string[]
|
|
title: string
|
|
description?: string
|
|
tags?: string[]
|
|
aspectRatio: number
|
|
}
|
|
|
|
// ── Sidebar state ──
|
|
|
|
const activeToolKey = ref<string>('home')
|
|
const activeCategoryId = ref<string>('all')
|
|
|
|
const categories = ref<CategoryNode[]>([
|
|
{
|
|
id: 'all',
|
|
name: '全部灵感',
|
|
image: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=80&h=80&fit=crop',
|
|
count: 156,
|
|
},
|
|
{
|
|
id: 'design',
|
|
name: '设计',
|
|
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=80&h=80&fit=crop',
|
|
count: 42,
|
|
children: [
|
|
{
|
|
id: 'design-graphic',
|
|
name: '平面 / 海报',
|
|
image: 'https://images.unsplash.com/photo-1611532736597-de2d4265fba3?w=80&h=80&fit=crop',
|
|
count: 18,
|
|
},
|
|
{
|
|
id: 'design-product',
|
|
name: '产品 / UI',
|
|
image: 'https://images.unsplash.com/photo-1545235617-9465d2a55698?w=80&h=80&fit=crop',
|
|
count: 16,
|
|
},
|
|
{
|
|
id: 'design-typo',
|
|
name: '字体',
|
|
image: 'https://images.unsplash.com/photo-1561070791-2526d30994b8?w=80&h=80&fit=crop',
|
|
count: 8,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'photo',
|
|
name: '摄影',
|
|
image: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=80&h=80&fit=crop',
|
|
count: 38,
|
|
children: [
|
|
{
|
|
id: 'photo-street',
|
|
name: '街头',
|
|
image: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=80&h=80&fit=crop',
|
|
count: 14,
|
|
},
|
|
{
|
|
id: 'photo-portrait',
|
|
name: '人像',
|
|
image: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&fit=crop',
|
|
count: 12,
|
|
},
|
|
{
|
|
id: 'photo-landscape',
|
|
name: '风光',
|
|
image: 'https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=80&h=80&fit=crop',
|
|
count: 12,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'words',
|
|
name: '文字摘录',
|
|
image: 'https://images.unsplash.com/photo-1455390582262-044cdead277a?w=80&h=80&fit=crop',
|
|
count: 24,
|
|
},
|
|
{
|
|
id: 'projects',
|
|
name: '项目档案',
|
|
image: 'https://images.unsplash.com/photo-1467232004584-a241de8bcf5d?w=80&h=80&fit=crop',
|
|
count: 17,
|
|
children: [
|
|
{
|
|
id: 'projects-web',
|
|
name: 'Web',
|
|
image: 'https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=80&h=80&fit=crop',
|
|
count: 9,
|
|
},
|
|
{
|
|
id: 'projects-mobile',
|
|
name: 'Mobile',
|
|
image: 'https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?w=80&h=80&fit=crop',
|
|
count: 8,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'inspiration',
|
|
name: '随手收藏',
|
|
image: 'https://images.unsplash.com/photo-1490730141103-6cac27aaab94?w=80&h=80&fit=crop',
|
|
count: 35,
|
|
},
|
|
])
|
|
|
|
const activeCategoryName = computed(() => {
|
|
function find(nodes: CategoryNode[]): CategoryNode | null {
|
|
for (const n of nodes) {
|
|
if (n.id === activeCategoryId.value) return n
|
|
if (n.children) {
|
|
const r = find(n.children)
|
|
if (r) return r
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
return find(categories.value)?.name ?? '全部灵感'
|
|
})
|
|
|
|
function onSelectTool(key: string) {
|
|
activeToolKey.value = key
|
|
const labelMap: Record<string, string> = {
|
|
home: '主页',
|
|
search: '搜索',
|
|
tags: '标签',
|
|
highlights: '高亮',
|
|
archive: '归档',
|
|
}
|
|
$toast.info(`切换到 · ${labelMap[key] ?? key}`)
|
|
}
|
|
|
|
function onSelectCategory(id: string) {
|
|
activeCategoryId.value = id
|
|
if (drawerOpen.value) drawerOpen.value = false
|
|
}
|
|
|
|
function onAddCategory(parentId: string | null) {
|
|
const newName = window.prompt(parentId ? '新建子分类名称' : '新建分类名称')
|
|
if (!newName?.trim()) return
|
|
const node: CategoryNode = {
|
|
id: `cat-${Date.now()}`,
|
|
name: newName.trim(),
|
|
count: 0,
|
|
}
|
|
if (!parentId) {
|
|
categories.value.push(node)
|
|
$toast.success(`已创建分类「${node.name}」`)
|
|
return
|
|
}
|
|
function attach(nodes: CategoryNode[]): boolean {
|
|
for (const n of nodes) {
|
|
if (n.id === parentId) {
|
|
n.children = n.children ?? []
|
|
n.children.push(node)
|
|
return true
|
|
}
|
|
if (n.children && attach(n.children)) return true
|
|
}
|
|
return false
|
|
}
|
|
if (attach(categories.value)) {
|
|
$toast.success(`已添加子分类「${node.name}」`)
|
|
}
|
|
}
|
|
|
|
function findNode(nodes: CategoryNode[], id: string): CategoryNode | null {
|
|
for (const n of nodes) {
|
|
if (n.id === id) return n
|
|
if (n.children) {
|
|
const r = findNode(n.children, id)
|
|
if (r) return r
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function onRenameCategory(id: string) {
|
|
const node = findNode(categories.value, id)
|
|
if (!node) return
|
|
const next = window.prompt('重命名分类', node.name)
|
|
if (!next?.trim() || next.trim() === node.name) return
|
|
node.name = next.trim()
|
|
$toast.success('已重命名')
|
|
}
|
|
|
|
function onDeleteCategory(id: string) {
|
|
const node = findNode(categories.value, id)
|
|
if (!node) return
|
|
if (!window.confirm(`确定删除「${node.name}」吗?`)) return
|
|
function remove(nodes: CategoryNode[]): CategoryNode[] {
|
|
return nodes
|
|
.filter(n => n.id !== id)
|
|
.map(n => ({ ...n, children: n.children ? remove(n.children) : undefined }))
|
|
}
|
|
categories.value = remove(categories.value)
|
|
if (activeCategoryId.value === id) activeCategoryId.value = 'all'
|
|
$toast.success('已删除分类')
|
|
}
|
|
|
|
function onMoveCategory(_id: string) {
|
|
$toast.info('移动功能即将上线')
|
|
}
|
|
|
|
function onChangeCover(_id: string) {
|
|
$toast.info('封面更换即将上线')
|
|
}
|
|
|
|
// ── Right panel waterfall ──
|
|
|
|
const sentinel = ref<HTMLElement | null>(null)
|
|
const mainRef = ref<HTMLElement | null>(null)
|
|
const allItems = ref<CardItem[]>([])
|
|
const page = ref(1)
|
|
const hasMore = ref(true)
|
|
const loading = ref(false)
|
|
const initLoading = ref(true)
|
|
|
|
const columnCount = ref(1)
|
|
const columns = ref<CardItem[][]>([[]])
|
|
const columnHeights = ref<number[]>([0])
|
|
|
|
function containerWidth(): number {
|
|
return mainRef.value?.clientWidth ?? 800
|
|
}
|
|
|
|
function getColumnWidth(): number {
|
|
const padding = 48
|
|
const w = containerWidth() - padding
|
|
const gap = 18
|
|
return (w - gap * (columnCount.value - 1)) / columnCount.value
|
|
}
|
|
|
|
function estimateCardHeight(item: CardItem): number {
|
|
const colWidth = getColumnWidth()
|
|
if (item.type === 'text') {
|
|
const textWidth = colWidth - 48
|
|
const charsPerLine = Math.max(1, Math.floor(textWidth / 13.5))
|
|
const charCount = (item.description || '').length
|
|
const lines = Math.ceil(charCount / charsPerLine)
|
|
return 109 + lines * 24
|
|
}
|
|
return colWidth / item.aspectRatio
|
|
}
|
|
|
|
function addToShortestColumn(item: CardItem) {
|
|
let minIdx = 0
|
|
for (let i = 1; i < columnCount.value; i++) {
|
|
if (columnHeights.value[i]! < columnHeights.value[minIdx]!) minIdx = i
|
|
}
|
|
columns.value[minIdx]!.push(item)
|
|
columnHeights.value[minIdx]! += estimateCardHeight(item)
|
|
}
|
|
|
|
function distributeAll() {
|
|
const n = columnCount.value
|
|
columns.value = Array.from({ length: n }, () => [])
|
|
columnHeights.value = new Array(n).fill(0)
|
|
for (const item of allItems.value) addToShortestColumn(item)
|
|
}
|
|
|
|
function decideColumnCount(): number {
|
|
if (isMobile.value) return 1
|
|
const w = containerWidth()
|
|
if (w < 900) return 2
|
|
if (w < 1200) return 3
|
|
return 4
|
|
}
|
|
|
|
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
|
function onContainerResize() {
|
|
if (resizeTimer) clearTimeout(resizeTimer)
|
|
resizeTimer = setTimeout(() => {
|
|
const n = decideColumnCount()
|
|
if (n !== columnCount.value) {
|
|
columnCount.value = n
|
|
distributeAll()
|
|
}
|
|
}, 120)
|
|
}
|
|
|
|
async function loadMore() {
|
|
if (loading.value || !hasMore.value) return
|
|
loading.value = true
|
|
try {
|
|
const data = await $fetch<{ items: CardItem[], hasMore: boolean }>('/api/cards', {
|
|
query: { page: page.value, pageSize: 12 },
|
|
})
|
|
for (const item of data.items) {
|
|
allItems.value.push(item)
|
|
addToShortestColumn(item)
|
|
}
|
|
hasMore.value = data.hasMore
|
|
page.value++
|
|
} catch {
|
|
$toast.error('加载失败,请稍后重试')
|
|
} finally {
|
|
loading.value = false
|
|
initLoading.value = false
|
|
}
|
|
}
|
|
|
|
// Reset when category switches (mock: just reshuffle existing items)
|
|
watch(activeCategoryId, () => {
|
|
page.value = 1
|
|
hasMore.value = true
|
|
allItems.value = []
|
|
columns.value = Array.from({ length: columnCount.value }, () => [])
|
|
columnHeights.value = new Array(columnCount.value).fill(0)
|
|
initLoading.value = true
|
|
loadMore()
|
|
})
|
|
|
|
watch(isMobile, () => {
|
|
const n = decideColumnCount()
|
|
if (n !== columnCount.value) {
|
|
columnCount.value = n
|
|
distributeAll()
|
|
}
|
|
})
|
|
|
|
let observer: IntersectionObserver | null = null
|
|
let resizeObserver: ResizeObserver | null = null
|
|
|
|
watch(sentinel, (el) => {
|
|
observer?.disconnect()
|
|
if (!el) return
|
|
observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry!.isIntersecting) loadMore()
|
|
},
|
|
{ rootMargin: '320px' },
|
|
)
|
|
observer.observe(el)
|
|
})
|
|
|
|
onMounted(() => {
|
|
if (mainRef.value) {
|
|
columnCount.value = decideColumnCount()
|
|
distributeAll()
|
|
resizeObserver = new ResizeObserver(onContainerResize)
|
|
resizeObserver.observe(mainRef.value)
|
|
}
|
|
loadMore()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
observer?.disconnect()
|
|
resizeObserver?.disconnect()
|
|
if (resizeTimer) clearTimeout(resizeTimer)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="home-layout">
|
|
<IndexLeftSidebar
|
|
v-if="sidebarMode === 'rail'"
|
|
class="home-sidebar"
|
|
:mode="sidebarMode"
|
|
:categories="categories"
|
|
:active-tool-key="activeToolKey"
|
|
:active-category-id="activeCategoryId"
|
|
@select-tool="onSelectTool"
|
|
@select-category="onSelectCategory"
|
|
@add-category="onAddCategory"
|
|
@rename-category="onRenameCategory"
|
|
@delete-category="onDeleteCategory"
|
|
@move-category="onMoveCategory"
|
|
@change-cover="onChangeCover"
|
|
/>
|
|
|
|
<IndexLeftSidebar
|
|
v-else
|
|
:mode="sidebarMode"
|
|
:drawer-open="drawerOpen"
|
|
:categories="categories"
|
|
:active-tool-key="activeToolKey"
|
|
:active-category-id="activeCategoryId"
|
|
@update:drawer-open="(v: boolean) => drawerOpen = v"
|
|
@select-tool="onSelectTool"
|
|
@select-category="onSelectCategory"
|
|
@add-category="onAddCategory"
|
|
@rename-category="onRenameCategory"
|
|
@delete-category="onDeleteCategory"
|
|
@move-category="onMoveCategory"
|
|
@change-cover="onChangeCover"
|
|
/>
|
|
|
|
<main ref="mainRef" class="home-main">
|
|
<header class="main-header">
|
|
<!-- Desktop / tablet layout (>= 768px) -->
|
|
<template v-if="!isMobile">
|
|
<div class="header-left">
|
|
<span class="header-eyebrow">分类</span>
|
|
<h1 class="header-title">{{ activeCategoryName }}</h1>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button type="button" class="action-btn" @click="$toast.info('视图切换即将上线')">
|
|
<Icon name="lucide:layout-grid" />
|
|
</button>
|
|
<button type="button" class="action-btn" @click="$toast.info('筛选即将上线')">
|
|
<Icon name="lucide:sliders-horizontal" />
|
|
</button>
|
|
<button type="button" class="action-btn primary" @click="$toast.info('上传即将上线')">
|
|
<Icon name="lucide:plus" />
|
|
<span>新增</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Mobile layout (< 768px) -->
|
|
<template v-else>
|
|
<div class="header-row header-row--top">
|
|
<span class="header-eyebrow">分类</span>
|
|
<div class="header-actions">
|
|
<button type="button" class="action-btn action-btn--icon" aria-label="视图" @click="$toast.info('视图切换即将上线')">
|
|
<Icon name="lucide:layout-grid" />
|
|
</button>
|
|
<button type="button" class="action-btn action-btn--icon" aria-label="筛选" @click="$toast.info('筛选即将上线')">
|
|
<Icon name="lucide:sliders-horizontal" />
|
|
</button>
|
|
<button type="button" class="action-btn action-btn--icon action-btn--primary" aria-label="新增" @click="$toast.info('上传即将上线')">
|
|
<Icon name="lucide:plus" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h1 class="header-title">{{ activeCategoryName }}</h1>
|
|
|
|
<div class="header-row header-row--bottom">
|
|
<button
|
|
type="button"
|
|
class="drawer-trigger"
|
|
aria-label="打开分类抽屉"
|
|
@click="drawerOpen = true"
|
|
>
|
|
<Icon name="lucide:menu" />
|
|
<span>分类</span>
|
|
</button>
|
|
<span class="category-chip">{{ activeCategoryName }}</span>
|
|
</div>
|
|
</template>
|
|
</header>
|
|
|
|
<div class="masonry">
|
|
<div
|
|
v-for="(col, ci) in columns"
|
|
:key="ci"
|
|
class="masonry-col"
|
|
>
|
|
<template v-for="(item, ri) in col" :key="item.id">
|
|
<div
|
|
class="card-reveal"
|
|
:style="{ '--enter-delay': `${ci * 80 + ri * 60}ms` }"
|
|
>
|
|
<IndexWaterfallCard
|
|
v-if="item.type === 'image-text'"
|
|
:image="item.image!"
|
|
:title="item.title"
|
|
:description="item.description!"
|
|
:aspect-ratio="item.aspectRatio"
|
|
/>
|
|
<IndexWaterfallTextCard
|
|
v-else-if="item.type === 'text'"
|
|
:title="item.title"
|
|
:description="item.description!"
|
|
:aspect-ratio="item.aspectRatio"
|
|
/>
|
|
<IndexWaterfallImageCard
|
|
v-else-if="item.type === 'image'"
|
|
:image="item.image!"
|
|
:title="item.title"
|
|
:aspect-ratio="item.aspectRatio"
|
|
/>
|
|
<IndexWaterfallPortfolioCard
|
|
v-else-if="item.type === 'portfolio'"
|
|
:images="item.images!"
|
|
:title="item.title"
|
|
:description="item.description!"
|
|
:aspect-ratio="item.aspectRatio"
|
|
/>
|
|
<IndexWaterfallProjectCard
|
|
v-else
|
|
:image="item.image!"
|
|
:title="item.title"
|
|
:description="item.description!"
|
|
:tags="item.tags!"
|
|
:aspect-ratio="item.aspectRatio"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div ref="sentinel" class="sentinel">
|
|
<template v-if="initLoading || loading">
|
|
<div class="loading-bars">
|
|
<span v-for="i in 3" :key="i" class="bar" :style="{ animationDelay: `${(i - 1) * 0.15}s` }" />
|
|
</div>
|
|
</template>
|
|
<span v-else-if="hasMore" class="sentinel-text">继续滚动查看更多</span>
|
|
<span v-else class="sentinel-text end">—— 已经到底了 ——</span>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.home-layout {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
align-items: start;
|
|
gap: 0;
|
|
}
|
|
|
|
.home-sidebar {
|
|
position: sticky;
|
|
top: 64px;
|
|
height: calc(100vh - 64px);
|
|
align-self: start;
|
|
}
|
|
|
|
.home-main {
|
|
min-width: 0;
|
|
padding: 28px 24px 48px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
|
|
.main-header {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
padding-bottom: 24px;
|
|
margin-bottom: 24px;
|
|
border-bottom: 1px solid var(--color-hairline);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.header-eyebrow {
|
|
font-family: var(--font-body);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.header-title {
|
|
font-family: var(--font-display);
|
|
font-size: clamp(28px, 4vw, 44px);
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
letter-spacing: -0.8px;
|
|
line-height: 1.05;
|
|
margin: 0;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
height: 36px;
|
|
padding: 0 12px;
|
|
border-radius: 9999px;
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border: 1px solid var(--color-hairline);
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--color-body);
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.action-btn :deep(svg) {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
border-color: var(--color-ink);
|
|
color: var(--color-ink);
|
|
}
|
|
|
|
.action-btn.primary {
|
|
background: var(--color-primary);
|
|
border-color: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.action-btn.primary:hover {
|
|
background: var(--color-primary-active);
|
|
border-color: var(--color-primary-active);
|
|
}
|
|
|
|
/* ── Masonry ── */
|
|
|
|
.masonry {
|
|
display: flex;
|
|
gap: 18px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.masonry-col {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.card-reveal {
|
|
animation: card-enter 0.6s var(--ease-out-expo) both;
|
|
animation-delay: var(--enter-delay, 0ms);
|
|
}
|
|
|
|
@keyframes card-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* ── Sentinel ── */
|
|
|
|
.sentinel {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 40px 0 24px;
|
|
min-height: 56px;
|
|
}
|
|
|
|
.sentinel-text {
|
|
font-size: 13px;
|
|
color: var(--color-muted-soft);
|
|
}
|
|
|
|
.sentinel-text.end {
|
|
color: var(--color-hairline);
|
|
}
|
|
|
|
.loading-bars {
|
|
display: flex;
|
|
gap: 5px;
|
|
align-items: center;
|
|
}
|
|
|
|
.bar {
|
|
width: 4px;
|
|
height: 18px;
|
|
background: var(--color-primary);
|
|
border-radius: 2px;
|
|
opacity: 0.5;
|
|
animation: pulse 0.9s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.3; transform: scaleY(0.6); }
|
|
50% { opacity: 0.8; transform: scaleY(1); }
|
|
}
|
|
|
|
/* ── Responsive (DESIGN.md breakpoints) ── */
|
|
|
|
@media (max-width: 767.98px) {
|
|
.home-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.home-sidebar {
|
|
display: none; /* rail 节点在移动端被 v-if 隐藏;drawer 由 BoDrawer 渲染到 body */
|
|
}
|
|
|
|
.home-main {
|
|
padding: 16px 16px 32px;
|
|
}
|
|
|
|
.main-header {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 12px;
|
|
padding-bottom: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: clamp(22px, 7vw, 30px);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) and (max-width: 1023.98px) {
|
|
.home-layout {
|
|
grid-template-columns: 240px 1fr;
|
|
}
|
|
|
|
.home-main {
|
|
padding: 20px 16px 32px;
|
|
}
|
|
}
|
|
|
|
/* ── Mobile header rows ── */
|
|
|
|
.header-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.header-row--top {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.header-row--bottom {
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn--icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
padding: 0;
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-btn--icon.action-btn--primary {
|
|
background: var(--color-primary);
|
|
border-color: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
}
|
|
|
|
.drawer-trigger {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 44px;
|
|
padding: 0 16px;
|
|
border-radius: var(--rounded-md, 8px);
|
|
background: transparent;
|
|
border: 1px solid var(--color-hairline);
|
|
font-family: var(--font-body);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
cursor: pointer;
|
|
min-width: 44px;
|
|
transition: border-color 0.15s ease, color 0.15s ease;
|
|
}
|
|
|
|
.drawer-trigger:hover {
|
|
border-color: var(--color-ink);
|
|
}
|
|
|
|
.drawer-trigger :deep(svg) {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.category-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
height: 32px;
|
|
padding: 0 14px;
|
|
border-radius: 9999px;
|
|
background: var(--color-surface-card);
|
|
color: var(--color-ink);
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
</style>
|
|
|