mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 16:59:45 +00:00
Merge branch 'main' into sno-fix-playwright-pnpm
This commit is contained in:
@@ -4,17 +4,26 @@
|
|||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme default */
|
/* Light theme default - with explicit color to override media queries */
|
||||||
body {
|
body:not(.dark-theme) {
|
||||||
background-color: #ffffff;
|
background-color: #fff !important;
|
||||||
color: #1a1a1a;
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override browser dark mode preference for light theme */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body:not(.dark-theme) {
|
||||||
|
color: #000 !important;
|
||||||
|
--fg-color: #000 !important;
|
||||||
|
--bg-color: #fff !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme styles */
|
/* Dark theme styles */
|
||||||
body.dark-theme,
|
body.dark-theme,
|
||||||
.dark-theme body {
|
.dark-theme body {
|
||||||
background-color: #0a0a0a;
|
background-color: #202020;
|
||||||
color: #e5e5e5;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure Storybook canvas follows theme */
|
/* Ensure Storybook canvas follows theme */
|
||||||
@@ -24,11 +33,33 @@
|
|||||||
|
|
||||||
.dark-theme .sb-show-main,
|
.dark-theme .sb-show-main,
|
||||||
.dark-theme .docs-story {
|
.dark-theme .docs-story {
|
||||||
background-color: #0a0a0a !important;
|
background-color: #202020 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for Storybook controls panel in dark mode */
|
/* CSS Variables for theme consistency */
|
||||||
.dark-theme .docblock-argstable-body {
|
body:not(.dark-theme) {
|
||||||
color: #e5e5e5;
|
--fg-color: #000;
|
||||||
|
--bg-color: #fff;
|
||||||
|
--content-bg: #e0e0e0;
|
||||||
|
--content-fg: #000;
|
||||||
|
--content-hover-bg: #adadad;
|
||||||
|
--content-hover-fg: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme {
|
||||||
|
--fg-color: #fff;
|
||||||
|
--bg-color: #202020;
|
||||||
|
--content-bg: #4e4e4e;
|
||||||
|
--content-fg: #fff;
|
||||||
|
--content-hover-bg: #222;
|
||||||
|
--content-hover-fg: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override Storybook's problematic & selector styles */
|
||||||
|
/* Reset only the specific properties that Storybook injects */
|
||||||
|
#storybook-root li+li,
|
||||||
|
#storybook-docs li+li {
|
||||||
|
margin: inherit;
|
||||||
|
padding: inherit;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -24,22 +24,22 @@ export const Basic: Story = {
|
|||||||
<MoreButton>
|
<MoreButton>
|
||||||
<template #default="{ close }">
|
<template #default="{ close }">
|
||||||
<IconTextButton
|
<IconTextButton
|
||||||
type="secondary"
|
type="transparent"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
@click="() => { close() }"
|
@click="() => { close() }"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Download />
|
<Download :size="16" />
|
||||||
</template>
|
</template>
|
||||||
</IconTextButton>
|
</IconTextButton>
|
||||||
|
|
||||||
<IconTextButton
|
<IconTextButton
|
||||||
type="primary"
|
type="transparent"
|
||||||
label="Profile"
|
label="Profile"
|
||||||
@click="() => { close() }"
|
@click="() => { close() }"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ScrollText />
|
<ScrollText :size="16" />
|
||||||
</template>
|
</template>
|
||||||
</IconTextButton>
|
</IconTextButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
import type { MultiSelectProps } from 'primevue/multiselect'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import MultiSelect from './MultiSelect.vue'
|
import MultiSelect from './MultiSelect.vue'
|
||||||
|
|
||||||
const meta: Meta<typeof MultiSelect> = {
|
// Combine our component props with PrimeVue MultiSelect props
|
||||||
|
// Since we use v-bind="$attrs", all PrimeVue props are available
|
||||||
|
interface ExtendedProps extends Partial<MultiSelectProps> {
|
||||||
|
// Our custom props
|
||||||
|
label?: string
|
||||||
|
showSearchBox?: boolean
|
||||||
|
showSelectedCount?: boolean
|
||||||
|
showClearButton?: boolean
|
||||||
|
searchPlaceholder?: string
|
||||||
|
// Override modelValue type to match our Option type
|
||||||
|
modelValue?: Array<{ name: string; value: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta: Meta<ExtendedProps> = {
|
||||||
title: 'Components/Input/MultiSelect',
|
title: 'Components/Input/MultiSelect',
|
||||||
component: MultiSelect,
|
component: MultiSelect,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
@@ -13,7 +27,35 @@ const meta: Meta<typeof MultiSelect> = {
|
|||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
control: 'object'
|
control: 'object'
|
||||||
|
},
|
||||||
|
showSearchBox: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Toggle searchBar visibility'
|
||||||
|
},
|
||||||
|
showSelectedCount: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Toggle selected count visibility'
|
||||||
|
},
|
||||||
|
showClearButton: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Toggle clear button visibility'
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
control: 'text'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
label: 'Select',
|
||||||
|
options: [
|
||||||
|
{ name: 'Vue', value: 'vue' },
|
||||||
|
{ name: 'React', value: 'react' },
|
||||||
|
{ name: 'Angular', value: 'angular' },
|
||||||
|
{ name: 'Svelte', value: 'svelte' }
|
||||||
|
],
|
||||||
|
showSearchBox: false,
|
||||||
|
showSelectedCount: false,
|
||||||
|
showClearButton: false,
|
||||||
|
searchPlaceholder: 'Search...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +67,7 @@ export const Default: Story = {
|
|||||||
components: { MultiSelect },
|
components: { MultiSelect },
|
||||||
setup() {
|
setup() {
|
||||||
const selected = ref([])
|
const selected = ref([])
|
||||||
const options = [
|
const options = args.options || [
|
||||||
{ name: 'Vue', value: 'vue' },
|
{ name: 'Vue', value: 'vue' },
|
||||||
{ name: 'React', value: 'react' },
|
{ name: 'React', value: 'react' },
|
||||||
{ name: 'Angular', value: 'angular' },
|
{ name: 'Angular', value: 'angular' },
|
||||||
@@ -38,8 +80,11 @@ export const Default: Story = {
|
|||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
:options="options"
|
:options="options"
|
||||||
label="Select Frameworks"
|
:label="args.label"
|
||||||
v-bind="args"
|
:showSearchBox="args.showSearchBox"
|
||||||
|
:showSelectedCount="args.showSelectedCount"
|
||||||
|
:showClearButton="args.showClearButton"
|
||||||
|
:searchPlaceholder="args.searchPlaceholder"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
||||||
<p class="text-sm">Selected: {{ selected.length > 0 ? selected.map(s => s.name).join(', ') : 'None' }}</p>
|
<p class="text-sm">Selected: {{ selected.length > 0 ? selected.map(s => s.name).join(', ') : 'None' }}</p>
|
||||||
@@ -50,10 +95,10 @@ export const Default: Story = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const WithPreselectedValues: Story = {
|
export const WithPreselectedValues: Story = {
|
||||||
render: () => ({
|
render: (args) => ({
|
||||||
components: { MultiSelect },
|
components: { MultiSelect },
|
||||||
setup() {
|
setup() {
|
||||||
const options = [
|
const options = args.options || [
|
||||||
{ name: 'JavaScript', value: 'js' },
|
{ name: 'JavaScript', value: 'js' },
|
||||||
{ name: 'TypeScript', value: 'ts' },
|
{ name: 'TypeScript', value: 'ts' },
|
||||||
{ name: 'Python', value: 'python' },
|
{ name: 'Python', value: 'python' },
|
||||||
@@ -61,25 +106,43 @@ export const WithPreselectedValues: Story = {
|
|||||||
{ name: 'Rust', value: 'rust' }
|
{ name: 'Rust', value: 'rust' }
|
||||||
]
|
]
|
||||||
const selected = ref([options[0], options[1]])
|
const selected = ref([options[0], options[1]])
|
||||||
return { selected, options }
|
return { selected, options, args }
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
:options="options"
|
:options="options"
|
||||||
label="Select Languages"
|
:label="args.label"
|
||||||
|
:showSearchBox="args.showSearchBox"
|
||||||
|
:showSelectedCount="args.showSelectedCount"
|
||||||
|
:showClearButton="args.showClearButton"
|
||||||
|
:searchPlaceholder="args.searchPlaceholder"
|
||||||
/>
|
/>
|
||||||
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
||||||
<p class="text-sm">Selected: {{ selected.map(s => s.name).join(', ') }}</p>
|
<p class="text-sm">Selected: {{ selected.map(s => s.name).join(', ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
}),
|
||||||
|
args: {
|
||||||
|
label: 'Select Languages',
|
||||||
|
options: [
|
||||||
|
{ name: 'JavaScript', value: 'js' },
|
||||||
|
{ name: 'TypeScript', value: 'ts' },
|
||||||
|
{ name: 'Python', value: 'python' },
|
||||||
|
{ name: 'Go', value: 'go' },
|
||||||
|
{ name: 'Rust', value: 'rust' }
|
||||||
|
],
|
||||||
|
showSearchBox: false,
|
||||||
|
showSelectedCount: false,
|
||||||
|
showClearButton: false,
|
||||||
|
searchPlaceholder: 'Search...'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultipleSelectors: Story = {
|
export const MultipleSelectors: Story = {
|
||||||
render: () => ({
|
render: (args) => ({
|
||||||
components: { MultiSelect },
|
components: { MultiSelect },
|
||||||
setup() {
|
setup() {
|
||||||
const frameworkOptions = ref([
|
const frameworkOptions = ref([
|
||||||
@@ -114,7 +177,8 @@ export const MultipleSelectors: Story = {
|
|||||||
tagOptions,
|
tagOptions,
|
||||||
selectedFrameworks,
|
selectedFrameworks,
|
||||||
selectedProjects,
|
selectedProjects,
|
||||||
selectedTags
|
selectedTags,
|
||||||
|
args
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -124,22 +188,34 @@ export const MultipleSelectors: Story = {
|
|||||||
v-model="selectedFrameworks"
|
v-model="selectedFrameworks"
|
||||||
:options="frameworkOptions"
|
:options="frameworkOptions"
|
||||||
label="Select Frameworks"
|
label="Select Frameworks"
|
||||||
|
:showSearchBox="args.showSearchBox"
|
||||||
|
:showSelectedCount="args.showSelectedCount"
|
||||||
|
:showClearButton="args.showClearButton"
|
||||||
|
:searchPlaceholder="args.searchPlaceholder"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selectedProjects"
|
v-model="selectedProjects"
|
||||||
:options="projectOptions"
|
:options="projectOptions"
|
||||||
label="Select Projects"
|
label="Select Projects"
|
||||||
|
:showSearchBox="args.showSearchBox"
|
||||||
|
:showSelectedCount="args.showSelectedCount"
|
||||||
|
:showClearButton="args.showClearButton"
|
||||||
|
:searchPlaceholder="args.searchPlaceholder"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selectedTags"
|
v-model="selectedTags"
|
||||||
:options="tagOptions"
|
:options="tagOptions"
|
||||||
label="Select Tags"
|
label="Select Tags"
|
||||||
|
:showSearchBox="args.showSearchBox"
|
||||||
|
:showSelectedCount="args.showSelectedCount"
|
||||||
|
:showClearButton="args.showClearButton"
|
||||||
|
:searchPlaceholder="args.searchPlaceholder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
<div class="p-4 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
||||||
<h4 class="font-medium mb-2">Current Selection:</h4>
|
<h4 class="font-medium mt-0">Current Selection:</h4>
|
||||||
<div class="space-y-1 text-sm">
|
<div class="flex flex-col text-sm">
|
||||||
<p>Frameworks: {{ selectedFrameworks.length > 0 ? selectedFrameworks.map(s => s.name).join(', ') : 'None' }}</p>
|
<p>Frameworks: {{ selectedFrameworks.length > 0 ? selectedFrameworks.map(s => s.name).join(', ') : 'None' }}</p>
|
||||||
<p>Projects: {{ selectedProjects.length > 0 ? selectedProjects.map(s => s.name).join(', ') : 'None' }}</p>
|
<p>Projects: {{ selectedProjects.length > 0 ? selectedProjects.map(s => s.name).join(', ') : 'None' }}</p>
|
||||||
<p>Tags: {{ selectedTags.length > 0 ? selectedTags.map(s => s.name).join(', ') : 'None' }}</p>
|
<p>Tags: {{ selectedTags.length > 0 ? selectedTags.map(s => s.name).join(', ') : 'None' }}</p>
|
||||||
@@ -147,5 +223,54 @@ export const MultipleSelectors: Story = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
}),
|
||||||
|
args: {
|
||||||
|
showSearchBox: false,
|
||||||
|
showSelectedCount: false,
|
||||||
|
showClearButton: false,
|
||||||
|
searchPlaceholder: 'Search...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithSearchBox: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
showSearchBox: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithSelectedCount: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
showSelectedCount: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithClearButton: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
showClearButton: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllHeaderFeatures: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
showSearchBox: true,
|
||||||
|
showSelectedCount: true,
|
||||||
|
showClearButton: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomSearchPlaceholder: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
...Default.args,
|
||||||
|
showSearchBox: true,
|
||||||
|
searchPlaceholder: 'Filter packages...'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative inline-block">
|
<!--
|
||||||
<MultiSelect
|
Note: Unlike SingleSelect, we don't need an explicit options prop because:
|
||||||
v-model="selectedItems"
|
1. Our value template only shows a static label (not dynamic based on selection)
|
||||||
:options="options"
|
2. We display a count badge instead of actual selected labels
|
||||||
option-label="name"
|
3. All PrimeVue props (including options) are passed via v-bind="$attrs"
|
||||||
unstyled
|
|
||||||
:placeholder="label"
|
option-label="name" is required because our option template directly accesses option.name
|
||||||
:max-selected-labels="0"
|
max-selected-labels="0" is required to show count badge instead of selected item labels
|
||||||
:pt="pt"
|
-->
|
||||||
|
<MultiSelect
|
||||||
|
v-model="selectedItems"
|
||||||
|
v-bind="$attrs"
|
||||||
|
option-label="name"
|
||||||
|
unstyled
|
||||||
|
:max-selected-labels="0"
|
||||||
|
:pt="pt"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-if="showSearchBox || showSelectedCount || showClearButton"
|
||||||
|
#header
|
||||||
>
|
>
|
||||||
<template
|
<div class="p-2 flex flex-col pb-0">
|
||||||
v-if="hasSearchBox || showSelectedCount || hasClearButton"
|
<SearchBox
|
||||||
#header
|
v-if="showSearchBox"
|
||||||
>
|
v-model="searchQuery"
|
||||||
<div class="p-2 flex flex-col gap-y-4 pb-0">
|
:class="showSelectedCount || showClearButton ? 'mb-2' : ''"
|
||||||
<SearchBox
|
:show-order="true"
|
||||||
v-if="hasSearchBox"
|
:place-holder="searchPlaceholder"
|
||||||
v-model="searchQuery"
|
/>
|
||||||
:has-border="true"
|
<div
|
||||||
:place-holder="searchPlaceholder"
|
v-if="showSelectedCount || showClearButton"
|
||||||
/>
|
class="mt-2 flex items-center justify-between"
|
||||||
<div class="flex items-center justify-between">
|
>
|
||||||
<span
|
<span
|
||||||
v-if="showSelectedCount"
|
v-if="showSelectedCount"
|
||||||
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
|
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
|
||||||
>
|
|
||||||
{{
|
|
||||||
selectedCount > 0
|
|
||||||
? $t('g.itemsSelected', { selectedCount })
|
|
||||||
: $t('g.itemSelected', { selectedCount })
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<TextButton
|
|
||||||
v-if="hasClearButton"
|
|
||||||
:label="$t('g.clearAll')"
|
|
||||||
type="transparent"
|
|
||||||
size="fit-content"
|
|
||||||
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
|
|
||||||
@click.stop="selectedItems = []"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Trigger value (keep text scale identical) -->
|
|
||||||
<template #value>
|
|
||||||
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
|
|
||||||
{{ label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Chevron size identical to current -->
|
|
||||||
<template #dropdownicon>
|
|
||||||
<i-lucide:chevron-down class="text-lg text-neutral-400" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
|
||||||
<template #option="slotProps">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded border-[3px] transition-all duration-200"
|
|
||||||
:class="
|
|
||||||
slotProps.selected
|
|
||||||
? 'border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
|
|
||||||
: 'border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<i-lucide:check
|
{{
|
||||||
v-if="slotProps.selected"
|
selectedCount > 0
|
||||||
class="text-xs text-bold text-white"
|
? $t('g.itemsSelected', { selectedCount })
|
||||||
/>
|
: $t('g.itemSelected', { selectedCount })
|
||||||
</div>
|
}}
|
||||||
<span>{{ slotProps.option.name }}</span>
|
</span>
|
||||||
|
<TextButton
|
||||||
|
v-if="showClearButton"
|
||||||
|
:label="$t('g.clearAll')"
|
||||||
|
type="transparent"
|
||||||
|
size="fit-content"
|
||||||
|
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
|
||||||
|
@click.stop="selectedItems = []"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="mt-4 h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
|
||||||
</MultiSelect>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Selected count badge -->
|
<!-- Trigger value (keep text scale identical) -->
|
||||||
<div
|
<template #value>
|
||||||
v-if="selectedCount > 0"
|
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
|
||||||
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
|
{{ label }}
|
||||||
>
|
</span>
|
||||||
{{ selectedCount }}
|
<span
|
||||||
</div>
|
v-if="selectedCount > 0"
|
||||||
</div>
|
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
|
||||||
|
>
|
||||||
|
{{ selectedCount }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Chevron size identical to current -->
|
||||||
|
<template #dropdownicon>
|
||||||
|
<i-lucide:chevron-down class="text-lg text-neutral-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||||
|
<template #option="slotProps">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded transition-all duration-200"
|
||||||
|
:class="
|
||||||
|
slotProps.selected
|
||||||
|
? 'border-[3px] border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
|
||||||
|
: 'border-[1px] border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i-lucide:check
|
||||||
|
v-if="slotProps.selected"
|
||||||
|
class="text-xs text-bold text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button class="border-none outline-none bg-transparent" unstyled>{{
|
||||||
|
slotProps.option.name
|
||||||
|
}}</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MultiSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
import MultiSelect, {
|
import MultiSelect, {
|
||||||
MultiSelectPassThroughMethodOptions
|
MultiSelectPassThroughMethodOptions
|
||||||
} from 'primevue/multiselect'
|
} from 'primevue/multiselect'
|
||||||
@@ -99,26 +110,29 @@ import TextButton from '../button/TextButton.vue'
|
|||||||
|
|
||||||
type Option = { name: string; value: string }
|
type Option = { name: string; value: string }
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Input label shown on the trigger button */
|
/** Input label shown on the trigger button */
|
||||||
label?: string
|
label?: string
|
||||||
/** Static options for the multiselect (when not using async search) */
|
|
||||||
options: Option[]
|
|
||||||
/** Show search box in the panel header */
|
/** Show search box in the panel header */
|
||||||
hasSearchBox?: boolean
|
showSearchBox?: boolean
|
||||||
/** Show selected count text in the panel header */
|
/** Show selected count text in the panel header */
|
||||||
showSelectedCount?: boolean
|
showSelectedCount?: boolean
|
||||||
/** Show "Clear all" action in the panel header */
|
/** Show "Clear all" action in the panel header */
|
||||||
hasClearButton?: boolean
|
showClearButton?: boolean
|
||||||
/** Placeholder for the search input */
|
/** Placeholder for the search input */
|
||||||
searchPlaceholder?: string
|
searchPlaceholder?: string
|
||||||
|
// Note: options prop is intentionally omitted.
|
||||||
|
// It's passed via $attrs to maximize PrimeVue API compatibility
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
options,
|
showSearchBox = false,
|
||||||
hasSearchBox = false,
|
|
||||||
showSelectedCount = false,
|
showSelectedCount = false,
|
||||||
hasClearButton = false,
|
showClearButton = false,
|
||||||
searchPlaceholder = 'Search...'
|
searchPlaceholder = 'Search...'
|
||||||
} = defineProps<Props>()
|
} = defineProps<Props>()
|
||||||
|
|
||||||
@@ -131,7 +145,7 @@ const selectedCount = computed(() => selectedItems.value.length)
|
|||||||
const pt = computed(() => ({
|
const pt = computed(() => ({
|
||||||
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||||
class: [
|
class: [
|
||||||
'relative inline-flex cursor-pointer select-none w-full',
|
'relative inline-flex cursor-pointer select-none',
|
||||||
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
||||||
'transition-all duration-200 ease-in-out',
|
'transition-all duration-200 ease-in-out',
|
||||||
'border-[2.5px] border-solid',
|
'border-[2.5px] border-solid',
|
||||||
@@ -153,7 +167,7 @@ const pt = computed(() => ({
|
|||||||
},
|
},
|
||||||
header: () => ({
|
header: () => ({
|
||||||
class:
|
class:
|
||||||
hasSearchBox || showSelectedCount || hasClearButton ? 'block' : 'hidden'
|
showSearchBox || showSelectedCount || showClearButton ? 'block' : 'hidden'
|
||||||
}),
|
}),
|
||||||
// Overlay & list visuals unchanged
|
// Overlay & list visuals unchanged
|
||||||
overlay:
|
overlay:
|
||||||
@@ -161,9 +175,17 @@ const pt = computed(() => ({
|
|||||||
list: {
|
list: {
|
||||||
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
||||||
},
|
},
|
||||||
// Option row hover tone identical
|
// Option row hover and focus tone
|
||||||
option:
|
option: ({ context }: MultiSelectPassThroughMethodOptions) => ({
|
||||||
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
class: [
|
||||||
|
'flex gap-1 items-center p-2',
|
||||||
|
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||||
|
// Add focus/highlight state for keyboard navigation
|
||||||
|
{
|
||||||
|
'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context?.focused
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
// Hide built-in checkboxes entirely via PT (no :deep)
|
// Hide built-in checkboxes entirely via PT (no :deep)
|
||||||
pcHeaderCheckbox: {
|
pcHeaderCheckbox: {
|
||||||
root: { class: 'hidden' },
|
root: { class: 'hidden' },
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ const meta: Meta<typeof SearchBox> = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
placeHolder: {
|
placeHolder: {
|
||||||
control: 'text'
|
control: 'text'
|
||||||
|
},
|
||||||
|
showBorder: {
|
||||||
|
control: 'boolean',
|
||||||
|
description: 'Toggle border prop'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
placeHolder: 'Search...',
|
||||||
|
showBorder: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,9 +33,23 @@ export const Default: Story = {
|
|||||||
return { searchText, args }
|
return { searchText, args }
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div style="max-width: 320px;">
|
||||||
<SearchBox v-model:="searchQuery" />
|
<SearchBox v-bind="args" v-model="searchText" />
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WithBorder: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
showBorder: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NoBorder: Story = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
showBorder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,19 +15,20 @@
|
|||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const { placeHolder, hasBorder = false } = defineProps<{
|
const { placeHolder, showBorder = false } = defineProps<{
|
||||||
placeHolder?: string
|
placeHolder?: string
|
||||||
hasBorder?: boolean
|
showBorder?: boolean
|
||||||
}>()
|
}>()
|
||||||
const searchQuery = defineModel<string>('')
|
// defineModel without arguments uses 'modelValue' as the prop name
|
||||||
|
const searchQuery = defineModel<string>()
|
||||||
|
|
||||||
const wrapperStyle = computed(() => {
|
const wrapperStyle = computed(() => {
|
||||||
return hasBorder
|
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 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'
|
: 'flex w-full items-center rounded px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800'
|
||||||
})
|
})
|
||||||
|
|
||||||
const iconColorStyle = computed(() => {
|
const iconColorStyle = computed(() => {
|
||||||
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
|
return !showBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,12 +4,24 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
import SingleSelect from './SingleSelect.vue'
|
import SingleSelect from './SingleSelect.vue'
|
||||||
|
|
||||||
|
// SingleSelect already includes options prop, so no need to extend
|
||||||
const meta: Meta<typeof SingleSelect> = {
|
const meta: Meta<typeof SingleSelect> = {
|
||||||
title: 'Components/Input/SingleSelect',
|
title: 'Components/Input/SingleSelect',
|
||||||
component: SingleSelect,
|
component: SingleSelect,
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
label: { control: 'text' }
|
label: { control: 'text' },
|
||||||
|
options: { control: 'object' }
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
label: 'Sorting Type',
|
||||||
|
options: [
|
||||||
|
{ name: 'Popular', value: 'popular' },
|
||||||
|
{ name: 'Newest', value: 'newest' },
|
||||||
|
{ name: 'Oldest', value: 'oldest' },
|
||||||
|
{ name: 'A → Z', value: 'az' },
|
||||||
|
{ name: 'Z → A', value: 'za' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,19 +41,18 @@ export const Default: Story = {
|
|||||||
components: { SingleSelect },
|
components: { SingleSelect },
|
||||||
setup() {
|
setup() {
|
||||||
const selected = ref<string | null>(null)
|
const selected = ref<string | null>(null)
|
||||||
const options = sampleOptions
|
const options = args.options || sampleOptions
|
||||||
return { selected, options, args }
|
return { selected, options, args }
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<SingleSelect v-model="selected" :options="options" :label="args.label || 'Sorting Type'" />
|
<SingleSelect v-model="selected" :options="options" :label="args.label" />
|
||||||
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
<div class="mt-4 p-3 bg-gray-50 dark-theme:bg-zinc-800 rounded">
|
||||||
<p class="text-sm">Selected: {{ selected ?? 'None' }}</p>
|
<p class="text-sm">Selected: {{ selected ?? 'None' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}),
|
})
|
||||||
args: { label: 'Sorting Type' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithIcon: Story = {
|
export const WithIcon: Story = {
|
||||||
|
|||||||
@@ -1,58 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative inline-flex items-center">
|
<!--
|
||||||
<Select
|
Note: We explicitly pass options here (not just via $attrs) because:
|
||||||
v-model="selectedItem"
|
1. Our custom value template needs options to look up labels from values
|
||||||
:options="options"
|
2. PrimeVue's value slot only provides 'value' and 'placeholder', not the selected item's label
|
||||||
option-label="name"
|
3. We need to maintain the icon slot functionality in the value template
|
||||||
option-value="value"
|
|
||||||
unstyled
|
option-label="name" is required because our option template directly accesses option.name
|
||||||
:placeholder="label"
|
-->
|
||||||
:pt="pt"
|
<Select
|
||||||
>
|
v-model="selectedItem"
|
||||||
<!-- Trigger value -->
|
v-bind="$attrs"
|
||||||
<template #value="slotProps">
|
:options="options"
|
||||||
<div class="flex items-center gap-2 text-sm">
|
option-label="name"
|
||||||
<slot name="icon" />
|
option-value="value"
|
||||||
<span
|
unstyled
|
||||||
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
:pt="pt"
|
||||||
class="text-zinc-700 dark-theme:text-gray-200"
|
>
|
||||||
>
|
<!-- Trigger value -->
|
||||||
{{ getLabel(slotProps.value) }}
|
<template #value="slotProps">
|
||||||
</span>
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
|
<slot name="icon" />
|
||||||
{{ label }}
|
<span
|
||||||
</span>
|
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
||||||
</div>
|
class="text-zinc-700 dark-theme:text-gray-200"
|
||||||
</template>
|
>
|
||||||
|
{{ getLabel(slotProps.value) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Trigger caret -->
|
<!-- Trigger caret -->
|
||||||
<template #dropdownicon>
|
<template #dropdownicon>
|
||||||
<i-lucide:chevron-down
|
<i-lucide:chevron-down
|
||||||
class="text-base text-neutral-400 dark-theme:text-gray-300"
|
class="text-base text-neutral-400 dark-theme:text-gray-300"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Option row -->
|
||||||
|
<template #option="{ option, selected }">
|
||||||
|
<div class="flex items-center justify-between gap-3 w-full">
|
||||||
|
<span class="truncate">{{ option.name }}</span>
|
||||||
|
<i-lucide:check
|
||||||
|
v-if="selected"
|
||||||
|
class="text-neutral-900 dark-theme:text-white"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
<!-- Option row -->
|
</Select>
|
||||||
<template #option="{ option, selected }">
|
|
||||||
<div class="flex items-center justify-between gap-3 w-full">
|
|
||||||
<span class="truncate">{{ option.name }}</span>
|
|
||||||
<i-lucide:check
|
|
||||||
v-if="selected"
|
|
||||||
class="text-neutral-900 dark-theme:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
|
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
const { label, options } = defineProps<{
|
const { label, options } = defineProps<{
|
||||||
label?: string
|
label?: string
|
||||||
options: {
|
/**
|
||||||
|
* Required for displaying the selected item's label.
|
||||||
|
* Cannot rely on $attrs alone because we need to access options
|
||||||
|
* in getLabel() to map values to their display names.
|
||||||
|
*/
|
||||||
|
options?: {
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
}[]
|
}[]
|
||||||
@@ -60,8 +75,14 @@ const { label, options } = defineProps<{
|
|||||||
|
|
||||||
const selectedItem = defineModel<string | null>({ required: true })
|
const selectedItem = defineModel<string | null>({ required: true })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a value to its display label.
|
||||||
|
* Necessary because PrimeVue's value slot doesn't provide the selected item's label,
|
||||||
|
* only the raw value. We need this to show the correct text when an item is selected.
|
||||||
|
*/
|
||||||
const getLabel = (val: string | null | undefined) => {
|
const getLabel = (val: string | null | undefined) => {
|
||||||
if (val == null) return label ?? ''
|
if (val == null) return label ?? ''
|
||||||
|
if (!options) return label ?? ''
|
||||||
const found = options.find((o) => o.value === val)
|
const found = options.find((o) => o.value === val)
|
||||||
return found ? found.name : label ?? ''
|
return found ? found.name : label ?? ''
|
||||||
}
|
}
|
||||||
@@ -77,7 +98,7 @@ const pt = computed(() => ({
|
|||||||
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
|
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
|
||||||
class: [
|
class: [
|
||||||
// container
|
// container
|
||||||
'relative inline-flex w-full cursor-pointer select-none items-center',
|
'relative inline-flex cursor-pointer select-none items-center',
|
||||||
// trigger surface
|
// trigger surface
|
||||||
'rounded-md',
|
'rounded-md',
|
||||||
'bg-transparent text-neutral dark-theme:text-white',
|
'bg-transparent text-neutral dark-theme:text-white',
|
||||||
@@ -115,7 +136,9 @@ const pt = computed(() => ({
|
|||||||
'flex items-center justify-between gap-3 px-3 py-2',
|
'flex items-center justify-between gap-3 px-3 py-2',
|
||||||
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||||
// Selected state + check icon
|
// Selected state + check icon
|
||||||
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected }
|
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected },
|
||||||
|
// Add focus state for keyboard navigation
|
||||||
|
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.focused }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
optionLabel: {
|
optionLabel: {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<SearchBox v-model:="searchQuery" class="max-w-[384px]" />
|
<SearchBox v-model="searchQuery" class="max-w-[384px]" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header-right-area>
|
<template #header-right-area>
|
||||||
@@ -59,12 +59,13 @@
|
|||||||
<div class="relative px-6 pt-2 pb-4 flex gap-2">
|
<div class="relative px-6 pt-2 pb-4 flex gap-2">
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selectedFrameworks"
|
v-model="selectedFrameworks"
|
||||||
|
v-model:search-query="searchText"
|
||||||
class="w-[250px]"
|
class="w-[250px]"
|
||||||
:has-search-box="true"
|
|
||||||
:show-selected-count="true"
|
|
||||||
:has-clear-button="true"
|
|
||||||
label="Select Frameworks"
|
label="Select Frameworks"
|
||||||
:options="frameworkOptions"
|
:options="frameworkOptions"
|
||||||
|
:show-search-box="true"
|
||||||
|
:show-selected-count="true"
|
||||||
|
:show-clear-button="true"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selectedProjects"
|
v-model="selectedProjects"
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { provide, ref } from 'vue'
|
import { provide, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import IconButton from '@/components/button/IconButton.vue'
|
import IconButton from '@/components/button/IconButton.vue'
|
||||||
@@ -201,9 +202,18 @@ const { onClose } = defineProps<{
|
|||||||
provide(OnCloseKey, onClose)
|
provide(OnCloseKey, onClose)
|
||||||
|
|
||||||
const searchQuery = ref<string>('')
|
const searchQuery = ref<string>('')
|
||||||
|
const searchText = ref<string>('')
|
||||||
const selectedFrameworks = ref([])
|
const selectedFrameworks = ref([])
|
||||||
const selectedProjects = ref([])
|
const selectedProjects = ref([])
|
||||||
const selectedSort = ref<string>('popular')
|
const selectedSort = ref<string>('popular')
|
||||||
|
|
||||||
const selectedNavItem = ref<string | null>('installed')
|
const selectedNavItem = ref<string | null>('installed')
|
||||||
|
|
||||||
|
watch(searchText, (newQuery) => {
|
||||||
|
console.log('searchText:', searchText.value, newQuery)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(searchQuery, (newQuery) => {
|
||||||
|
console.log('searchQuery:', searchQuery.value, newQuery)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -240,6 +240,9 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
|||||||
v-model="selectedFrameworks"
|
v-model="selectedFrameworks"
|
||||||
label="Select Frameworks"
|
label="Select Frameworks"
|
||||||
:options="frameworkOptions"
|
:options="frameworkOptions"
|
||||||
|
:has-search-box="true"
|
||||||
|
:show-selected-count="true"
|
||||||
|
:has-clear-button="true"
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="selectedProjects"
|
v-model="selectedProjects"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ModelSelector from '@/components/widget/ModelSelector.vue'
|
import SampleModelSelector from '@/components/widget/SampleModelSelector.vue'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export const useModelSelectorDialog = () => {
|
|||||||
function show() {
|
function show() {
|
||||||
dialogService.showLayoutDialog({
|
dialogService.showLayoutDialog({
|
||||||
key: DIALOG_KEY,
|
key: DIALOG_KEY,
|
||||||
component: ModelSelector,
|
component: SampleModelSelector,
|
||||||
props: {
|
props: {
|
||||||
onClose: hide
|
onClose: hide
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user