From f8b8b1c6edb15b1a0a8c563c015e0e5e772e8ae0 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Sun, 31 Aug 2025 14:50:49 +0900 Subject: [PATCH] [feat] Enhance MultiSelect component and Storybook stories (#5154) --- .storybook/preview-head.html | 51 ++++- src/components/button/MoreButton.stories.ts | 8 +- src/components/input/MultiSelect.stories.ts | 153 +++++++++++-- src/components/input/MultiSelect.vue | 210 ++++++++++-------- src/components/input/SearchBox.stories.ts | 26 ++- src/components/input/SearchBox.vue | 11 +- src/components/input/SingleSelect.stories.ts | 21 +- src/components/input/SingleSelect.vue | 115 ++++++---- ...elSelector.vue => SampleModelSelector.vue} | 20 +- .../widget/layout/BaseWidget.stories.ts | 3 + src/composables/useModelSelectorDialog.ts | 4 +- 11 files changed, 435 insertions(+), 187 deletions(-) rename src/components/widget/{ModelSelector.vue => SampleModelSelector.vue} (93%) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index ae97c82dd..76aca2401 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -4,17 +4,26 @@ transition: background-color 0.3s ease, color 0.3s ease; } - /* Light theme default */ - body { - background-color: #ffffff; - color: #1a1a1a; + /* Light theme default - with explicit color to override media queries */ + body:not(.dark-theme) { + background-color: #fff !important; + 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 */ body.dark-theme, .dark-theme body { - background-color: #0a0a0a; - color: #e5e5e5; + background-color: #202020; + color: #fff; } /* Ensure Storybook canvas follows theme */ @@ -24,11 +33,33 @@ .dark-theme .sb-show-main, .dark-theme .docs-story { - background-color: #0a0a0a !important; + background-color: #202020 !important; } - /* Fix for Storybook controls panel in dark mode */ - .dark-theme .docblock-argstable-body { - color: #e5e5e5; + /* CSS Variables for theme consistency */ + body:not(.dark-theme) { + --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; } \ No newline at end of file diff --git a/src/components/button/MoreButton.stories.ts b/src/components/button/MoreButton.stories.ts index 1a2171b09..3ec3dd491 100644 --- a/src/components/button/MoreButton.stories.ts +++ b/src/components/button/MoreButton.stories.ts @@ -24,22 +24,22 @@ export const Basic: Story = { diff --git a/src/components/input/MultiSelect.stories.ts b/src/components/input/MultiSelect.stories.ts index fa8d7668c..e4b41d68f 100644 --- a/src/components/input/MultiSelect.stories.ts +++ b/src/components/input/MultiSelect.stories.ts @@ -1,9 +1,23 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' +import type { MultiSelectProps } from 'primevue/multiselect' import { ref } from 'vue' import MultiSelect from './MultiSelect.vue' -const meta: Meta = { +// Combine our component props with PrimeVue MultiSelect props +// Since we use v-bind="$attrs", all PrimeVue props are available +interface ExtendedProps extends Partial { + // 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 = { title: 'Components/Input/MultiSelect', component: MultiSelect, tags: ['autodocs'], @@ -13,7 +27,35 @@ const meta: Meta = { }, options: { 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 }, setup() { const selected = ref([]) - const options = [ + const options = args.options || [ { name: 'Vue', value: 'vue' }, { name: 'React', value: 'react' }, { name: 'Angular', value: 'angular' }, @@ -38,8 +80,11 @@ export const Default: Story = {

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

@@ -50,10 +95,10 @@ export const Default: Story = { } export const WithPreselectedValues: Story = { - render: () => ({ + render: (args) => ({ components: { MultiSelect }, setup() { - const options = [ + const options = args.options || [ { name: 'JavaScript', value: 'js' }, { name: 'TypeScript', value: 'ts' }, { name: 'Python', value: 'python' }, @@ -61,25 +106,43 @@ export const WithPreselectedValues: Story = { { name: 'Rust', value: 'rust' } ] const selected = ref([options[0], options[1]]) - return { selected, options } + return { selected, options, args } }, template: `

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

` - }) + }), + 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 = { - render: () => ({ + render: (args) => ({ components: { MultiSelect }, setup() { const frameworkOptions = ref([ @@ -114,7 +177,8 @@ export const MultipleSelectors: Story = { tagOptions, selectedFrameworks, selectedProjects, - selectedTags + selectedTags, + args } }, template: ` @@ -124,22 +188,34 @@ export const MultipleSelectors: Story = { v-model="selectedFrameworks" :options="frameworkOptions" label="Select Frameworks" + :showSearchBox="args.showSearchBox" + :showSelectedCount="args.showSelectedCount" + :showClearButton="args.showClearButton" + :searchPlaceholder="args.searchPlaceholder" />
-

Current Selection:

-
+

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' }}

@@ -147,5 +223,54 @@ export const MultipleSelectors: Story = {
` - }) + }), + 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...' + } } diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue index d1d02a4f1..1dbb53b46 100644 --- a/src/components/input/MultiSelect.vue +++ b/src/components/input/MultiSelect.vue @@ -1,93 +1,104 @@ diff --git a/src/components/input/SingleSelect.stories.ts b/src/components/input/SingleSelect.stories.ts index ba802197c..c0a279822 100644 --- a/src/components/input/SingleSelect.stories.ts +++ b/src/components/input/SingleSelect.stories.ts @@ -4,12 +4,24 @@ import { ref } from 'vue' import SingleSelect from './SingleSelect.vue' +// SingleSelect already includes options prop, so no need to extend const meta: Meta = { title: 'Components/Input/SingleSelect', component: SingleSelect, tags: ['autodocs'], 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 }, setup() { const selected = ref(null) - const options = sampleOptions + const options = args.options || sampleOptions return { selected, options, args } }, template: `
- +

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

` - }), - args: { label: 'Sorting Type' } + }) } export const WithIcon: Story = { diff --git a/src/components/input/SingleSelect.vue b/src/components/input/SingleSelect.vue index 9ef74bacd..7687a16ea 100644 --- a/src/components/input/SingleSelect.vue +++ b/src/components/input/SingleSelect.vue @@ -1,58 +1,73 @@ diff --git a/src/components/widget/layout/BaseWidget.stories.ts b/src/components/widget/layout/BaseWidget.stories.ts index 31e988cb2..0ac0341de 100644 --- a/src/components/widget/layout/BaseWidget.stories.ts +++ b/src/components/widget/layout/BaseWidget.stories.ts @@ -240,6 +240,9 @@ const createStoryTemplate = (args: StoryArgs) => ({ v-model="selectedFrameworks" label="Select Frameworks" :options="frameworkOptions" + :has-search-box="true" + :show-selected-count="true" + :has-clear-button="true" /> { function show() { dialogService.showLayoutDialog({ key: DIALOG_KEY, - component: ModelSelector, + component: SampleModelSelector, props: { onClose: hide }