feat(menu): collapsible category
This commit is contained in:
		
							parent
							
								
									849981d1ec
								
							
						
					
					
						commit
						24ba0ff5fa
					
				
							
								
								
									
										136
									
								
								src/components/CollapsibleToolMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/components/CollapsibleToolMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | |||||||
|  | <template> | ||||||
|  |   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> | ||||||
|  |     <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> | ||||||
|  |       <n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" /> | ||||||
|  | 
 | ||||||
|  |       <span> | ||||||
|  |         {{ name }} | ||||||
|  |       </span> | ||||||
|  |     </n-text> | ||||||
|  | 
 | ||||||
|  |     <n-collapse-transition :show="!isCollapsed"> | ||||||
|  |       <div class="menu-wrapper"> | ||||||
|  |         <div class="toggle-bar" @click="toggleCategoryCollapse({ name })" /> | ||||||
|  | 
 | ||||||
|  |         <n-menu | ||||||
|  |           class="menu" | ||||||
|  |           :value="(route.name as string)" | ||||||
|  |           :collapsed-width="64" | ||||||
|  |           :collapsed-icon-size="22" | ||||||
|  |           :options="tools" | ||||||
|  |           :indent="8" | ||||||
|  |           :default-expand-all="true" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </n-collapse-transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||||
|  | import { ChevronRight } from '@vicons/tabler'; | ||||||
|  | import { useStorage } from '@vueuse/core'; | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { toRefs, computed, h } from 'vue'; | ||||||
|  | import { RouterLink, useRoute } from 'vue-router'; | ||||||
|  | import MenuIconItem from './MenuIconItem.vue'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); | ||||||
|  | const { toolsByCategory } = toRefs(props); | ||||||
|  | const route = useRoute(); | ||||||
|  | 
 | ||||||
|  | const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name }); | ||||||
|  | const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool }); | ||||||
|  | 
 | ||||||
|  | const collapsedCategories = useStorage<Record<string, boolean>>( | ||||||
|  |   'menu-tool-option:collapsed-categories', | ||||||
|  |   {}, | ||||||
|  |   undefined, | ||||||
|  |   { | ||||||
|  |     deep: true, | ||||||
|  |     serializer: { | ||||||
|  |       read: (v) => (v ? JSON.parse(v) : null), | ||||||
|  |       write: (v) => JSON.stringify(v), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | function toggleCategoryCollapse({ name }: { name: string }) { | ||||||
|  |   collapsedCategories.value[name] = !collapsedCategories.value[name]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const menuOptions = computed(() => | ||||||
|  |   toolsByCategory.value.map(({ name, components }) => ({ | ||||||
|  |     name: name, | ||||||
|  |     isCollapsed: collapsedCategories.value[name], | ||||||
|  |     tools: components.map((tool) => ({ | ||||||
|  |       label: makeLabel(tool), | ||||||
|  |       icon: makeIcon(tool), | ||||||
|  |       key: tool.name, | ||||||
|  |     })), | ||||||
|  |   })), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const themeVars = useThemeVars(); | ||||||
|  | 
 | ||||||
|  | console.log(themeVars.value); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped lang="less"> | ||||||
|  | .category-name { | ||||||
|  |   font-size: 0.93em; | ||||||
|  |   padding: 12px 0 0px 0; | ||||||
|  |   cursor: pointer; | ||||||
|  | 
 | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   align-items: center; | ||||||
|  |   .n-icon { | ||||||
|  |     transition: transform ease 0.5s; | ||||||
|  |     transform: rotate(90deg); | ||||||
|  |     margin: 0 10px 0 7px; | ||||||
|  |     opacity: 0.5; | ||||||
|  | 
 | ||||||
|  |     &.rotated { | ||||||
|  |       transform: rotate(0deg); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .menu-wrapper { | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: row; | ||||||
|  |   .menu { | ||||||
|  |     flex: 1; | ||||||
|  |     margin-bottom: 5px; | ||||||
|  | 
 | ||||||
|  |     ::v-deep(.n-menu-item-content::before) { | ||||||
|  |       left: 0; | ||||||
|  |       right: 13px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .toggle-bar { | ||||||
|  |     width: 25px; | ||||||
|  |     opacity: 0.1; | ||||||
|  |     transition: opacity ease 0.2s; | ||||||
|  |     position: relative; | ||||||
|  |     cursor: pointer; | ||||||
|  | 
 | ||||||
|  |     &::before { | ||||||
|  |       width: 2px; | ||||||
|  |       height: 100%; | ||||||
|  |       content: ' '; | ||||||
|  |       background-color: v-bind('themeVars.textColor3'); | ||||||
|  |       border-radius: 2px; | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 14.5px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:hover { | ||||||
|  |       opacity: 0.5; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -1,5 +1,5 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { NIcon, useThemeVars, type MenuGroupOption } from 'naive-ui'; | import { NIcon, useThemeVars, type MenuGroupOption, type MenuOption } from 'naive-ui'; | ||||||
| import { computed, h } from 'vue'; | import { computed, h } from 'vue'; | ||||||
| import { RouterLink, useRoute } from 'vue-router'; | import { RouterLink, useRoute } from 'vue-router'; | ||||||
| import { Heart, Menu2, Home2 } from '@vicons/tabler'; | import { Heart, Menu2, Home2 } from '@vicons/tabler'; | ||||||
| @ -7,9 +7,10 @@ import { toolsByCategory } from '@/tools'; | |||||||
| import { useStyleStore } from '@/stores/style.store'; | import { useStyleStore } from '@/stores/style.store'; | ||||||
| import { config } from '@/config'; | import { config } from '@/config'; | ||||||
| import MenuIconItem from '@/components/MenuIconItem.vue'; | import MenuIconItem from '@/components/MenuIconItem.vue'; | ||||||
| import type { Tool } from '@/tools/tools.types'; | import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||||
| import { useToolStore } from '@/tools/tools.store'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | import { useTracker } from '@/modules/tracker/tracker.services'; | ||||||
|  | import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; | ||||||
| import SearchBar from '../components/SearchBar.vue'; | import SearchBar from '../components/SearchBar.vue'; | ||||||
| import HeroGradient from '../assets/hero-gradient.svg?component'; | import HeroGradient from '../assets/hero-gradient.svg?component'; | ||||||
| import MenuLayout from '../components/MenuLayout.vue'; | import MenuLayout from '../components/MenuLayout.vue'; | ||||||
| @ -21,30 +22,14 @@ const styleStore = useStyleStore(); | |||||||
| const version = config.app.version; | const version = config.app.version; | ||||||
| const commitSha = config.app.lastCommitSha.slice(0, 7); | const commitSha = config.app.lastCommitSha.slice(0, 7); | ||||||
| 
 | 
 | ||||||
| const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name }); |  | ||||||
| const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool }); |  | ||||||
| 
 |  | ||||||
| const { tracker } = useTracker(); | const { tracker } = useTracker(); | ||||||
| 
 | 
 | ||||||
| const toolStore = useToolStore(); | const toolStore = useToolStore(); | ||||||
| 
 | 
 | ||||||
| const menuOptions = computed<MenuGroupOption[]>(() => | const tools = computed<ToolCategory[]>(() => [ | ||||||
|   [ |   ...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []), | ||||||
|     ...(toolStore.favoriteTools.length > 0 |   ...toolsByCategory, | ||||||
|       ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] | ]); | ||||||
|       : []), |  | ||||||
|     ...toolsByCategory, |  | ||||||
|   ].map((category) => ({ |  | ||||||
|     label: category.name, |  | ||||||
|     key: category.name, |  | ||||||
|     type: 'group', |  | ||||||
|     children: category.components.map((tool) => ({ |  | ||||||
|       label: makeLabel(tool), |  | ||||||
|       icon: makeIcon(tool), |  | ||||||
|       key: tool.name, |  | ||||||
|     })), |  | ||||||
|   })), |  | ||||||
| ); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
| @ -64,14 +49,7 @@ const menuOptions = computed<MenuGroupOption[]>(() => | |||||||
|           <navbar-buttons /> |           <navbar-buttons /> | ||||||
|         </n-space> |         </n-space> | ||||||
| 
 | 
 | ||||||
|         <n-menu |         <collapsible-tool-menu :tools-by-category="tools" /> | ||||||
|           class="menu" |  | ||||||
|           :value="(route.name as string)" |  | ||||||
|           :collapsed-width="64" |  | ||||||
|           :collapsed-icon-size="22" |  | ||||||
|           :options="menuOptions" |  | ||||||
|           :indent="20" |  | ||||||
|         /> |  | ||||||
| 
 | 
 | ||||||
|         <div class="footer"> |         <div class="footer"> | ||||||
|           <div> |           <div> | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user