mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: add dynamic icon support for NavItem components (#5285)
* feat: add dynamic icon support for NavItem components - Created NavIcon component with switch-case based icon rendering - Added iconName prop to NavItem and NavItemData interface - Updated LeftSidePanel to pass icon names to nav items - Added sample icons to SampleModelSelector navigation (download, tag, layers, grid) - Uses i-lucide syntax without imports for better tree-shaking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add Storybook stories for navigation components - Add NavIcon.stories.ts with interactive icon selector and all icons gallery - Add NavItem.stories.ts with text customization and interactive list examples - Add LeftSidePanel.stories.ts with various navigation configurations - Remove old Navigation.stories.ts (replaced with component-specific stories) - Configure slot visibility and hide update:modelValue event in controls * refactor: simplify NavIcon component and improve type definitions * fix: add icon size specification for Lucide icons in Storybook * feature: NavItem story modified * fix: disable knip unresolved imports rule for virtual icon modules Add unresolved: 'off' to knip configuration to ignore virtual module imports from unplugin-icons (~icons/*). These are generated at build time and cannot be resolved statically. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: v-if condition added * chore: knip ignoreUnresolved added based on knip issue PR * refactor: navItem types added & deleting any type on storybook files --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,8 @@ const config: KnipConfig = {
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch'
|
||||
]
|
||||
],
|
||||
ignoreUnresolved: ['^~icons/']
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@@ -87,8 +87,6 @@
|
||||
|
||||
<template #content>
|
||||
<!-- Card Examples -->
|
||||
<!-- <div class="min-h-0 px-6 py-4 overflow-y-auto scrollbar-hide"> -->
|
||||
<!-- <h2 class="text-xxl py-4 pt-0 m-0">{{ $t('Checkpoints') }}</h2> -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<CardContainer
|
||||
v-for="i in 100"
|
||||
@@ -138,6 +136,10 @@
|
||||
<script setup lang="ts">
|
||||
import { provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import DownloadIcon from '~icons/lucide/download'
|
||||
import Grid3x3Icon from '~icons/lucide/grid-3-x-3'
|
||||
import LayersIcon from '~icons/lucide/layers'
|
||||
import TagIcon from '~icons/lucide/tag'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
@@ -175,20 +177,20 @@ const sortOptions = ref([
|
||||
])
|
||||
|
||||
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
||||
{ id: 'installed', label: 'Installed' },
|
||||
{ id: 'installed', label: 'Installed', icon: DownloadIcon },
|
||||
{
|
||||
title: 'TAGS',
|
||||
items: [
|
||||
{ id: 'tag-sd15', label: 'SD 1.5' },
|
||||
{ id: 'tag-sdxl', label: 'SDXL' },
|
||||
{ id: 'tag-utility', label: 'Utility' }
|
||||
{ id: 'tag-sd15', label: 'SD 1.5', icon: TagIcon },
|
||||
{ id: 'tag-sdxl', label: 'SDXL', icon: TagIcon },
|
||||
{ id: 'tag-utility', label: 'Utility', icon: TagIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'CATEGORIES',
|
||||
items: [
|
||||
{ id: 'cat-models', label: 'Models' },
|
||||
{ id: 'cat-nodes', label: 'Nodes' }
|
||||
{ id: 'cat-models', label: 'Models', icon: LayersIcon },
|
||||
{ id: 'cat-nodes', label: 'Nodes', icon: Grid3x3Icon }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Upload,
|
||||
X
|
||||
} from 'lucide-vue-next'
|
||||
import { provide, ref } from 'vue'
|
||||
import { h, provide, ref } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
@@ -118,20 +118,44 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
||||
{ id: 'installed', label: 'Installed' },
|
||||
{
|
||||
id: 'installed',
|
||||
label: 'Installed',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
},
|
||||
{
|
||||
title: 'TAGS',
|
||||
items: [
|
||||
{ id: 'tag-sd15', label: 'SD 1.5' },
|
||||
{ id: 'tag-sdxl', label: 'SDXL' },
|
||||
{ id: 'tag-utility', label: 'Utility' }
|
||||
{
|
||||
id: 'tag-sd15',
|
||||
label: 'SD 1.5',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
},
|
||||
{
|
||||
id: 'tag-sdxl',
|
||||
label: 'SDXL',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
},
|
||||
{
|
||||
id: 'tag-utility',
|
||||
label: 'Utility',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'CATEGORIES',
|
||||
items: [
|
||||
{ id: 'cat-models', label: 'Models' },
|
||||
{ id: 'cat-nodes', label: 'Nodes' }
|
||||
{
|
||||
id: 'cat-models',
|
||||
label: 'Models',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
},
|
||||
{
|
||||
id: 'cat-nodes',
|
||||
label: 'Nodes',
|
||||
icon: { render: () => h(Folder, { size: 14 }) } as any
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
13
src/components/widget/nav/NavIcon.vue
Normal file
13
src/components/widget/nav/NavIcon.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<span v-if="icon" class="text-xs text-neutral">
|
||||
<component :is="icon" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NavItemData } from '@/types/navTypes'
|
||||
|
||||
defineProps<{
|
||||
icon: NavItemData['icon']
|
||||
}>()
|
||||
</script>
|
||||
123
src/components/widget/nav/NavItem.stories.ts
Normal file
123
src/components/widget/nav/NavItem.stories.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { Download, Folder, Grid3x3, Layers, Tag, Wrench } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import NavItem from './NavItem.vue'
|
||||
|
||||
const meta: Meta<typeof NavItem> = {
|
||||
title: 'Components/Widget/Nav/NavItem',
|
||||
component: NavItem,
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: 'select',
|
||||
description: 'Icon component to display'
|
||||
},
|
||||
active: {
|
||||
control: 'boolean',
|
||||
description: 'Active state of the nav item'
|
||||
},
|
||||
onClick: {
|
||||
table: { disable: true }
|
||||
},
|
||||
default: {
|
||||
control: 'text',
|
||||
description: 'Text content for the nav item'
|
||||
}
|
||||
},
|
||||
args: {
|
||||
active: false,
|
||||
onClick: () => {},
|
||||
default: 'Navigation Item'
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
icon: Folder,
|
||||
active: false,
|
||||
default: 'Navigation Item'
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { NavItem },
|
||||
setup() {
|
||||
const IconComponent = args.icon
|
||||
const WrappedIcon = {
|
||||
render() {
|
||||
return h(IconComponent, { size: 14 })
|
||||
}
|
||||
}
|
||||
return { args, WrappedIcon }
|
||||
},
|
||||
template: `
|
||||
<NavItem :icon="WrappedIcon" :active="args.active" :on-click="() => {}">
|
||||
{{ args.default }}
|
||||
</NavItem>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const InteractiveList: Story = {
|
||||
render: () => ({
|
||||
components: { NavItem },
|
||||
template: `
|
||||
<div class="space-y-1">
|
||||
<NavItem
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:icon="item.wrappedIcon"
|
||||
:active="selectedId === item.id"
|
||||
:on-click="() => selectedId = item.id"
|
||||
>
|
||||
{{ item.label }}
|
||||
</NavItem>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
selectedId: 'downloads'
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const items = [
|
||||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
wrappedIcon: () => h(Download, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'models',
|
||||
label: 'Models',
|
||||
wrappedIcon: () => h(Layers, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
label: 'Nodes',
|
||||
wrappedIcon: () => h(Grid3x3, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'tags',
|
||||
label: 'Tags',
|
||||
wrappedIcon: () => h(Tag, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
wrappedIcon: () => h(Wrench, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
label: 'Default Icon',
|
||||
wrappedIcon: () => h(Folder, { size: 14 })
|
||||
}
|
||||
]
|
||||
|
||||
return { items }
|
||||
}
|
||||
}),
|
||||
parameters: {
|
||||
controls: { disable: true }
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<i-lucide:folder v-if="hasFolderIcon" class="text-xs text-neutral" />
|
||||
<NavIcon v-if="icon" :icon="icon" />
|
||||
<i-lucide:folder v-else class="text-xs text-neutral" />
|
||||
<span class="flex items-center">
|
||||
<slot></slot>
|
||||
</span>
|
||||
@@ -17,12 +18,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
hasFolderIcon = true,
|
||||
active,
|
||||
onClick
|
||||
} = defineProps<{
|
||||
hasFolderIcon?: boolean
|
||||
import { NavItemData } from '@/types/navTypes'
|
||||
|
||||
import NavIcon from './NavIcon.vue'
|
||||
|
||||
const { icon, active, onClick } = defineProps<{
|
||||
icon: NavItemData['icon']
|
||||
active?: boolean
|
||||
onClick: () => void
|
||||
}>()
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import {
|
||||
BarChart3,
|
||||
Bell,
|
||||
BookOpen,
|
||||
FolderOpen,
|
||||
GraduationCap,
|
||||
Home,
|
||||
LogOut,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
User,
|
||||
Users
|
||||
} from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import LeftSidePanel from '../panel/LeftSidePanel.vue'
|
||||
import NavItem from './NavItem.vue'
|
||||
import NavTitle from './NavTitle.vue'
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/Widget/Navigation',
|
||||
tags: ['autodocs']
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const NavigationItem: Story = {
|
||||
render: () => ({
|
||||
components: { NavItem },
|
||||
template: `
|
||||
<div class="space-y-2">
|
||||
<NavItem>Dashboard</NavItem>
|
||||
<NavItem>Projects</NavItem>
|
||||
<NavItem>Messages</NavItem>
|
||||
<NavItem>Settings</NavItem>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const CustomNavigation: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
NavTitle,
|
||||
NavItem,
|
||||
Home,
|
||||
FolderOpen,
|
||||
BarChart3,
|
||||
Users,
|
||||
BookOpen,
|
||||
GraduationCap,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
User,
|
||||
Bell,
|
||||
LogOut
|
||||
},
|
||||
template: `
|
||||
<nav class="w-64 p-4 bg-white dark-theme:bg-zinc-800 rounded-lg">
|
||||
<NavTitle title="Main Menu" />
|
||||
<div class="mt-4 space-y-2">
|
||||
<NavItem :hasFolderIcon="false"><Home :size="16" class="inline mr-2" />Dashboard</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><FolderOpen :size="16" class="inline mr-2" />Projects</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><BarChart3 :size="16" class="inline mr-2" />Analytics</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><Users :size="16" class="inline mr-2" />Team</NavItem>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<NavTitle title="Resources" />
|
||||
<div class="mt-4 space-y-2">
|
||||
<NavItem :hasFolderIcon="false"><BookOpen :size="16" class="inline mr-2" />Documentation</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><GraduationCap :size="16" class="inline mr-2" />Tutorials</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><MessageSquare :size="16" class="inline mr-2" />Community</NavItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<NavTitle title="Account" />
|
||||
<div class="mt-4 space-y-2">
|
||||
<NavItem :hasFolderIcon="false"><Settings :size="16" class="inline mr-2" />Settings</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><User :size="16" class="inline mr-2" />Profile</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><Bell :size="16" class="inline mr-2" />Notifications</NavItem>
|
||||
<NavItem :hasFolderIcon="false"><LogOut :size="16" class="inline mr-2" />Logout</NavItem>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const LeftSidePanelDemo: Story = {
|
||||
render: () => ({
|
||||
components: { LeftSidePanel, FolderOpen },
|
||||
setup() {
|
||||
const navItems = [
|
||||
{
|
||||
title: 'Workspace',
|
||||
items: [
|
||||
{ id: 'dashboard', label: 'Dashboard' },
|
||||
{ id: 'projects', label: 'Projects' },
|
||||
{ id: 'workflows', label: 'Workflows' },
|
||||
{ id: 'models', label: 'Models' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Tools',
|
||||
items: [
|
||||
{ id: 'node-editor', label: 'Node Editor' },
|
||||
{ id: 'image-browser', label: 'Image Browser' },
|
||||
{ id: 'queue-manager', label: 'Queue Manager' },
|
||||
{ id: 'extensions', label: 'Extensions' }
|
||||
]
|
||||
},
|
||||
{ id: 'settings', label: 'Settings' }
|
||||
]
|
||||
const active = ref<string | null>(null)
|
||||
return { navItems, active }
|
||||
},
|
||||
template: `
|
||||
<div class="w-full h-[560px] flex">
|
||||
<div class="w-64 rounded-lg">
|
||||
<LeftSidePanel v-model="active" :nav-items="navItems">
|
||||
<template #header-icon>
|
||||
<FolderOpen :size="14" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
Navigation
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-3 text-sm bg-gray-50 dark-theme:bg-zinc-900 border-t border-zinc-200 dark-theme:border-zinc-700">
|
||||
Active: {{ active ?? 'None' }}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
253
src/components/widget/panel/LeftSidePanel.stories.ts
Normal file
253
src/components/widget/panel/LeftSidePanel.stories.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import {
|
||||
Download,
|
||||
Folder,
|
||||
Grid3x3,
|
||||
Layers,
|
||||
Puzzle,
|
||||
Settings,
|
||||
Tag,
|
||||
Wrench,
|
||||
Zap
|
||||
} from 'lucide-vue-next'
|
||||
import { h, ref } from 'vue'
|
||||
|
||||
import LeftSidePanel from './LeftSidePanel.vue'
|
||||
|
||||
const meta: Meta<typeof LeftSidePanel> = {
|
||||
title: 'Components/Widget/Panel/LeftSidePanel',
|
||||
component: LeftSidePanel,
|
||||
argTypes: {
|
||||
'header-icon': {
|
||||
table: {
|
||||
type: { summary: 'slot' },
|
||||
defaultValue: { summary: 'undefined' }
|
||||
},
|
||||
control: false
|
||||
},
|
||||
'header-title': {
|
||||
table: {
|
||||
type: { summary: 'slot' },
|
||||
defaultValue: { summary: 'undefined' }
|
||||
},
|
||||
control: false
|
||||
},
|
||||
'onUpdate:modelValue': {
|
||||
table: { disable: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
modelValue: 'installed',
|
||||
navItems: [
|
||||
{
|
||||
id: 'installed',
|
||||
label: 'Installed',
|
||||
icon: () => h(Download, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'models',
|
||||
label: 'Models',
|
||||
icon: () => h(Layers, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
label: 'Nodes',
|
||||
icon: () => h(Grid3x3, { size: 14 })
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { LeftSidePanel, Puzzle },
|
||||
setup() {
|
||||
const selectedItem = ref(args.modelValue)
|
||||
return { args, selectedItem }
|
||||
},
|
||||
template: `
|
||||
<div style="height: 500px; width: 256px;">
|
||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
||||
<template #header-icon>
|
||||
<Puzzle :size="16" class="text-neutral" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">Navigation</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithGroups: Story = {
|
||||
args: {
|
||||
modelValue: 'tag-sd15',
|
||||
navItems: [
|
||||
{
|
||||
id: 'installed',
|
||||
label: 'Installed',
|
||||
icon: () => h(Download, { size: 14 })
|
||||
},
|
||||
{
|
||||
title: 'TAGS',
|
||||
items: [
|
||||
{
|
||||
id: 'tag-sd15',
|
||||
label: 'SD 1.5',
|
||||
icon: () => h(Tag, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'tag-sdxl',
|
||||
label: 'SDXL',
|
||||
icon: () => h(Tag, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'tag-utility',
|
||||
label: 'Utility',
|
||||
icon: () => h(Tag, { size: 14 })
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'CATEGORIES',
|
||||
items: [
|
||||
{
|
||||
id: 'cat-models',
|
||||
label: 'Models',
|
||||
icon: () => h(Layers, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'cat-nodes',
|
||||
label: 'Nodes',
|
||||
icon: () => h(Grid3x3, { size: 14 })
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { LeftSidePanel, Puzzle },
|
||||
setup() {
|
||||
const selectedItem = ref(args.modelValue)
|
||||
return { args, selectedItem }
|
||||
},
|
||||
template: `
|
||||
<div style="height: 500px; width: 256px;">
|
||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
||||
<template #header-icon>
|
||||
<Puzzle :size="16" class="text-neutral" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">Model Selector</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
<div class="mt-4 p-2 text-sm">
|
||||
Selected: {{ selectedItem }}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const DefaultIcons: Story = {
|
||||
args: {
|
||||
modelValue: 'home',
|
||||
navItems: [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Home',
|
||||
icon: () => h(Folder, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
label: 'Documents',
|
||||
icon: () => h(Folder, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'downloads',
|
||||
label: 'Downloads',
|
||||
icon: () => h(Folder, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'desktop',
|
||||
label: 'Desktop',
|
||||
icon: () => h(Folder, { size: 14 })
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { LeftSidePanel, Folder },
|
||||
setup() {
|
||||
const selectedItem = ref(args.modelValue)
|
||||
return { args, selectedItem }
|
||||
},
|
||||
template: `
|
||||
<div style="height: 400px; width: 256px;">
|
||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
||||
<template #header-icon>
|
||||
<Folder :size="16" class="text-neutral" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">Files</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const LongLabels: Story = {
|
||||
args: {
|
||||
modelValue: 'general',
|
||||
navItems: [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General Settings',
|
||||
icon: () => h(() => Wrench, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
label: 'Appearance & Themes Configuration',
|
||||
icon: () => h(() => Wrench, { size: 14 })
|
||||
},
|
||||
{
|
||||
title: 'ADVANCED OPTIONS',
|
||||
items: [
|
||||
{
|
||||
id: 'performance',
|
||||
label: 'Performance & Optimization Settings',
|
||||
icon: () => h(() => Zap, { size: 14 })
|
||||
},
|
||||
{
|
||||
id: 'experimental',
|
||||
label: 'Experimental Features (Beta)',
|
||||
icon: () => h(() => Puzzle, { size: 14 })
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { LeftSidePanel, Settings },
|
||||
setup() {
|
||||
const selectedItem = ref(args.modelValue)
|
||||
return { args, selectedItem }
|
||||
},
|
||||
template: `
|
||||
<div style="height: 500px; width: 256px;">
|
||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
||||
<template #header-icon>
|
||||
<Settings :size="16" class="text-neutral" />
|
||||
</template>
|
||||
<template #header-title>
|
||||
<span class="text-neutral text-base">Settings</span>
|
||||
</template>
|
||||
</LeftSidePanel>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<NavItem
|
||||
v-for="subItem in item.items"
|
||||
:key="subItem.id"
|
||||
:icon="subItem.icon"
|
||||
:active="activeItem === subItem.id"
|
||||
@click="activeItem = subItem.id"
|
||||
>
|
||||
@@ -22,6 +23,7 @@
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<NavItem
|
||||
:icon="item.icon"
|
||||
:active="activeItem === item.id"
|
||||
@click="activeItem = item.id"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { DefineComponent, FunctionalComponent } from 'vue'
|
||||
|
||||
export interface NavItemData {
|
||||
id: string
|
||||
label: string
|
||||
icon: DefineComponent | FunctionalComponent
|
||||
}
|
||||
|
||||
export interface NavGroupData {
|
||||
|
||||
Reference in New Issue
Block a user