+
+
+
diff --git a/src/components/button/IconTextButton.vue b/src/components/button/IconTextButton.vue
index bdf40d4ed..a62aab08b 100644
--- a/src/components/button/IconTextButton.vue
+++ b/src/components/button/IconTextButton.vue
@@ -23,6 +23,7 @@ import {
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
+import { cn } from '@/utils/tailwindUtil'
defineOptions({
inheritAttrs: false
@@ -52,8 +53,6 @@ const buttonStyle = computed(() => {
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
- return [baseClasses, sizeClasses, typeClasses, className]
- .filter(Boolean)
- .join(' ')
+ return cn(baseClasses, sizeClasses, typeClasses, className)
})
diff --git a/src/components/button/MoreButton.vue b/src/components/button/MoreButton.vue
index 5f4f81607..0ea645c7b 100644
--- a/src/components/button/MoreButton.vue
+++ b/src/components/button/MoreButton.vue
@@ -14,7 +14,7 @@
unstyled
:pt="pt"
>
-
+
@@ -25,6 +25,8 @@
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
+import { cn } from '@/utils/tailwindUtil'
+
import IconButton from './IconButton.vue'
const popover = ref
>()
@@ -39,13 +41,16 @@ const hide = () => {
const pt = computed(() => ({
root: {
- class: 'absolute z-50'
+ class: cn('absolute z-50')
},
content: {
- class: [
- 'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg',
- 'shadow-lg border border-zinc-200 dark-theme:border-zinc-700'
- ]
+ class: cn(
+ 'mt-2 rounded-lg',
+ 'bg-white dark-theme:bg-zinc-800',
+ 'text-neutral dark-theme:text-white',
+ 'shadow-lg',
+ 'border border-zinc-200 dark-theme:border-zinc-700'
+ )
}
}))
diff --git a/src/components/button/TextButton.vue b/src/components/button/TextButton.vue
index 14d32cc73..4dbe53b9c 100644
--- a/src/components/button/TextButton.vue
+++ b/src/components/button/TextButton.vue
@@ -21,6 +21,7 @@ import {
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
+import { cn } from '@/utils/tailwindUtil'
interface TextButtonProps extends BaseButtonProps {
label: string
@@ -48,8 +49,6 @@ const buttonStyle = computed(() => {
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
- return [baseClasses, sizeClasses, typeClasses, className]
- .filter(Boolean)
- .join(' ')
+ return cn(baseClasses, sizeClasses, typeClasses, className)
})
diff --git a/src/components/card/Card.stories.ts b/src/components/card/Card.stories.ts
index 923327a79..f4252644e 100644
--- a/src/components/card/Card.stories.ts
+++ b/src/components/card/Card.stories.ts
@@ -49,14 +49,6 @@ const meta: Meta = {
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'],
@@ -155,11 +147,10 @@ const createCardTemplate = (args: CardStoryArgs) => ({
}
},
template: `
-
+
@@ -214,7 +205,7 @@ const createCardTemplate = (args: CardStoryArgs) => ({
-
+
{{ args.title }}
{{ args.description }}
@@ -228,8 +219,6 @@ export const Default: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
- maxWidth: 300,
- minWidth: 200,
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
@@ -255,8 +244,6 @@ export const SquareCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
- maxWidth: 400,
- minWidth: 250,
topRatio: 'landscape',
showTopLeft: false,
showTopRight: true,
@@ -282,8 +269,6 @@ export const TallPortraitCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
- maxWidth: 280,
- minWidth: 180,
topRatio: 'square',
showTopLeft: true,
showTopRight: true,
@@ -309,8 +294,6 @@ export const ImageCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
- maxWidth: 350,
- minWidth: 220,
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
@@ -335,8 +318,6 @@ export const MinimalCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
- maxWidth: 300,
- minWidth: 200,
topRatio: 'landscape',
showTopLeft: false,
showTopRight: false,
@@ -361,8 +342,6 @@ export const FullFeaturedCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
- maxWidth: 320,
- minWidth: 240,
topRatio: 'square',
showTopLeft: true,
showTopRight: true,
@@ -376,270 +355,10 @@ export const FullFeaturedCard: Story = {
backgroundColor: '#ef4444',
showImage: false,
imageUrl: '',
- tags: ['Bundle', 'Premium', 'SDXL'],
+ tags: ['Bundle', 'SDXL'],
showFileSize: true,
fileSize: '5.4 GB',
showFileType: true,
fileType: 'pack'
}
}
-
-export const GridOfCards: Story = {
- render: () => ({
- components: {
- CardContainer,
- CardTop,
- CardBottom,
- CardTitle,
- CardDescription,
- IconButton,
- SquareChip
- },
- 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
-
-
-
-
-
-
-
-
-
- console.log('Info:', card.title)"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ card.title }}
- {{ card.description }}
-
-
-
-
-
- `
- })
-}
-
-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)
-
-
-
-
-
-
-
-
-
- {{ card.title }}
- {{ card.description }}
-
-
-
-
-
-
-
-
Portrait Cards (2:3)
-
-
-
-
-
-
-
-
-
- {{ card.title }}
-
-
-
-
-
-
-
-
Tall Portrait Cards (2:4)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ card.title }}
- {{ card.description }}
-
-
-
-
-
-
- `
- }),
- parameters: {
- controls: { disable: true },
- actions: { disable: true }
- }
-}
diff --git a/src/components/card/CardContainer.vue b/src/components/card/CardContainer.vue
index e3204ee51..1a17d5659 100644
--- a/src/components/card/CardContainer.vue
+++ b/src/components/card/CardContainer.vue
@@ -1,5 +1,5 @@
-
+
@@ -8,13 +8,7 @@
diff --git a/src/components/card/CardGridList.stories.ts b/src/components/card/CardGridList.stories.ts
new file mode 100644
index 000000000..8ed61a691
--- /dev/null
+++ b/src/components/card/CardGridList.stories.ts
@@ -0,0 +1,69 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import { createGridStyle } from '@/utils/gridUtil'
+
+import CardBottom from './CardBottom.vue'
+import CardContainer from './CardContainer.vue'
+import CardTop from './CardTop.vue'
+
+const meta: Meta = {
+ title: 'Components/Card/CardGridList',
+ tags: ['autodocs'],
+ argTypes: {
+ minWidth: {
+ control: 'text',
+ description: 'Minimum width for each grid item'
+ },
+ maxWidth: {
+ control: 'text',
+ description: 'Maximum width for each grid item'
+ },
+ padding: {
+ control: 'text',
+ description: 'Padding around the grid'
+ },
+ gap: {
+ control: 'text',
+ description: 'Gap between grid items'
+ },
+ columns: {
+ control: 'number',
+ description: 'Fixed number of columns (overrides auto-fill)'
+ }
+ },
+ args: {
+ minWidth: '15rem',
+ maxWidth: '1fr',
+ padding: '0rem',
+ gap: '1rem'
+ }
+}
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ render: (args) => ({
+ components: { CardContainer, CardTop, CardBottom },
+ setup() {
+ const gridStyle = createGridStyle(args)
+ return { gridStyle }
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ })
+}
diff --git a/src/components/input/MultiSelect.stories.ts b/src/components/input/MultiSelect.stories.ts
index e4b41d68f..1ae58db8e 100644
--- a/src/components/input/MultiSelect.stories.ts
+++ b/src/components/input/MultiSelect.stories.ts
@@ -13,6 +13,9 @@ interface ExtendedProps extends Partial {
showSelectedCount?: boolean
showClearButton?: boolean
searchPlaceholder?: string
+ listMaxHeight?: string
+ popoverMinWidth?: string
+ popoverMaxWidth?: string
// Override modelValue type to match our Option type
modelValue?: Array<{ name: string; value: string }>
}
@@ -42,6 +45,18 @@ const meta: Meta = {
},
searchPlaceholder: {
control: 'text'
+ },
+ listMaxHeight: {
+ control: 'text',
+ description: 'Maximum height of the dropdown list'
+ },
+ popoverMinWidth: {
+ control: 'text',
+ description: 'Minimum width of the popover'
+ },
+ popoverMaxWidth: {
+ control: 'text',
+ description: 'Maximum width of the popover'
}
},
args: {
@@ -274,3 +289,140 @@ export const CustomSearchPlaceholder: Story = {
searchPlaceholder: 'Filter packages...'
}
}
+
+export const CustomMaxHeight: Story = {
+ render: () => ({
+ components: { MultiSelect },
+ setup() {
+ const selected1 = ref([])
+ const selected2 = ref([])
+ const selected3 = ref([])
+ const manyOptions = Array.from({ length: 20 }, (_, i) => ({
+ name: `Option ${i + 1}`,
+ value: `option${i + 1}`
+ }))
+ return { selected1, selected2, selected3, manyOptions }
+ },
+ template: `
+
+
+
Small Height (10rem)
+
+
+
+
Default Height (28rem)
+
+
+
+
Large Height (32rem)
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
+
+export const CustomMinWidth: Story = {
+ render: () => ({
+ components: { MultiSelect },
+ setup() {
+ const selected1 = ref([])
+ const selected2 = ref([])
+ const selected3 = ref([])
+ const options = [
+ { name: 'A', value: 'a' },
+ { name: 'B', value: 'b' },
+ { name: 'Very Long Option Name Here', value: 'long' }
+ ]
+ return { selected1, selected2, selected3, options }
+ },
+ template: `
+
+
+
Auto Width
+
+
+
+
Min Width 18rem
+
+
+
+
Min Width 28rem
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
+
+export const CustomMaxWidth: Story = {
+ render: () => ({
+ components: { MultiSelect },
+ setup() {
+ const selected1 = ref([])
+ const selected2 = ref([])
+ const selected3 = ref([])
+ const longOptions = [
+ { name: 'Short', value: 'short' },
+ {
+ name: 'This is a very long option name that would normally expand the dropdown',
+ value: 'long1'
+ },
+ {
+ name: 'Another extremely long option that demonstrates max-width constraint',
+ value: 'long2'
+ }
+ ]
+ return { selected1, selected2, selected3, longOptions }
+ },
+ template: `
+
+
+
Auto Width
+
+
+
+
Max Width 18rem
+
+
+
+
Min 12rem Max 22rem
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue
index ffafde707..5ca6060a3 100644
--- a/src/components/input/MultiSelect.vue
+++ b/src/components/input/MultiSelect.vue
@@ -1,10 +1,9 @@
-
@@ -20,12 +19,13 @@
v-if="showSearchBox || showSelectedCount || showClearButton"
#header
>
-
@@ -75,13 +75,13 @@
-
+
-
+
@@ -105,6 +107,8 @@ import MultiSelect, {
import { computed } from 'vue'
import SearchBox from '@/components/input/SearchBox.vue'
+import { usePopoverSizing } from '@/composables/usePopoverSizing'
+import { cn } from '@/utils/tailwindUtil'
import TextButton from '../button/TextButton.vue'
@@ -125,6 +129,12 @@ interface Props {
showClearButton?: boolean
/** Placeholder for the search input */
searchPlaceholder?: string
+ /** Maximum height of the dropdown panel (default: 28rem) */
+ listMaxHeight?: string
+ /** Minimum width of the popover (default: auto) */
+ popoverMinWidth?: string
+ /** Maximum width of the popover (default: auto) */
+ popoverMaxWidth?: string
// Note: options prop is intentionally omitted.
// It's passed via $attrs to maximize PrimeVue API compatibility
}
@@ -133,7 +143,10 @@ const {
showSearchBox = false,
showSelectedCount = false,
showClearButton = false,
- searchPlaceholder = 'Search...'
+ searchPlaceholder = 'Search...',
+ listMaxHeight = '28rem',
+ popoverMinWidth,
+ popoverMaxWidth
} = defineProps
()
const selectedItems = defineModel
@@ -15,20 +15,56 @@
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
-const { placeHolder, showBorder = false } = defineProps<{
+import { cn } from '@/utils/tailwindUtil'
+
+const {
+ placeHolder,
+ showBorder = false,
+ size = 'md'
+} = defineProps<{
placeHolder?: string
showBorder?: boolean
+ size?: 'md' | 'lg'
}>()
// defineModel without arguments uses 'modelValue' as the prop name
const searchQuery = defineModel()
const wrapperStyle = computed(() => {
- return showBorder
- ? 'flex w-full items-center rounded gap-2 bg-white dark-theme:bg-zinc-800 p-1 border border-solid border-zinc-200 dark-theme:border-zinc-700'
- : 'flex w-full items-center rounded px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800'
+ const baseClasses = [
+ 'relative flex w-full items-center gap-2',
+ 'bg-white dark-theme:bg-zinc-800',
+ 'cursor-text'
+ ]
+
+ if (showBorder) {
+ return cn(
+ ...baseClasses,
+ 'rounded p-2',
+ 'border border-solid',
+ 'border-zinc-200 dark-theme:border-zinc-700'
+ )
+ }
+
+ // Size-specific classes matching button sizes for consistency
+ const sizeClasses = {
+ md: 'h-8 px-2 py-1.5', // Matches button sm size
+ lg: 'h-10 px-4 py-2' // Matches button md size
+ }[size]
+
+ return cn(...baseClasses, 'rounded-lg', sizeClasses)
+})
+
+const inputStyle = computed(() => {
+ return cn(
+ 'absolute inset-0 w-full h-full pl-11',
+ 'border-none outline-none bg-transparent',
+ 'text-sm text-neutral dark-theme:text-white'
+ )
})
const iconColorStyle = computed(() => {
- return !showBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
+ return cn(
+ !showBorder ? 'text-neutral' : ['text-zinc-300', 'dark-theme:text-zinc-700']
+ )
})
diff --git a/src/components/input/SingleSelect.stories.ts b/src/components/input/SingleSelect.stories.ts
index a529a2fe1..b130780e2 100644
--- a/src/components/input/SingleSelect.stories.ts
+++ b/src/components/input/SingleSelect.stories.ts
@@ -10,7 +10,19 @@ const meta: Meta = {
tags: ['autodocs'],
argTypes: {
label: { control: 'text' },
- options: { control: 'object' }
+ options: { control: 'object' },
+ listMaxHeight: {
+ control: 'text',
+ description: 'Maximum height of the dropdown list'
+ },
+ popoverMinWidth: {
+ control: 'text',
+ description: 'Minimum width of the popover'
+ },
+ popoverMaxWidth: {
+ control: 'text',
+ description: 'Maximum width of the popover'
+ }
},
args: {
label: 'Sorting Type',
@@ -121,6 +133,124 @@ export const AllVariants: Story = {
}),
parameters: {
controls: { disable: true },
- actions: { disable: true }
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
+
+export const CustomMaxHeight: Story = {
+ render: () => ({
+ components: { SingleSelect },
+ setup() {
+ const selected = ref(null)
+ const manyOptions = Array.from({ length: 20 }, (_, i) => ({
+ name: `Option ${i + 1}`,
+ value: `option${i + 1}`
+ }))
+ return { selected, manyOptions }
+ },
+ template: `
+
+
+
Small Height (10rem)
+
+
+
+
Default Height (28rem)
+
+
+
+
Large Height (32rem)
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
+
+export const CustomMinWidth: Story = {
+ render: () => ({
+ components: { SingleSelect },
+ setup() {
+ const selected1 = ref(null)
+ const selected2 = ref(null)
+ const selected3 = ref(null)
+ const options = [
+ { name: 'A', value: 'a' },
+ { name: 'B', value: 'b' },
+ { name: 'Very Long Option Name Here', value: 'long' }
+ ]
+ return { selected1, selected2, selected3, options }
+ },
+ template: `
+
+
+
Auto Width
+
+
+
+
Min Width 15rem
+
+
+
+
Min Width 25rem
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
+ }
+}
+
+export const CustomMaxWidth: Story = {
+ render: () => ({
+ components: { SingleSelect },
+ setup() {
+ const selected1 = ref(null)
+ const selected2 = ref(null)
+ const selected3 = ref(null)
+ const longOptions = [
+ { name: 'Short', value: 'short' },
+ {
+ name: 'This is a very long option name that would normally expand the dropdown',
+ value: 'long1'
+ },
+ {
+ name: 'Another extremely long option that demonstrates max-width constraint',
+ value: 'long2'
+ }
+ ]
+ return { selected1, selected2, selected3, longOptions }
+ },
+ template: `
+
+
+
Auto Width
+
+
+
+
Max Width 15rem
+
+
+
+
Min 10rem Max 20rem
+
+
+
+ `
+ }),
+ parameters: {
+ controls: { disable: true },
+ actions: { disable: true },
+ slot: { disable: true }
}
}
diff --git a/src/components/input/SingleSelect.vue b/src/components/input/SingleSelect.vue
index c46a28a7a..6335c6f10 100644
--- a/src/components/input/SingleSelect.vue
+++ b/src/components/input/SingleSelect.vue
@@ -1,10 +1,9 @@
-