diff --git a/.storybook/README.md b/.storybook/README.md index eff19a67d..902397471 100644 --- a/.storybook/README.md +++ b/.storybook/README.md @@ -209,4 +209,22 @@ This Storybook setup includes: - PrimeVue component library integration - Proper alias resolution for `@/` imports -For component-specific examples, see the NodePreview stories in `src/components/node/`. +## Icon Usage in Storybook + +In this project, the `` syntax from unplugin-icons is not supported in Storybook. + +**Example:** + +```vue + + + +``` + +This approach ensures icons render correctly in Storybook and remain consistent with the rest of the app. + diff --git a/package-lock.json b/package-lock.json index 75311a1c8..2fa4e751f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,7 @@ "identity-obj-proxy": "^3.0.0", "knip": "^5.62.0", "lint-staged": "^15.2.7", + "lucide-vue-next": "^0.540.0", "postcss": "^8.4.39", "prettier": "^3.3.2", "storybook": "^9.1.1", @@ -12224,6 +12225,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-vue-next": { + "version": "0.540.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.540.0.tgz", + "integrity": "sha512-H7qhKVNKLyoFMo05pWcGSWBiLPiI3zJmWV65SuXWHlrIGIcvDer10xAyWcRJ0KLzIH5k5+yi7AGw/Xi1VF8Pbw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index 5f9f81745..305b33f7e 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "vitest": "^2.0.0", "vue-tsc": "^2.1.10", "zip-dir": "^2.0.0", - "zod-to-json-schema": "^3.24.1" + "zod-to-json-schema": "^3.24.1", + "lucide-vue-next": "^0.540.0" }, "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/src/components/button/IconButton.stories.ts b/src/components/button/IconButton.stories.ts new file mode 100644 index 000000000..a0194a240 --- /dev/null +++ b/src/components/button/IconButton.stories.ts @@ -0,0 +1,145 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Bell, Download, Heart, Settings, Trophy, X } from 'lucide-vue-next' + +import IconButton from './IconButton.vue' + +const meta: Meta = { + title: 'Components/Button/IconButton', + component: IconButton, + tags: ['autodocs'], + argTypes: { + size: { + control: { type: 'select' }, + options: ['sm', 'md'] + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'] + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + render: (args) => ({ + components: { IconButton, Trophy }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + render: (args) => ({ + components: { IconButton, Settings }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + render: (args) => ({ + components: { IconButton, X }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'transparent', + size: 'md' + } +} + +export const Small: Story = { + render: (args) => ({ + components: { IconButton, Bell }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'secondary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { IconButton, Trophy, Settings, X, Bell, Heart, Download }, + template: ` +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + + + + +
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/button/IconGroup.stories.ts b/src/components/button/IconGroup.stories.ts new file mode 100644 index 000000000..1eb0d6e0a --- /dev/null +++ b/src/components/button/IconGroup.stories.ts @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Download, ExternalLink, Heart } from 'lucide-vue-next' + +import IconButton from './IconButton.vue' +import IconGroup from './IconGroup.vue' + +const meta: Meta = { + title: 'Components/Button/IconGroup', + component: IconGroup, + parameters: { + layout: 'centered' + } +} +export default meta + +type Story = StoryObj + +export const Basic: Story = { + render: () => ({ + components: { IconGroup, IconButton, Download, ExternalLink, Heart }, + template: ` + + + + + + + + + + + + ` + }) +} diff --git a/src/components/button/IconTextButton.stories.ts b/src/components/button/IconTextButton.stories.ts new file mode 100644 index 000000000..3c08c418a --- /dev/null +++ b/src/components/button/IconTextButton.stories.ts @@ -0,0 +1,221 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { + ChevronLeft, + ChevronRight, + Download, + Package, + Save, + Settings, + Trash2, + X +} from 'lucide-vue-next' + +import IconTextButton from './IconTextButton.vue' + +const meta: Meta = { + title: 'Components/Button/IconTextButton', + component: IconTextButton, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md'] + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'] + }, + iconPosition: { + control: { type: 'select' }, + options: ['left', 'right'] + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + render: (args) => ({ + components: { IconTextButton, Package }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Deploy', + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + render: (args) => ({ + components: { IconTextButton, Settings }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Settings', + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + render: (args) => ({ + components: { IconTextButton, X }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Cancel', + type: 'transparent', + size: 'md' + } +} + +export const WithIconRight: Story = { + render: (args) => ({ + components: { IconTextButton, ChevronRight }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Next', + type: 'primary', + size: 'md', + iconPosition: 'right' + } +} + +export const Small: Story = { + render: (args) => ({ + components: { IconTextButton, Save }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Save', + type: 'primary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { + IconTextButton, + Download, + Settings, + Trash2, + ChevronRight, + ChevronLeft, + Save + }, + template: ` +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + + + + +
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/button/MoreButton.stories.ts b/src/components/button/MoreButton.stories.ts new file mode 100644 index 000000000..1a2171b09 --- /dev/null +++ b/src/components/button/MoreButton.stories.ts @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Download, ScrollText } from 'lucide-vue-next' + +import IconTextButton from './IconTextButton.vue' +import MoreButton from './MoreButton.vue' + +const meta: Meta = { + title: 'Components/Button/MoreButton', + component: MoreButton, + parameters: { + layout: 'centered' + }, + argTypes: {} +} +export default meta + +type Story = StoryObj + +export const Basic: Story = { + render: () => ({ + components: { MoreButton, IconTextButton, Download, ScrollText }, + template: ` +
+ + + +
+ ` + }) +} diff --git a/src/components/button/TextButton.stories.ts b/src/components/button/TextButton.stories.ts new file mode 100644 index 000000000..c21dc280e --- /dev/null +++ b/src/components/button/TextButton.stories.ts @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import TextButton from './TextButton.vue' + +const meta: Meta = { + title: 'Components/Button/TextButton', + component: TextButton, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text', + defaultValue: 'Click me' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md'], + defaultValue: 'md' + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'], + defaultValue: 'primary' + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + args: { + label: 'Primary Button', + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + args: { + label: 'Secondary Button', + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + args: { + label: 'Transparent Button', + type: 'transparent', + size: 'md' + } +} + +export const Small: Story = { + args: { + label: 'Small Button', + type: 'primary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { TextButton }, + template: ` +
+
+ + +
+
+ + +
+
+ + +
+
+ ` + }) +} diff --git a/src/components/card/Card.stories.ts b/src/components/card/Card.stories.ts new file mode 100644 index 000000000..0d8bf4385 --- /dev/null +++ b/src/components/card/Card.stories.ts @@ -0,0 +1,665 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { + Download, + Folder, + Heart, + Info, + MoreVertical, + Star, + Upload +} from 'lucide-vue-next' +import { ref } from 'vue' + +import IconButton from '../button/IconButton.vue' +import SquareChip from '../chip/SquareChip.vue' +import CardBottom from './CardBottom.vue' +import CardContainer from './CardContainer.vue' +import CardDescription from './CardDescription.vue' +import CardTitle from './CardTitle.vue' +import CardTop from './CardTop.vue' + +interface CardStoryArgs { + // CardContainer props + containerRatio: 'square' | 'portrait' | 'tallPortrait' + maxWidth: number + minWidth: number + + // CardTop props + topRatio: 'square' | 'landscape' + + // Content props + showTopLeft: boolean + showTopRight: boolean + showBottomLeft: boolean + showBottomRight: boolean + showTitle: boolean + showDescription: boolean + title: string + description: string + + // Visual props + backgroundColor: string + showImage: boolean + imageUrl: string + + // Tag props + tags: string[] + showFileSize: boolean + fileSize: string + showFileType: boolean + fileType: string +} + +const meta: Meta = { + title: 'Components/Card/Card', + argTypes: { + containerRatio: { + control: 'select', + options: ['square', 'portrait', 'tallPortrait'], + description: 'Card container aspect ratio' + }, + maxWidth: { + control: { type: 'range', min: 200, max: 600, step: 10 }, + description: 'Maximum width in pixels' + }, + minWidth: { + control: { type: 'range', min: 150, max: 400, step: 10 }, + description: 'Minimum width in pixels' + }, + topRatio: { + control: 'select', + options: ['square', 'landscape'], + description: 'Top section aspect ratio' + }, + showTopLeft: { + control: 'boolean', + description: 'Show top-left slot content' + }, + showTopRight: { + control: 'boolean', + description: 'Show top-right slot content' + }, + showBottomLeft: { + control: 'boolean', + description: 'Show bottom-left slot content' + }, + showBottomRight: { + control: 'boolean', + description: 'Show bottom-right slot content' + }, + showTitle: { + control: 'boolean', + description: 'Show card title' + }, + showDescription: { + control: 'boolean', + description: 'Show card description' + }, + title: { + control: 'text', + description: 'Card title text' + }, + description: { + control: 'text', + description: 'Card description text' + }, + backgroundColor: { + control: 'color', + description: 'Background color for card top' + }, + showImage: { + control: 'boolean', + description: 'Show image instead of color background' + }, + imageUrl: { + control: 'text', + description: 'Image URL for card top' + }, + tags: { + control: 'object', + description: 'Tags to display (array of strings)' + }, + showFileSize: { + control: 'boolean', + description: 'Show file size tag' + }, + fileSize: { + control: 'text', + description: 'File size text' + }, + showFileType: { + control: 'boolean', + description: 'Show file type tag' + }, + fileType: { + control: 'text', + description: 'File type text' + } + } +} + +export default meta +type Story = StoryObj + +const createCardTemplate = (args: CardStoryArgs) => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + IconButton, + SquareChip, + Info, + Folder, + Heart, + Download, + Star, + Upload, + MoreVertical + }, + setup() { + const favorited = ref(false) + const toggleFavorite = () => { + favorited.value = !favorited.value + } + + return { + args, + favorited, + toggleFavorite + } + }, + template: ` +
+ + + + + +
+ ` +}) + +export const Default: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'portrait', + maxWidth: 300, + minWidth: 200, + topRatio: 'square', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Model Name', + description: + 'This is a detailed description of the model that can span multiple lines', + backgroundColor: '#3b82f6', + showImage: false, + imageUrl: '', + tags: ['LoRA', 'SDXL'], + showFileSize: true, + fileSize: '1.2 MB', + showFileType: true, + fileType: 'safetensors' + } +} + +export const SquareCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'square', + maxWidth: 400, + minWidth: 250, + topRatio: 'landscape', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Workflow Bundle', + description: + 'Complete workflow for image generation with all necessary nodes', + backgroundColor: '#10b981', + showImage: false, + imageUrl: '', + tags: ['Workflow'], + showFileSize: true, + fileSize: '245 KB', + showFileType: true, + fileType: 'json' + } +} + +export const TallPortraitCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'tallPortrait', + maxWidth: 280, + minWidth: 180, + topRatio: 'square', + showTopLeft: true, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Premium Model', + description: + 'High-quality photorealistic model trained on professional photography', + backgroundColor: '#8b5cf6', + showImage: false, + imageUrl: '', + tags: ['SD 1.5', 'Checkpoint'], + showFileSize: true, + fileSize: '2.1 GB', + showFileType: true, + fileType: 'ckpt' + } +} + +export const ImageCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'portrait', + maxWidth: 350, + minWidth: 220, + topRatio: 'square', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Generated Image', + description: 'Created with DreamShaper XL', + backgroundColor: '#3b82f6', + showImage: true, + imageUrl: 'https://picsum.photos/400/400', + tags: ['Output'], + showFileSize: true, + fileSize: '856 KB', + showFileType: true, + fileType: 'png' + } +} + +export const MinimalCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'square', + maxWidth: 300, + minWidth: 200, + topRatio: 'landscape', + showTopLeft: false, + showTopRight: false, + showBottomLeft: false, + showBottomRight: false, + showTitle: true, + showDescription: false, + title: 'Simple Card', + description: '', + backgroundColor: '#64748b', + showImage: false, + imageUrl: '', + tags: [], + showFileSize: false, + fileSize: '', + showFileType: false, + fileType: '' + } +} + +export const FullFeaturedCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'tallPortrait', + maxWidth: 320, + minWidth: 240, + topRatio: 'square', + showTopLeft: true, + showTopRight: true, + showBottomLeft: true, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Ultimate Model Pack', + description: + 'Complete collection with checkpoints, LoRAs, embeddings, and VAE models for professional use', + backgroundColor: '#ef4444', + showImage: false, + imageUrl: '', + tags: ['Bundle', 'Premium', 'SDXL'], + showFileSize: true, + fileSize: '5.4 GB', + showFileType: true, + fileType: 'pack' + } +} + +export const GridOfCards: Story = { + render: () => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + IconButton, + SquareChip, + Info, + Folder, + Heart, + Download + }, + setup() { + const cards = ref([ + { + id: 1, + title: 'Realistic Vision', + description: 'Photorealistic model for portraits', + color: 'from-blue-400 to-blue-600', + ratio: 'portrait' as const, + tags: ['SD 1.5'], + size: '2.1 GB' + }, + { + id: 2, + title: 'DreamShaper XL', + description: 'Artistic style model with enhanced details', + color: 'from-purple-400 to-pink-600', + ratio: 'portrait' as const, + tags: ['SDXL'], + size: '6.5 GB' + }, + { + id: 3, + title: 'Anime LoRA', + description: 'Character style LoRA', + color: 'from-green-400 to-teal-600', + ratio: 'portrait' as const, + tags: ['LoRA'], + size: '144 MB' + }, + { + id: 4, + title: 'VAE Model', + description: 'Enhanced color VAE', + color: 'from-orange-400 to-red-600', + ratio: 'portrait' as const, + tags: ['VAE'], + size: '335 MB' + }, + { + id: 5, + title: 'Workflow Bundle', + description: 'Complete workflow setup', + color: 'from-indigo-400 to-blue-600', + ratio: 'portrait' as const, + tags: ['Workflow'], + size: '45 KB' + }, + { + id: 6, + title: 'Embedding Pack', + description: 'Negative embeddings collection', + color: 'from-yellow-400 to-orange-600', + ratio: 'portrait' as const, + tags: ['Embedding'], + size: '2.3 MB' + } + ]) + + return { cards } + }, + template: ` +
+

Model Gallery

+
+ + + + + +
+
+ ` + }) +} + +export const ResponsiveGrid: Story = { + render: () => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + SquareChip + }, + setup() { + const generateCards = ( + count: number, + ratio: 'square' | 'portrait' | 'tallPortrait' + ) => { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + title: `Model ${i + 1}`, + description: `Description for model ${i + 1}`, + ratio, + color: `hsl(${(i * 60) % 360}, 70%, 60%)` + })) + } + + const squareCards = ref(generateCards(4, 'square')) + const portraitCards = ref(generateCards(6, 'portrait')) + const tallCards = ref(generateCards(5, 'tallPortrait')) + + return { + squareCards, + portraitCards, + tallCards + } + }, + template: ` +
+
+

Square Cards (1:1)

+
+ + + + +
+
+ +
+

Portrait Cards (2:3)

+
+ + + + +
+
+ +
+

Tall Portrait Cards (2:4)

+
+ + + + +
+
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/card/CardContainer.vue b/src/components/card/CardContainer.vue index 597429a9e..ebcac78e8 100644 --- a/src/components/card/CardContainer.vue +++ b/src/components/card/CardContainer.vue @@ -13,8 +13,8 @@ const { maxWidth, minWidth } = defineProps<{ - maxWidth: number - minWidth: number + maxWidth?: number + minWidth?: number ratio?: 'square' | 'portrait' | 'tallPortrait' }>() @@ -31,8 +31,12 @@ const containerClasses = computed(() => { return `${baseClasses} ${ratioClasses[ratio]}` }) -const containerStyle = computed(() => ({ - maxWidth: `${maxWidth}px`, - minWidth: `${minWidth}px` -})) +const containerStyle = computed(() => + maxWidth || minWidth + ? { + maxWidth: `${maxWidth}px`, + minWidth: `${minWidth}px` + } + : {} +) diff --git a/src/components/chip/SquareChip.stories.ts b/src/components/chip/SquareChip.stories.ts new file mode 100644 index 000000000..6ae12b1e9 --- /dev/null +++ b/src/components/chip/SquareChip.stories.ts @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import SquareChip from './SquareChip.vue' + +const meta: Meta = { + title: 'Components/SquareChip', + component: SquareChip, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text', + defaultValue: 'Tag' + } + } +} + +export default meta +type Story = StoryObj + +export const TagList: Story = { + render: () => ({ + components: { SquareChip }, + template: ` +
+ + + + + + + + +
+ ` + }) +} diff --git a/src/components/input/MultiSelect.stories.ts b/src/components/input/MultiSelect.stories.ts new file mode 100644 index 000000000..fa8d7668c --- /dev/null +++ b/src/components/input/MultiSelect.stories.ts @@ -0,0 +1,151 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import MultiSelect from './MultiSelect.vue' + +const meta: Meta = { + title: 'Components/Input/MultiSelect', + component: MultiSelect, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text' + }, + options: { + control: 'object' + } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ({ + components: { MultiSelect }, + setup() { + const selected = ref([]) + const options = [ + { name: 'Vue', value: 'vue' }, + { name: 'React', value: 'react' }, + { name: 'Angular', value: 'angular' }, + { name: 'Svelte', value: 'svelte' } + ] + return { selected, options, args } + }, + template: ` +
+ +
+

Selected: {{ selected.length > 0 ? selected.map(s => s.name).join(', ') : 'None' }}

+
+
+ ` + }) +} + +export const WithPreselectedValues: Story = { + render: () => ({ + components: { MultiSelect }, + setup() { + const options = [ + { name: 'JavaScript', value: 'js' }, + { name: 'TypeScript', value: 'ts' }, + { name: 'Python', value: 'python' }, + { name: 'Go', value: 'go' }, + { name: 'Rust', value: 'rust' } + ] + const selected = ref([options[0], options[1]]) + return { selected, options } + }, + template: ` +
+ +
+

Selected: {{ selected.map(s => s.name).join(', ') }}

+
+
+ ` + }) +} + +export const MultipleSelectors: Story = { + render: () => ({ + components: { MultiSelect }, + setup() { + const frameworkOptions = ref([ + { name: 'Vue', value: 'vue' }, + { name: 'React', value: 'react' }, + { name: 'Angular', value: 'angular' }, + { name: 'Svelte', value: 'svelte' } + ]) + + const projectOptions = ref([ + { name: 'Project A', value: 'proj-a' }, + { name: 'Project B', value: 'proj-b' }, + { name: 'Project C', value: 'proj-c' }, + { name: 'Project D', value: 'proj-d' } + ]) + + const tagOptions = ref([ + { name: 'Frontend', value: 'frontend' }, + { name: 'Backend', value: 'backend' }, + { name: 'Database', value: 'database' }, + { name: 'DevOps', value: 'devops' }, + { name: 'Testing', value: 'testing' } + ]) + + const selectedFrameworks = ref([]) + const selectedProjects = ref([]) + const selectedTags = ref([]) + + return { + frameworkOptions, + projectOptions, + tagOptions, + selectedFrameworks, + selectedProjects, + selectedTags + } + }, + template: ` +
+
+ + + +
+ +
+

Current Selection:

+
+

Frameworks: {{ selectedFrameworks.length > 0 ? selectedFrameworks.map(s => s.name).join(', ') : 'None' }}

+

Projects: {{ selectedProjects.length > 0 ? selectedProjects.map(s => s.name).join(', ') : 'None' }}

+

Tags: {{ selectedTags.length > 0 ? selectedTags.map(s => s.name).join(', ') : 'None' }}

+
+
+
+ ` + }) +} diff --git a/src/components/input/SearchBox.stories.ts b/src/components/input/SearchBox.stories.ts new file mode 100644 index 000000000..da2ff7458 --- /dev/null +++ b/src/components/input/SearchBox.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import SearchBox from './SearchBox.vue' + +const meta: Meta = { + title: 'Components/Input/SearchBox', + component: SearchBox, + tags: ['autodocs'], + argTypes: { + placeHolder: { + control: 'text' + } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ({ + components: { SearchBox }, + setup() { + const searchText = ref('') + return { searchText, args } + }, + template: ` +
+ +
+ ` + }) +} diff --git a/src/components/input/SingleSelect.stories.ts b/src/components/input/SingleSelect.stories.ts new file mode 100644 index 000000000..ba802197c --- /dev/null +++ b/src/components/input/SingleSelect.stories.ts @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ArrowUpDown } from 'lucide-vue-next' +import { ref } from 'vue' + +import SingleSelect from './SingleSelect.vue' + +const meta: Meta = { + title: 'Components/Input/SingleSelect', + component: SingleSelect, + tags: ['autodocs'], + argTypes: { + label: { control: 'text' } + } +} + +export default meta +export type Story = StoryObj + +const sampleOptions = [ + { name: 'Popular', value: 'popular' }, + { name: 'Newest', value: 'newest' }, + { name: 'Oldest', value: 'oldest' }, + { name: 'A → Z', value: 'az' }, + { name: 'Z → A', value: 'za' } +] + +export const Default: Story = { + render: (args) => ({ + components: { SingleSelect }, + setup() { + const selected = ref(null) + const options = sampleOptions + return { selected, options, args } + }, + template: ` +
+ +
+

Selected: {{ selected ?? 'None' }}

+
+
+ ` + }), + args: { label: 'Sorting Type' } +} + +export const WithIcon: Story = { + render: () => ({ + components: { SingleSelect, ArrowUpDown }, + setup() { + const selected = ref('popular') + const options = sampleOptions + return { selected, options } + }, + template: ` +
+ + + +
+

Selected: {{ selected }}

+
+
+ ` + }) +} + +export const Preselected: Story = { + render: () => ({ + components: { SingleSelect }, + setup() { + const selected = ref('newest') + const options = sampleOptions + return { selected, options } + }, + template: ` + + ` + }) +} + +export const AllVariants: Story = { + render: () => ({ + components: { SingleSelect, ArrowUpDown }, + setup() { + const options = sampleOptions + const a = ref(null) + const b = ref('popular') + const c = ref('az') + return { options, a, b, c } + }, + template: ` +
+
+ +
+
+ + + +
+
+ +
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/widget/layout/BaseWidget.stories.ts b/src/components/widget/layout/BaseWidget.stories.ts new file mode 100644 index 000000000..31e988cb2 --- /dev/null +++ b/src/components/widget/layout/BaseWidget.stories.ts @@ -0,0 +1,556 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { + Download, + Filter, + Folder, + Info, + PanelLeft, + PanelLeftClose, + PanelRight, + PanelRightClose, + Puzzle, + Scroll, + Settings, + Upload, + X +} from 'lucide-vue-next' +import { provide, ref } from 'vue' + +import IconButton from '@/components/button/IconButton.vue' +import IconTextButton from '@/components/button/IconTextButton.vue' +import MoreButton from '@/components/button/MoreButton.vue' +import CardBottom from '@/components/card/CardBottom.vue' +import CardContainer from '@/components/card/CardContainer.vue' +import CardTop from '@/components/card/CardTop.vue' +import SquareChip from '@/components/chip/SquareChip.vue' +import MultiSelect from '@/components/input/MultiSelect.vue' +import SearchBox from '@/components/input/SearchBox.vue' +import SingleSelect from '@/components/input/SingleSelect.vue' +import type { NavGroupData, NavItemData } from '@/types/navTypes' +import { OnCloseKey } from '@/types/widgetTypes' + +import LeftSidePanel from '../panel/LeftSidePanel.vue' +import RightSidePanel from '../panel/RightSidePanel.vue' +import BaseWidgetLayout from './BaseWidgetLayout.vue' + +interface StoryArgs { + contentTitle: string + hasLeftPanel: boolean + hasRightPanel: boolean + hasHeader: boolean + hasContentFilter: boolean + hasHeaderRightArea: boolean + cardCount: number +} + +const meta: Meta = { + title: 'Components/Widget/Layout/BaseWidgetLayout', + argTypes: { + contentTitle: { + control: 'text', + description: 'Title shown when no left panel is present' + }, + hasLeftPanel: { + control: 'boolean', + description: 'Toggle left panel visibility' + }, + hasRightPanel: { + control: 'boolean', + description: 'Toggle right panel visibility' + }, + hasHeader: { + control: 'boolean', + description: 'Toggle header visibility' + }, + hasContentFilter: { + control: 'boolean', + description: 'Toggle content filter visibility' + }, + hasHeaderRightArea: { + control: 'boolean', + description: 'Toggle header right area visibility' + }, + cardCount: { + control: { type: 'range', min: 0, max: 50, step: 1 }, + description: 'Number of cards to display in content' + } + } +} + +export default meta +type Story = StoryObj + +const createStoryTemplate = (args: StoryArgs) => ({ + components: { + BaseWidgetLayout, + LeftSidePanel, + RightSidePanel, + SearchBox, + MultiSelect, + SingleSelect, + IconButton, + IconTextButton, + MoreButton, + CardContainer, + CardTop, + CardBottom, + SquareChip, + Settings, + Upload, + Download, + Scroll, + Info, + Filter, + Folder, + Puzzle, + PanelLeft, + PanelLeftClose, + PanelRight, + PanelRightClose, + X + }, + setup() { + const t = (k: string) => k + + const onClose = () => { + console.log('OnClose invoked') + } + provide(OnCloseKey, onClose) + + const tempNavigation = ref<(NavItemData | NavGroupData)[]>([ + { id: 'installed', label: 'Installed' }, + { + title: 'TAGS', + items: [ + { id: 'tag-sd15', label: 'SD 1.5' }, + { id: 'tag-sdxl', label: 'SDXL' }, + { id: 'tag-utility', label: 'Utility' } + ] + }, + { + title: 'CATEGORIES', + items: [ + { id: 'cat-models', label: 'Models' }, + { id: 'cat-nodes', label: 'Nodes' } + ] + } + ]) + const selectedNavItem = ref('installed') + + const searchQuery = ref('') + + const frameworkOptions = ref([ + { name: 'Vue', value: 'vue' }, + { name: 'React', value: 'react' }, + { name: 'Angular', value: 'angular' }, + { name: 'Svelte', value: 'svelte' } + ]) + const projectOptions = ref([ + { name: 'Project A', value: 'proj-a' }, + { name: 'Project B', value: 'proj-b' }, + { name: 'Project C', value: 'proj-c' } + ]) + const sortOptions = ref([ + { name: 'Popular', value: 'popular' }, + { name: 'Latest', value: 'latest' }, + { name: 'A → Z', value: 'az' } + ]) + + const selectedFrameworks = ref([]) + const selectedProjects = ref([]) + const selectedSort = ref('popular') + + return { + args, + t, + tempNavigation, + selectedNavItem, + searchQuery, + frameworkOptions, + projectOptions, + sortOptions, + selectedFrameworks, + selectedProjects, + selectedSort + } + }, + template: ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ` +}) + +export const Default: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Content Title', + hasLeftPanel: true, + hasRightPanel: true, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 12 + } +} + +export const BothPanels: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Content Title', + hasLeftPanel: true, + hasRightPanel: true, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 12 + } +} + +export const LeftPanelOnly: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Content Title', + hasLeftPanel: true, + hasRightPanel: false, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 12 + } +} + +export const RightPanelOnly: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Content Title', + hasLeftPanel: false, + hasRightPanel: true, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 12 + } +} + +export const NoPanels: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Content Title', + hasLeftPanel: false, + hasRightPanel: false, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 12 + } +} + +export const MinimalLayout: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Simple Content', + hasLeftPanel: false, + hasRightPanel: false, + hasHeader: false, + hasContentFilter: false, + hasHeaderRightArea: false, + cardCount: 6 + } +} + +export const NoContent: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Empty State', + hasLeftPanel: true, + hasRightPanel: true, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 0 + } +} + +export const HeaderOnly: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Header Layout', + hasLeftPanel: false, + hasRightPanel: false, + hasHeader: true, + hasContentFilter: false, + hasHeaderRightArea: true, + cardCount: 8 + } +} + +export const FilterOnly: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Filter Layout', + hasLeftPanel: false, + hasRightPanel: false, + hasHeader: false, + hasContentFilter: true, + hasHeaderRightArea: false, + cardCount: 8 + } +} + +export const MaxContent: Story = { + render: (args: StoryArgs) => createStoryTemplate(args), + args: { + contentTitle: 'Full Content', + hasLeftPanel: true, + hasRightPanel: true, + hasHeader: true, + hasContentFilter: true, + hasHeaderRightArea: true, + cardCount: 50 + } +} diff --git a/src/components/widget/nav/Navigation.stories.ts b/src/components/widget/nav/Navigation.stories.ts new file mode 100644 index 000000000..78e11996f --- /dev/null +++ b/src/components/widget/nav/Navigation.stories.ts @@ -0,0 +1,138 @@ +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 + +export const NavigationItem: Story = { + render: () => ({ + components: { NavItem }, + template: ` +
+ Dashboard + Projects + Messages + Settings +
+ ` + }) +} + +export const CustomNavigation: Story = { + render: () => ({ + components: { + NavTitle, + NavItem, + Home, + FolderOpen, + BarChart3, + Users, + BookOpen, + GraduationCap, + MessageSquare, + Settings, + User, + Bell, + LogOut + }, + template: ` + + ` + }) +} + +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(null) + return { navItems, active } + }, + template: ` +
+
+ + + + +
+ +
+ Active: {{ active ?? 'None' }} +
+
+ ` + }) +}