diff --git a/app/components/index/LeftSidebar.vue b/app/components/index/LeftSidebar.vue index 27a0b52..07707a1 100644 --- a/app/components/index/LeftSidebar.vue +++ b/app/components/index/LeftSidebar.vue @@ -13,7 +13,15 @@ const props = withDefaults(defineProps<{ categories: CategoryNode[] activeToolKey?: string activeCategoryId?: string + mode?: 'rail' | 'drawer' + drawerOpen?: boolean + width?: string + side?: 'left' | 'right' }>(), { + mode: 'rail', + drawerOpen: false, + width: '300px', + side: 'left', tools: () => [ { key: 'home', label: '主页', icon: 'lucide:home' }, { key: 'search', label: '搜索', icon: 'lucide:search' }, @@ -31,6 +39,7 @@ const emit = defineEmits<{ deleteCategory: [id: string] moveCategory: [id: string] changeCover: [id: string] + 'update:drawerOpen': [v: boolean] }>() const expandedIds = ref>(new Set()) @@ -111,68 +120,142 @@ function onAddRoot() { diff --git a/docs/superpowers/plans/2026-06-04-homepage-mobile-adaptation.md b/docs/superpowers/plans/2026-06-04-homepage-mobile-adaptation.md new file mode 100644 index 0000000..b662f4b --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-homepage-mobile-adaptation.md @@ -0,0 +1,1074 @@ +# 首页移动端适配实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 改造首页在 `<768px` 视口下的布局与交互:左侧栏改为抽屉触发,header 三行布局,瀑布流列数与现有阈值一致,桌面端 100% 回归。 + +**Architecture:** 新增 `bolt-ui/components/Drawer` 通用抽屉组件(基于现有 `BoMask`),新增 `bolt-ui/hooks/useMediaQuery` 响应式 composable;`IndexLeftSidebar` 增加 `mode: 'rail' | 'drawer'` 切换;首页用 `useMediaQuery` 驱动 mode 与 drawer 状态。 + +**Tech Stack:** Vue 3.5, Nuxt 4.4, bolt-ui (SCSS + useNamespace pattern), @nuxt/icon, vue3-toastify + +**Conventions to follow:** +- bolt-ui 组件:`components//index.ts` + `src/.vue` + `style/{index.ts,css.ts}` + `theme-chalk/src/.scss` +- 命名空间:`useNamespace('drawer')` → `bo-drawer`, `bo-drawer__x`, `bo-drawer--x` +- 注册:`withInstall(_Drawer)` → `` 全局可用(由 `bolt-ui/nuxt` 模块自动注册) +- Composable:`hooks//index.ts` → 自动导入为 `useXxx` +- 锁定版本:禁止升级依赖;如需新增,手动 `bun add` + +--- + +## 文件结构 + +``` +packages/bolt-ui/ +├── components/ +│ ├── Drawer/ [NEW] +│ │ ├── index.ts +│ │ ├── src/ +│ │ │ └── Drawer.vue +│ │ └── style/ +│ │ ├── index.ts +│ │ └── css.ts +│ └── index.ts [MODIFY] 添加 export * from './Drawer' +├── hooks/ +│ └── useMediaQuery/ [NEW] +│ └── index.ts +├── theme-chalk/src/ +│ ├── components/drawer/ [NEW] +│ │ ├── _index.scss (主入口 @use 子文件) +│ │ ├── _var.scss +│ │ ├── panel.scss +│ │ ├── panel-left.scss +│ │ ├── panel-right.scss +│ │ ├── mask.scss +│ │ └── anim/ +│ │ ├── _var.scss +│ │ ├── panel-left-fade.scss +│ │ ├── panel-right-fade.scss +│ │ └── mask-fade.scss +│ └── drawer.scss [NEW] 主入口(被 bolt-ui/nuxt 自动注入) +└── README.md [MODIFY] 追加 Drawer 章节 + +app/ +├── components/ +│ └── index/LeftSidebar.vue [MODIFY] 新增 mode/drawerOpen 等 props +└── pages/index/index.vue [MODIFY] drawerOpen 状态、header 三行、断点重写 +``` + +--- + +## Task 1: useMediaQuery composable + +**Files:** +- Create: `packages/bolt-ui/hooks/useMediaQuery/index.ts` + +- [ ] **Step 1: 写 composable** + +```ts +import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue' + +/** + * 响应式地跟踪 CSS 媒体查询匹配状态。 + * + * SSR 安全:服务端渲染期间始终返回 `false`,避免 hydration mismatch; + * 客户端 `onMounted` 之后才订阅 `matchMedia`。 + * + * @param query - 合法 CSS media query 字符串 + * @returns Ref - 当前是否匹配 + */ +export function useMediaQuery(query: string): Ref { + const matches = ref(false) + let mql: MediaQueryList | null = null + const handler = (e: MediaQueryListEvent) => { + matches.value = e.matches + } + + onMounted(() => { + if (typeof window === 'undefined' || !window.matchMedia) return + mql = window.matchMedia(query) + matches.value = mql.matches + mql.addEventListener('change', handler) + }) + + onBeforeUnmount(() => { + mql?.removeEventListener('change', handler) + }) + + return matches +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add packages/bolt-ui/hooks/useMediaQuery/index.ts +git -c commit.gpgsign=false commit -m "feat(bolt-ui): add useMediaQuery composable" +``` + +> 备注:bolt-ui/nuxt 模块会自动扫描 `hooks//index.ts` 并把 `useXxx` 导出注册为 auto-import,所以无需修改 `nuxt.ts`。 + +--- + +## Task 2: BoDrawer 组件脚手架(props/emits/template/状态) + +**Files:** +- Create: `packages/bolt-ui/components/Drawer/src/Drawer.vue` +- Create: `packages/bolt-ui/components/Drawer/index.ts` + +- [ ] **Step 1: 写 Drawer.vue 模板 + props + emits + 基础状态** + +`packages/bolt-ui/components/Drawer/src/Drawer.vue`: + +```vue + + + +``` + +- [ ] **Step 2: 写 index.ts** + +`packages/bolt-ui/components/Drawer/index.ts`: + +```ts +import _Drawer from './src/Drawer.vue' +import { withInstall } from 'bolt-ui/utils/vue/install' + +export type BoDrawer = InstanceType + +export const BoDrawer = withInstall(_Drawer) +export default BoDrawer +``` + +- [ ] **Step 3: 在 components/index.ts 注册导出** + +编辑 `packages/bolt-ui/components/index.ts`,在末尾追加: + +```ts +export * from './Drawer' +``` + +- [ ] **Step 4: 提交** + +```bash +git add packages/bolt-ui/components/Drawer packages/bolt-ui/components/index.ts +git -c commit.gpgsign=false commit -m "feat(bolt-ui): add BoDrawer component scaffold" +``` + +--- + +## Task 3: BoDrawer 主题样式(panel + mask + 动画) + +**Files:** +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss` +- Create: `packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss` +- Create: `packages/bolt-ui/theme-chalk/src/drawer.scss` +- Create: `packages/bolt-ui/components/Drawer/style/index.ts` +- Create: `packages/bolt-ui/components/Drawer/style/css.ts` + +- [ ] **Step 1: 写 SCSS 变量与动画基础** + +`packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss`: + +```scss +$drawer-time: 0.28s !default; +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss`: + +```scss +@use "_var" as *; + +.bo-drawer-mask-fade-enter-active, +.bo-drawer-mask-fade-leave-active { + transition: opacity $drawer-time ease; +} + +.bo-drawer-mask-fade-enter-from, +.bo-drawer-mask-fade-leave-to { + opacity: 0; +} +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss`: + +```scss +@use "_var" as *; + +.bo-drawer-panel-left-fade-enter-active, +.bo-drawer-panel-left-fade-leave-active { + transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); +} + +.bo-drawer-panel-left-fade-enter-from, +.bo-drawer-panel-left-fade-leave-to { + transform: translateX(-100%); +} +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss`: + +```scss +@use "_var" as *; + +.bo-drawer-panel-right-fade-enter-active, +.bo-drawer-panel-right-fade-leave-active { + transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); +} + +.bo-drawer-panel-right-fade-enter-from, +.bo-drawer-panel-right-fade-leave-to { + transform: translateX(100%); +} +``` + +- [ ] **Step 2: 写 panel / mask 样式** + +`packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss`: + +```scss +$bo-drawer-bg: var(--color-canvas) !default; +$bo-drawer-shadow: 0 8px 32px rgba(20, 20, 19, 0.12) !default; +$bo-drawer-mask-bg: rgba(20, 20, 19, 0.32) !default; +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss`: + +```scss +@use "./_var" as *; + +.bo-drawer { + position: fixed; + top: 0; + bottom: 0; + background: $bo-drawer-bg; + box-shadow: $bo-drawer-shadow; + display: flex; + flex-direction: column; + overflow: hidden; + outline: none; + -webkit-overflow-scrolling: touch; +} +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss`: + +```scss +.bo-drawer--left { + left: 0; +} +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss`: + +```scss +.bo-drawer--right { + right: 0; +} +``` + +`packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss`: + +```scss +@use "./_var" as *; + +.bo-drawer__mask { + position: fixed; + inset: 0; + background: $bo-drawer-mask-bg; + z-index: 199; // 比 panel 低 1 + backdrop-filter: blur(2px); +} +``` + +- [ ] **Step 3: 写组件目录入口** + +`packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss`: + +```scss +@use "./anim/mask-fade.scss"; +@use "./anim/panel-left-fade.scss"; +@use "./anim/panel-right-fade.scss"; +@use "./panel.scss"; +@use "./panel-left.scss"; +@use "./panel-right.scss"; +@use "./mask.scss"; +``` + +- [ ] **Step 4: 写主题根入口(被 bolt-ui/nuxt 自动注入)** + +`packages/bolt-ui/theme-chalk/src/drawer.scss`: + +```scss +@use "components/drawer/_index.scss"; +``` + +- [ ] **Step 5: 写 css bridge 文件** + +`packages/bolt-ui/components/Drawer/style/index.ts`: + +```ts +import 'bolt-ui/theme-chalk/src/drawer.scss' +``` + +`packages/bolt-ui/components/Drawer/style/css.ts`: + +```ts +import 'bolt-ui/theme-chalk/src/drawer.scss' +``` + +- [ ] **Step 6: 提交** + +```bash +git add packages/bolt-ui/theme-chalk/src/components/drawer packages/bolt-ui/theme-chalk/src/drawer.scss packages/bolt-ui/components/Drawer/style +git -c commit.gpgsign=false commit -m "feat(bolt-ui): add Drawer theme styles and css bridge" +``` + +> 备注:bolt-ui/nuxt 模块会扫描 `theme-chalk/src/drawer.scss` 并按需注入到使用 `BoDrawer` 的 .vue/.ts/.tsx 文件。无需手动 import。 + +--- + +## Task 4: LeftSidebar 增加 mode 与 drawer 模式分支 + +**Files:** +- Modify: `app/components/index/LeftSidebar.vue`(props、emits、template 顶部) + +- [ ] **Step 1: 扩展 props 类型** + +编辑 `app/components/index/LeftSidebar.vue` 的 ` diff --git a/packages/bolt-ui/components/Drawer/style/css.ts b/packages/bolt-ui/components/Drawer/style/css.ts new file mode 100644 index 0000000..86a1735 --- /dev/null +++ b/packages/bolt-ui/components/Drawer/style/css.ts @@ -0,0 +1 @@ +import 'bolt-ui/theme-chalk/src/drawer.scss' diff --git a/packages/bolt-ui/components/Drawer/style/index.ts b/packages/bolt-ui/components/Drawer/style/index.ts new file mode 100644 index 0000000..86a1735 --- /dev/null +++ b/packages/bolt-ui/components/Drawer/style/index.ts @@ -0,0 +1 @@ +import 'bolt-ui/theme-chalk/src/drawer.scss' diff --git a/packages/bolt-ui/components/index.ts b/packages/bolt-ui/components/index.ts index 598ae4c..fd6fdc2 100644 --- a/packages/bolt-ui/components/index.ts +++ b/packages/bolt-ui/components/index.ts @@ -2,4 +2,5 @@ export * from './Button' export * from './ConfigProvider' export * from './Container' export * from './Dialog' -export * from './Mask' \ No newline at end of file +export * from './Mask' +export * from './Drawer' \ No newline at end of file diff --git a/packages/bolt-ui/hooks/index.ts b/packages/bolt-ui/hooks/index.ts index 5569b19..b5bf720 100644 --- a/packages/bolt-ui/hooks/index.ts +++ b/packages/bolt-ui/hooks/index.ts @@ -1 +1,2 @@ export * from './useClickOutside' +export * from './useMediaQuery' \ No newline at end of file diff --git a/packages/bolt-ui/hooks/useMediaQuery/index.ts b/packages/bolt-ui/hooks/useMediaQuery/index.ts new file mode 100644 index 0000000..0512f2d --- /dev/null +++ b/packages/bolt-ui/hooks/useMediaQuery/index.ts @@ -0,0 +1,31 @@ +import { onBeforeUnmount, onMounted, ref, type Ref } from 'vue' + +/** + * 响应式地跟踪 CSS 媒体查询匹配状态。 + * + * SSR 安全:服务端渲染期间始终返回 `false`,避免 hydration mismatch; + * 客户端 `onMounted` 之后才订阅 `matchMedia`。 + * + * @param query - 合法 CSS media query 字符串 + * @returns Ref - 当前是否匹配 + */ +export function useMediaQuery(query: string): Ref { + const matches = ref(false) + let mql: MediaQueryList | null = null + const handler = (e: MediaQueryListEvent) => { + matches.value = e.matches + } + + onMounted(() => { + if (typeof window === 'undefined' || !window.matchMedia) return + mql = window.matchMedia(query) + matches.value = mql.matches + mql.addEventListener('change', handler) + }) + + onBeforeUnmount(() => { + mql?.removeEventListener('change', handler) + }) + + return matches +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss new file mode 100644 index 0000000..069df0a --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/_index.scss @@ -0,0 +1,7 @@ +@use "./anim/mask-fade.scss"; +@use "./anim/panel-left-fade.scss"; +@use "./anim/panel-right-fade.scss"; +@use "./panel.scss"; +@use "./panel-left.scss"; +@use "./panel-right.scss"; +@use "./mask.scss"; diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss new file mode 100644 index 0000000..c4ad747 --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/_var.scss @@ -0,0 +1,3 @@ +$bo-drawer-bg: var(--color-canvas) !default; +$bo-drawer-shadow: 0 8px 32px rgba(20, 20, 19, 0.12) !default; +$bo-drawer-mask-bg: rgba(20, 20, 19, 0.32) !default; diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss new file mode 100644 index 0000000..0538eca --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/_var.scss @@ -0,0 +1 @@ +$drawer-time: 0.28s !default; diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss new file mode 100644 index 0000000..eb7c9c7 --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/mask-fade.scss @@ -0,0 +1,11 @@ +@use "_var" as *; + +.bo-drawer-mask-fade-enter-active, +.bo-drawer-mask-fade-leave-active { + transition: opacity $drawer-time ease; +} + +.bo-drawer-mask-fade-enter-from, +.bo-drawer-mask-fade-leave-to { + opacity: 0; +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss new file mode 100644 index 0000000..0ab70dc --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-left-fade.scss @@ -0,0 +1,11 @@ +@use "_var" as *; + +.bo-drawer-panel-left-fade-enter-active, +.bo-drawer-panel-left-fade-leave-active { + transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); +} + +.bo-drawer-panel-left-fade-enter-from, +.bo-drawer-panel-left-fade-leave-to { + transform: translateX(-100%); +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss new file mode 100644 index 0000000..609101b --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/anim/panel-right-fade.scss @@ -0,0 +1,11 @@ +@use "_var" as *; + +.bo-drawer-panel-right-fade-enter-active, +.bo-drawer-panel-right-fade-leave-active { + transition: transform $drawer-time cubic-bezier(0.4, 0, 0.2, 1); +} + +.bo-drawer-panel-right-fade-enter-from, +.bo-drawer-panel-right-fade-leave-to { + transform: translateX(100%); +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss new file mode 100644 index 0000000..027e0a7 --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/mask.scss @@ -0,0 +1,9 @@ +@use "./_var" as *; + +.bo-drawer__mask { + position: fixed; + inset: 0; + background: $bo-drawer-mask-bg; + z-index: 199; // 比 panel 低 1 + backdrop-filter: blur(2px); +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss new file mode 100644 index 0000000..4040b3e --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/panel-left.scss @@ -0,0 +1,3 @@ +.bo-drawer--left { + left: 0; +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss new file mode 100644 index 0000000..b25f21f --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/panel-right.scss @@ -0,0 +1,3 @@ +.bo-drawer--right { + right: 0; +} diff --git a/packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss b/packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss new file mode 100644 index 0000000..d0c4304 --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/components/drawer/panel.scss @@ -0,0 +1,14 @@ +@use "./_var" as *; + +.bo-drawer { + position: fixed; + top: 0; + bottom: 0; + background: $bo-drawer-bg; + box-shadow: $bo-drawer-shadow; + display: flex; + flex-direction: column; + overflow: hidden; + outline: none; + -webkit-overflow-scrolling: touch; +} diff --git a/packages/bolt-ui/theme-chalk/src/dialog.scss b/packages/bolt-ui/theme-chalk/src/dialog.scss index 1466eae..b418c29 100644 --- a/packages/bolt-ui/theme-chalk/src/dialog.scss +++ b/packages/bolt-ui/theme-chalk/src/dialog.scss @@ -38,6 +38,9 @@ #{e('content')} { margin: auto; width: 30%; + @media (max-width: 768px) { + width: 90%; + } // margin-top: 20vh; } } diff --git a/packages/bolt-ui/theme-chalk/src/drawer.scss b/packages/bolt-ui/theme-chalk/src/drawer.scss new file mode 100644 index 0000000..bdecaa9 --- /dev/null +++ b/packages/bolt-ui/theme-chalk/src/drawer.scss @@ -0,0 +1 @@ +@use "components/drawer/_index.scss";