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:
Jin Yi
2025-09-09 13:34:36 +09:00
committed by snomiao
parent 9ea1bf3727
commit 36873fe167
10 changed files with 445 additions and 161 deletions

View 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>
`
})
}

View File

@@ -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"
>